// Shared utilities and data for all three Dune Ridge site variations.
// Updated per Feb 27 call: 3 buildings (Ridge / Bay / Waterfront), phased release,
// waterfront reserved + wetland-delayed, Ridge is the active sell.

// ─── IMAGE MANIFEST ────────────────────────────────────────────────
const IMG = {
  // Exterior renderings (final v5, 04-29-26)
  aerialBackA: 'images/aerial-back-a.jpg',
  aerialBackB: 'images/aerial-back-b.jpg',
  aerialBackC: 'images/aerial-back-c.jpg',
  aerialBayA: 'images/aerial-bay-a.jpg',
  aerialBayB: 'images/aerial-bay-b.jpg',
  aerialBayC: 'images/aerial-bay-c.jpg',
  aerialBayD: 'images/aerial-bay-d.jpg',
  dockMorning: 'images/dock-morning.jpg',
  dockNoon: 'images/dock-noon.jpg',
  dockSunset: 'images/dock-sunset.jpg',
  dockBSunset: 'images/dock-b-sunset.jpg',
  elevLake: 'images/elev-lake.jpg',
  elevRidge: 'images/elev-ridge.jpg',
  elevTrail: 'images/elev-trail.jpg',
  trailNoon: 'images/trail-noon.jpg',
  trailSunset: 'images/trail-sunset.jpg',

  // Interior renderings — 2BR Unit (final v4, 04-27-26)
  int2brLiving: 'images/int-2br-living.jpg',
  int2brDining: 'images/int-2br-dining.jpg',
  int2brKitchenA: 'images/int-2br-kitchen-a.jpg',
  int2brKitchenB: 'images/int-2br-kitchen-b.jpg',
  int2brBed1: 'images/int-2br-bed-1.jpg',
  int2brBed2: 'images/int-2br-bed-2.jpg',
  int2brBath1: 'images/int-2br-bath-1.jpg',
  int2brBath2: 'images/int-2br-bath-2.jpg',
  int2brBalcony: 'images/int-2br-balcony.jpg',
  int2brBalconyB: 'images/int-2br-balcony-b.jpg',

  // Interior renderings — 3BR Walkout Unit (final v4, 04-27-26)
  int3brLiving: 'images/int-3br-living.jpg?v=4',
  int3brLivingKitchen: 'images/int-3br-living-kitchen.jpg?v=4',
  int3brDiningLiving: 'images/int-3br-dining-living.jpg?v=4',
  int3brKitchen: 'images/int-3br-kitchen.jpg?v=4',
  int3brFamily: 'images/int-3br-family.jpg?v=4',
  int3brBed1: 'images/int-3br-bed-1.jpg?v=4',
  int3brBed2: 'images/int-3br-bed-2.jpg?v=4',
  int3brBed3: 'images/int-3br-bed-3.jpg?v=4',
  int3brBath1: 'images/int-3br-bath-1.jpg?v=4',
  int3brBath2: 'images/int-3br-bath-2.jpg?v=4',
  int3brPatio: 'images/int-3br-patio.jpg?v=4',

  // 3D unit floor plans (final v4 jpg, v2 transparent PNG)
  plan2br3d: 'images/plan-2br-3d.jpg',
  plan3brUpper3d: 'images/plan-3br-upper-3d.jpg',
  plan3brLower3d: 'images/plan-3br-lower-3d.jpg',
  plan2br3dT: 'images/plan-2br-3d-transparent.png',
  plan3brUpper3dT: 'images/plan-3br-upper-3d-transparent.png',
  plan3brLower3dT: 'images/plan-3br-lower-3d-transparent.png',

  // 2D site plan sketch
  sitePlan2d: 'images/site-plan-2d.jpg',
  sitePlanAerial: 'images/site-plan-aerial.jpg'
};

// Hero time-of-day sets
const HERO_TIMES = [
{ key: 'morning', label: 'Morning', src: IMG.aerialBayA, caption: 'Morning, Betsie Bay' },
{ key: 'noon', label: 'Noon', src: IMG.aerialBayC, caption: 'Noon, looking west' },
{ key: 'sunset', label: 'Sunset', src: IMG.aerialBackA, caption: 'Sunset over Lake Michigan' }];


const GALLERY = [
{ src: IMG.aerialBackA, title: 'Aerial · Sunset', tag: 'Rendering' },
{ src: IMG.aerialBackB, title: 'Ridge at dusk', tag: 'Rendering' },
{ src: IMG.aerialBayA, title: 'From the bay', tag: 'Rendering' },
{ src: IMG.aerialBayB, title: 'Full site plan', tag: 'Rendering' },
{ src: IMG.dockSunset, title: 'Amenity deck', tag: 'Amenity' },
{ src: IMG.dockBSunset, title: 'Shared firepit', tag: 'Amenity' },
{ src: IMG.elevLake, title: 'Lake elevation', tag: 'Elevation' },
{ src: IMG.elevRidge, title: 'Ridge elevation', tag: 'Elevation' },
{ src: IMG.elevTrail, title: 'Trail elevation', tag: 'Elevation' },
{ src: IMG.trailSunset, title: 'Trail at sunset', tag: 'Exterior' }];


