// 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 = {
  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',
};

// Hero time-of-day sets
const HERO_TIMES = [
  { key: 'morning', label: 'Morning', src: IMG.aerialBayA, caption: 'Morning, Betsy Lake' },
  { 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' },
];

// ─── 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',
  email: 'kari.king@century21northland.com',
  phone: '(231) 555-0142',
  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 buildings, each a distinct "phase" with its own availability state
const BUILDINGS = [
  {
    id: 'R',
    name: 'The Ridge',
    status: 'available',           // now the primary sell
    statusLabel: 'Now available',
    units: 6,
    reserved: 4,
    floors: 3,
    orientation: 'Upland · West-facing',
    tagline: 'On the bluff, looking out to Lake Michigan.',
    priceFrom: 'Low $900s',
    description: 'Six residences stepped up the ridge with the development\'s finest views. Walkout lower levels, vaulted top-floor ceilings, and private balconies facing the lake at sunset.',
  },
  {
    id: 'B',
    name: 'The Bay',
    status: 'next',
    statusLabel: 'Next release · Fall 2026',
    units: 12,
    reserved: 0,
    floors: 3,
    orientation: 'Mid-slope · Bay view',
    tagline: 'Elevated bay views, a short walk to the dock.',
    priceFrom: 'Low $700s',
    description: 'Twelve residences set along the mid-slope with south-facing balconies over Betsy Lake and the marina. Final floor plans confirmed after pricing lock, March 2026.',
  },
  {
    id: 'W',
    name: 'Waterfront',
    status: 'reserved',
    statusLabel: '12 of 12 reserved',
    units: 12,
    reserved: 12,
    floors: 2,
    orientation: 'Lakeside · East-facing',
    tagline: 'Dock-adjacent. Delivery to follow permitting.',
    priceFrom: 'By request',
    description: 'The original release — all twelve residences reserved in eight days. Delivery schedule pending final wetland delineation and EGLE permit, expected mid-2027.',
  },
];

// ─── 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');
  // Note: on plan, waterfront is closest to water, bay mid, ridge furthest up the hill.
  const buildings = [
    { id: 'W', x: 180, y: 400, w: 320, h: 46, label: 'Waterfront', meta: BUILDINGS.find((b) => b.id === 'W') },
    { id: 'B', x: 260, y: 300, w: 320, h: 52, label: 'The Bay',   meta: BUILDINGS.find((b) => b.id === 'B') },
    { 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) => {
    if (status === 'available') return C.ink;
    if (status === 'reserved') return C.muted;
    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}>M22 BIKE 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: 10, 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: 15, color: C.muted, marginBottom: 20, fontStyle: 'italic' }}>{act.meta.tagline}</div>
        <p style={{ fontSize: 14, 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: 10, 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: 13 }}>
      <span style={{ color: C.muted, fontFamily: 'Geist Mono, monospace', letterSpacing: 1.5, textTransform: 'uppercase', fontSize: 10 }}>{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: 12, 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: 10, 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: 9, 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: 10, 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: 9, 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: 10, 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: 10, 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 ──────────────────────────────────────
// Mirrors the Summit at Bay Harbor pattern Cam/Chad referenced — switch between flat 2D and isometric 3D.
function FloorPlan2D3D({ variant = 0, C, mode = '2D' }) {
  // 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>
    );
  };

  // 3D isometric
  const draw3D = (v) => {
    // Isometric projection helpers
    const iso = (x, y, z = 0) => {
      const ix = (x - y) * 0.866;
      const iy = (x + y) * 0.5 - z;
      return `${ix + 200},${iy + 120}`;
    };
    if (v === 0) {
      return (
        <svg viewBox="0 0 400 280" style={{ width: '100%', height: '100%' }}>
          {/* floor */}
          <polygon points={`${iso(0,0)} ${iso(240,0)} ${iso(240,140)} ${iso(0,140)}`} fill={C.bgAlt || C.sand} stroke={C.ink} strokeWidth="0.8" opacity="0.6" />
          {/* interior walls (low-height) */}
          <g stroke={C.ink} strokeWidth="1" fill="none">
            {/* living-kitchen wall */}
            <polygon points={`${iso(140,0)} ${iso(140,0,18)} ${iso(140,90,18)} ${iso(140,90)}`} fill={C.bg} />
            <polygon points={`${iso(0,90)} ${iso(0,90,18)} ${iso(140,90,18)} ${iso(140,90)}`} fill={C.bg} />
            {/* bedroom separator */}
            <polygon points={`${iso(140,60)} ${iso(140,60,18)} ${iso(240,60,18)} ${iso(240,60)}`} fill={C.bg} />
          </g>
          {/* island */}
          <polygon points={`${iso(60,30)} ${iso(100,30)} ${iso(100,50)} ${iso(60,50)}`} fill={C.ink} opacity="0.35" />
          {/* beds */}
          <polygon points={`${iso(160,10)} ${iso(220,10)} ${iso(220,50)} ${iso(160,50)}`} fill={C.ink} opacity="0.12" />
          <polygon points={`${iso(160,70)} ${iso(220,70)} ${iso(220,120)} ${iso(160,120)}`} fill={C.ink} opacity="0.12" />
          {/* balcony */}
          <polygon points={`${iso(140,100)} ${iso(240,100)} ${iso(240,140)} ${iso(140,140)}`} fill={C.accent} opacity="0.22" stroke={C.ink} strokeWidth="0.3" strokeDasharray="3 2" />
          {/* outer walls (taller) */}
          <g stroke={C.ink} strokeWidth="1.2" fill={C.bg} opacity="0.95">
            <polygon points={`${iso(0,0)} ${iso(0,0,32)} ${iso(240,0,32)} ${iso(240,0)}`} />
            <polygon points={`${iso(0,0)} ${iso(0,0,32)} ${iso(0,140,32)} ${iso(0,140)}`} />
          </g>
          {/* labels */}
          <text x={iso(40, 40).split(',')[0]} y={iso(40, 40).split(',')[1]} fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.2" fill={C.muted}>LIVING</text>
          <text x={iso(180, 30).split(',')[0] - 10} y={iso(180, 30).split(',')[1]} fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.2" fill={C.muted}>BED 1</text>
          <text x={iso(180, 90).split(',')[0] - 10} y={iso(180, 90).split(',')[1]} fontFamily="Geist Mono, monospace" fontSize="8" letterSpacing="1.2" fill={C.muted}>BED 2</text>
        </svg>
      );
    }
    // v1 — 4BR combined walkout, simplified
    return (
      <svg viewBox="0 0 400 280" style={{ width: '100%', height: '100%' }}>
        <polygon points={`${iso(0,0)} ${iso(300,0)} ${iso(300,160)} ${iso(0,160)}`} fill={C.bgAlt || C.sand} stroke={C.ink} strokeWidth="0.8" opacity="0.6" />
        <g stroke={C.ink} strokeWidth="1" fill="none">
          <polygon points={`${iso(150,0)} ${iso(150,0,18)} ${iso(150,160,18)} ${iso(150,160)}`} fill={C.bg} />
        </g>
        <polygon points={`${iso(30,30)} ${iso(110,30)} ${iso(110,60)} ${iso(30,60)}`} fill={C.ink} opacity="0.12" />
        <polygon points={`${iso(170,30)} ${iso(250,30)} ${iso(250,60)} ${iso(170,60)}`} fill={C.ink} opacity="0.12" />
        <polygon points={`${iso(30,100)} ${iso(110,100)} ${iso(110,140)} ${iso(30,140)}`} fill={C.ink} opacity="0.12" />
        <polygon points={`${iso(170,100)} ${iso(250,100)} ${iso(250,140)} ${iso(170,140)}`} fill={C.ink} opacity="0.12" />
        <polygon points={`${iso(130,70)} ${iso(170,70)} ${iso(170,90)} ${iso(130,90)}`} fill={C.accent} opacity="0.3" />
        <g stroke={C.ink} strokeWidth="1.2" fill={C.bg} opacity="0.95">
          <polygon points={`${iso(0,0)} ${iso(0,0,32)} ${iso(300,0,32)} ${iso(300,0)}`} />
          <polygon points={`${iso(0,0)} ${iso(0,0,32)} ${iso(0,160,32)} ${iso(0,160)}`} />
        </g>
        <text x={iso(130,78).split(',')[0] - 10} y={iso(130,78).split(',')[1]} fontFamily="Geist Mono, monospace" fontSize="7" letterSpacing="1" fill={C.accent}>LINK</text>
      </svg>
    );
  };

  return mode === '3D' ? draw3D(variant) : draw2D(variant);
}

