/* eslint-disable */
/* cityform v2 — Editorial shell + motion utilities + real-UK atlas.

   Loaded AFTER the v1 page files. Re-exports a small set of names that
   override v1 (TopNav, Footer, AtlasMap, ProductCard). New utilities
   (useReveal, useScrollProgress, OsContourBg, RisingText, etc.) live
   alongside them on `window` so the home/page files can import freely.
   ============================================================== */

/* ────────────────────────────── Motion utilities */
function useReveal(opts = {}) {
  const ref = React.useRef(null);
  // Reveal is a progressive enhancement, never a gate: start visible when the
  // user/browser opted out of motion, and otherwise guarantee the content
  // un-hides even if the IntersectionObserver never fires (it can silently
  // never deliver an intersecting entry if the observed node has zero area at
  // observe time — e.g. it mounts inside an already-clipped ancestor before
  // first layout in this in-browser-Babel SPA).
  const optOut = (() => {
    try {
      if (typeof matchMedia !== 'undefined'
          && matchMedia('(prefers-reduced-motion: reduce)').matches) return true;
      if (typeof document !== 'undefined'
          && document.documentElement.getAttribute('data-motion') === 'off') return true;
    } catch (e) { /* matchMedia/SSR guards */ }
    return false;
  })();
  const [seen, setSeen] = React.useState(optOut);
  React.useEffect(() => {
    if (seen) return;
    let done = false;
    const finish = () => {
      if (done) return;
      done = true;
      setSeen(true);
      if (io) io.disconnect();
      clearTimeout(failTimer);
    };
    let io = null;
    if (ref.current && typeof IntersectionObserver !== 'undefined') {
      io = new IntersectionObserver((entries) => {
        for (const e of entries) {
          if (e.isIntersecting) { finish(); break; }
        }
      }, { rootMargin: opts.rootMargin || '0px 0px -10% 0px', threshold: opts.threshold || 0.12 });
      io.observe(ref.current);
    }
    // Fail-open: if the observer never delivers (or doesn't exist), reveal
    // anyway after a short beat so the real clip-path transition still plays.
    const failTimer = setTimeout(finish, 1200);
    return () => {
      done = true;
      if (io) io.disconnect();
      clearTimeout(failTimer);
    };
  }, []);
  return [ref, seen];
}

function Reveal({ as: Tag = 'div', kind = 'reveal', delay = 0, children, ...rest }) {
  const [ref, seen] = useReveal();
  const cls = `${kind} ${seen ? 'in' : ''} ${rest.className || ''}`.trim();
  const style = { ...(rest.style || {}), '--rd': `${delay}ms` };
  return <Tag ref={ref} {...rest} className={cls} style={style}>{children}</Tag>;
}

/* Rising-text: split into lines, each rises from below independently. */
function RisingText({ lines, delay = 0, lineDelay = 90, className = '', style = {} }) {
  const [ref, seen] = useReveal();
  return (
    <span ref={ref} className={className} style={style}>
      {lines.map((line, i) => (
        <span key={i} className="reveal-rise" style={{ display:'block' }}>
          <span style={{ transitionDelay: `${delay + i * lineDelay}ms`, transform: seen ? 'translateY(0)' : 'translateY(110%)' }}>
            {line}
          </span>
        </span>
      ))}
    </span>
  );
}

/* Lightweight scroll-progress hook — returns 0..1 for an element entering
   and leaving the viewport. Used by the "how it's made" scene. */
function useScrollProgress() {
  const ref = React.useRef(null);
  const [p, setP] = React.useState(0);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const r = el.getBoundingClientRect();
        const h = window.innerHeight || 800;
        const total = r.height + h * 0.6;     // start before, end after
        const passed = h * 0.6 - r.top;
        setP(Math.max(0, Math.min(1, passed / total)));
      });
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', onScroll);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', onScroll);
    };
  }, []);
  return [ref, p];
}

/* Parallax — translate Y by `amount * progress(-0.5..0.5)`. */
function useParallax(amount = 40) {
  const ref = React.useRef(null);
  const [y, setY] = React.useState(0);
  React.useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const onScroll = () => {
      cancelAnimationFrame(raf);
      raf = requestAnimationFrame(() => {
        const r = el.getBoundingClientRect();
        const h = window.innerHeight || 800;
        const centre = r.top + r.height / 2;
        const t = (centre - h / 2) / h;
        setY(-t * amount);
      });
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => { cancelAnimationFrame(raf); window.removeEventListener('scroll', onScroll); };
  }, [amount]);
  return [ref, y];
}