// ─── BRAND ─────────────────────────────────────────────────────────
const BRAND = {
  name: 'Dune Ridge',
  full: 'Dune Ridge at Betsie Bay',
  tagline: 'at Betsie Bay',
  logoSrc: 'images/logo-dune-ridge-tonal.png'
};

// LogoMark — transparent PNG with near-black ink. Sits tonally on any surface.
// On dark backgrounds we invert so the ink reads as light.
function LogoMark({ height = 64, mode = 'light', alt = 'Dune Ridge at Betsie Bay' }) {
  const isDark = mode === 'dark';
  return (
    <img
      src={BRAND.logoSrc}
      alt={alt}
      style={{
        height,
        width: 'auto',
        display: 'block',
        filter: isDark ? 'invert(1)' : 'none'
      }} />);


}

// ─── PROJECT DATA — reflects Feb 27 call ─────────────────────────────
// Broker info is now tweakable. Default = Kari King at Century 21 Northland.
// Everything that used to read from BROKER directly now reads from BrokerContext.
const BROKER_DEFAULT = {
  name: 'Kari King',
  role: 'Listing Broker · Dune Ridge at Betsie Bay',
  email: 'KariKing@c21northland.com',
  phone: '231-651-0923',
  firm: 'Century 21 Northland'
};
const BrokerContext = React.createContext(BROKER_DEFAULT);
function useBroker() {return React.useContext(BrokerContext);}
function initialsOf(name) {
  return (name || '').split(/\s+/).filter(Boolean).map((w) => w[0].toUpperCase()).slice(0, 2).join('') || '—';
}
function firstNameOf(name) {
  return (name || '').split(/\s+/)[0] || 'your broker';
}

// Three phases — all three releases now accepting reservations.
// Construction begins Summer 2026 across the site.
const BUILDINGS = [
{
  id: 'L',
  name: 'The Lake',
  status: 'available',
  statusLabel: 'Now reserving',
  units: 12,
  reserved: 0,
  floors: 2,
  orientation: 'Lakeside · On the water',
  tagline: 'Directly on the water.',
  priceFrom: 'By request',
  description: 'Twelve residences set directly on the water with private dockage, walk-out patios, and morning light off the bay. Two stories, lake-level living.'
},
{
  id: 'R',
  name: 'The Ridge',
  status: 'available',
  statusLabel: 'Now reserving',
  units: 18,
  reserved: 0,
  floors: 3,
  orientation: 'Upland · West-facing',
  tagline: 'Expansive views of Lake Michigan.',
  priceFrom: 'Pricing TBD',
  description: 'Eighteen residences stepped up the dune with expansive Lake Michigan views. Walkout lower levels (3BR/2BA), open upper floors (2BR/2BA), and private west-facing balconies for sunset.'
},
{
  id: 'T',
  name: 'The Trail',
  status: 'available',
  statusLabel: 'Now reserving',
  units: 12,
  reserved: 0,
  floors: 3,
  orientation: 'Mid-slope · Trail-side',
  tagline: 'Steps from the Betsie Valley Trail.',
  priceFrom: 'Pricing TBD',
  description: 'Twelve residences set mid-slope along the Betsie Valley Trail — direct trail access, filtered Lake Michigan views, and the quietest pocket on the property.'
}];


// ─── HOOKS ────────────────────────────────────────────────────────
function useInView(ref, rootMargin = '-10% 0px') {
  const [v, setV] = React.useState(false);
  React.useEffect(() => {
    if (!ref.current) return;
    const o = new IntersectionObserver(([e]) => {if (e.isIntersecting) setV(true);}, { rootMargin });
    o.observe(ref.current);
    return () => o.disconnect();
  }, []);
  return v;
}

