// Direction 1 — Search, Listing, Booking, Upsells, Dashboard, Admin screens.

function D1Search({ p, serif, sans, go, properties }) {
  const [view, setView] = React.useState('grid');
  const searchCtx = (window.DYRA && window.DYRA._search) || {};
  const guestTotal = searchCtx.guests
    ? (searchCtx.guests.adults || 0) + (searchCtx.guests.kids || 0)
    : 0;
  const all = properties || (window.DYRA && window.DYRA.PROPERTIES) || [];
  const props = all.filter(x => {
    if (guestTotal && x.sleeps < guestTotal) return false;
    return true;
  });

  return (
    <div style={{ padding: '32px 48px 64px' }}>
      <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 32 }}>
        <div style={{ fontSize: 13, color: p.muted }}>
          {guestTotal
            ? <>Showing homes that sleep <strong style={{ color: p.ink }}>{guestTotal}+ guests</strong> · {props.length} available</>
            : <>{props.length} homes in Crown Heights</>}
        </div>
        <div style={{ display: 'flex', gap: 8, fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase' }}>
          {['grid', 'list', 'map'].map(v => (
            <button key={v} onClick={() => setView(v)} style={{
              background: view === v ? p.ink : 'transparent', color: view === v ? p.bg : p.muted,
              border: `1px solid ${view === v ? p.ink : p.line}`, padding: '6px 14px', borderRadius: 999, cursor: 'pointer',
              fontFamily: 'inherit', fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase',
            }}>{v}</button>
          ))}
        </div>
      </div>

      {view === 'map' ? (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1.3fr', gap: 24, height: 600 }}>
          <div style={{ overflow: 'auto', display: 'flex', flexDirection: 'column', gap: 14 }}>
            {props.map(prop => <D1SearchRow key={prop.id} prop={prop} p={p} serif={serif} onClick={() => go('listing', prop.id)}/>)}
          </div>
          <div style={{ position: 'relative', display: 'flex', flexDirection: 'column', gap: 8 }}>
            {window.DyraListingsMap ? (
              <window.DyraListingsMap
                listings={props}
                palette={{ ink: p.ink, accent: p.ink, surface: p.card, muted: p.muted, line: p.line }}
                height={580}
                onSelect={(prop) => go('listing', prop.id)}
              />
            ) : (
              <div style={{ height: 580, borderRadius: 8, border: `1px solid ${p.line}`, background: p.card, display: 'flex', alignItems: 'center', justifyContent: 'center', color: p.muted, fontSize: 13 }}>
                Loading map…
              </div>
            )}
            <div style={{ fontSize: 11, color: p.muted, textAlign: 'center' }}>
              Pins show approximate locations within ~25m to protect host privacy. Exact addresses are shared after booking.
            </div>
          </div>
        </div>
      ) : view === 'list' ? (
        <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
          {props.map(prop => <D1SearchRow key={prop.id} prop={prop} p={p} serif={serif} onClick={() => go('listing', prop.id)} wide/>)}
        </div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 28 }}>
          {props.map(prop => <D1FeaturedCard key={prop.id} prop={prop} p={p} serif={serif} cardStyle="editorial" onClick={() => go('listing', prop.id)}/>)}
        </div>
      )}
    </div>
  );
}

function FilterPill({ p, label, value, options, display, onChange }) {
  const [open, setOpen] = React.useState(false);
  return (
    <div style={{ position: 'relative' }}>
      <button onClick={() => setOpen(!open)} style={{
        background: p.card, border: `1px solid ${p.line}`, padding: '8px 16px', borderRadius: 999,
        fontSize: 12, cursor: 'pointer', display: 'flex', gap: 8, alignItems: 'center',
      }}>
        <span style={{ color: p.muted }}>{label}</span>
        <span>{display ? display(value) : value}</span>
        <span style={{ color: p.muted, fontSize: 10 }}>▾</span>
      </button>
      {open && (
        <div style={{ position: 'absolute', top: 'calc(100% + 6px)', left: 0, background: p.card, border: `1px solid ${p.line}`, borderRadius: 6, padding: 6, zIndex: 10, minWidth: 140 }}>
          {options.map(o => (
            <div key={o} onClick={() => { onChange(o); setOpen(false); }} style={{
              padding: '6px 10px', borderRadius: 4, cursor: 'pointer', fontSize: 12,
              background: value === o ? p.bg : 'transparent',
            }}>{display ? display(o) : o}</div>
          ))}
        </div>
      )}
    </div>
  );
}

function Toggle({ p, label, value, onChange }) {
  return (
    <button onClick={() => onChange && onChange(!value)} style={{
      background: value ? p.ink : p.card, color: value ? p.bg : p.ink,
      border: `1px solid ${value ? p.ink : p.line}`, padding: '8px 14px', borderRadius: 999,
      fontSize: 12, cursor: 'pointer', display: 'flex', gap: 6, alignItems: 'center',
    }}>
      <Ico name={value ? 'check' : 'plus'} size={12}/> {label}
    </button>
  );
}