/* ────────────────────────────── OS-map contour-line background
   Hand-drawn looking contour-line motif, used as a subtle backdrop in hero
   and section starters. Generated deterministically from a seed so it's
   stable across renders. */
function OsContourBg({ seed = 'sheffield', density = 14, opacity = 0.18, color = 'var(--ink)' }) {
  const lines = React.useMemo(() => {
    const rng = window.makeRng(seed + ':contours');
    const out = [];
    for (let i = 0; i < density; i++) {
      const cy = rng() * 1000;
      const amp = 30 + rng() * 60;
      const freq = 0.6 + rng() * 1.2;
      const phase = rng() * Math.PI * 2;
      const drift = (rng() - 0.5) * 40;
      let d = `M 0 ${cy}`;
      for (let x = 50; x <= 1600; x += 40) {
        const y = cy + Math.sin((x / 1000) * Math.PI * freq + phase) * amp + (x / 1600) * drift;
        d += ` L ${x} ${y.toFixed(1)}`;
      }
      out.push({ d, w: 0.4 + rng() * 0.4 });
    }
    return out;
  }, [seed, density]);
  return (
    <div className="contour-bg" style={{ opacity }}>
      <svg viewBox="0 0 1600 1000" preserveAspectRatio="xMidYMid slice">
        <g fill="none" stroke={color}>
          {lines.map((l, i) => (
            <path key={i} d={l.d} strokeWidth={l.w}/>
          ))}
        </g>
      </svg>
    </div>
  );
}

/* Corner crop marks — used to frame hero blocks like printed plates. */
function CornerMarks() {
  return (
    <>
      <span className="corner-mark tl"/>
      <span className="corner-mark tr"/>
      <span className="corner-mark bl"/>
      <span className="corner-mark br"/>
    </>
  );
}

/* North marker — a thin SVG glyph reused throughout. */
function NorthMark({ size = 36, color = 'var(--ink)', opacity = 0.7 }) {
  return (
    <svg viewBox="0 0 40 56" width={size} height={size * 1.4} style={{ opacity }}>
      <line x1="20" y1="6" x2="20" y2="50" stroke={color} strokeWidth="1"/>
      <path d="M 20 6 L 26 22 L 20 16 L 14 22 Z" fill={color}/>
      <text x="20" y="4" textAnchor="middle" fontFamily="Inter" fontSize="9" letterSpacing="2" fontWeight="500" fill={color}>N</text>
    </svg>
  );
}