function CountUp({ to, duration = 1400, format = (n) => n, suffix = '', prefix = '', className, style }) {
  const ref = React.useRef(null);
  const inView = useInView(ref);
  const [n, setN] = React.useState(0);
  React.useEffect(() => {
    if (!inView) return;
    const start = performance.now();
    let raf;
    const tick = (now) => {
      const t = Math.min(1, (now - start) / duration);
      const e = 1 - Math.pow(1 - t, 3);
      setN(to * e);
      if (t < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [inView, to]);
  return <span ref={ref} className={className} style={style}>{prefix}{format(Math.round(n))}{suffix}</span>;
}

function useScrollY() {
  const [y, setY] = React.useState(0);
  React.useEffect(() => {
    const el = document.scrollingElement || document.documentElement;
    const on = () => setY(window.scrollY || el.scrollTop || 0);
    on();
    window.addEventListener('scroll', on, { passive: true });
    document.addEventListener('scroll', on, { passive: true, capture: true });
    return () => {window.removeEventListener('scroll', on);document.removeEventListener('scroll', on, { capture: true });};
  }, []);
  return y;
}

function useParallax(ref, speed = 0.3) {
  const [offset, setOffset] = React.useState(0);
  React.useEffect(() => {
    if (!ref.current) return;
    let raf;
    const update = () => {
      if (!ref.current) return;
      const r = ref.current.getBoundingClientRect();
      const vpH = window.innerHeight;
      const prog = (vpH - r.top) / (vpH + r.height);
      setOffset((prog - 0.5) * speed * 200);
    };
    const on = () => {cancelAnimationFrame(raf);raf = requestAnimationFrame(update);};
    update();
    window.addEventListener('scroll', on, { passive: true, capture: true });
    window.addEventListener('resize', on);
    return () => {cancelAnimationFrame(raf);window.removeEventListener('scroll', on, { capture: true });window.removeEventListener('resize', on);};
  }, []);
  return offset;
}

// ─── SITE PLAN — schematic top-down SVG ─────────────────────────────
// Three buildings stepped up the ridge: Waterfront (on the lake), Bay (mid), Ridge (top).
function SitePlan({ palette, onSelect }) {
  const [active, setActive] = React.useState('R');
  // The Lake is closest to water, The Trail mid-slope, The Ridge atop the dune.
  const buildings = [
  { id: 'L', x: 180, y: 400, w: 320, h: 46, label: 'The Lake', meta: BUILDINGS.find((b) => b.id === 'L') },
  { id: 'T', x: 260, y: 300, w: 320, h: 52, label: 'The Trail', meta: BUILDINGS.find((b) => b.id === 'T') },
  { id: 'R', x: 360, y: 180, w: 280, h: 56, label: 'The Ridge', meta: BUILDINGS.find((b) => b.id === 'R') }];

  const act = buildings.find((b) => b.id === active);
  const C = palette;

  const statusFill = (status) => {
    // All releases now accepting reservations — use a single outlined treatment
    // for inactive buildings; the active one fills with ink.
    return 'none';
  };

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '1fr 340px', gap: 48, alignItems: 'start' }}>
      <svg viewBox="0 0 1100 620" style={{ width: '100%', height: 'auto', background: C.bg, border: `1px solid ${C.line}` }}>
        {/* Water */}
        <rect x="0" y="450" width="1100" height="170" fill={C.water} />
        {Array.from({ length: 5 }).map((_, i) =>
        <path key={i} d={`M0 ${475 + i * 25} Q 275 ${465 + i * 25} 550 ${475 + i * 25} T 1100 ${475 + i * 25}`} stroke={C.line} strokeOpacity="0.25" fill="none" strokeWidth="0.6" />
        )}
        {/* Sand */}
        <path d="M0 440 Q 300 432 620 442 T 1100 440 L 1100 454 Q 800 448 500 454 T 0 454 Z" fill={C.sand} opacity="0.9" />

        {/* Ridge contours */}
        <path d="M20 140 Q 280 100 520 160 T 1080 180" stroke={C.line} fill="none" strokeWidth="0.6" strokeDasharray="2 4" />
        <path d="M20 220 Q 280 180 520 240 T 1080 260" stroke={C.line} fill="none" strokeWidth="0.6" strokeDasharray="2 4" />
        <path d="M20 310 Q 280 270 520 330 T 1080 350" stroke={C.line} fill="none" strokeWidth="0.6" strokeDasharray="2 4" />

        {/* Road (M22) */}
        <path d="M40 540 Q 280 520 540 500 T 1060 470" stroke={C.ink} strokeOpacity="0.35" fill="none" strokeWidth="1" />
        <text x="60" y="534" fontFamily="Geist Mono, monospace" fontSize="10" letterSpacing="1.5" fill={C.ink} opacity="0.6">M22 HIGHWAY</text>

        {/* Marina — 25 slips */}
        <g transform="translate(820, 450)">
          <line x1="0" y1="0" x2="0" y2="140" stroke={C.ink} strokeWidth="2" />
          {Array.from({ length: 5 }).map((_, r) =>
          <g key={r} transform={`translate(0, ${20 + r * 26})`}>
              <line x1="0" y1="0" x2="120" y2="0" stroke={C.ink} strokeWidth="1.2" />
              {Array.from({ length: 5 }).map((__, c) =>
            <rect key={c} x={15 + c * 22} y={-5} width="16" height="10" fill="none" stroke={C.ink} strokeWidth="0.6" />
            )}
            </g>
          )}
        </g>
        <text x="960" y="468" fontFamily="Geist Mono, monospace" fontSize="10" letterSpacing="1.5" fill={C.ink}>25 BOAT SLIPS</text>

        {/* Trail */}
        <path d="M20 110 Q 300 90 580 70 T 1080 60" stroke={C.accent} fill="none" strokeWidth="1.4" strokeDasharray="6 3" />
        <text x="40" y="104" fontFamily="Geist Mono, monospace" fontSize="10" letterSpacing="1.5" fill={C.accent}>BETSIE VALLEY TRAIL →</text>

        {/* Trees */}
        {[[120, 260, 6], [80, 380, 5], [680, 240, 6], [820, 300, 5], [1000, 360, 6], [70, 180, 5], [340, 130, 5], [640, 140, 6], [920, 120, 5], [420, 380, 5], [560, 410, 5]].map(([cx, cy, r], i) =>
        <circle key={i} cx={cx} cy={cy} r={r} fill="none" stroke={C.line} strokeWidth="0.8" />
        )}

        {/* Buildings — rendered back to front (ridge → bay → waterfront) */}
        {buildings.map((b) => {
          const isActive = b.id === active;
          const fill = isActive ? C.ink : statusFill(b.meta.status);
          const strokeOpacity = b.meta.status === 'reserved' ? 0.5 : 1;
          return (
            <g key={b.id} onClick={() => {setActive(b.id);onSelect && onSelect(b.id);}} style={{ cursor: 'pointer' }}>
              <rect x={b.x} y={b.y} width={b.w} height={b.h} fill={fill} stroke={C.ink} strokeWidth={1.2} strokeOpacity={isActive ? 1 : strokeOpacity} />
              {/* roofline hatching if not filled solid */}
              {!isActive && b.meta.status !== 'available' && Array.from({ length: 16 }).map((_, i) =>
              <line key={i} x1={b.x + i * (b.w / 16)} y1={b.y} x2={b.x + i * (b.w / 16) + 10} y2={b.y + b.h} stroke={C.line} strokeWidth="0.5" />
              )}
              <text x={b.x + b.w / 2} y={b.y + b.h / 2 + 5} textAnchor="middle" fontFamily="Geist Mono, monospace" fontSize="14" fontWeight="500" letterSpacing="3" fill={isActive || b.meta.status === 'available' ? C.bg : C.ink}>
                {b.label.toUpperCase()}
              </text>
              {/* status dot */}
              <circle cx={b.x + 12} cy={b.y + 12} r="3" fill={b.meta.status === 'available' ? '#3f8a4f' : b.meta.status === 'reserved' ? C.muted : C.accent} />
            </g>);

        })}

        {/* North arrow */}
        <g transform="translate(60, 80)">
          <line x1="0" y1="0" x2="0" y2="-30" stroke={C.ink} strokeWidth="1" />
          <polygon points="0,-34 -4,-26 4,-26" fill={C.ink} />
          <text x="0" y="12" textAnchor="middle" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.ink}>N</text>
        </g>
        {/* Scale */}
        <g transform="translate(60, 580)">
          <line x1="0" y1="0" x2="100" y2="0" stroke={C.ink} strokeWidth="1" />
          <line x1="0" y1="-3" x2="0" y2="3" stroke={C.ink} strokeWidth="1" />
          <line x1="100" y1="-3" x2="100" y2="3" stroke={C.ink} strokeWidth="1" />
          <text x="50" y="15" textAnchor="middle" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.ink}>50 FT</text>
        </g>
      </svg>

      {/* Info panel */}
      <div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2.5, color: C.muted, textTransform: 'uppercase', marginBottom: 16 }}>
          <span style={{ width: 8, height: 8, borderRadius: '50%', background: act.meta.status === 'available' ? '#3f8a4f' : act.meta.status === 'reserved' ? C.muted : C.accent }} />
          {act.meta.statusLabel}
        </div>
        <div style={{ fontSize: 54, fontWeight: 300, letterSpacing: -1.5, lineHeight: 1, color: C.ink, marginBottom: 6 }}>{act.meta.name}</div>
        <div style={{ fontSize: 18, color: C.muted, marginBottom: 20, fontStyle: 'italic' }}>{act.meta.tagline}</div>
        <p style={{ fontSize: 18, lineHeight: 1.6, color: C.ink, margin: '0 0 24px' }}>{act.meta.description}</p>
        <div style={{ borderTop: `1px solid ${C.line}`, paddingTop: 12, marginBottom: 20 }}>
          <Row label="Residences" value={act.meta.units} C={C} />
          <Row label="Reserved" value={`${act.meta.reserved} of ${act.meta.units}`} C={C} />
          <Row label="Floors" value={act.meta.floors} C={C} />
          <Row label="Orientation" value={act.meta.orientation} C={C} />
          <Row label="Priced from" value={act.meta.priceFrom} C={C} />
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          {buildings.map((b) =>
          <button key={b.id} onClick={() => setActive(b.id)} style={{ flex: 1, padding: '12px 8px', border: `1px solid ${b.id === active ? C.ink : C.line}`, background: b.id === active ? C.ink : 'transparent', color: b.id === active ? C.bg : C.ink, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, cursor: 'pointer', borderRadius: 0, textTransform: 'uppercase' }}>{b.label}</button>
          )}
        </div>
      </div>
    </div>);

}