function D1SearchRow({ prop, p, serif, onClick, wide }) {
  const tones = { luxury: 'clay', premium: 'cream', standard: 'warm', value: 'ivory' };
  return (
    <div onClick={onClick} style={{
      display: 'grid', gridTemplateColumns: wide ? '280px 1fr auto' : '140px 1fr auto', gap: 20,
      padding: wide ? 16 : 12, border: `1px solid ${p.line}`, borderRadius: 8, background: p.card, cursor: 'pointer',
    }}>
      <Placeholder src={prop.photos && prop.photos[0] && prop.photos[0].src} objectPosition={(prop.photos && prop.photos[0] && prop.photos[0].pos) || 'center'} label={prop.hero} tone={tones[prop.tier]} density="tight" style={{ aspectRatio: wide ? '4/3' : '1', borderRadius: 4 }}/>
      <div>
        <div style={{ fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase', color: p.accent, marginBottom: 6 }}>
          {prop.cardEyebrow || `${prop.tier} · ${prop.walkMinTo770} min walk to 770`}
        </div>
        <h3 style={{ fontFamily: serif, fontSize: wide ? 26 : 20, fontWeight: 500, margin: '0 0 4px', letterSpacing: '-0.01em' }}>{prop.name}</h3>
        <div style={{ fontSize: 13, color: p.muted, marginBottom: wide ? 12 : 6 }}>{prop.tagline} · Sleeps {prop.sleeps}</div>
        {wide && (
          <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
            {prop.shabbos.slice(0, 4).map((s, i) => (
              <span key={i} style={{ fontSize: 10, color: p.muted, background: p.bg, padding: '3px 8px', borderRadius: 999, border: `1px solid ${p.line}` }}>{s}</span>
            ))}
          </div>
        )}
      </div>
      <div style={{ textAlign: 'right' }}>
        <div style={{ fontFamily: serif, fontSize: wide ? 28 : 22, fontWeight: 500 }}>${prop.priceFrom}</div>
        <div style={{ fontSize: 11, color: p.muted }}>per night</div>
      </div>
    </div>
  );
}

// ─────── LISTING ───────
// Live add-on services for the customer checkout. Sourced from
// window.DYRA.UPSELLS, which the live data shim (/api/m/data) populates
// from the upsells table (supabase/migrations/0005_upsells.sql). Falls
// back to a tiny seed list if the page renders before the shim has run.
const UPSELL_FALLBACK = [
  { id: 'fallback-shab',   n: 'Shabbos Classic', price: 185, perNight: false },
  { id: 'fallback-pantry', n: 'Pantry Starter',  price: 95,  perNight: false },
];
const getUpsellItems = () => {
  const raw = (typeof window !== 'undefined' && window.DYRA && Array.isArray(window.DYRA.UPSELLS))
    ? window.DYRA.UPSELLS
    : null;
  if (!raw || raw.length === 0) return UPSELL_FALLBACK;
  return raw.map(u => ({
    id: u.id,
    n: u.n || u.name,
    price: u.price ?? u.priceFrom ?? 0,
    perNight: !!u.perNight,
    variants: Array.isArray(u.variants) && u.variants.length
      ? u.variants.map(v => ({ id: String(v.id), label: String(v.label), price: Number(v.price) }))
      : null,
  }));
};
const upsellUnitPrice = (u, sel) => {
  if (!u.variants) return u.price;
  const v = u.variants.find(x => x.id === sel) || u.variants[0];
  return v.price;
};
const upsellCost = (u, sel, nights) => {
  const unit = upsellUnitPrice(u, sel);
  return u.perNight ? unit * nights : unit;
};
// Scan forward from today for the earliest run of `length` consecutive
// open nights for this property. Considers admin/owner blockedRanges plus
// any confirmed reservations the store has loaded. Falls back to today +
// length nights if nothing is bookable within the horizon (which would only
// happen on a fully booked calendar).
function findFirstAvailableStay(property, length) {
  if (!property) return null;
  const today = new Date(); today.setHours(0, 0, 0, 0);
  const blocked = new Set();
  (property.blockedRanges || []).forEach(r => {
    if (!r || !r.start || !r.end) return;
    const a = new Date(r.start + 'T00:00:00');
    const b = new Date(r.end + 'T00:00:00');
    for (let d = new Date(a); d < b; d.setDate(d.getDate() + 1)) {
      blocked.add(d.toISOString().slice(0, 10));
    }
  });
  if (typeof window !== 'undefined' && window.DyraStore && window.DyraStore.blockedDateSet) {
    try {
      window.DyraStore.blockedDateSet(property.id).forEach(d => blocked.add(d));
    } catch (e) { /* store not ready — fall through with just blockedRanges */ }
  }
  const toISO = (d) => d.toISOString().slice(0, 10);
  for (let i = 0; i < 365; i++) {
    const start = new Date(today); start.setDate(start.getDate() + i);
    let ok = true;
    for (let j = 0; j < length; j++) {
      const d = new Date(start); d.setDate(d.getDate() + j);
      if (blocked.has(toISO(d))) { ok = false; break; }
    }
    if (ok) {
      const end = new Date(start); end.setDate(end.getDate() + length);
      return { checkIn: toISO(start), checkOut: toISO(end) };
    }
  }
  const fallbackEnd = new Date(today); fallbackEnd.setDate(fallbackEnd.getDate() + length);
  return { checkIn: toISO(today), checkOut: toISO(fallbackEnd) };
}

function D1Listing({ p, serif, sans, hebFont, heb, property, go }) {
  if (!property) return null;
  const tones = { luxury: 'clay', premium: 'cream', standard: 'warm', value: 'ivory' };
  const [lightbox, setLightbox] = React.useState(null); // index or null
  const upsellItems = React.useMemo(() => getUpsellItems(), []);
  const [addedUpsells, setAddedUpsells] = React.useState({});
  const [upsellVariant, setUpsellVariant] = React.useState({ Housekeeping: '4hr' });
  const [housekeepingDate, setHousekeepingDate] = React.useState('');
  // Default to the first run of `max(3, minNights)` consecutive open nights
  // on/after today. Anything earlier would land the parsha-for-your-stay
  // block on a Shabbos that's already passed.
  const _defaultStay = React.useMemo(
    () => findFirstAvailableStay(property, Math.max(3, property.minNights || 3)),
    [property.id, property.minNights]
  );
  const [checkIn, setCheckIn] = React.useState(_defaultStay.checkIn);
  const [checkOut, setCheckOut] = React.useState(_defaultStay.checkOut);
  const [adults, setAdults] = React.useState(2);
  const [children, setChildren] = React.useState(3);
  const [infants, setInfants] = React.useState(1);
  const [editingDates, setEditingDates] = React.useState(false);
  const [editingGuests, setEditingGuests] = React.useState(false);
  const nights = Math.max(1, Math.round((new Date(checkOut) - new Date(checkIn)) / (1000 * 60 * 60 * 24)));
  const fmtDate = (iso) => {
    const d = new Date(iso + 'T00:00:00');
    return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
  };
  const HEB_MONTH = { 4: 'Nisan', 5: 'Iyar', 6: 'Sivan' };
  const hebForDate = (iso) => {
    // Rough anchor: May 14 2026 = 12 Iyar. Offset ±1 day per day.
    const base = new Date('2026-05-14T00:00:00');
    const delta = Math.round((new Date(iso + 'T00:00:00') - base) / (1000 * 60 * 60 * 24));
    const day = 12 + delta;
    const month = day < 1 ? 'Nisan' : (day > 29 ? 'Sivan' : 'Iyar');
    const displayDay = day < 1 ? 30 + day : (day > 29 ? day - 29 : day);
    return `${displayDay} ${month}`;
  };
  const guestSummary = () => {
    const parts = [];
    parts.push(`${adults} adult${adults === 1 ? '' : 's'}`);
    if (children > 0) parts.push(`${children} child${children === 1 ? '' : 'ren'}`);
    if (infants > 0) parts.push(`${infants} infant${infants === 1 ? '' : 's'}`);
    return parts.join(' · ');
  };
  const hasPhotos = property.photos && property.photos.length > 0;

  // Shabbos lookup: find the Saturday that falls within the stay (checkIn ≤ Sat ≤ checkOut),
  // then return Parsha + candle-lighting + havdalah for that week.
  const SHABBOS_BY_SATURDAY = {
    '2026-04-11': { parsha: 'Shemini',    light: '7:14pm', havdalah: '8:17pm' },
    '2026-04-18': { parsha: 'Tazria-Metzora', light: '7:21pm', havdalah: '8:25pm' },
    '2026-04-25': { parsha: 'Acharei-Kedoshim', light: '7:29pm', havdalah: '8:33pm' },
    '2026-05-02': { parsha: 'Emor',       light: '7:36pm', havdalah: '8:40pm' },
    '2026-05-09': { parsha: 'Behar',      light: '7:43pm', havdalah: '8:48pm' },
    '2026-05-16': { parsha: 'Bechukosai', light: '7:50pm', havdalah: '8:56pm' },
    '2026-05-23': { parsha: 'Bamidbar',   light: '7:56pm', havdalah: '9:03pm' },
    '2026-05-30': { parsha: 'Nasso',      light: '8:02pm', havdalah: '9:09pm' },
    '2026-06-06': { parsha: 'Beha’aloscha', light: '8:06pm', havdalah: '9:14pm' },
    '2026-06-13': { parsha: 'Shelach',    light: '8:10pm', havdalah: '9:18pm' },
    '2026-06-20': { parsha: 'Korach',     light: '8:12pm', havdalah: '9:20pm' },
    '2026-06-27': { parsha: 'Chukas',     light: '8:13pm', havdalah: '9:20pm' },
    '2026-07-04': { parsha: 'Balak',      light: '8:11pm', havdalah: '9:18pm' },
    '2026-07-11': { parsha: 'Pinchas',    light: '8:07pm', havdalah: '9:13pm' },
    '2026-07-18': { parsha: 'Matos-Masei', light: '8:02pm', havdalah: '9:06pm' },
  };
  const shabbosInStay = (() => {
    const start = new Date(checkIn + 'T00:00:00');
    const end   = new Date(checkOut + 'T00:00:00');
    for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
      if (d.getDay() === 6) {
        const iso = d.toISOString().slice(0, 10);
        if (SHABBOS_BY_SATURDAY[iso]) return { iso, ...SHABBOS_BY_SATURDAY[iso] };
      }
    }
    return null;
  })();

  React.useEffect(() => {
    if (lightbox === null) return;
    const onKey = (e) => {
      if (e.key === 'Escape') setLightbox(null);
      else if (e.key === 'ArrowRight') setLightbox(i => (i + 1) % property.photos.length);
      else if (e.key === 'ArrowLeft')  setLightbox(i => (i - 1 + property.photos.length) % property.photos.length);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [lightbox, property]);

  return (
    <div style={{ padding: '24px 48px 72px' }}>
      <div style={{ fontSize: 12, color: p.muted, marginBottom: 16 }}>
        <span onClick={() => go('search')} style={{ cursor: 'pointer' }}>← All homes</span>
        <span style={{ margin: '0 10px' }}>/</span>
        <span>{property.name}</span>
      </div>

      {/* GALLERY */}
      <div style={{ position: 'relative', display: 'grid', gridTemplateColumns: '2fr 1fr 1fr', gridTemplateRows: '1fr 1fr', gap: 6, height: 420, marginBottom: 32, borderRadius: 10, overflow: 'hidden' }}>
        <div onClick={() => hasPhotos && setLightbox(0)} style={{ gridRow: 'span 2', cursor: hasPhotos ? 'zoom-in' : 'default' }}>
          <Placeholder
            src={property.photos && property.photos[0] && property.photos[0].src}
            objectPosition={(property.photos && property.photos[0] && property.photos[0].pos) || 'center'}
            label={property.hero} tone={tones[property.tier]} density="loose" style={{ width: '100%', height: '100%' }}/>
        </div>
        {(property.photos
          ? property.photos.slice(1, 5).map((ph, i) => ({ src: ph.src, pos: ph.pos || 'center', label: ph.caption, idx: i + 1 }))
          : property.gallery.slice(0, 4).map((g, i) => ({ src: null, pos: 'center', label: g, idx: null }))
        ).map((g, i) => (
          <div key={i} onClick={() => g.idx !== null && setLightbox(g.idx)} style={{ cursor: g.idx !== null ? 'zoom-in' : 'default' }}>
            <Placeholder src={g.src} objectPosition={g.pos} label={g.label} tone={i % 2 ? 'cream' : 'warm'} density="med" style={{ width: '100%', height: '100%' }}/>
          </div>
        ))}
        {hasPhotos && property.photos.length > 5 && (
          <button onClick={() => setLightbox(0)} style={{
            position: 'absolute', right: 16, bottom: 16, background: 'rgba(255,255,255,0.94)', color: '#1a1612',
            border: '1px solid rgba(0,0,0,0.08)', borderRadius: 999, padding: '10px 18px',
            fontSize: 12, letterSpacing: '0.08em', textTransform: 'uppercase', fontWeight: 500,
            cursor: 'pointer', fontFamily: 'inherit', display: 'inline-flex', alignItems: 'center', gap: 8,
            boxShadow: '0 4px 14px rgba(0,0,0,0.12)',
          }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
            View all {property.photos.length} photos
          </button>
        )}
      </div>

      {/* LIGHTBOX */}
      {lightbox !== null && hasPhotos && (
        <D1Lightbox photos={property.photos} index={lightbox} onIndex={setLightbox} onClose={() => setLightbox(null)} p={p} serif={serif}/>
      )}

      <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 56 }}>
        {/* LEFT */}
        <div>
          <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 10 }}>
            {property.cardEyebrow || `${property.tier} · ${property.walkMinTo770} min walk to 770`}
          </div>
          <h1 style={{ fontFamily: serif, fontSize: 56, fontWeight: 400, letterSpacing: '-0.02em', margin: '0 0 8px', lineHeight: 1 }}>
            {property.name}
          </h1>
          <div style={{ fontSize: 17, color: p.muted, marginBottom: 32 }}>{property.tagline}</div>

          <div style={{ display: 'flex', gap: 36, padding: '22px 0', borderTop: `1px solid ${p.line}`, borderBottom: `1px solid ${p.line}`, marginBottom: 24 }}>
            <Stat ico="guests" v={property.sleeps} l="Sleeps" p={p}/>
            <Stat ico="bed" v={property.bedrooms} l="Bedrooms" p={p}/>
            <Stat ico="bed" v={property.beds} l="Beds" p={p}/>
            <Stat ico="bath" v={property.baths} l="Baths" p={p}/>
          </div>

          {/* WHATSAPP → HOST */}
          <a
            href={`https://wa.me/18625207797?text=${encodeURIComponent(`Hi, I'm inquiring about ${property.name}`)}`}
            target="_blank" rel="noopener noreferrer"
            style={{
              display: 'inline-flex', alignItems: 'center', gap: 10,
              background: '#25D366', color: '#fff', textDecoration: 'none',
              padding: '10px 18px', borderRadius: 999, fontSize: 13, fontWeight: 500,
              marginBottom: 36,
            }}>
            <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
              <path d="M17.472 14.382c-.297-.149-1.758-.867-2.031-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.297-.497.1-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
            </svg>
            Message the host on WhatsApp
          </a>

          {/* REVIEWS (opt-in, genuine) */}
          <D1Reviews p={p} serif={serif} slug={property.slug || property.id}/>

          {/* WALK-THROUGH VIDEO — The Presidential only. Same `walkthrough.mp4`
              that backs the /presidential landing page hero. */}
          {property.id === 'presidential' && (
            <div style={{ marginBottom: 40 }}>
              <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 14 }}>Walk-through video</div>
              <h2 style={{ fontFamily: serif, fontSize: 32, fontWeight: 400, margin: '0 0 20px', letterSpacing: '-0.01em' }}>See every room</h2>
              <div style={{ position: 'relative', aspectRatio: '16/9', borderRadius: 10, overflow: 'hidden', background: '#000' }}>
                <video
                  src="/presidential/assets/presidential/walkthrough.mp4"
                  controls
                  playsInline
                  preload="metadata"
                  style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }}
                />
              </div>
              <p style={{ fontSize: 13, color: p.muted, margin: '12px 0 0', maxWidth: 620 }}>
                A guided walkthrough of every floor — parlor, kitchen, bedrooms, and the upstairs sitting area.
              </p>
            </div>
          )}

          {/* SHABBOS DETAILS — scroll target from the right rail */}
          {shabbosInStay && (
            <div id="shabbos-details" style={{ marginBottom: 40, padding: 24, border: `1px solid ${p.line}`, borderRadius: 10, background: p.card, scrollMarginTop: 24 }}>
              <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 10 }}>Your Shabbos in Crown Heights</div>
              <h2 style={{ fontFamily: serif, fontSize: 32, fontWeight: 400, margin: '0 0 18px', letterSpacing: '-0.01em' }}>Shabbos {shabbosInStay.parsha}</h2>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16 }}>
                <div style={{ padding: '14px 16px', background: p.bg, borderRadius: 8 }}>
                  <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 4 }}>Candle-lighting</div>
                  <div style={{ fontSize: 22, fontFamily: serif }}>{shabbosInStay.light}</div>
                </div>
                <div style={{ padding: '14px 16px', background: p.bg, borderRadius: 8 }}>
                  <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 4 }}>Havdalah</div>
                  <div style={{ fontSize: 22, fontFamily: serif }}>{shabbosInStay.havdalah}</div>
                </div>
              </div>
            </div>
          )}

          {/* ABOUT THE UNIT */}
          <div style={{ marginBottom: 40 }}>
            <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 14 }}>About the unit</div>
            <h2 style={{ fontFamily: serif, fontSize: 32, fontWeight: 400, margin: '0 0 20px', letterSpacing: '-0.01em' }}>{property.name}</h2>
            <p style={{ fontSize: 16, lineHeight: 1.75, color: p.ink, margin: 0, maxWidth: 620, textWrap: 'pretty' }}>
              {property.about}
            </p>
          </div>

          {/* WHAT'S INCLUDED — owner-edited list from admin ("What's included"
              tile in the listing edit screen). Renders exactly what was saved.
              When the list is empty the section is hidden so admins can fully
              clear it. New listings are seeded with defaults at create time
              (DB-side), so an empty list always means an explicit admin clear. */}
          {(() => {
            if (!Array.isArray(property.included) || property.included.length === 0) return null;
            const items = property.included.map(x => typeof x === 'string' ? x : (x && x.v) || '').filter(Boolean);
            if (items.length === 0) return null;
            return (
              <div style={{ marginBottom: 40 }}>
                <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 14 }}>What's included</div>
                <h2 style={{ fontFamily: serif, fontSize: 32, fontWeight: 400, margin: '0 0 20px', letterSpacing: '-0.01em' }}>Everything's set up</h2>
                <ul style={{
                  display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 28px',
                  margin: 0, padding: 0, listStyle: 'none', maxWidth: 620,
                }}>
                  {items.map((v, i) => (
                    <li key={i} style={{
                      display: 'flex', alignItems: 'flex-start', gap: 10,
                      fontSize: 15, color: p.ink, lineHeight: 1.5,
                    }}>
                      <span aria-hidden="true" style={{
                        flexShrink: 0, marginTop: 6, width: 5, height: 5,
                        borderRadius: '50%', background: p.accent,
                      }}/>
                      <span>{v}</span>
                    </li>
                  ))}
                </ul>
              </div>
            );
          })()}

        </div>

        {/* RIGHT — BOOKING CARD */}
        <div style={{ position: 'sticky', top: 20, alignSelf: 'start' }}>
          <div style={{ background: p.card, border: `1px solid ${p.line}`, borderRadius: 10, padding: 24 }}>
            {(() => {
              // Show the effective min for the currently-selected dates so
              // the guest sees "5-night min" during Pesach automatically.
              // Falls back to the listing's normal min if no dates picked.
              const baseMin = property.minNights || 3;
              const dynMin = (window.DyraDateRules && checkIn && checkOut)
                ? window.DyraDateRules.getEffectiveMinNights(
                    property.id, baseMin, checkIn, checkOut, window.DyraDateRules.liveRules())
                : baseMin;
              return (
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 8, marginBottom: 20 }}>
                  <span style={{ fontFamily: serif, fontSize: 38, fontWeight: 500 }}>${property.priceFrom}</span>
                  <span style={{ fontSize: 13, color: p.muted }}>/ night · {dynMin}-night min</span>
                </div>
              );
            })()}
            <div style={{ border: `1px solid ${p.line}`, borderRadius: 8, overflow: 'hidden', marginBottom: 14 }}>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
                <button
                  onClick={() => { setEditingDates(v => !v); setEditingGuests(false); }}
                  style={{ textAlign: 'left', background: editingDates ? p.bg : 'transparent', border: 'none', cursor: 'pointer', padding: '12px 14px', borderRight: `1px solid ${p.line}`, borderBottom: `1px solid ${p.line}`, font: 'inherit', color: 'inherit' }}>
                  <div style={{ fontSize: 9, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted }}>Check in</div>
                  <div style={{ fontSize: 13, marginTop: 3 }}>{fmtDate(checkIn)}</div>
                  <div style={{ fontSize: 11, color: p.muted }}>{hebForDate(checkIn)}</div>
                </button>
                <button
                  onClick={() => { setEditingDates(v => !v); setEditingGuests(false); }}
                  style={{ textAlign: 'left', background: editingDates ? p.bg : 'transparent', border: 'none', cursor: 'pointer', padding: '12px 14px', borderBottom: `1px solid ${p.line}`, font: 'inherit', color: 'inherit' }}>
                  <div style={{ fontSize: 9, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted }}>Check out</div>
                  <div style={{ fontSize: 13, marginTop: 3 }}>{fmtDate(checkOut)}</div>
                  <div style={{ fontSize: 11, color: p.muted }}>{hebForDate(checkOut)}</div>
                </button>
              </div>
              {editingDates && (
                <div style={{ padding: '12px 14px', borderBottom: `1px solid ${p.line}`, background: p.bg }}>
                  {(() => {
                    // Effective min for the currently-selected dates: takes
                    // listing.minNights AND any overlapping date_rules into
                    // account. Mirrors src/lib/date-rules.ts so the picker
                    // disables impossible check-outs before the server has
                    // to. Falls back to plain listing min if the shared
                    // resolver isn't loaded yet (script ordering).
                    const baseMin = property.minNights || 2;
                    const minN = (window.DyraDateRules && checkIn && checkOut)
                      ? window.DyraDateRules.getEffectiveMinNights(
                          property.id, baseMin, checkIn, checkOut, window.DyraDateRules.liveRules())
                      : baseMin;
                    // Build the unavailable-dates Set from the property's
                    // blockedRanges so we can render the calendar with the
                    // booked days struck through + red-hatched, matching
                    // the booking-flow picker on this same site.
                    const blockedSet = new Set();
                    (property.blockedRanges || []).forEach(r => {
                      if (!r || !r.start || !r.end) return;
                      const a = new Date(r.start + 'T00:00:00');
                      const b = new Date(r.end + 'T00:00:00');
                      for (let d = new Date(a); d < b; d.setDate(d.getDate() + 1)) {
                        blockedSet.add(d.toISOString().slice(0, 10));
                      }
                    });
                    const todayISO = new Date().toISOString().slice(0, 10);
                    const initial = checkIn ? new Date(checkIn + 'T00:00:00') : new Date();
                    const [cursor, setCursor] = [
                      { y: initial.getFullYear(), m: initial.getMonth() },
                      null,
                    ];
                    return (
                      <ListingMiniMonth
                        p={p} serif={serif} hebFont={hebFont}
                        property={property}
                        checkIn={checkIn} checkOut={checkOut}
                        setCheckIn={setCheckIn} setCheckOut={setCheckOut}
                        minN={minN}
                        blockedSet={blockedSet}
                        todayISO={todayISO}
                        nights={nights}
                        onClose={() => setEditingDates(false)}
                      />
                    );
                  })()}
                </div>
              )}
              <button
                onClick={() => { setEditingGuests(v => !v); setEditingDates(false); }}
                style={{ display: 'block', textAlign: 'left', width: '100%', background: editingGuests ? p.bg : 'transparent', border: 'none', cursor: 'pointer', padding: '12px 14px', font: 'inherit', color: 'inherit' }}>
                <div style={{ fontSize: 9, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted }}>Guests</div>
                <div style={{ fontSize: 13, marginTop: 3 }}>{guestSummary()}</div>
              </button>
              {editingGuests && (
                <div style={{ padding: '12px 14px', borderTop: `1px solid ${p.line}`, background: p.bg, display: 'grid', gap: 10 }}>
                  {[
                    { label: 'Adults', sub: 'Age 13+', val: adults, set: setAdults, min: 1, max: property.sleeps },
                    { label: 'Children', sub: 'Ages 2–12', val: children, set: setChildren, min: 0, max: property.sleeps },
                    { label: 'Infants', sub: 'Under 2', val: infants, set: setInfants, min: 0, max: 5 },
                  ].map(row => (
                    <div key={row.label} style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                      <div>
                        <div style={{ fontSize: 12 }}>{row.label}</div>
                        <div style={{ fontSize: 10, color: p.muted }}>{row.sub}</div>
                      </div>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
                        <button onClick={() => row.set(Math.max(row.min, row.val - 1))} disabled={row.val <= row.min} style={{ width: 24, height: 24, borderRadius: '50%', border: `1px solid ${p.line}`, background: 'transparent', cursor: row.val <= row.min ? 'default' : 'pointer', color: p.ink, opacity: row.val <= row.min ? 0.4 : 1, fontSize: 14, lineHeight: 1 }}>−</button>
                        <span style={{ fontSize: 12, minWidth: 14, textAlign: 'center' }}>{row.val}</span>
                        <button onClick={() => row.set(Math.min(row.max, row.val + 1))} disabled={row.val >= row.max} style={{ width: 24, height: 24, borderRadius: '50%', border: `1px solid ${p.line}`, background: 'transparent', cursor: row.val >= row.max ? 'default' : 'pointer', color: p.ink, opacity: row.val >= row.max ? 0.4 : 1, fontSize: 14, lineHeight: 1 }}>+</button>
                      </div>
                    </div>
                  ))}
                  <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
                    <button onClick={() => setEditingGuests(false)} style={{ background: 'transparent', border: `1px solid ${p.line}`, padding: '4px 12px', borderRadius: 999, fontSize: 11, cursor: 'pointer', color: p.ink }}>Done</button>
                  </div>
                </div>
              )}
            </div>

            {shabbosInStay ? (
              <a href="#shabbos-details" onClick={(e) => {
                e.preventDefault();
                const el = document.getElementById('shabbos-details');
                if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
              }} style={{ background: p.bg, borderRadius: 6, padding: '10px 12px', fontSize: 11, color: p.muted, marginBottom: 14, display: 'flex', gap: 8, textDecoration: 'none', cursor: 'pointer' }}>
                <Ico name="candle" size={12} style={{ color: p.accent, flexShrink: 0, marginTop: 1 }}/>
                <span>Stay covers <strong style={{ color: p.ink }}>Shabbos {shabbosInStay.parsha}</strong>. Candle-lighting {shabbosInStay.light} · Havdalah {shabbosInStay.havdalah} <span style={{ color: p.accent, marginLeft: 4 }}>· See details ↓</span></span>
              </a>
            ) : (
              <div style={{ background: p.bg, borderRadius: 6, padding: '10px 12px', fontSize: 11, color: p.muted, marginBottom: 14, display: 'flex', gap: 8 }}>
                <Ico name="candle" size={12} style={{ color: p.accent, flexShrink: 0, marginTop: 1 }}/>
                <span>Stay does not span a Shabbos.</span>
              </div>
            )}

            {(() => {
              // Live availability check against the store (confirmed reservations + admin holds)
              const store = window.DyraStore;
              const available = store ? store.isPropertyAvailable(property.id, checkIn, checkOut) : true;
              // Effective-min + gap-fill gate (mirrors src/lib/date-rules.ts).
              // Disables Reserve when the selected dates violate the peak-
              // window min UNLESS the stay exactly fills an orphan night —
              // in which case it sails through even below the normal floor.
              const baseMin = property.minNights || 3;
              const dr = window.DyraDateRules;
              const ruleDecision = dr && checkIn && checkOut
                ? dr.canBook(property.id, baseMin, checkIn, checkOut, dr.liveReservations(), dr.liveRules())
                : { ok: true, effectiveMin: baseMin };
              if (available && ruleDecision.ok) {
                return (
                  <button onClick={() => go('booking', property.id, { checkIn, checkOut, nights, adults, children, infants })} style={{
                    width: '100%', background: p.accent, color: p.bg, border: 'none', padding: '14px', borderRadius: 8,
                    fontSize: 14, cursor: 'pointer', marginBottom: 14,
                  }}>Reserve</button>
                );
              }
              if (available && !ruleDecision.ok) {
                return (
                  <div style={{ marginBottom: 14 }}>
                    <button disabled style={{
                      width: '100%', background: p.line, color: p.muted, border: 'none', padding: '14px', borderRadius: 8,
                      fontSize: 14, cursor: 'not-allowed', marginBottom: 8,
                    }}>Reserve</button>
                    <div style={{ fontSize: 11, color: p.muted, textAlign: 'center' }}>
                      Minimum stay for these dates is {ruleDecision.effectiveMin} nights.
                    </div>
                  </div>
                );
              }
              // Suggest the next free range that fits minNights
              const minN = property.minNights || 2;
              const blocked = store.blockedDateSet(property.id);
              const findNext = () => {
                const start = new Date(checkIn + 'T00:00:00');
                for (let i = 0; i < 90; i++) {
                  const tryStart = new Date(start); tryStart.setDate(tryStart.getDate() + i);
                  let ok = true;
                  for (let j = 0; j < minN; j++) {
                    const d = new Date(tryStart); d.setDate(d.getDate() + j);
                    if (blocked.has(d.toISOString().slice(0, 10))) { ok = false; break; }
                  }
                  if (ok) {
                    const end = new Date(tryStart); end.setDate(end.getDate() + minN);
                    return { start: tryStart.toISOString().slice(0, 10), end: end.toISOString().slice(0, 10) };
                  }
                }
                return null;
              };
              const next = findNext();
              return (
                <>
                  <div style={{ background: 'rgba(184,83,42,0.08)', border: `1px solid ${p.accent}`, borderRadius: 8, padding: '12px 14px', marginBottom: 10 }}>
                    <div style={{ fontSize: 12, color: p.accent, fontWeight: 500, marginBottom: 4 }}>Not available for these dates</div>
                    <div style={{ fontSize: 11, color: p.muted, lineHeight: 1.5 }}>
                      Another guest has confirmed part of {fmtDate(checkIn)} – {fmtDate(checkOut)}.
                      {next && (
                        <> Try{' '}
                          <button onClick={() => { setCheckIn(next.start); setCheckOut(next.end); }} style={{
                            background: 'transparent', border: 'none', padding: 0, margin: 0,
                            color: p.accent, cursor: 'pointer', fontFamily: 'inherit', fontSize: 11,
                            textDecoration: 'underline',
                          }}>{fmtDate(next.start)} – {fmtDate(next.end)}</button>.
                        </>
                      )}
                    </div>
                  </div>
                  <button disabled style={{
                    width: '100%', background: p.line, color: p.muted, border: 'none', padding: '14px', borderRadius: 8,
                    fontSize: 14, cursor: 'not-allowed', marginBottom: 14,
                  }}>Unavailable for selected dates</button>
                </>
              );
            })()}

            <div style={{ fontSize: 11, color: p.muted, padding: '10px 12px', background: p.bg, borderRadius: 6, marginBottom: 10, lineHeight: 1.5 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}>
                <span>Pay 30% today</span><span style={{ color: p.ink, fontWeight: 500 }}>locks dates</span>
              </div>
              <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
                <span>Balance due</span><span>1 week before arrival</span>
              </div>
            </div>

            <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 11, color: p.muted, padding: '8px 12px', background: p.bg, borderRadius: 6, marginBottom: 14 }}>
              <span>Check-in <strong style={{ color: p.ink }}>4:00pm</strong></span>
              <span>Check-out <strong style={{ color: p.ink }}>11:00am</strong></span>
            </div>

            <div style={{ fontSize: 12, color: p.muted, lineHeight: 1.7 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between' }}><span>${property.priceFrom} × {nights} night{nights === 1 ? '' : 's'}</span><span>${property.priceFrom * nights}</span></div>
              {upsellItems.map(u => {
                if (!addedUpsells[u.n]) return null;
                const sel = upsellVariant[u.n];
                const unit = upsellUnitPrice(u, sel);
                const cost = upsellCost(u, sel, nights);
                const variantLabel = u.variants ? (u.variants.find(v => v.id === sel) || u.variants[0]).label : null;
                const label = u.perNight
                  ? `${u.n} · $${unit} × ${nights} nights`
                  : (variantLabel ? `${u.n} · ${variantLabel}` : u.n);
                return (
                  <div key={u.n} style={{ display: 'flex', justifyContent: 'space-between' }}>
                    <span>{label}</span><span>${cost}</span>
                  </div>
                );
              })}
              <div style={{ display: 'flex', justifyContent: 'space-between', paddingTop: 10, marginTop: 10, borderTop: `1px solid ${p.line}`, color: p.ink, fontWeight: 500, fontSize: 14 }}>
                <span>Total</span><span>${property.priceFrom * nights + upsellItems.reduce((s, u) => s + (addedUpsells[u.n] ? upsellCost(u, upsellVariant[u.n], nights) : 0), 0)}</span>
              </div>
            </div>
          </div>

          <div id="addons-full" style={{ marginTop: 14, padding: 18, border: `1px solid ${p.line}`, borderRadius: 10, background: p.bg }}>
            <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.muted, marginBottom: 10 }}>Add to your stay</div>
            {upsellItems.map((u, i) => {
              const added = !!addedUpsells[u.n];
              const sel = upsellVariant[u.n];
              const unit = upsellUnitPrice(u, sel);
              const sub = u.perNight ? `Total $${unit * nights} for ${nights} nights` : null;
              const pr = u.perNight ? `$${unit} / night` : `$${unit}`;
              return (
                <div key={i} style={{ padding: '10px 0', borderTop: i === 0 ? 'none' : `1px solid ${p.line}` }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
                    <span style={{ fontSize: 13 }}>{u.n}</span>
                    <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
                      <span style={{ fontSize: 12, color: p.muted }}>
                        {pr}{added && sub ? <span style={{ marginLeft: 6 }}>({sub})</span> : null}
                      </span>
                      <button
                        onClick={() => setAddedUpsells(s => ({ ...s, [u.n]: !s[u.n] }))}
                        style={{
                          background: added ? p.accent : 'transparent',
                          color: added ? p.bg : p.ink,
                          border: `1px solid ${added ? p.accent : p.line}`,
                          padding: '3px 10px', borderRadius: 999, fontSize: 11, cursor: 'pointer',
                        }}>{added ? 'Added' : 'Add'}</button>
                    </div>
                  </div>
                  {u.variants && added && (
                    <div style={{ display: 'flex', gap: 6, marginTop: 8 }}>
                      {u.variants.map(v => {
                        const active = (sel || u.variants[0].id) === v.id;
                        return (
                          <button key={v.id}
                            onClick={() => setUpsellVariant(x => ({ ...x, [u.n]: v.id }))}
                            style={{
                              background: active ? p.ink : 'transparent',
                              color: active ? p.bg : p.ink,
                              border: `1px solid ${active ? p.ink : p.line}`,
                              padding: '4px 10px', borderRadius: 999, fontSize: 11, cursor: 'pointer',
                            }}>{v.label} · ${v.price}</button>
                        );
                      })}
                    </div>
                  )}
                  {u.n === 'Housekeeping' && added && (
                    <div style={{ marginTop: 10 }}>
                      <label style={{ fontSize: 10, letterSpacing: '0.1em', textTransform: 'uppercase', color: p.muted, display: 'block', marginBottom: 4 }}>Preferred cleaning date</label>
                      <input type="date"
                        value={housekeepingDate}
                        min={checkIn} max={checkOut}
                        onChange={e => setHousekeepingDate(e.target.value)}
                        style={{ width: '100%', padding: '8px 10px', border: `1px solid ${p.line}`, borderRadius: 6, fontSize: 12, fontFamily: 'inherit', background: p.card, color: p.ink }}/>
                      {housekeepingDate ? (
                        <div style={{ fontSize: 10, color: p.muted, marginTop: 6, display: 'flex', alignItems: 'center', gap: 6 }}>
                          <Ico name="check" size={10} style={{ color: p.accent }}/>
                          Confirmed for {fmtDate(housekeepingDate)}. Request will be sent to the host.
                        </div>
                      ) : (
                        <div style={{ fontSize: 10, color: p.muted, marginTop: 6 }}>
                          Pick any date between {fmtDate(checkIn)} and {fmtDate(checkOut)}.
                        </div>
                      )}
                    </div>
                  )}
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

function Stat({ ico, v, l, p }) {
  return (
    <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
      <Ico name={ico} size={20} stroke={1.3} style={{ color: p.accent }}/>
      <div>
        <div style={{ fontSize: 16, fontWeight: 500 }}>{v}</div>
        <div style={{ fontSize: 11, color: p.muted, letterSpacing: '0.1em', textTransform: 'uppercase' }}>{l}</div>
      </div>
    </div>
  );
}

// Reviews — collapsed by default; guest can choose to see them. Real
// reviews are fetched lazily from /api/m/reviews on first expand so the
// page render stays cheap when nobody opens the section.
function D1Reviews({ p, serif, slug }) {
  const [show, setShow] = React.useState(false);
  const [reviews, setReviews] = React.useState(null); // null = not loaded, [] = loaded empty
  const [loading, setLoading] = React.useState(false);

  React.useEffect(() => {
    if (!show || reviews !== null || loading || !slug) return;
    setLoading(true);
    fetch(`/api/m/reviews?property_slug=${encodeURIComponent(slug)}`)
      .then(r => r.json())
      .then(j => setReviews(Array.isArray(j.reviews) ? j.reviews : []))
      .catch(() => setReviews([]))
      .finally(() => setLoading(false));
  }, [show, slug, reviews, loading]);

  return (
    <div style={{ marginBottom: 40 }}>
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 14 }}>
        <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent }}>Guest reviews</div>
        <button onClick={() => setShow(!show)} style={{
          background: 'transparent', border: `1px solid ${p.line}`, padding: '6px 14px', borderRadius: 999,
          fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase', cursor: 'pointer', color: p.ink, fontFamily: 'inherit',
        }}>{show ? 'Hide reviews' : 'Show reviews'}</button>
      </div>
      {!show ? (
        <p style={{ fontSize: 14, color: p.muted, lineHeight: 1.7, margin: 0, maxWidth: 560 }}>
          Reviews are only from guests who have actually stayed with us. Tap “Show reviews” to read them.
        </p>
      ) : loading ? (
        <div style={{ fontSize: 13, color: p.muted, fontStyle: 'italic' }}>Loading reviews…</div>
      ) : (
        <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 14, maxWidth: 640 }}>
          {(!reviews || reviews.length === 0) ? (
            <div style={{ fontSize: 13, color: p.muted, fontStyle: 'italic' }}>
              No published reviews yet — guest reviews will appear here after their stay.
            </div>
          ) : reviews.map(r => (
            <div key={r.id} style={{ borderTop: `1px solid ${p.line}`, paddingTop: 14 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
                <div style={{ color: p.accent, fontSize: 14, letterSpacing: '0.05em' }}>{'★'.repeat(r.rating)}<span style={{ color: p.line }}>{'★'.repeat(5 - r.rating)}</span></div>
                <div style={{ fontSize: 12, color: p.muted, letterSpacing: '0.04em' }}>
                  {r.guest_name}{r.created_at ? ` · ${new Date(r.created_at).toLocaleDateString('en-US', { month: 'short', year: 'numeric' })}` : ''}
                </div>
              </div>
              {r.title ? <div style={{ fontFamily: serif, fontSize: 17, color: p.ink, marginBottom: 4 }}>{r.title}</div> : null}
              {r.body ? <div style={{ fontSize: 14, color: p.ink, lineHeight: 1.7, whiteSpace: 'pre-wrap' }}>{r.body}</div> : null}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

window.D1Search = D1Search;
window.D1Listing = D1Listing;

// Photo lightbox — full-gallery carousel with arrow keys, thumbnails, captions.
function D1Lightbox({ photos, index, onIndex, onClose, p, serif }) {
  const cur = photos[index];
  const stripRef = React.useRef(null);
  React.useEffect(() => {
    if (!stripRef.current) return;
    const el = stripRef.current.querySelector(`[data-tn="${index}"]`);
    if (el) el.scrollIntoView({ inline: 'center', block: 'nearest', behavior: 'smooth' });
  }, [index]);
  const next = () => onIndex((index + 1) % photos.length);
  const prev = () => onIndex((index - 1 + photos.length) % photos.length);

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(20,16,12,0.94)', zIndex: 10000,
      display: 'flex', flexDirection: 'column',
    }}>
      {/* Top bar */}
      <div onClick={e => e.stopPropagation()} style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'center',
        padding: '18px 28px', color: '#f0ebe0',
      }}>
        <div style={{ fontFamily: serif, fontSize: 16 }}>
          {cur.caption} <span style={{ opacity: 0.5, marginLeft: 10, fontSize: 12, letterSpacing: '0.1em' }}>{index + 1} / {photos.length}</span>
        </div>
        <button onClick={onClose} style={{
          background: 'transparent', border: '1px solid rgba(255,255,255,0.25)', color: '#f0ebe0',
          borderRadius: 999, padding: '8px 16px', fontSize: 11, letterSpacing: '0.12em', textTransform: 'uppercase',
          cursor: 'pointer', fontFamily: 'inherit', display: 'inline-flex', alignItems: 'center', gap: 6,
        }}>Close ✕</button>
      </div>

      {/* Main image */}
      <div onClick={e => e.stopPropagation()} style={{
        flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center',
        position: 'relative', padding: '0 80px', minHeight: 0,
      }}>
        <button onClick={prev} style={navBtnStyle('left')}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M15 6l-6 6 6 6"/></svg>
        </button>
        <img src={cur.src} alt={cur.caption} style={{
          maxWidth: '100%', maxHeight: '100%', objectFit: 'contain',
          borderRadius: 4, boxShadow: '0 12px 40px rgba(0,0,0,0.4)',
        }}/>
        <button onClick={next} style={navBtnStyle('right')}>
          <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round"><path d="M9 6l6 6-6 6"/></svg>
        </button>
      </div>

      {/* Thumbnail strip */}
      <div onClick={e => e.stopPropagation()} ref={stripRef} style={{
        display: 'flex', gap: 8, padding: '18px 28px 24px', overflowX: 'auto',
        borderTop: '1px solid rgba(255,255,255,0.08)',
      }}>
        {photos.map((ph, i) => (
          <div key={i} data-tn={i} onClick={() => onIndex(i)} style={{
            flex: '0 0 auto', width: 96, height: 68, borderRadius: 4, overflow: 'hidden',
            cursor: 'pointer', opacity: i === index ? 1 : 0.48,
            outline: i === index ? '2px solid #f0ebe0' : '2px solid transparent',
            outlineOffset: 2, transition: 'opacity 0.15s',
          }}>
            <img src={ph.src} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}/>
          </div>
        ))}
      </div>
    </div>
  );
}

function navBtnStyle(side) {
  return {
    position: 'absolute', top: '50%', transform: 'translateY(-50%)',
    [side]: 20,
    width: 48, height: 48, borderRadius: 999,
    background: 'rgba(255,255,255,0.1)', border: '1px solid rgba(255,255,255,0.2)',
    color: '#f0ebe0', cursor: 'pointer',
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
    fontFamily: 'inherit',
  };
}

window.D1Lightbox = D1Lightbox;


// ─── Listing-page mini calendar ─────────────────────────────────────
// Used by D1Listing's right-rail check-in/checkout editor. Replaces the old
// native <input type="date"> pair so booked dates render with the same
// red-hatch + strikethrough treatment as the booking-flow picker, instead
// of the OS calendar that has no idea what's blocked.
function ListingMiniMonth({ p, serif, hebFont, property, checkIn, checkOut, setCheckIn, setCheckOut, minN, blockedSet, todayISO, nights, onClose }) {
  const initial = checkIn ? new Date(checkIn + 'T00:00:00') : new Date();
  const [cursor, setCursor] = React.useState({ y: initial.getFullYear(), m: initial.getMonth() });
  const [pickingMode, setPickingMode] = React.useState('checkin');
  const [warning, setWarning] = React.useState('');

  // Earliest legal check-out ISO given the current check-in. While the guest
  // is picking checkout (start chosen, end not yet), any cell strictly before
  // this date violates the minimum-stay rule and is rendered as disabled so
  // they never see an "available" cell that the next page would reject.
  const minCheckoutISO = (() => {
    if (pickingMode !== 'checkout' || !checkIn) return null;
    const a = new Date(checkIn + 'T00:00:00');
    a.setDate(a.getDate() + minN);
    return a.toISOString().slice(0, 10);
  })();

  const monthName = new Date(cursor.y, cursor.m, 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
  const firstWeekday = new Date(cursor.y, cursor.m, 1).getDay();
  const lastDay = new Date(cursor.y, cursor.m + 1, 0).getDate();
  const cells = [];
  for (let i = 0; i < firstWeekday; i++) cells.push(null);
  for (let d = 1; d <= lastDay; d++) {
    const iso = `${cursor.y}-${String(cursor.m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
    cells.push({ d, iso });
  }

  const prev = () => setCursor(c => c.m === 0 ? { y: c.y - 1, m: 11 } : { y: c.y, m: c.m - 1 });
  const next = () => setCursor(c => c.m === 11 ? { y: c.y + 1, m: 0 } : { y: c.y, m: c.m + 1 });

  const handleDayClick = (iso) => {
    setWarning('');
    if (iso < todayISO) { setWarning('Pick a date in the future.'); return; }
    if (blockedSet.has(iso)) { setWarning('Those nights are already booked.'); return; }
    if (pickingMode === 'checkin') {
      setCheckIn(iso);
      // Pre-set checkout to checkIn + minNights if current is too close.
      const minOut = new Date(iso + 'T00:00:00'); minOut.setDate(minOut.getDate() + minN);
      const minOutISO = minOut.toISOString().slice(0, 10);
      if (!checkOut || checkOut < minOutISO) setCheckOut(minOutISO);
      setPickingMode('checkout');
      return;
    }
    if (iso <= checkIn) { setCheckIn(iso); setPickingMode('checkout'); return; }
    const proposedNights = Math.round((new Date(iso + 'T00:00:00') - new Date(checkIn + 'T00:00:00')) / 86400000);
    if (proposedNights < minN) { setWarning(`Minimum stay is ${minN} night${minN === 1 ? '' : 's'}.`); return; }
    // Refuse to span across a booked night.
    const start = new Date(checkIn + 'T00:00:00');
    const end = new Date(iso + 'T00:00:00');
    for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
      if (blockedSet.has(d.toISOString().slice(0, 10))) {
        setWarning('Some nights inside that range are booked.');
        return;
      }
    }
    setCheckOut(iso);
    setPickingMode('checkin');
  };

  return (
    <div>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
        <button onClick={prev} aria-label="Previous month" style={{ width: 24, height: 24, borderRadius: 12, border: `1px solid ${p.line}`, background: 'transparent', color: p.ink, cursor: 'pointer', fontSize: 11, lineHeight: 1, padding: 0 }}>‹</button>
        <span style={{ fontFamily: serif, fontSize: 14, color: p.ink }}>{monthName}</span>
        <button onClick={next} aria-label="Next month" style={{ width: 24, height: 24, borderRadius: 12, border: `1px solid ${p.line}`, background: 'transparent', color: p.ink, cursor: 'pointer', fontSize: 11, lineHeight: 1, padding: 0 }}>›</button>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2, marginBottom: 4 }}>
        {['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => (
          <div key={i} style={{ textAlign: 'center', fontSize: 9, letterSpacing: '0.12em', color: p.muted, padding: '4px 0', textTransform: 'uppercase' }}>{d}</div>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2 }}>
        {cells.map((c, i) => {
          if (!c) return <div key={i}/>;
          const isPast = c.iso < todayISO;
          const isBooked = blockedSet.has(c.iso);
          // While picking checkout, days inside the [checkIn, checkIn+minN)
          // window are unavailable — they would produce a sub-minimum stay.
          const violatesMinStay =
            !!minCheckoutISO &&
            c.iso > checkIn &&
            c.iso < minCheckoutISO;
          const disabled = isPast || isBooked || violatesMinStay;
          const isCheckIn = c.iso === checkIn;
          const isCheckOut = c.iso === checkOut;
          const inRange = checkIn && checkOut && c.iso > checkIn && c.iso < checkOut;
          const bookedActive = isBooked && !isPast;
          return (
            <button key={i}
              onClick={() => !disabled && handleDayClick(c.iso)}
              disabled={disabled}
              title={
                isBooked
                  ? 'Booked'
                  : isPast
                    ? 'In the past'
                    : violatesMinStay
                      ? `Minimum stay is ${minN} night${minN === 1 ? '' : 's'}`
                      : ''
              }
              style={{
                aspectRatio: '1',
                background: isCheckIn || isCheckOut ? p.accent
                  : inRange ? p.accentSoft
                  : bookedActive
                    ? 'repeating-linear-gradient(135deg, rgba(193,72,49,0.10) 0px, rgba(193,72,49,0.10) 4px, rgba(193,72,49,0.20) 4px, rgba(193,72,49,0.20) 8px)'
                    : 'transparent',
                color: isCheckIn || isCheckOut ? '#fff' : disabled ? '#bbb1a0' : p.ink,
                border: 'none', borderRadius: 4,
                fontSize: 11, lineHeight: 1, padding: 0,
                cursor: disabled ? 'not-allowed' : 'pointer',
                fontFamily: 'inherit',
                textDecoration: isBooked ? 'line-through' : 'none',
                textDecorationColor: isBooked ? '#c14831' : undefined,
                textDecorationThickness: isBooked ? 2 : undefined,
                opacity: isPast ? 0.4 : isBooked ? 0.7 : 1,
              }}>{c.d}</button>
          );
        })}
      </div>
      {warning ? (
        <div style={{ fontSize: 11, color: p.accent, marginTop: 8 }}>{warning}</div>
      ) : null}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginTop: 10, fontSize: 10, color: p.muted }}>
        <span>{nights} night{nights === 1 ? '' : 's'} · min {minN}</span>
        <button onClick={onClose} style={{ background: 'transparent', border: `1px solid ${p.line}`, padding: '3px 10px', borderRadius: 999, fontSize: 10, cursor: 'pointer', color: p.ink }}>Done</button>
      </div>
    </div>
  );
}
