// ═══════════════════════════════════════════════════════════════════════
// Dyra Mobile — Screens 1
//   • Home
//   • Search overlay (date + guests + tier)
//   • Browse / search results
// ═══════════════════════════════════════════════════════════════════════

const { useState, useEffect, useRef, useMemo } = React;
const PM = DYRA_MOBILE_PALETTE;
const TM = DYRA_MOBILE_TYPE;

// ─── Photo helper — wraps Placeholder w/ photos[] support ────────────
function MPhoto({ property, idx = 0, style, label, fallbackTone = 'warm', objectPosition }) {
  const photo = property?.photos?.[idx];
  if (photo) {
    return <Placeholder src={photo.src} alt={photo.caption} style={style} objectPosition={photo.pos || objectPosition || 'center'}/>;
  }
  return <Placeholder label={label || property?.gallery?.[idx] || property?.hero || 'Photo'} tone={fallbackTone} style={style}/>;
}

// ═══════════════════════════════════════════════════════════════════════
// HOME
// ═══════════════════════════════════════════════════════════════════════
function MHome({ go, openSearch, openMenu }) {
  const [scrolled, setScrolled] = useState(false);
  const scrollRef = useRef(null);
  const props = window.DyraStore?.state?.properties?.filter(p => p.status === 'active') || window.DYRA?.PROPERTIES || [];
  const featured = props.slice(0, 3);
  const parsha = window.DYRA?.PARSHA?.[0];

  return (
    <div ref={scrollRef}
      onScroll={(e) => setScrolled(e.target.scrollTop > 40)}
      style={{ height: '100%', overflow: 'auto', background: PM.bg, WebkitOverflowScrolling: 'touch' }}>
      <MAppBar
        scrolled={scrolled}
        go={go}
        leading={<MIconBtn label="Menu" onClick={openMenu}><MIco name="menu" size={22} color={PM.ink} /></MIconBtn>}
        trailing={parsha ? (
          <div style={{
            background: PM.card,
            border: `0.5px solid ${PM.line}`,
            borderRadius: 10,
            padding: '4px 10px',
            display: 'flex', flexDirection: 'column',
            lineHeight: 1.05, textAlign: 'right',
            marginRight: 4,
          }}>
            <span style={{
              fontSize: 8, letterSpacing: '0.18em', textTransform: 'uppercase',
              color: PM.shabbos, fontFamily: TM.sans,
            }}>This Shabbos</span>
            <span style={{ fontFamily: TM.serif, fontSize: 13, color: PM.ink }}>
              {parsha.parsha}
            </span>
            <span style={{ fontSize: 9, color: PM.muted, fontFamily: TM.sans }}>
              {parsha.candleLight} · {parsha.havdalah}
            </span>
          </div>
        ) : null}
      />

      {/* ── HERO ────────────────────────────────────────────────────
           Trimmed-down version of the desktop centered hero:
              · Eyebrow ("Dyra Kosher Rentals")
              · Headline with gold-foil "gold standard"
              · Search pill as the primary CTA (replaces old "Find your
                stay" button — the user asked for one CTA only)
           The Hebrew accent line, descriptive paragraph, and "Host with
           Dyra" button have been promoted out of the hero. The two
           top-of-page links live in the sticky header (m-shell.jsx). */}
      <div style={{ position: 'relative' }}>
        {/* Striped backdrop, full-bleed (accent-tinted, no label) */}
        <Placeholder tone="accent" density="loose" hideLabel
          style={{ position: 'absolute', inset: 0 }}
        />
        {/* Soft wash so type stays legible against the stripes */}
        <div style={{
          position: 'absolute', inset: 0,
          background: `linear-gradient(180deg, ${PM.bg}d9 0%, ${PM.bg}b3 45%, ${PM.bg}d9 100%)`,
        }} />

        {/* Centered editorial column */}
        <div style={{
          position: 'relative', minHeight: 480,
          display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
          padding: '48px 22px 36px', textAlign: 'center',
        }}>
          <div style={{
            fontSize: 10, letterSpacing: '0.28em', textTransform: 'uppercase',
            color: PM.accent, marginBottom: 20,
          }}>Dyra Kosher Rentals</div>

          <h1 style={{
            fontFamily: TM.serif, fontSize: 44, fontWeight: 400,
            lineHeight: 0.98, letterSpacing: '-0.02em',
            margin: 0, color: PM.ink, textWrap: 'balance',
          }}>
            The <em style={{
              background: 'linear-gradient(180deg, #d4a541 0%, #b8862a 55%, #8a5e15 100%)',
              WebkitBackgroundClip: 'text',
              backgroundClip: 'text',
              WebkitTextFillColor: 'transparent',
              color: '#b8862a',
              fontStyle: 'italic',
              // The italic glyphs + linear-gradient clip can chop the trailing
              // 'd' of "standard" on iOS — give the inline-block a touch of
              // right padding so descenders/ascenders stay inside its box.
              display: 'inline-block',
              paddingRight: '0.12em',
              marginRight: '-0.06em',
            }}>gold standard</em> of kosher stays in the heart of Crown Heights.
          </h1>

          {/* Search pill — primary (and only) hero CTA */}
          <button onClick={openSearch} style={{
            marginTop: 32, width: '100%', maxWidth: 360,
            background: PM.card,
            border: `1px solid ${PM.line}`,
            borderRadius: 999,
            padding: '12px 12px 12px 22px',
            display: 'grid', gridTemplateColumns: '1fr auto', alignItems: 'center', gap: 12,
            boxShadow: '0 4px 14px rgba(0,0,0,0.08)',
            cursor: 'pointer', WebkitTapHighlightColor: 'transparent',
            textAlign: 'left', fontFamily: 'inherit',
          }}>
            <div>
              <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: PM.muted, marginBottom: 2 }}>
                When · Who
              </div>
              <div style={{ fontFamily: TM.sans, fontSize: 14, color: PM.ink, fontWeight: 500 }}>
                Pick dates · Add guests
              </div>
            </div>
            <div style={{
              width: 44, height: 44, borderRadius: 22,
              background: PM.accent, color: '#fff',
              display: 'flex', alignItems: 'center', justifyContent: 'center',
            }}>
              <MIco name="search" size={20} color="#fff"/>
            </div>
          </button>
        </div>
      </div>

      {/* ── FEATURED PROPERTIES ─────────────────────────────────
           Same role as desktop "The portfolio." section, but laid out
           as a horizontal scroll carousel for thumb-friendly browsing. */}
      <div style={{ marginTop: 40 }}>
        <div style={{ padding: '0 16px 14px', display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
          <h2 style={{
            fontFamily: TM.serif, fontSize: 32, fontWeight: 400,
            letterSpacing: '-0.02em', margin: 0, color: PM.ink, lineHeight: 1.05,
          }}>
            The <em style={{ color: PM.accent, fontStyle: 'italic' }}>portfolio.</em>
          </h2>
          <button onClick={() => go('browse')} style={{
            background: 'transparent', border: 'none',
            fontFamily: TM.sans, fontSize: 13, color: PM.muted, fontWeight: 400,
            cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 4,
          }}>See all <MIco name="forward" size={12} color={PM.muted}/></button>
        </div>

        {/* Horizontal carousel */}
        <div style={{
          display: 'flex', gap: 12, overflowX: 'auto', overflowY: 'hidden',
          padding: '0 16px 8px', scrollSnapType: 'x mandatory',
          WebkitOverflowScrolling: 'touch',
        }}>
          {featured.map(p => (
            <button
              key={p.id}
              onClick={() => {
                // Pull any sticky search dates into the listing URL too,
                // so a tap from Home lands on the user's chosen dates.
                const sticky = readStickySearch();
                const sp = new URLSearchParams();
                if (sticky?.dates?.checkIn)  sp.set('checkIn', sticky.dates.checkIn);
                if (sticky?.dates?.checkOut) sp.set('checkOut', sticky.dates.checkOut);
                const total = (sticky?.guests?.adults || 0) + (sticky?.guests?.children || 0);
                if (total) sp.set('guests', String(total));
                const qs = sp.toString();
                window.location.hash = `listing:${p.id}${qs ? '?' + qs : ''}`;
              }}
              style={{
                minWidth: 290, maxWidth: 290, flexShrink: 0,
                background: PM.surface,
                border: `0.5px solid ${PM.lineSoft}`,
                borderRadius: 16, overflow: 'hidden',
                scrollSnapAlign: 'start',
                cursor: 'pointer', textAlign: 'left', padding: 0,
                fontFamily: 'inherit',
                WebkitTapHighlightColor: 'transparent',
              }}
            >
              <MPhoto property={p} idx={0} style={{ width: '100%', height: 220 }}/>
              <div style={{ padding: 14 }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
                  <div style={{ fontFamily: TM.serif, fontSize: 17, color: PM.ink, fontWeight: 500, lineHeight: 1.15, flex: 1 }}>
                    {p.name}
                  </div>
                  {p.rating && p.reviews > 0 ? (
                    <div style={{ display: 'flex', alignItems: 'center', gap: 3, color: PM.ink, fontSize: 12, fontFamily: TM.sans, fontWeight: 500 }}>
                      <MIco name="star" size={12} color={PM.accent}/>
                      {p.rating}
                    </div>
                  ) : null}
                </div>
                <div style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted, marginTop: 4 }}>
                  {p.bedrooms} bed · {p.baths} bath · sleeps {p.sleeps}
                </div>
                <div style={{ display: 'flex', alignItems: 'baseline', gap: 4, marginTop: 10 }}>
                  <span style={{ fontFamily: TM.serif, fontSize: 22, color: PM.ink, fontWeight: 500 }}>${p.priceFrom}</span>
                  <span style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted }}>/ night</span>
                </div>
              </div>
            </button>
          ))}
        </div>
      </div>

      {/* ── DYRA PROMISE ─────────────────────────────────────────
           Same six pillars as the desktop home, restyled as a vertical
           list of editorial cards. Icon → headline → body. Anchored on
           the dark ink background to read as the brand commitment. */}
      <div style={{
        margin: '36px 16px 0',
        background: PM.ink, color: PM.bg,
        borderRadius: 18, padding: '24px 22px 26px',
      }}>
        <MEyebrow color={PM.accentSoft}>The Dyra Promise</MEyebrow>
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 22 }}>
          {[
            { ico: 'pin',     t: 'Unmatched location',         s: 'Every home is walking distance to 770, close to mikvah, shuls, and Kingston.' },
            { ico: 'flame',   t: 'Kosher kitchen',             s: 'Every home is strictly kosher. Separate meat and dairy \u2014 sinks, counters, dishes, silverware.' },
            { ico: 'candle',  t: 'Shabbos-ready',              s: 'Every home comes with an urn, a blech / hot plate, and Shabbos lamps ready to go.' },
            { ico: 'home',    t: 'Elevated comfort',           s: 'Freshly-washed linens, plush towels, full-sized appliances, and thoughtful details. Designed to feel like home, only better.' },
            { ico: 'sparkle', t: 'Fast, responsive support',   s: 'A real person on WhatsApp. Most messages answered in under ten minutes \u2014 before, during, and after your stay.' },
            { ico: 'shield',  t: 'Reliable every time',        s: 'Same standard, every home. Every turnover. No surprises on arrival, no improvisation \u2014 just a place that\u2019s ready for you.' },
          ].map((x, i) => (
            <div key={i}>
              <div style={{ color: PM.accent, marginBottom: 10 }}>
                <MIco name={x.ico} size={20} color={PM.accent}/>
              </div>
              <h3 style={{
                fontFamily: TM.serif, fontSize: 19, fontWeight: 500, margin: '0 0 6px',
                letterSpacing: '-0.01em', color: PM.bg, lineHeight: 1.2,
              }}>{x.t}</h3>
              <p style={{
                fontFamily: TM.sans, fontSize: 13, lineHeight: 1.55,
                color: PM.bg, opacity: 0.78, margin: 0,
              }}>{x.s}</p>
            </div>
          ))}
        </div>
      </div>

      {/* ── CONTACT FORM ─────────────────────────────────────────
           Same role as the desktop home's contact block: send a
           message, populates admin Inquiries via DyraStore. */}
      <div style={{ marginTop: 36 }}>
        <MContactForm/>
      </div>

      {/* ── CONTACT FOOTER ──────────────────────────────────────── */}
      <div style={{
        margin: '40px 16px 0', padding: '24px 0 0',
        borderTop: `0.5px solid ${PM.line}`,
      }}>
        <MEyebrow>Talk to us</MEyebrow>
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 10 }}>
          <a href="https://wa.me/18625207797" style={{
            display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px',
            background: PM.surface, border: `0.5px solid ${PM.line}`, borderRadius: 12,
            color: PM.ink, textDecoration: 'none',
          }}>
            <span style={{
              width: 36, height: 36, borderRadius: 18, background: '#25D366',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}><MIco name="whatsapp" size={20} color="#fff"/></span>
            <div style={{ flex: 1 }}>
              <div style={{ fontFamily: TM.sans, fontSize: 13, color: PM.muted }}>WhatsApp</div>
              <div style={{ fontFamily: TM.sans, fontSize: 15, color: PM.ink, fontWeight: 500 }}>862-520-7797</div>
            </div>
            <MIco name="forward" size={18} color={PM.muted}/>
          </a>
          <a href="mailto:dyrakosherrentals@gmail.com" style={{
            display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px',
            background: PM.surface, border: `0.5px solid ${PM.line}`, borderRadius: 12,
            color: PM.ink, textDecoration: 'none',
          }}>
            <span style={{
              width: 36, height: 36, borderRadius: 18, background: PM.accent,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            }}><MIco name="mail" size={18} color="#fff"/></span>
            <div style={{ flex: 1 }}>
              <div style={{ fontFamily: TM.sans, fontSize: 13, color: PM.muted }}>Email</div>
              <div style={{ fontFamily: TM.sans, fontSize: 14, color: PM.ink, fontWeight: 500 }}>dyrakosherrentals@gmail.com</div>
            </div>
            <MIco name="forward" size={18} color={PM.muted}/>
          </a>
        </div>
        <div style={{
          padding: '32px 0 24px',
          fontFamily: TM.sans, fontSize: 11, color: PM.muted, letterSpacing: '0.04em',
          textAlign: 'center',
        }}>
          Dyra Kosher Rentals · Crown Heights, Brooklyn
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════
// Date helpers — used by both the homepage search sheet AND the inline
// listing date picker. Dates flow as ISO yyyy-mm-dd strings throughout so
// they round-trip cleanly through the URL query string.
// ═══════════════════════════════════════════════════════════════════════
const ISO_FMT = (d) => {
  const y = d.getFullYear(), m = String(d.getMonth()+1).padStart(2,'0'), dd = String(d.getDate()).padStart(2,'0');
  return `${y}-${m}-${dd}`;
};
const PARSE_ISO = (iso) => {
  if (!iso) return null;
  const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
  if (!m) return null;
  return new Date(Number(m[1]), Number(m[2])-1, Number(m[3]));
};
const FRIENDLY_DATE = (iso) => {
  const d = PARSE_ISO(iso);
  if (!d) return '';
  return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
};

// Read previously-chosen dates/guests from sessionStorage so they survive
// hash navigations on the same tab.
function readStickySearch() {
  try {
    const raw = sessionStorage.getItem('dyra.search');
    if (!raw) return null;
    return JSON.parse(raw);
  } catch (e) { return null; }
}
function writeStickySearch(s) {
  try { sessionStorage.setItem('dyra.search', JSON.stringify(s)); } catch (e) {}
}

// ═══════════════════════════════════════════════════════════════════════
// SEARCH SHEET — full-bleed bottom sheet
// ═══════════════════════════════════════════════════════════════════════
function MSearchSheet({ open, onClose, go }) {
  const [step, setStep] = useState('dates'); // dates | guests
  // Default to no dates so the calendar starts CLEAN — the user's first tap
  // becomes check-in, second tap becomes check-out (the previous build
  // pre-highlighted May 8–11, which then stayed highlighted alongside the
  // user's pick).
  const sticky = useMemo(() => readStickySearch(), []);
  const [dates, setDates] = useState(() => sticky?.dates || { checkIn: '', checkOut: '' });
  const [guests, setGuests] = useState(() => sticky?.guests || { adults: 2, children: 0, infants: 0 });
  if (!open) return null;
  const totalGuests = guests.adults + guests.children;
  const sleepsNeeded = guests.adults + guests.children; // infants don't count toward sleeps

  function onSearch() {
    const params = new URLSearchParams();
    if (dates.checkIn)  params.set('checkIn', dates.checkIn);
    if (dates.checkOut) params.set('checkOut', dates.checkOut);
    params.set('guests', String(sleepsNeeded));
    writeStickySearch({ dates, guests });
    onClose();
    // Browse will read the URL params via window.location.search.
    window.location.hash = `browse?${params.toString()}`;
  }

  return (
    <MSheet open={open} onClose={onClose} title="Find your stay" height="92%">
      {/* segmented stepper */}
      <div style={{ display: 'flex', gap: 6, padding: '12px 20px 4px' }}>
        {['dates', 'guests'].map(s => (
          <button key={s} onClick={() => setStep(s)} style={{
            flex: 1, padding: '8px 0', borderRadius: 8,
            background: step === s ? PM.ink : 'transparent',
            color: step === s ? PM.bg : PM.muted,
            border: step === s ? 'none' : `0.5px solid ${PM.line}`,
            fontFamily: TM.sans, fontSize: 12, fontWeight: 500,
            textTransform: 'capitalize', letterSpacing: '0.04em',
            cursor: 'pointer',
          }}>{s}</button>
        ))}
      </div>

      {/* Step bodies */}
      <div style={{ padding: '20px 20px 80px' }}>
        {step === 'dates' && <MMiniCalendar dates={dates} setDates={setDates}/>}
        {step === 'guests' && <MGuestPicker guests={guests} setGuests={setGuests}/>}
      </div>

      {/* sticky footer */}
      <div style={{
        position: 'absolute', bottom: 0, left: 0, right: 0,
        padding: '14px 20px 28px',
        background: PM.bg,
        borderTop: `0.5px solid ${PM.line}`,
        display: 'flex', gap: 10, alignItems: 'center',
      }}>
        <button onClick={() => { setDates({ checkIn: '', checkOut: '' }); setGuests({adults: 2, children: 0, infants: 0}); }} style={{
          background: 'transparent', border: 'none',
          fontFamily: TM.sans, fontSize: 14, color: PM.muted, fontWeight: 500,
          padding: '0 8px', cursor: 'pointer',
        }}>Clear</button>
        <div style={{ flex: 1 }}>
          <MButton onClick={onSearch}>
            <span style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8 }}>
              <MIco name="search" size={18} color="#fff"/>
              Search {totalGuests} guest{totalGuests !== 1 ? 's' : ''}
            </span>
          </MButton>
        </div>
      </div>
    </MSheet>
  );
}