function Row({ label, value, C }) {
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', padding: '9px 0', borderBottom: `1px solid ${C.line}`, fontSize: 18 }}>
      <span style={{ color: C.muted, fontFamily: 'Geist Mono, monospace', letterSpacing: 1.5, textTransform: 'uppercase', fontSize: 18 }}>{label}</span>
      <span style={{ color: C.ink }}>{value}</span>
    </div>);

}

// ─── LIGHTBOX ────────────────────────────────────────────────────────
function useLightbox(items) {
  const [idx, setIdx] = React.useState(null);
  const open = (i) => setIdx(i);
  const close = () => setIdx(null);
  const prev = () => setIdx((i) => (i - 1 + items.length) % items.length);
  const next = () => setIdx((i) => (i + 1) % items.length);
  React.useEffect(() => {
    if (idx === null) return;
    const on = (e) => {
      if (e.key === 'Escape') close();
      if (e.key === 'ArrowLeft') prev();
      if (e.key === 'ArrowRight') next();
    };
    window.addEventListener('keydown', on);
    return () => window.removeEventListener('keydown', on);
  }, [idx]);

  const node = idx !== null ?
  <div onClick={close} style={{ position: 'fixed', inset: 0, background: 'rgba(10,8,5,0.94)', zIndex: 9999, display: 'grid', placeItems: 'center', padding: 60 }}>
      <img src={items[idx].src} alt="" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
      <div style={{ position: 'absolute', top: 24, left: 24, right: 24, display: 'flex', justifyContent: 'space-between', color: '#f5f0e6', fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2 }}>
        <span>{String(idx + 1).padStart(2, '0')} / {String(items.length).padStart(2, '0')} · {items[idx].title}</span>
        <button onClick={(e) => {e.stopPropagation();close();}} style={{ background: 'none', border: 'none', color: '#f5f0e6', fontSize: 24, cursor: 'pointer' }}>×</button>
      </div>
      <button onClick={(e) => {e.stopPropagation();prev();}} style={{ position: 'absolute', left: 24, top: '50%', transform: 'translateY(-50%)', background: 'none', border: '1px solid rgba(245,240,230,0.3)', color: '#f5f0e6', width: 48, height: 48, cursor: 'pointer', fontSize: 18 }}>‹</button>
      <button onClick={(e) => {e.stopPropagation();next();}} style={{ position: 'absolute', right: 24, top: '50%', transform: 'translateY(-50%)', background: 'none', border: '1px solid rgba(245,240,230,0.3)', color: '#f5f0e6', width: 48, height: 48, cursor: 'pointer', fontSize: 18 }}>›</button>
    </div> :
  null;

  return { open, node };
}