/* ────────────────────────────── Refined TopNav (v2) */
function TopNav({ route, go, cartCount }) {
  const items = [
    { id: 'atlas',   label: 'Collection' },
    { id: 'atelier', label: 'Commission' },
    { id: 'journal', label: 'Journal' },
    { id: 'about',   label: 'Studio' },
  ];
  const [scrolled, setScrolled] = React.useState(false);
  React.useEffect(() => {
    const f = () => setScrolled(window.scrollY > 8);
    f();
    window.addEventListener('scroll', f, { passive: true });
    return () => window.removeEventListener('scroll', f);
  }, []);
  return (
    <div style={{
      position:'sticky', top:0, zIndex:50,
      background: scrolled ? 'rgba(225, 229, 224, 0.92)' : 'var(--bg)',
      WebkitBackdropFilter: scrolled ? 'blur(14px)' : 'none',
      backdropFilter: scrolled ? 'blur(14px)' : 'none',
      borderBottom:'1px solid var(--line)',
      transition: 'background .2s',
    }}>
      <div className="container" style={{ display:'flex', alignItems:'center', height:72 }}>
        <button onClick={() => go('home')} style={{ marginRight:48, display:'flex', alignItems:'center', gap:14 }}>
          <Wordmark size={26}/>
          <span style={{ width:1, height:18, background:'var(--line-strong)' }}/>
          <span className="eyebrow" style={{ fontSize:9 }}>EST. SHEFFIELD · 2024</span>
        </button>
        <nav style={{ display:'flex', gap:36 }}>
          {items.map(it => (
            <button key={it.id} onClick={() => go(it.id)}
              style={{
                color: route === it.id ? 'var(--ink)' : 'var(--stone)',
                fontWeight: route === it.id ? 600 : 500,
                fontSize: 13.5,
                letterSpacing: '-0.005em',
                position: 'relative',
                paddingBottom: 4,
                transition: 'color .15s',
              }}
              onMouseEnter={e => { e.currentTarget.style.color = 'var(--ink)'; }}
              onMouseLeave={e => { if (route !== it.id) e.currentTarget.style.color = 'var(--stone)'; }}
            >
              {it.label}
              {route === it.id && (
                <span style={{
                  position:'absolute', bottom:-2, left:0, right:0, height:1, background:'var(--ink)',
                }}/>
              )}
            </button>
          ))}
        </nav>
        <div style={{ flex:1 }}/>
        <div style={{ display:'flex', alignItems:'center', gap:24, color:'var(--ink-soft)' }}>
          <button title="Search" style={{ display:'flex', alignItems:'center', gap:8 }}>
            {Icon.search}
            <span style={{ fontSize: 12, color: 'var(--stone)' }}>Search the atlas</span>
          </button>
          <span style={{ width: 1, height: 18, background: 'var(--line-strong)' }}/>
          <button title="Account">{Icon.user}</button>
          <button onClick={() => go('cart')} style={{ display:'flex', alignItems:'center', gap:8 }} title="Bag">
            {Icon.bag}
            <span className="tnum" style={{ fontSize: 12, minWidth: 14 }}>{String(cartCount).padStart(2,'0')}</span>
          </button>
        </div>
      </div>
    </div>
  );
}