// Hebrew month length table + Aleph–Lamed letter labels for day numbers.
// Anchored at 2026-05-14 = 26 Iyar 5786; the calendar walks forward/backward
// to roll across month boundaries so every Gregorian date in the picker has
// a Hebrew equivalent (not just the current month).
const M_HEB_MONTHS = [
  { name: 'Tishrei',  len: 30 }, { name: 'Cheshvan', len: 29 }, { name: 'Kislev',  len: 30 },
  { name: 'Tevet',    len: 29 }, { name: 'Shvat',    len: 30 }, { name: 'Adar',    len: 29 },
  { name: 'Nisan',    len: 30 }, { name: 'Iyar',     len: 29 }, { name: 'Sivan',   len: 30 },
  { name: 'Tammuz',   len: 29 }, { name: 'Av',       len: 30 }, { name: 'Elul',    len: 29 },
];
const M_HEB_LETTER_DAY = [
  '', 'א','ב','ג','ד','ה','ו','ז','ח','ט','י',
  'יא','יב','יג','יד','טו','טז','יז','יח','יט','כ',
  'כא','כב','כג','כד','כה','כו','כז','כח','כט','ל',
];
function mHebrewForISO(iso) {
  if (!iso) return null;
  const anchor = new Date('2026-05-14T00:00:00');
  const d = new Date(iso + 'T00:00:00');
  let dayOffset = Math.round((d - anchor) / 86400000);
  let monthIdx = 7;          // Iyar
  let dayInMonth = 26 + dayOffset;
  while (dayInMonth > M_HEB_MONTHS[monthIdx].len) {
    dayInMonth -= M_HEB_MONTHS[monthIdx].len;
    monthIdx = (monthIdx + 1) % 12;
  }
  while (dayInMonth < 1) {
    monthIdx = (monthIdx - 1 + 12) % 12;
    dayInMonth += M_HEB_MONTHS[monthIdx].len;
  }
  return {
    monthName: M_HEB_MONTHS[monthIdx].name,
    monthIdx,
    day: dayInMonth,
    letter: M_HEB_LETTER_DAY[dayInMonth] || String(dayInMonth),
  };
}