// ─── VIRTUAL TOUR — Kuula 360° embed ───────────────────────────────
// Preliminary interactive rendering walk-through. Click to engage, then drag /
// scroll inside the pane to look around. The URL is tweakable so we can swap
// in a new tour (or a final render) without touching code.
const TOUR_URL_DEFAULT = 'https://kuula.co/share/collection/7M8zV?logo=1&info=0&fs=1&vr=1&sd=1&initload=0&thumbs=1';
const TourContext = React.createContext(TOUR_URL_DEFAULT);
function useTourUrl() {return React.useContext(TourContext);}

function VirtualTour({ palette, label = 'Interactive walkthrough · Ridge unit', caption = 'Preliminary rendering · drag to look around', url }) {
  const [engaged, setEngaged] = React.useState(false);
  const ctxUrl = useTourUrl();
  const src = url || ctxUrl;
  const C = palette;
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%', background: C.ink, overflow: 'hidden' }}>
      {/* The iframe itself — always mounted so the tour loads once, but pointer-events are gated until engaged */}
      <iframe
        src={src}
        title={label}
        allow="xr-spatial-tracking; gyroscope; accelerometer; fullscreen"
        allowFullScreen
        style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 0, background: C.ink, pointerEvents: engaged ? 'auto' : 'none', filter: engaged ? 'none' : 'brightness(0.55) saturate(0.85)' }} />
      
      {/* Chrome overlay — hides once engaged so nothing fights the tour UI */}
      {!engaged &&
      <>
          <div style={{ position: 'absolute', top: 20, left: 20, right: 20, display: 'flex', justifyContent: 'space-between', color: C.bg, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2.5, textTransform: 'uppercase', pointerEvents: 'none' }}>
            <span>● 360° · KUULA</span>
            <span style={{ opacity: 0.75 }}>RIDGE UNIT · INT → BALCONY</span>
          </div>
          <button onClick={() => setEngaged(true)} style={{ position: 'absolute', inset: 0, margin: 'auto', width: 140, height: 140, borderRadius: '50%', border: `1px solid ${C.bg}`, background: 'rgba(245,240,230,0.08)', backdropFilter: 'blur(6px)', color: C.bg, cursor: 'pointer', display: 'grid', placeItems: 'center', gap: 6 }}>
            <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.4">
              <circle cx="12" cy="12" r="9" />
              <ellipse cx="12" cy="12" rx="9" ry="4" />
              <path d="M3 12 h18" />
            </svg>
            <div style={{ fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2.5, textTransform: 'uppercase', opacity: 0.9 }}>Enter tour</div>
          </button>
          <div style={{ position: 'absolute', bottom: 20, left: 20, right: 20, color: C.bg, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', pointerEvents: 'none' }}>
            <span>{label}</span>
            <span style={{ opacity: 0.7 }}>{caption}</span>
          </div>
        </>
      }
      {/* Re-show chrome toggle — small corner tab when engaged */}
      {engaged &&
      <button onClick={() => setEngaged(false)} style={{ position: 'absolute', top: 12, right: 12, padding: '6px 10px', background: 'rgba(10,8,5,0.55)', color: C.bg, border: `1px solid rgba(245,240,230,0.35)`, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, textTransform: 'uppercase', cursor: 'pointer', zIndex: 2 }}>Exit</button>
      }
    </div>);

}