// ─── RESERVATION MODAL — routes to the active listing broker ────────
function ReservationModal({ open, onClose, palette, accentLabel = 'Dune Ridge' }) {
  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: 11, letterSpacing: 2.5, color: C.muted, textTransform: 'uppercase', marginBottom: 12 }}>{accentLabel} · The Ridge · 4 of 6 reserved</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: 11, letterSpacing: 2, color: C.muted, textTransform: 'uppercase', marginBottom: 10 }}>Interest</div>
              <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                {['The Ridge · 2BR', 'The Ridge · walkout', 'Combined 4BR', 'Waitlist · Bay', '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: 12, 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: 12, 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: 12, 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: 10, 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: 15, 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 }}>
      {/* initials portrait placeholder — real headshot to be supplied */}
      <div style={{ width: 96, height: 120, background: C.bgAlt || C.sand, border: `1px solid ${C.line}`, display: 'grid', placeItems: 'center', flexShrink: 0, position: 'relative' }}>
        <span style={{ fontFamily: displayFont, fontSize: 42, fontWeight: 300, color: C.ink, letterSpacing: -1 }}>{initialsOf(BROKER.name)}</span>
        <span style={{ position: 'absolute', bottom: 6, fontFamily: monoFont, fontSize: 8, letterSpacing: 2, color: C.muted, textTransform: 'uppercase' }}>Portrait</span>
      </div>
      <div style={{ flex: 1 }}>
        <div style={{ fontFamily: monoFont, fontSize: 10, 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: 13, color: C.muted, marginTop: 4, marginBottom: 16 }}>{BROKER.role}</div>
        <div style={{ display: 'grid', gap: 6, fontFamily: bodyFont, fontSize: 14, 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: 10, 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: 10, 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: 10, 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: 10, 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, 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,
});