// ─── Mini calendar — multi-month, click first date = check-in, click second
// date = check-out, click again = restart. Default is EMPTY (no pre-fill)
// so the user's selection is the only thing highlighted.
//
// `blockedRanges` (optional) is a list of {start, end} ISO date pairs marking
// days that are already booked or admin-blocked. The picker greys those days
// out, refuses to start a check-in on them, and refuses to span across one
// while picking a check-out.
function MMiniCalendar({ dates, setDates, onPick, blockedRanges, minNights }) {
  const today = useMemo(() => { const d = new Date(); d.setHours(0,0,0,0); return d; }, []);
  const initial = PARSE_ISO(dates.checkIn) || today;
  const [cursor, setCursor] = useState(() => new Date(initial.getFullYear(), initial.getMonth(), 1));
  const [warning, setWarning] = useState('');

  const monthName = cursor.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
  const firstWeekday = cursor.getDay(); // 0=Sun
  const lastDay = new Date(cursor.getFullYear(), cursor.getMonth()+1, 0).getDate();

  const startISO = dates.checkIn;
  const endISO   = dates.checkOut;
  const minN = Math.max(1, Number(minNights) || 1);

  // 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 should be disabled.
  const minCheckoutISO = useMemo(() => {
    if (!startISO || endISO) return null;
    const a = new Date(startISO + 'T00:00:00');
    a.setDate(a.getDate() + minN);
    return a.toISOString().slice(0, 10);
  }, [startISO, endISO, minN]);

  // Expand blocked ranges into a Set of unavailable ISO dates. Each range is
  // half-open [start, end) — the check-out day is free for the next guest.
  const blockedSet = useMemo(() => {
    const s = new Set();
    (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)) {
        s.add(d.toISOString().slice(0, 10));
      }
    });
    return s;
  }, [blockedRanges]);

  function rangeContainsBlocked(fromISO, toISO) {
    if (!fromISO || !toISO) return false;
    const a = new Date(fromISO + 'T00:00:00');
    const b = new Date(toISO + 'T00:00:00');
    for (let d = new Date(a); d < b; d.setDate(d.getDate() + 1)) {
      if (blockedSet.has(d.toISOString().slice(0, 10))) return true;
    }
    return false;
  }

  function pickDay(iso) {
    setWarning('');
    if (blockedSet.has(iso)) return; // can't start a stay on a booked day
    if (!startISO || (startISO && endISO)) {
      // first tap (or restart): set check-in, clear check-out
      setDates({ checkIn: iso, checkOut: '' });
      onPick && onPick({ checkIn: iso, checkOut: '' });
      return;
    }
    // second tap: pick check-out, but if user tapped the same day or earlier,
    // treat it as restarting check-in.
    if (iso <= startISO) {
      setDates({ checkIn: iso, checkOut: '' });
      onPick && onPick({ checkIn: iso, checkOut: '' });
      return;
    }
    // Enforce minimum-night stay before committing the checkout. The cell is
    // also visually disabled, but a defensive check here prevents accidental
    // commits if a guest somehow taps through it (e.g. screen-reader path).
    const proposedNights = Math.round(
      (new Date(iso + 'T00:00:00') - new Date(startISO + 'T00:00:00')) / 86400000
    );
    if (proposedNights < minN) {
      setWarning(`Minimum stay is ${minN} night${minN === 1 ? '' : 's'}.`);
      return;
    }
    // Refuse to span across a blocked day.
    if (rangeContainsBlocked(startISO, iso)) {
      setDates({ checkIn: iso, checkOut: '' });
      onPick && onPick({ checkIn: iso, checkOut: '' });
      return;
    }
    setDates({ checkIn: startISO, checkOut: iso });
    onPick && onPick({ checkIn: startISO, checkOut: iso });
  }

  // Day-of-week headers — Sunday-first to match the cell layout below.
  const dow = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];

  function shiftMonth(delta) {
    setCursor(c => new Date(c.getFullYear(), c.getMonth() + delta, 1));
  }
  const prevDisabled = cursor.getFullYear() < today.getFullYear() ||
    (cursor.getFullYear() === today.getFullYear() && cursor.getMonth() <= today.getMonth());

  return (
    <div>
      <div style={{ fontFamily: TM.sans, fontSize: 13, color: PM.muted, letterSpacing: '0.06em', textTransform: 'uppercase', marginBottom: 12 }}>
        Check-in / check-out
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginBottom: 24 }}>
        <div style={{ padding: '14px 14px', borderRadius: 12, border: `1.5px solid ${startISO ? PM.accent : PM.line}` }}>
          <div style={{ fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase', color: PM.muted }}>Check-in</div>
          <div style={{ fontFamily: TM.serif, fontSize: 20, color: PM.ink, marginTop: 2 }}>{FRIENDLY_DATE(startISO) || '—'}</div>
        </div>
        <div style={{ padding: '14px 14px', borderRadius: 12, border: `1.5px solid ${endISO ? PM.accent : PM.line}` }}>
          <div style={{ fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase', color: PM.muted }}>Check-out</div>
          <div style={{ fontFamily: TM.serif, fontSize: 20, color: PM.ink, marginTop: 2 }}>{FRIENDLY_DATE(endISO) || '—'}</div>
        </div>
      </div>

      {/* Month header w/ working prev/next nav */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 12 }}>
        <button onClick={() => !prevDisabled && shiftMonth(-1)} disabled={prevDisabled}
          aria-label="Previous month"
          style={{
            width: 36, height: 36, borderRadius: 18, border: `1px solid ${PM.line}`,
            background: 'transparent', color: prevDisabled ? PM.lineSoft : PM.ink,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: prevDisabled ? 'default' : 'pointer',
          }}><MIco name="back" size={16}/></button>
        <div style={{ fontFamily: TM.serif, fontSize: 18, color: PM.ink, fontWeight: 500 }}>{monthName}</div>
        <button onClick={() => shiftMonth(1)} aria-label="Next month"
          style={{
            width: 36, height: 36, borderRadius: 18, border: `1px solid ${PM.line}`,
            background: 'transparent', color: PM.ink,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            cursor: 'pointer',
          }}><MIco name="forward" size={16}/></button>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4, marginBottom: 4 }}>
        {dow.map((d, i) => (
          <div key={i} style={{ textAlign: 'center', fontSize: 10, fontFamily: TM.sans, color: PM.muted, letterSpacing: '0.1em' }}>{d}</div>
        ))}
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4 }}>
        {Array.from({ length: 42 }).map((_, i) => {
          const dayNum = i - firstWeekday + 1;
          const valid = dayNum >= 1 && dayNum <= lastDay;
          if (!valid) return <div key={i}/>;
          const cellDate = new Date(cursor.getFullYear(), cursor.getMonth(), dayNum);
          const cellISO = ISO_FMT(cellDate);
          const past = cellDate < today;
          const isCheckIn  = cellISO === startISO;
          const isCheckOut = cellISO === endISO;
          const inRange = startISO && endISO && cellISO > startISO && cellISO < endISO;
          const isShabbos = cellDate.getDay() === 6; // Saturday
          const isBlocked = blockedSet.has(cellISO);
          // While picking checkout (start set, end not), grey out any day that
          // falls inside the [start, start+minNights) window — those cannot
          // produce a legal stay. The check-in day itself stays selected, not
          // disabled.
          const violatesMinStay =
            !!minCheckoutISO &&
            cellISO > startISO &&
            cellISO < minCheckoutISO;
          const unavailable = past || isBlocked || violatesMinStay;
          const heb = mHebrewForISO(cellISO);
          return (
            <button
              key={i}
              onClick={() => !unavailable && pickDay(cellISO)}
              disabled={unavailable}
              title={
                isBlocked
                  ? 'Booked'
                  : violatesMinStay
                    ? `Minimum stay is ${minN} night${minN === 1 ? '' : 's'}`
                    : undefined
              }
              style={{
                aspectRatio: '1', border: 'none',
                // Booked days get a soft red wash AND a diagonal stroke so
                // they read as "unavailable" at a glance, even before the
                // strikethrough on the number itself.
                background: isCheckIn || isCheckOut
                  ? PM.accent
                  : inRange
                    ? PM.accentSoft
                    : isBlocked && !past
                      ? 'repeating-linear-gradient(135deg, rgba(193,72,49,0.08) 0px, rgba(193,72,49,0.08) 4px, rgba(193,72,49,0.18) 4px, rgba(193,72,49,0.18) 8px)'
                      : 'transparent',
                color: isCheckIn || isCheckOut ? '#fff' : unavailable ? PM.lineSoft : PM.ink,
                fontFamily: TM.sans, fontSize: 13,
                fontWeight: isCheckIn || isCheckOut ? 600 : 400,
                borderRadius: 8,
                cursor: unavailable ? 'default' : 'pointer',
                position: 'relative',
                opacity: past ? 0.45 : isBlocked ? 0.7 : 1,
                textDecoration: isBlocked && !past ? 'line-through' : 'none',
                textDecorationColor: isBlocked && !past ? '#c14831' : undefined,
                textDecorationThickness: isBlocked && !past ? 2 : undefined,
                display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center',
                gap: 1,
              }}>
              <span style={{ lineHeight: 1 }}>{dayNum}</span>
              {heb && (
                <span style={{
                  fontFamily: 'Frank Ruhl Libre, Noto Serif Hebrew, serif',
                  fontSize: 9,
                  direction: 'rtl',
                  opacity: isCheckIn || isCheckOut ? 0.92 : 0.55,
                  lineHeight: 1,
                }}>{heb.letter}</span>
              )}
              {isShabbos && !isCheckIn && !isCheckOut && (
                <span style={{
                  position: 'absolute', bottom: 2, left: '50%', transform: 'translateX(-50%)',
                  width: 4, height: 4, borderRadius: 2, background: PM.shabbos,
                }}/>
              )}
            </button>
          );
        })}
      </div>
      {warning ? (
        <div style={{
          marginTop: 12, padding: '8px 10px', borderRadius: 8,
          background: 'rgba(193,72,49,0.08)', color: PM.accent,
          fontFamily: TM.sans, fontSize: 12,
        }}>{warning}</div>
      ) : null}
      <div style={{ marginTop: 16, fontFamily: TM.sans, fontSize: 11, color: PM.muted, display: 'flex', flexDirection: 'column', gap: 4 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
          <span style={{ width: 6, height: 6, borderRadius: 3, background: PM.shabbos, display: 'inline-block' }}/>
          Shabbos · candle-lighting times shown on listing
        </div>
        {minN > 1 && (
          <div>Minimum stay: {minN} nights</div>
        )}
        {blockedSet.size > 0 && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ display: 'inline-block', width: 16, color: PM.lineSoft, textDecoration: 'line-through', textAlign: 'center' }}>15</span>
            Booked or held — not available
          </div>
        )}
      </div>
    </div>
  );
}