// ─── FLYOVER / VIDEO PLACEHOLDER ────────────────────────────────────
// Retained as a fallback for when no tour URL is available — e.g. if the
// tweak is cleared. Used by V3 Quiet Modern as a static hero strip.
function FlyoverPlaceholder({ palette, label = 'Flyover · Ridge unit to patio', src }) {
  const [playing, setPlaying] = React.useState(false);
  const C = palette;
  return (
    <div style={{ position: 'relative', width: '100%', height: '100%', background: C.ink, overflow: 'hidden' }}>
      <img src={src || IMG.aerialBackA} alt="" style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover', opacity: 0.8, filter: playing ? 'none' : 'brightness(0.65)' }} />
      {/* timecode overlay */}
      <div style={{ position: 'absolute', top: 20, left: 20, right: 20, display: 'flex', justifyContent: 'space-between', color: C.bg, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2.5, textTransform: 'uppercase' }}>
        <span>● REC · 00:00:00:00</span>
        <span style={{ opacity: 0.7 }}>DRONE A / RIDGE UNIT / INT→EXT</span>
      </div>
      {/* center play */}
      <button onClick={() => setPlaying(!playing)} style={{ position: 'absolute', inset: 0, margin: 'auto', width: 120, height: 120, borderRadius: '50%', border: `1px solid ${C.bg}`, background: 'rgba(245,240,230,0.08)', backdropFilter: 'blur(6px)', color: C.bg, cursor: 'pointer', display: 'grid', placeItems: 'center' }}>
        <svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor"><polygon points="6,4 20,12 6,20" /></svg>
      </button>
      {/* bottom caption */}
      <div style={{ position: 'absolute', bottom: 20, left: 20, right: 20, color: C.bg, fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between' }}>
        <span>{label}</span>
        <span style={{ opacity: 0.7 }}>Preview · arrives April</span>
      </div>
      {/* scanlines to feel "not yet real" */}
      <div style={{ position: 'absolute', inset: 0, backgroundImage: `repeating-linear-gradient(0deg, rgba(0,0,0,0) 0px, rgba(0,0,0,0) 2px, rgba(0,0,0,0.12) 2px, rgba(0,0,0,0.12) 3px)`, pointerEvents: 'none', mixBlendMode: 'multiply' }} />
    </div>);

}

// ─── 2D / 3D FLOOR PLAN TOGGLE ──────────────────────────────────────
// 2D mode = schematic SVG drawn below. 3D mode = real isometric rendering
// from the architect, with an Upper / Lower switcher for the walkout plan.
function FloorPlan2D3D({ variant = 0, C, mode = '2D' }) {
  const [level, setLevel] = React.useState('upper'); // for variant 1 (walkout) in 3D mode
  const monoFamily = 'Geist Mono, ui-monospace, monospace';

  if (mode === '3D') {
    // 2BR — single rendering
    if (variant === 0) {
      return (
        <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden', background: C.bgAlt || C.sand }}>
          <img src={IMG.plan2br3d} alt="2BR plan, isometric" style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />
          <div style={{ position: 'absolute', top: 12, left: 12, padding: '5px 9px', background: C.bg, fontFamily: monoFamily, fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', border: `1px solid ${C.line}` }}>2BR</div>
        </div>);

    }
    // 3BR walkout — two-level toggle
    const isUpper = level === 'upper';
    return (
      <div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden', background: C.bgAlt || C.sand }}>
        <img src={isUpper ? IMG.plan3brUpper3d : IMG.plan3brLower3d} alt={`3BR walkout · ${level} level`} style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} />
        <div style={{ position: 'absolute', top: 12, left: 12, padding: '5px 9px', background: C.bg, fontFamily: monoFamily, fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', border: `1px solid ${C.line}` }}>3BR</div>
        <div style={{ position: 'absolute', bottom: 12, right: 12, display: 'inline-flex', background: C.bg, border: `1px solid ${C.line}`, padding: 2 }}>
          {['upper', 'lower'].map((lv) =>
          <button key={lv} onClick={(e) => {e.preventDefault();setLevel(lv);}} style={{ padding: '7px 12px', background: level === lv ? C.ink : 'transparent', color: level === lv ? C.bg : C.ink, border: 'none', fontFamily: monoFamily, fontSize: 18, letterSpacing: 2, textTransform: 'uppercase', cursor: 'pointer', borderRadius: 0 }}>{lv}</button>
          )}
        </div>
      </div>);

  }
  // 2D top-down schematic
  const draw2D = (v) => {
    if (v === 0) {
      return (
        <svg viewBox="0 0 400 280" style={{ width: '100%', height: '100%' }}>
          <rect x="20" y="20" width="360" height="240" fill="none" stroke={C.ink} strokeWidth="1.5" />
          <rect x="20" y="20" width="220" height="160" fill="none" stroke={C.ink} strokeWidth="0.5" />
          <text x="40" y="50" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.muted}>LIVING · KITCHEN</text>
          <rect x="140" y="100" width="70" height="18" fill={C.ink} opacity="0.15" />
          <rect x="240" y="20" width="140" height="100" fill="none" stroke={C.ink} strokeWidth="0.5" />
          <text x="258" y="50" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.muted}>BEDROOM 1</text>
          <rect x="240" y="120" width="140" height="80" fill="none" stroke={C.ink} strokeWidth="0.5" />
          <text x="258" y="148" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.muted}>BEDROOM 2</text>
          <rect x="20" y="180" width="220" height="80" fill="none" stroke={C.ink} strokeWidth="0.5" />
          <text x="40" y="208" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.muted}>BATH · UTILITY</text>
          <rect x="160" y="200" width="80" height="60" fill={C.ink} opacity="0.08" />
          <rect x="240" y="200" width="140" height="60" fill={C.ink} opacity="0.05" stroke={C.ink} strokeWidth="0.3" strokeDasharray="2 2" />
          <text x="260" y="236" fontFamily="Geist Mono, monospace" fontSize="9" letterSpacing="1.5" fill={C.muted}>BALCONY ↓ LAKE</text>
        </svg>);

    }
    // variant 1: 4BR combined walkout
    return (
      <svg viewBox="0 0 400 280" style={{ width: '100%', height: '100%' }}>
        <rect x="10" y="20" width="380" height="240" fill="none" stroke={C.ink} strokeWidth="1.5" />
        <line x1="200" y1="20" x2="200" y2="260" stroke={C.ink} strokeWidth="0.5" strokeDasharray="3 3" />
        <rect x="10" y="20" width="190" height="140" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="24" y="46" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>GREAT ROOM A</text>
        <rect x="80" y="90" width="60" height="14" fill={C.ink} opacity="0.15" />
        <rect x="10" y="160" width="95" height="100" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="22" y="184" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>BED 1</text>
        <rect x="105" y="160" width="95" height="100" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="117" y="184" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>BED 2</text>
        <rect x="200" y="20" width="190" height="140" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="214" y="46" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>GREAT ROOM B</text>
        <rect x="260" y="90" width="60" height="14" fill={C.ink} opacity="0.15" />
        <rect x="200" y="160" width="95" height="100" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="212" y="184" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>BED 3</text>
        <rect x="295" y="160" width="95" height="100" fill="none" stroke={C.ink} strokeWidth="0.5" />
        <text x="307" y="184" fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.5" fill={C.muted}>BED 4</text>
        <rect x="180" y="120" width="40" height="30" fill={C.accent} opacity="0.15" />
        <text x="200" y="140" textAnchor="middle" fontFamily="Geist Mono, monospace" fontSize="7" letterSpacing="1" fill={C.accent}>LINK</text>
      </svg>);

  };

  return draw2D(variant);
}

// ─── RESERVATION MODAL — routes to the active listing broker ────────
function ReservationModal({ open, onClose, palette, accentLabel = 'Dune Ridge at Betsie Bay' }) {
  const BROKER = useBroker();
  const [step, setStep] = React.useState(0);
  const [form, setForm] = React.useState({ name: '', email: '', phone: '', interest: 'The Ridge · 2BR', date: '', notes: '' });
  const C = palette;
  if (!open) return null;
  const submit = (e) => {e.preventDefault();setStep(2);};
  const first = firstNameOf(BROKER.name);
  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(10,8,5,0.7)', backdropFilter: 'blur(8px)', zIndex: 9999, display: 'grid', placeItems: 'center', padding: 40 }}>
      <div onClick={(e) => e.stopPropagation()} style={{ background: C.bg, color: C.ink, maxWidth: 680, width: '100%', padding: 56, border: `1px solid ${C.line}`, position: 'relative' }}>
        <button onClick={onClose} style={{ position: 'absolute', top: 20, right: 20, background: 'none', border: 'none', color: C.ink, fontSize: 22, cursor: 'pointer' }}>×</button>
        <div style={{ fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2.5, color: C.muted, textTransform: 'uppercase', marginBottom: 12 }}>{accentLabel} · Reservations open · Construction Summer 2026</div>
        {step === 0 &&
        <>
            <h2 style={{ fontSize: 44, fontWeight: 300, letterSpacing: -1, margin: '0 0 8px', lineHeight: 1.05, fontFamily: 'inherit' }}>Reserve your visit.</h2>
            <p style={{ color: C.muted, marginBottom: 24, maxWidth: 480 }}>Inquiries route directly to {BROKER.name}, our listing broker at {BROKER.firm}. {first === 'your broker' ? 'They' : first} will confirm within one business day.</p>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 20 }}>
              <Field label="Name" value={form.name} onChange={(v) => setForm({ ...form, name: v })} C={C} />
              <Field label="Email" value={form.email} onChange={(v) => setForm({ ...form, email: v })} C={C} type="email" />
              <Field label="Phone" value={form.phone} onChange={(v) => setForm({ ...form, phone: v })} C={C} />
              <Field label="Preferred date" value={form.date} onChange={(v) => setForm({ ...form, date: v })} C={C} type="date" />
            </div>
            <div style={{ marginBottom: 24 }}>
              <div style={{ fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', marginBottom: 10 }}>Interest</div>
              <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                {['The Lake · waterfront', 'The Ridge · 2BR', 'The Ridge · 3BR walkout', 'The Trail · trail-side', 'Undecided'].map((x) =>
              <button key={x} onClick={() => setForm({ ...form, interest: x })} style={{ padding: '12px 14px', border: `1px solid ${form.interest === x ? C.ink : C.line}`, background: form.interest === x ? C.ink : 'transparent', color: form.interest === x ? C.bg : C.ink, cursor: 'pointer', fontSize: 18, letterSpacing: 0.5, borderRadius: 0, fontFamily: 'Geist Mono, monospace', textTransform: 'uppercase' }}>{x}</button>
              )}
              </div>
            </div>
            <button onClick={submit} style={{ width: '100%', padding: '18px', background: C.ink, color: C.bg, border: 'none', cursor: 'pointer', fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 3, textTransform: 'uppercase', borderRadius: 0 }}>Send to {first} →</button>
          </>
        }
        {step === 2 &&
        <>
            <h2 style={{ fontSize: 44, fontWeight: 300, letterSpacing: -1, margin: '0 0 12px', lineHeight: 1.05, fontFamily: 'inherit' }}>Sent to {first}.</h2>
            <p style={{ color: C.muted, marginBottom: 32, maxWidth: 480 }}>{first} will reach out within one business day to confirm {form.date || 'your visit'}. Watch for a note from {BROKER.email}.</p>
            <button onClick={onClose} style={{ padding: '16px 28px', background: 'transparent', color: C.ink, border: `1px solid ${C.ink}`, cursor: 'pointer', fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 3, textTransform: 'uppercase', borderRadius: 0 }}>Close</button>
          </>
        }
      </div>
    </div>);

}