/* ────────────────────────────── Refined Footer (v2) */
function Footer({ go }) {
  return (
    <footer style={{ background:'var(--ink)', color:'var(--mist)', position:'relative', overflow:'hidden' }}>
      {/* Massive faded wordmark backdrop */}
      <div style={{
        position:'absolute', left:0, right:0, top:-30,
        fontFamily:'Inter, sans-serif',
        fontVariationSettings: "'opsz' 32",
        fontWeight:700,
        letterSpacing:'-0.045em',
        textAlign:'center',
        fontSize:'clamp(280px, 30vw, 480px)',
        lineHeight:0.85,
        color:'rgba(225,229,224,0.05)',
        textTransform:'lowercase',
        userSelect:'none',
        pointerEvents:'none',
      }}>cityform</div>

      <div className="container" style={{ paddingTop:96, paddingBottom:36, position:'relative' }}>
        <div style={{ display:'grid', gridTemplateColumns:'1.6fr 1fr 1fr 1fr', gap:48, marginBottom:80 }}>
          <div>
            <Wordmark size={48} surface="reverse"/>
            <p style={{ marginTop:24, maxWidth:380, fontSize:14.5, lineHeight:1.6, color:'rgba(225,229,224,0.72)' }}>
              Architecturally considered miniature models of British cities. Cut from the same square kilometre of public survey data that planners use, scaled to nine centimetres on your shelf.
            </p>
            <div style={{ marginTop:32, display:'flex', alignItems:'center', gap:18 }}>
              <span className="eyebrow" style={{ color:'rgba(225,229,224,0.55)', fontSize:9 }}>53.3811° N · 1.4701° W</span>
              <span style={{ width:1, height:12, background:'rgba(225,229,224,0.2)' }}/>
              <span className="eyebrow" style={{ color:'rgba(225,229,224,0.55)', fontSize:9 }}>SHEFFIELD STUDIO</span>
            </div>
          </div>
          {[
            { h: 'Shop',       items: [['The collection','atlas'],['Now shipping','atlas'],['In development','atlas'],['Gift cards','home']] },
            { h: 'Commission', items: [['Start a commission','atelier'],['Lead times','atelier'],['Sample request','atelier']] },
            { h: 'Studio',     items: [['Process','about'],['Data sources','about'],['Press','journal'],['Contact','about']] },
          ].map(col => (
            <div key={col.h}>
              <div style={{ fontSize:9, letterSpacing:'0.32em', fontWeight:500, color:'rgba(225,229,224,0.5)', marginBottom:22 }}>{col.h.toUpperCase()}</div>
              <ul style={{ listStyle:'none', padding:0, margin:0 }}>
                {col.items.map(([label, r]) => (
                  <li key={label} style={{ marginBottom:12 }}>
                    <button onClick={() => go(r)} style={{ fontSize:14, color:'rgba(225,229,224,0.85)', textAlign:'left' }}>{label}</button>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </div>

        {/* Newsletter strip */}
        <div style={{
          padding:'32px 0', borderTop:'1px solid rgba(225,229,224,0.12)', borderBottom:'1px solid rgba(225,229,224,0.12)',
          display:'grid', gridTemplateColumns:'1.2fr 1fr', gap:32, alignItems:'center',
        }}>
          <div>
            <div className="display-section" style={{ fontSize:38, color:'var(--mist)' }}>A short letter when a new city ships.</div>
            <p style={{ marginTop:10, fontSize:13, color:'rgba(225,229,224,0.6)' }}>One email a month, on average. Sometimes less.</p>
          </div>
          <div style={{ display:'flex', gap:0, border:'1px solid rgba(225,229,224,0.25)' }}>
            <input type="email" placeholder="your.address@somewhere.co.uk"
              style={{
                flex:1, background:'transparent', border:0, outline:'none',
                padding:'0 20px', height:52,
                fontFamily:'Inter, sans-serif', fontSize:13.5,
                color:'var(--mist)',
              }}/>
            <button style={{
              padding:'0 24px', height:52, background:'var(--mist)', color:'var(--ink)',
              fontSize:12, fontWeight:600, letterSpacing:'0.18em',
            }}>SUBSCRIBE</button>
          </div>
        </div>

        <div style={{ marginTop:36, display:'flex', justifyContent:'space-between', fontSize:11, color:'rgba(225,229,224,0.55)', letterSpacing:'0.06em' }}>
          <span>© 2026 Cityform Ltd · Sheffield, UK · Co. 13422118</span>
          <span style={{ display:'flex', gap:18 }}>
            <span>Contains EA LIDAR (OGL v3)</span>
            <span>© OpenStreetMap (ODbL)</span>
            <span>Crown copyright</span>
          </span>
        </div>
      </div>
    </footer>
  );
}

/* ────────────────────────────── ProductCard v2 — photo-led, no sticker
   The sticker has been DEMOTED out of the marketing cards. Each card now
   leans entirely on its on-base render, with the accent appearing only
   as a thin top bar and a tiny dot in the meta row. */
function ProductCard({ item, onClick, density='standard', variant='photo' }) {
  const [hover, setHover] = React.useState(false);
  const photos = item.photos || [item.on_base || item.photo].filter(Boolean);
  const primary = photos[0];
  const secondary = photos[1] || primary;
  return (
    <button onClick={onClick}
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      className="card-lift"
      style={{ display:'block', width:'100%', height:'100%', textAlign:'left' }}>
      <div style={{
        background:'var(--bone)',
        border:'1px solid var(--line)',
        overflow:'hidden',
        height:'100%',
        display:'flex', flexDirection:'column',
        transition: 'border-color .2s',
        borderColor: hover ? 'var(--ink)' : 'var(--line)',
      }}>
        <div style={{
          aspectRatio: '4 / 5',
          flexShrink:0,
          background:'var(--surface-2)',
          position:'relative',
          overflow:'hidden',
        }}>
          {/* Two stacked photos — base + secondary, cross-fade on hover.
              When only one exists, the layer simply doesn't fade. */}
          <img src={primary} alt={item.name}
            className="img-editorial"
            style={{
              position:'absolute', inset:0,
              width: '100%', height: '100%', objectFit: 'cover',
              opacity: hover && secondary !== primary ? 0 : 1,
              transition: 'opacity .35s ease, transform 1s cubic-bezier(.2,.6,.2,1)',
              transform: hover ? 'scale(1.03)' : 'scale(1)',
            }}/>
          {secondary !== primary && (
            <img src={secondary} alt=""
              className="img-editorial"
              style={{
                position:'absolute', inset:0,
                width: '100%', height: '100%', objectFit: 'cover',
                opacity: hover ? 1 : 0,
                transition: 'opacity .35s ease, transform 1s cubic-bezier(.2,.6,.2,1)',
                transform: hover ? 'scale(1.03)' : 'scale(1.06)',
              }}/>
          )}
          {/* accent bar — top, animates in on hover */}
          {item.accent && (
            <div style={{
              position:'absolute', top:0, left:0, right:0, height:3,
              background: `var(--${item.accent})`,
              transformOrigin:'left',
              transform: hover ? 'scaleX(1)' : 'scaleX(0)',
              transition: 'transform .5s cubic-bezier(.2,.6,.2,1)',
            }}/>
          )}
          {/* coords pill bottom-left */}
          <div style={{
            position:'absolute', bottom:12, left:12,
            padding:'5px 10px', background:'var(--bone)', border:'1px solid var(--ink)',
            fontSize:9, letterSpacing:'0.18em', fontWeight:500, fontVariantNumeric:'tabular-nums',
          }}>
            {item.coords.lat.toFixed(3)}°{item.coords.ns} · {Math.abs(item.coords.lng).toFixed(3)}°{item.coords.ew}
          </div>
          {/* "View" arrow appears on hover, where the sticker peek used to be */}
          <div style={{
            position:'absolute', bottom:12, right:12,
            background:'var(--ink)', color:'var(--mist)', padding:'6px 12px',
            display:'flex', alignItems:'center', gap:8,
            opacity: hover ? 1 : 0,
            transform: hover ? 'translateX(0)' : 'translateX(8px)',
            transition: 'opacity .3s ease, transform .35s cubic-bezier(.2,.6,.2,1)',
          }}>
            <span style={{ fontSize:10, letterSpacing:'0.22em', fontWeight:500 }}>VIEW</span>
            {Icon.arrow}
          </div>
          {item.status === 'soon' && (
            <div style={{
              position:'absolute', top:12, left:12,
              background:'var(--ink)', color:'var(--mist)',
              padding:'4px 10px', fontSize:9, letterSpacing:'0.22em', fontWeight:500,
            }}>IN DEVELOPMENT</div>
          )}
        </div>
        <div style={{ padding: density === 'dense' ? '14px 16px' : '20px 22px' }}>
          <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline', gap: 8 }}>
            <div className="h3" style={{
              fontSize: density === 'dense' ? 16 : 19,
              fontWeight: 600,
              minWidth: 0, overflow:'hidden', textOverflow:'ellipsis', whiteSpace:'nowrap',
            }}>{item.name}</div>
            <div className="tnum" style={{ fontSize:12.5, color:'var(--stone)', flexShrink:0 }}>
              {item.status === 'soon' ? '—' : `£${item.price || 95}`}
            </div>
          </div>
          <div style={{ marginTop:8, display:'flex', alignItems:'center', gap:8 }}>
            {item.accent ? (
              <>
                <span style={{ width:7, height:7, background:`var(--${item.accent})` }}/>
                <span className="eyebrow" style={{ fontSize:9 }}>{(item.landmark || item.region).toUpperCase()}</span>
              </>
            ) : (
              <span className="eyebrow" style={{ fontSize:9, color:'var(--stone)' }}>{item.region.toUpperCase()}</span>
            )}
          </div>
        </div>
      </div>
    </button>
  );
}

/* ────────────────────────────── UK Atlas — real map, Leaflet first, SVG fallback
   Uses CartoDB Positron via the global L (Leaflet) loaded by the host HTML.
   If Leaflet didn't load (offline / blocked), we silently render the existing
   styled SVG map underneath so the page still works. */
function AtlasMap({ items, t, go }) {
  const wrapRef = React.useRef(null);
  const [hovered, setHovered] = React.useState(null);
  const [leafletReady, setLeafletReady] = React.useState(!!window.L);
  const itemsRef = React.useRef(items);
  itemsRef.current = items;
  const goRef = React.useRef(go);
  goRef.current = go;

  // If Leaflet isn't loaded yet, poll briefly — the script loads async.
  React.useEffect(() => {
    if (leafletReady) return;
    let t = 0;
    const id = setInterval(() => {
      if (window.L) { setLeafletReady(true); clearInterval(id); return; }
      if (++t > 30) clearInterval(id);
    }, 200);
    return () => clearInterval(id);
  }, []);

  React.useEffect(() => {
    if (!leafletReady || !wrapRef.current) return;
    const L = window.L;
    const el = wrapRef.current;

    // Tight UK extent — Land's End to Shetland.
    const map = L.map(el, {
      zoomControl: false,
      scrollWheelZoom: false,
      attributionControl: true,
    }).setView([54.4, -3.0], 5);
    el.classList.add('cf-atlas-leaflet');

    // CartoDB Positron — clean, light, brand-aligned. Our CSS then desaturates.
    L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}{r}.png', {
      attribution: '© OpenStreetMap · © CartoDB',
      subdomains: 'abcd', maxZoom: 18,
    }).addTo(map);
    L.tileLayer('https://{s}.basemaps.cartocdn.com/light_only_labels/{z}/{x}/{y}{r}.png', {
      attribution: '', subdomains: 'abcd', maxZoom: 18, opacity: 0.7,
    }).addTo(map);

    // City pins
    const popupHtml = (it) => {
      const accent = it.accent ? `var(--${it.accent})` : 'var(--stone)';
      const photo = (it.photos && it.photos[0]) || it.photo || '';
      return `
        <div class="cf-pop" style="display:flex;gap:14px;align-items:flex-start;cursor:pointer">
          <div style="width:64px;height:64px;background:var(--surface-2);overflow:hidden;flex-shrink:0;border:1px solid var(--line)">
            ${photo ? `<img src="${photo}" alt="" style="width:100%;height:100%;object-fit:cover;filter:grayscale(0.45) contrast(1.05)"/>` : ''}
          </div>
          <div style="flex:1;min-width:0">
            <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
              <span style="width:8px;height:8px;background:${accent};display:inline-block"></span>
              <span style="font-size:9px;letter-spacing:0.22em;font-weight:500;color:var(--stone)">${(it.landmark || it.type).toUpperCase()}</span>
            </div>
            <div style="font-weight:600;font-size:18px;letter-spacing:-0.005em">${it.name}</div>
            <div style="font-size:11px;color:var(--stone);margin-top:4px;font-variant-numeric:tabular-nums;letter-spacing:0.06em">${it.coords.lat.toFixed(3)}°${it.coords.ns} · ${Math.abs(it.coords.lng).toFixed(3)}°${it.coords.ew}</div>
            <div style="margin-top:10px;font-size:11px;letter-spacing:0.22em;font-weight:500;color:var(--ink);border-bottom:1px solid var(--ink);display:inline-block;padding-bottom:1px">VIEW MODEL →</div>
          </div>
        </div>
      `;
    };

    const markers = [];
    items.forEach((it) => {
      const lat = it.coords.ns === 'S' ? -it.coords.lat : it.coords.lat;
      const lng = it.coords.ew === 'W' ? -it.coords.lng : it.coords.lng;
      const accent = it.accent ? `var(--${it.accent})` : 'var(--ink)';
      const html = `
        <div class="cf-pin" style="color:${accent}">
          <div class="cf-pin-ring"></div>
          <div class="cf-pin-dot ${it.accent ? '' : 'hollow'}" style="background:${it.accent ? accent : 'var(--bone)'}"></div>
        </div>
      `;
      const icon = L.divIcon({
        className: 'cf-pin-wrap',
        html,
        iconSize: [12, 12],
        iconAnchor: [6, 6],
      });
      const m = L.marker([lat, lng], { icon }).addTo(map);
      const pop = L.popup({
        offset: [0, -6], closeOnClick: false, autoClose: false, closeButton: false,
        className: 'cf-popup', maxWidth: 320,
      }).setContent(popupHtml(it));
      m.on('mouseover', () => { m.bindPopup(pop).openPopup(); });
      m.on('mouseout',  () => { setTimeout(() => m.closePopup(), 1500); });
      m.on('click', () => { goRef.current('product', it.id); });
      markers.push(m);
    });

    // Fit bounds with breathing room
    try {
      const grp = L.featureGroup(markers);
      map.fitBounds(grp.getBounds().pad(0.25));
    } catch (e) { /* ignore */ }

    // Enable wheel zoom only when user clicks into the map
    map.on('click', () => map.scrollWheelZoom.enable());
    map.on('mouseout', () => map.scrollWheelZoom.disable());

    return () => { map.remove(); };
  }, [leafletReady]);

  return (
    <div style={{ display:'grid', gridTemplateColumns:'1fr 360px', gap:32, alignItems:'flex-start' }}>
      <div style={{
        border:'1px solid var(--ink)', background:'#ECEFEB',
        position:'relative', overflow:'hidden', height: 760,
      }}>
        <div ref={wrapRef} style={{ position:'absolute', inset:0 }}/>
        {!leafletReady && (
          <div style={{ position:'absolute', inset:0, display:'flex', alignItems:'center', justifyContent:'center' }}>
            <div className="eyebrow" style={{ color:'var(--stone)' }}>LOADING ORDNANCE MAP…</div>
          </div>
        )}
        {/* Corner marks for the printed-plate feel */}
        <CornerMarks/>
        {/* Map title plate */}
        <div style={{
          position:'absolute', top:24, left:24,
          background:'var(--bone)', border:'1px solid var(--ink)',
          padding:'14px 18px', maxWidth:300,
          zIndex: 600,
        }}>
          <div className="eyebrow">FIGURE 01 · ATLAS</div>
          <div className="h3" style={{ marginTop:6, fontSize:18, fontWeight:600 }}>Every cityform we've made.</div>
          <div className="tnum" style={{ marginTop:6, fontSize:10.5, color:'var(--stone)' }}>n = {items.length} · UK · 1:11000 each</div>
        </div>
        {/* Scale bar */}
        <div style={{
          position:'absolute', bottom:24, right:60,
          display:'flex', flexDirection:'column', alignItems:'flex-end',
          background:'var(--bone)', border:'1px solid var(--ink)', padding:'10px 14px',
          zIndex: 600,
        }}>
          <div style={{ display:'flex', alignItems:'center', gap:0, marginBottom:6 }}>
            {[0,1,2,3].map(i => (
              <span key={i} style={{ width:24, height:6, background: i % 2 ? 'var(--ink)' : 'var(--bone)', border:'1px solid var(--ink)' }}/>
            ))}
          </div>
          <span className="tnum" style={{ fontSize:9, letterSpacing:'0.18em', fontWeight:500 }}>0&nbsp;&nbsp;&nbsp;100&nbsp;&nbsp;&nbsp;200 KM</span>
        </div>
        {/* North marker */}
        <div style={{ position:'absolute', top:24, right:60, zIndex: 600 }}>
          <NorthMark size={32}/>
        </div>
      </div>

      <div style={{ position:'sticky', top:152 }}>
        <div className="eyebrow">Pin a place</div>
        <h3 className="display-section" style={{ fontSize:38, marginTop:14 }}>
          The Isles, charted by what we've cut.
        </h3>
        <p className="body" style={{ fontSize:14.5, color:'var(--stone)', lineHeight:1.6, marginTop:14 }}>
          Filled squares ship from the collection. Hollow squares are commissions or in development. Hover to see a place; click to open its city.
        </p>
        <button className="btn-v2-ghost btn-v2" style={{ marginTop:24 }} onClick={() => go('atelier')}>
          Suggest a new place&nbsp;
          <span className="btn-arrow">{Icon.plus}</span>
        </button>
        <div style={{ marginTop:40, paddingTop:20, borderTop:'1px solid var(--line)' }}>
          <div className="eyebrow" style={{ marginBottom:14 }}>Signature index</div>
          <div style={{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:'12px 18px' }}>
            {Object.entries(window.ACCENTS).map(([k, v]) => (
              <div key={k} style={{ display:'flex', alignItems:'center', gap:10, fontSize:12 }}>
                <span style={{ width:12, height:12, background:v.hex }}/>
                <div>
                  <div style={{ fontWeight:500 }}>{v.name}</div>
                  <div style={{ fontSize:10, color:'var(--stone)', marginTop:2 }}>{v.note}</div>
                </div>
              </div>
            ))}
          </div>
        </div>
        <div style={{ marginTop:32, paddingTop:20, borderTop:'1px solid var(--line)' }}>
          <div className="eyebrow" style={{ marginBottom:10 }}>Sources</div>
          <div style={{ fontSize:11.5, color:'var(--stone)', lineHeight:1.6 }}>
            Base layer: © OpenStreetMap, styled by CartoDB.<br/>
            Each model: EA LIDAR (OGL v3) + OSM (ODbL).
          </div>
        </div>
      </div>
    </div>
  );
}

Object.assign(window, {
  useReveal, Reveal, RisingText, useScrollProgress, useParallax,
  OsContourBg, CornerMarks, NorthMark,
  TopNav, Footer, ProductCard, AtlasMap,
});