function MGuestPicker({ guests, setGuests }) {
  const rows = [
    { id: 'adults',   label: 'Adults',   sub: 'Ages 13+',          min: 1 },
    { id: 'children', label: 'Children', sub: 'Ages 2–12',         min: 0 },
    { id: 'infants',  label: 'Infants',  sub: 'Under 2 · don\'t count toward sleeps', min: 0 },
  ];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
      <div style={{ fontFamily: TM.sans, fontSize: 13, color: PM.muted, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
        Who's coming?
      </div>
      {rows.map(r => (
        <div key={r.id} style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          padding: '16px 0', borderBottom: `0.5px solid ${PM.lineSoft}`,
        }}>
          <div>
            <div style={{ fontFamily: TM.sans, fontSize: 16, color: PM.ink, fontWeight: 500 }}>{r.label}</div>
            <div style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted, marginTop: 2 }}>{r.sub}</div>
          </div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
            <button
              onClick={() => setGuests(g => ({ ...g, [r.id]: Math.max(r.min, g[r.id] - 1) }))}
              disabled={guests[r.id] <= r.min}
              style={{
                width: 36, height: 36, borderRadius: 18,
                border: `1px solid ${guests[r.id] <= r.min ? PM.lineSoft : PM.line}`,
                background: 'transparent',
                color: guests[r.id] <= r.min ? PM.lineSoft : PM.ink,
                cursor: guests[r.id] <= r.min ? 'default' : 'pointer',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}><MIco name="minus" size={16}/></button>
            <span style={{ fontFamily: TM.sans, fontSize: 18, color: PM.ink, minWidth: 24, textAlign: 'center', fontWeight: 500 }}>
              {guests[r.id]}
            </span>
            <button
              onClick={() => setGuests(g => ({ ...g, [r.id]: g[r.id] + 1 }))}
              style={{
                width: 36, height: 36, borderRadius: 18,
                border: `1px solid ${PM.line}`,
                background: 'transparent', color: PM.ink, cursor: 'pointer',
                display: 'flex', alignItems: 'center', justifyContent: 'center',
              }}><MIco name="plus" size={16}/></button>
          </div>
        </div>
      ))}
    </div>
  );
}