function Field({ label, value, onChange, C, type = 'text' }) {
  return (
    <label style={{ display: 'block' }}>
      <div style={{ fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', marginBottom: 6 }}>{label}</div>
      <input type={type} value={value} onChange={(e) => onChange(e.target.value)} style={{ width: '100%', background: 'transparent', border: 'none', borderBottom: `1px solid ${C.line}`, padding: '10px 0', color: C.ink, fontSize: 18, outline: 'none', fontFamily: 'inherit' }} />
    </label>);

}

// ─── BROKER CARD ───────────────────────────────────────────────────
// Reads from BrokerContext — swap the value via TweaksPanel to re-skin
// every mention across all three site variations at once.
function BrokerCard({ palette, displayFont, bodyFont, monoFont, style = {} }) {
  const BROKER = useBroker();
  const C = palette;
  return (
    <div style={{ background: C.bg, border: `1px solid ${C.line}`, padding: 32, display: 'flex', gap: 24, alignItems: 'flex-start', ...style }}>
      <div style={{ flex: 1 }}>
        <div style={{ fontFamily: monoFont, fontSize: 18, letterSpacing: 2.5, textTransform: 'uppercase', color: C.muted, marginBottom: 8 }}>Your direct contact</div>
        <div style={{ fontFamily: displayFont, fontSize: 28, fontWeight: 400, letterSpacing: -0.5, lineHeight: 1.1, color: C.ink }}>{BROKER.name}</div>
        <div style={{ fontFamily: bodyFont, fontSize: 18, color: C.muted, marginTop: 4, marginBottom: 16 }}>{BROKER.role}</div>
        <div style={{ display: 'grid', gap: 6, fontFamily: bodyFont, fontSize: 18, color: C.ink }}>
          <a href={`mailto:${BROKER.email}`} style={{ color: 'inherit', textDecoration: 'none', borderBottom: `1px solid ${C.line}`, paddingBottom: 4 }}>{BROKER.email}</a>
          <a href={`tel:${BROKER.phone.replace(/\D/g, '')}`} style={{ color: 'inherit', textDecoration: 'none', borderBottom: `1px solid ${C.line}`, paddingBottom: 4 }}>{BROKER.phone}</a>
        </div>
        <div style={{ fontFamily: monoFont, fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', marginTop: 14 }}>{BROKER.firm}</div>
      </div>
    </div>);

}

// ─── PHASE STATUS BAR ──────────────────────────────────────────────
// Small strip showing all three releases + reservation counts.
function PhaseBar({ palette, displayFont, monoFont }) {
  const C = palette;
  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', border: `1px solid ${C.line}`, background: C.bg }}>
      {BUILDINGS.map((b, i) => {
        const dotColor = b.status === 'available' ? '#3f8a4f' : b.status === 'reserved' ? C.muted : C.accent;
        return (
          <div key={b.id} style={{ padding: '20px 24px', borderLeft: i === 0 ? 'none' : `1px solid ${C.line}` }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
              <span style={{ width: 7, height: 7, borderRadius: '50%', background: dotColor }} />
              <span style={{ fontFamily: monoFont, fontSize: 18, letterSpacing: 2, color: C.muted, textTransform: 'uppercase' }}>{b.statusLabel}</span>
            </div>
            <div style={{ fontFamily: displayFont, fontSize: 26, fontWeight: 400, letterSpacing: -0.5, color: C.ink, lineHeight: 1.1 }}>{b.name}</div>
            <div style={{ fontFamily: monoFont, fontSize: 18, letterSpacing: 1.5, color: C.muted, marginTop: 6, textTransform: 'uppercase' }}>{b.units} res · {b.orientation}</div>
          </div>);

      })}
    </div>);

}

// ─── SMALL UI BITS ─────────────────────────────────────────────────
function Divider({ color, label }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 16, color }}>
      <div style={{ flex: 1, height: 1, background: color, opacity: 0.3 }} />
      {label && <span style={{ fontFamily: 'Geist Mono, monospace', fontSize: 18, letterSpacing: 3, textTransform: 'uppercase' }}>{label}</span>}
      <div style={{ flex: 1, height: 1, background: color, opacity: 0.3 }} />
    </div>);

}

// Export
Object.assign(window, {
  IMG, HERO_TIMES, GALLERY, BRAND, LogoMark, BROKER_DEFAULT, BrokerContext, useBroker, initialsOf, firstNameOf, BUILDINGS,
  TOUR_URL_DEFAULT, TourContext, useTourUrl, VirtualTour,
  useInView, CountUp, useScrollY, useParallax,
  SitePlan, useLightbox, ReservationModal, Field, Divider,
  FlyoverPlaceholder, FloorPlan2D3D, BrokerCard, PhaseBar
});