function MTierPicker({ tier, setTier }) {
  const tiers = [
    { id: 'any',      label: 'Any tier',   sub: 'Show everything we have' },
    { id: 'value',    label: 'Value',      sub: '$200–$300/night · clean & kosher' },
    { id: 'standard', label: 'Standard',   sub: '$275–$450/night · most popular' },
    { id: 'premium',  label: 'Premium',    sub: '$445–$650/night · top of inventory' },
    { id: 'luxury',   label: 'Luxury',     sub: '$1,000+/night · The Presidential' },
  ];
  return (
    <div>
      <div style={{ fontFamily: TM.sans, fontSize: 13, color: PM.muted, letterSpacing: '0.06em', textTransform: 'uppercase', marginBottom: 12 }}>
        What level?
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {tiers.map(t => (
          <button key={t.id} onClick={() => setTier(t.id)} style={{
            padding: '14px 16px', borderRadius: 12,
            border: `1.5px solid ${tier === t.id ? PM.accent : PM.line}`,
            background: tier === t.id ? PM.accentSoft : PM.surface,
            display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
            cursor: 'pointer', textAlign: 'left',
          }}>
            <div>
              <div style={{ fontFamily: TM.serif, fontSize: 17, color: PM.ink, fontWeight: 500 }}>{t.label}</div>
              <div style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted, marginTop: 2 }}>{t.sub}</div>
            </div>
            {tier === t.id && (
              <span style={{
                width: 24, height: 24, borderRadius: 12, background: PM.accent,
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
              }}><MIco name="check" size={14} color="#fff"/></span>
            )}
          </button>
        ))}
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════════════════
// BROWSE / SEARCH RESULTS
// ═══════════════════════════════════════════════════════════════════════
function MBrowse({ go, openMenu }) {
  const [scrolled, setScrolled] = useState(false);
  const [filter, setFilter] = useState('all');
  // Default = Grid, toggle to Map. Stored as the same 'list'/'map' values
  // the rest of the file uses; "Grid" is the user-facing label for the
  // stacked-card layout that follows mobile listing convention.
  const [view, setView] = useState('list');
  const props = window.DyraStore?.state?.properties?.filter(p => p.status === 'active') || window.DYRA?.PROPERTIES || [];
  // Search context lives on the hash query string ("#browse?checkIn=…&guests=…").
  // Falling back to sessionStorage means re-loads keep the user's pick.
  const search = useMemo(() => {
    const fromHash = (() => {
      const h = window.location.hash || '';
      const qi = h.indexOf('?');
      if (qi < 0) return null;
      const sp = new URLSearchParams(h.slice(qi+1));
      return {
        checkIn: sp.get('checkIn') || '',
        checkOut: sp.get('checkOut') || '',
        guests: Number(sp.get('guests') || 0) || 0,
      };
    })();
    if (fromHash && (fromHash.checkIn || fromHash.guests)) return fromHash;
    const sticky = readStickySearch();
    if (sticky) {
      const total = (sticky.guests?.adults || 0) + (sticky.guests?.children || 0);
      return { checkIn: sticky.dates?.checkIn || '', checkOut: sticky.dates?.checkOut || '', guests: total };
    }
    return { checkIn: '', checkOut: '', guests: 0 };
  }, []);

  const filters = [
    { id: 'all',   label: 'All' },
    { id: 'big',   label: '4+ bedrooms' },
    { id: 'small', label: '1–2 bed' },
    { id: 'lux',   label: 'Luxury' },
  ];
  const filtered = useMemo(() => {
    let list = props;
    // Sleeps gate — if the user told us how many guests, only show homes
    // that fit them.
    if (search.guests > 0) list = list.filter(p => (p.sleeps || 0) >= search.guests);
    // Calendar availability gate — drop homes whose existing reservations or
    // blockedRanges conflict with the requested window.
    if (search.checkIn && search.checkOut && window.DyraStore?.isPropertyAvailable) {
      list = list.filter(p =>
        window.DyraStore.isPropertyAvailable(p.id, search.checkIn, search.checkOut)
      );
    }
    if (filter === 'all')   return list;
    if (filter === 'big')   return list.filter(p => p.bedrooms >= 4);
    if (filter === 'small') return list.filter(p => p.bedrooms <= 2);
    if (filter === 'lux')   return list.filter(p => p.tier === 'luxury' || p.tier === 'premium');
    return list;
  }, [filter, props, search.guests, search.checkIn, search.checkOut]);

  // Build the listing-link target so dates carry into each detail page.
  const detailQS = (() => {
    const sp = new URLSearchParams();
    if (search.checkIn)  sp.set('checkIn', search.checkIn);
    if (search.checkOut) sp.set('checkOut', search.checkOut);
    if (search.guests)   sp.set('guests', String(search.guests));
    const s = sp.toString();
    return s ? `?${s}` : '';
  })();

  return (
    <div style={{ height: '100%', display: 'flex', flexDirection: 'column', background: PM.bg }}>
      <div onScroll={(e) => setScrolled(e.target.scrollTop > 4)}
        style={{ flex: 1, overflow: 'auto' }}>
        <MAppBar
          scrolled={scrolled}
          title="Browse stays"
          leading={<MIconBtn label="Menu" onClick={openMenu}><MIco name="menu" size={22}/></MIconBtn>}
          trailing={<MIconBtn label="Toggle map" onClick={() => setView(v => v === 'list' ? 'map' : 'list')}>
            <MIco name={view === 'list' ? 'pin' : 'menu'} size={22}/>
          </MIconBtn>}
        />

        {/* search recap card */}
        <div style={{ padding: '16px 16px 4px' }}>
          <button onClick={() => { window.location.hash = 'home'; }} style={{
            width: '100%', padding: '12px 16px',
            background: PM.surface, border: `0.5px solid ${PM.line}`,
            borderRadius: 14,
            display: 'flex', alignItems: 'center', gap: 10,
            cursor: 'pointer', textAlign: 'left',
          }}>
            <MIco name="search" size={18} color={PM.muted}/>
            <div style={{ flex: 1 }}>
              <div style={{ fontFamily: TM.sans, fontSize: 14, color: PM.ink, fontWeight: 500 }}>
                {search.checkIn && search.checkOut
                  ? `${FRIENDLY_DATE(search.checkIn)} – ${FRIENDLY_DATE(search.checkOut)}`
                  : 'Any dates'} · {search.guests || 'Any'} guest{search.guests === 1 ? '' : 's'}
              </div>
              <div style={{ fontFamily: TM.sans, fontSize: 11, color: PM.muted, marginTop: 1 }}>
                Crown Heights
              </div>
            </div>
            <span style={{ color: PM.accent, fontFamily: TM.sans, fontSize: 12, fontWeight: 500 }}>Edit</span>
          </button>
        </div>

        {/* filter chips */}
        <div style={{
          display: 'flex', gap: 8, padding: '12px 16px',
          overflowX: 'auto', WebkitOverflowScrolling: 'touch',
        }}>
          {filters.map(f => {
            const active = filter === f.id;
            return (
              <button key={f.id} onClick={() => setFilter(f.id)} style={{
                padding: '8px 14px', borderRadius: 100,
                background: active ? PM.ink : PM.surface,
                color: active ? PM.bg : PM.ink,
                border: active ? 'none' : `0.5px solid ${PM.line}`,
                fontFamily: TM.sans, fontSize: 13, fontWeight: 500,
                whiteSpace: 'nowrap', flexShrink: 0,
                cursor: 'pointer',
              }}>{f.label}</button>
            );
          })}
        </div>

        {/* Grid / Map toggle + count */}
        <div style={{ padding: '4px 20px 12px', display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
          <div style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted }}>
            {filtered.length} home{filtered.length !== 1 ? 's' : ''} · sorted by closest to 770
          </div>
          <div style={{ display: 'flex', background: PM.surface, border: `0.5px solid ${PM.line}`, borderRadius: 100, padding: 2 }}>
            {[['list', 'Grid'], ['map', 'Map']].map(([v, label]) => {
              const active = view === v;
              return (
                <button key={v} onClick={() => setView(v)} style={{
                  padding: '6px 14px', borderRadius: 100,
                  background: active ? PM.ink : 'transparent',
                  color: active ? PM.bg : PM.ink,
                  border: 'none',
                  fontFamily: TM.sans, fontSize: 12, fontWeight: 500,
                  cursor: 'pointer',
                }}>{label}</button>
              );
            })}
          </div>
        </div>

        {view === 'map' ? (
          <div style={{ padding: '0 16px 24px' }}>
            {window.DyraListingsMap ? (
              <window.DyraListingsMap
                listings={filtered}
                palette={{ ink: PM.ink, accent: PM.ink, surface: PM.surface, muted: PM.muted, line: PM.line }}
                height={480}
                onSelect={(p) => { window.location.hash = `listing:${p.id}${detailQS}`; }}
              />
            ) : (
              <div style={{ padding: 24, textAlign: 'center', color: PM.muted, fontFamily: TM.sans, fontSize: 12 }}>
                Loading map…
              </div>
            )}
            <div style={{ marginTop: 10, fontFamily: TM.sans, fontSize: 11, color: PM.muted, textAlign: 'center', lineHeight: 1.4 }}>
              Pins show approximate locations within ~25m to protect host privacy. Exact addresses are shared after booking.
            </div>
          </div>
        ) : (
          <div style={{ padding: '0 16px 24px', display: 'flex', flexDirection: 'column', gap: 16 }}>
            {filtered.map(p => (
              <button key={p.id} onClick={() => {
                // Persist current search dates/guests into the listing URL
                // so the listing detail page lands on those dates.
                window.location.hash = `listing:${p.id}${detailQS}`;
              }} style={{
                background: PM.surface,
                border: `0.5px solid ${PM.lineSoft}`,
                borderRadius: 16, overflow: 'hidden',
                cursor: 'pointer', textAlign: 'left', padding: 0,
                fontFamily: 'inherit', WebkitTapHighlightColor: 'transparent',
              }}>
                <div style={{ position: 'relative' }}>
                  <MPhoto property={p} idx={0} style={{ width: '100%', height: 240 }}/>
                  <div style={{
                    position: 'absolute', top: 12, left: 12,
                    background: 'rgba(20,15,10,0.7)',
                    backdropFilter: 'blur(8px)',
                    color: '#fff', fontFamily: TM.sans, fontSize: 10,
                    letterSpacing: '0.16em', textTransform: 'uppercase',
                    padding: '5px 10px', borderRadius: 100,
                  }}>
                    {p.cardEyebrow || (p.walkMinTo770 ? `${p.walkMinTo770} min walk to 770` : 'Walking distance to 770')}
                  </div>
                  {/* Heart / wishlist removed per user feedback. */}
                </div>
                <div style={{ padding: 14 }}>
                  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8 }}>
                    <div style={{ fontFamily: TM.serif, fontSize: 18, color: PM.ink, fontWeight: 500, lineHeight: 1.15, flex: 1 }}>
                      {p.name}
                    </div>
                    {p.rating && p.reviews > 0 ? (
                      <div style={{ display: 'flex', alignItems: 'center', gap: 3, fontSize: 12, fontFamily: TM.sans, color: PM.ink, fontWeight: 500 }}>
                        <MIco name="star" size={12} color={PM.accent}/>
                        {p.rating}
                        <span style={{ color: PM.muted, marginLeft: 2 }}>({p.reviews})</span>
                      </div>
                    ) : null}
                  </div>
                  <div style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted, marginTop: 4 }}>
                    {p.bedrooms} bed · {p.baths} bath · sleeps {p.sleeps} · {p.mikvah} mi to mikvah
                  </div>
                  <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginTop: 12 }}>
                    <div style={{ display: 'flex', alignItems: 'baseline', gap: 4 }}>
                      <span style={{ fontFamily: TM.serif, fontSize: 22, color: PM.ink, fontWeight: 500 }}>${p.priceFrom}</span>
                      <span style={{ fontFamily: TM.sans, fontSize: 12, color: PM.muted }}>/ night</span>
                    </div>
                    <span style={{ fontFamily: TM.sans, fontSize: 12, color: PM.accent, fontWeight: 500 }}>View →</span>
                  </div>
                </div>
              </button>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

Object.assign(window, {
  MHome, MSearchSheet, MBrowse, MPhoto, MMiniCalendar,
  // Date helpers used by m-screens-2 (listing inline picker).
  DYRA_ISO_FMT: ISO_FMT, DYRA_PARSE_ISO: PARSE_ISO, DYRA_FRIENDLY_DATE: FRIENDLY_DATE,
  readDyraStickySearch: readStickySearch, writeDyraStickySearch: writeStickySearch,
});
