// Direction 1 — Booking flow, Upsells, Dashboard, Admin

function D1Booking({ p, serif, sans, hebFont, property, step, setStep, goData, go }) {
  if (!property) return null;
  const steps = ['Dates & guests', 'Review & pay'];
  // Clamp legacy step values (was 0-3, now 0-1)
  if (step > 1) { setStep(1); }
  const [notes, setNotes] = React.useState('');
  const [pay, setPay] = React.useState({ card: '', name: '', email: '', phone: '' });
  const [agreed, setAgreed] = React.useState({ kosher: false, respect: false, quiet: false, damage: false, terminate: false, ack: false });
  const [processing, setProcessing] = React.useState(false);
  const [discountCode, setDiscountCode] = React.useState('');
  const [appliedOffer, setAppliedOffer] = React.useState(null);
  const [discountStatus, setDiscountStatus] = React.useState(''); // '', 'invalid', 'applied'

  // ── Live, editable date selection ──
  // Defaults seed from `goData` (set by the listing page) but the guest can change them
  // freely in the picker. minNights comes from the property config (fallback 2).
  const minNights = property.minNights || 2;
  const [checkIn, setCheckIn]   = React.useState((goData && goData.checkIn)  || '2026-05-14');
  const [checkOut, setCheckOut] = React.useState((goData && goData.checkOut) || '2026-05-17');
  const nights = Math.max(1, Math.round((new Date(checkOut + 'T00:00:00') - new Date(checkIn + 'T00:00:00')) / 86400000));

  // Stable cart-session key, persisted in sessionStorage so refreshes don't fork carts
  const cartKey = React.useMemo(() => {
    try {
      let k = sessionStorage.getItem('dyra.cartKey');
      if (!k) { k = 'cart-' + Math.random().toString(36).slice(2, 10); sessionStorage.setItem('dyra.cartKey', k); }
      return k;
    } catch (e) { return 'cart-' + Math.random().toString(36).slice(2, 10); }
  }, []);

  // Idempotency key for /api/checkout. Single UUID minted once per
  // checkout flow and reused on every retry within that flow — the
  // server returns the same client_secret instead of minting a fresh
  // PaymentIntent on each call. Without this, page refresh / form
  // re-submit / slow Element mount could each create their own PI and
  // multi-charge the same card (see chasya overcharge incident).
  // Cleared on successful confirm so a fresh booking starts fresh.
  const requestId = React.useMemo(() => {
    try {
      const k = `dyra.checkoutRequestId:${(property && property.id) || 'unknown'}`;
      let v = sessionStorage.getItem(k);
      if (!v) {
        v = (typeof crypto !== 'undefined' && crypto.randomUUID)
          ? crypto.randomUUID()
          : ('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx').replace(/[xy]/g, c => {
              const r = Math.random() * 16 | 0;
              return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
            });
        sessionStorage.setItem(k, v);
      }
      return v;
    } catch (e) { return null; }
  }, [property && property.id]);

  // Auto-apply an offer code coming in via URL (?offer=DYRAXXXX)
  React.useEffect(() => {
    try {
      const url = new URL(window.location.href);
      const code = url.searchParams.get('offer');
      if (code && window.DyraStore && DyraStore.findOfferByCode) {
        const offer = DyraStore.findOfferByCode(code);
        if (offer && offer.status !== 'redeemed' && offer.status !== 'expired') {
          setDiscountCode(code.toUpperCase());
          setAppliedOffer(offer);
          setDiscountStatus('applied');
        }
      }
    } catch (e) {}
  }, []);

  const applyDiscountCode = async () => {
    if (!discountCode.trim()) { setDiscountStatus(''); setAppliedOffer(null); return; }
    // 1) Try a local recovery offer first (DYRA-prefixed codes emailed by the recovery flow).
    if (window.DyraStore && DyraStore.findOfferByCode) {
      const offer = DyraStore.findOfferByCode(discountCode);
      if (offer && offer.status !== 'redeemed' && offer.status !== 'expired') {
        setAppliedOffer(offer);
        setDiscountStatus('applied');
        return;
      }
    }
    // 2) Fall back to the backend promo_codes table via /api/promo-validate.
    try {
      const r = await fetch('/api/promo-validate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          code: discountCode,
          property_id: property.id,
          nights,
          subtotal_cents: roomTotal * 100,
        }),
      });
      const data = await r.json();
      if (data.ok) {
        setAppliedOffer({
          code: data.code,
          percent: data.kind === 'percent' ? data.value : 0,
          _backendCents: data.discount_cents,
          _backendKind: data.kind,
        });
        setDiscountStatus('applied');
        return;
      }
    } catch (_) { /* fall through */ }
    setAppliedOffer(null);
    setDiscountStatus('invalid');
  };

  // Gematria captcha — random 4 letters from aleph (1) through tes (9).
  // Guest must enter the matching 4-digit numerical value. 3 wrong attempts = locked out.
  const HEB_LETTERS = [
    { letter: 'א', value: 1 }, { letter: 'ב', value: 2 }, { letter: 'ג', value: 3 },
    { letter: 'ד', value: 4 }, { letter: 'ה', value: 5 }, { letter: 'ו', value: 6 },
    { letter: 'ז', value: 7 }, { letter: 'ח', value: 8 }, { letter: 'ט', value: 9 },
  ];
  const genCaptcha = React.useCallback(() => {
    const out = [];
    for (let i = 0; i < 4; i++) out.push(HEB_LETTERS[Math.floor(Math.random() * 9)]);
    return out;
  }, []);
  const [captcha, setCaptcha] = React.useState(genCaptcha);
  const [captchaInput, setCaptchaInput] = React.useState('');
  const [captchaAttempts, setCaptchaAttempts] = React.useState(0); // wrong-attempt counter
  const [captchaPassed, setCaptchaPassed] = React.useState(false);
  const captchaLocked = captchaAttempts >= 3 && !captchaPassed;
  const captchaAnswer = captcha.map(l => l.value).join('');

  // Check answer when 4 digits are entered
  React.useEffect(() => {
    if (captchaPassed || captchaLocked) return;
    if (captchaInput.length !== 4) return;
    if (captchaInput === captchaAnswer) {
      setCaptchaPassed(true);
    } else {
      setCaptchaAttempts(a => a + 1);
    }
  }, [captchaInput, captchaAnswer, captchaPassed, captchaLocked]);

  // ── Stripe Payment Element ──
  // Creates a PaymentIntent + pending booking server-side as soon as name +
  // email + phone are valid, then mounts Stripe's hosted card form. The
  // "Confirm & pay deposit" button calls stripe.confirmPayment(); the
  // server webhook flips the booking to confirmed + sends the magic link.
  const stripeMountRef = React.useRef(null);
  const [stripeReady, setStripeReady] = React.useState(false);
  const [stripeInstance, setStripeInstance] = React.useState(null);
  const [elementsInstance, setElementsInstance] = React.useState(null);
  const [paymentElementInstance, setPaymentElementInstance] = React.useState(null);
  const [pendingBookingId, setPendingBookingId] = React.useState(null);
  const [pendingClientSecret, setPendingClientSecret] = React.useState(null);
  const [stripeError, setStripeError] = React.useState('');
  const [creatingIntent, setCreatingIntent] = React.useState(false);
  // Server-driven amounts. The server knows whether this PI is a 30%
  // deposit or the full amount today (short-lead OR client-forced for
  // off-session-incompatible PMs). Tile labels and the CTA key off this.
  const [chargeFullAmount, setChargeFullAmount] = React.useState(false);
  const [serverTotalCents, setServerTotalCents] = React.useState(null);
  const [serverDepositCents, setServerDepositCents] = React.useState(null);
  const [selectedPmType, setSelectedPmType] = React.useState('');
  const [forceFullPaymentMode, setForceFullPaymentMode] = React.useState(false);

  const nameOk = pay.name.trim().length > 0;
  const emailOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(pay.email.trim());
  const phoneOk = pay.phone.replace(/\D/g, '').length >= 7;
  const contactReady = nameOk && emailOk && phoneOk;
  // Per-field message so the gate isn't a mystery — tells the guest exactly
  // what's still needed instead of a generic "fill in name, email, phone".
  const missingContactLabel = !nameOk
    ? 'your full name'
    : !emailOk
    ? (pay.email.trim() ? 'a valid email address' : 'your email')
    : !phoneOk
    ? (pay.phone.trim() ? 'a valid phone number (digits only, at least 7)' : 'your phone number')
    : '';

  React.useEffect(() => {
    if (step !== 1) return;
    if (!contactReady) return;
    if (creatingIntent || pendingClientSecret) return;
    let cancelled = false;
    setCreatingIntent(true);
    setStripeError('');
    (async () => {
      try {
        const r = await fetch('/api/checkout', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            mode: 'inline',
            request_id: requestId,
            property_slug: property.id || property.slug,
            check_in: checkIn,
            check_out: checkOut,
            guests_count: (goData && goData.adults) || 2,
            guest_name: pay.name.trim(),
            guest_email: pay.email.trim(),
            guest_phone: pay.phone.trim(),
            notes: notes || null,
            promo_code: (appliedOffer && appliedOffer._backendCents != null) ? appliedOffer.code : null,
            // Tells the server to mint a full-amount PI (no
            // setup_future_usage) so non-card PMs are offered.
            force_full_amount: forceFullPaymentMode,
          }),
        });
        const j = await r.json().catch(() => ({}));
        if (cancelled) return;
        if (!r.ok || !j.client_secret) {
          setStripeError(j.error || 'Could not start checkout — try again.');
          setCreatingIntent(false);
          return;
        }
        const pubKey = j.publishable_key || window.DYRA_STRIPE_PUBLISHABLE_KEY || '';
        if (!window.Stripe || !pubKey) {
          setStripeError('Payments are not configured. Contact support.');
          setCreatingIntent(false);
          return;
        }
        setPendingBookingId(j.booking_id);
        setPendingClientSecret(j.client_secret);
        setChargeFullAmount(!!j.charge_full_amount);
        setServerTotalCents(typeof j.total_cents === 'number' ? j.total_cents : null);
        setServerDepositCents(typeof j.deposit_cents === 'number' ? j.deposit_cents : null);
        const sInst = window.Stripe(pubKey);
        const eInst = sInst.elements({ clientSecret: j.client_secret, appearance: { theme: 'stripe' } });
        const paymentEl = eInst.create('payment', { layout: 'tabs' });
        try {
          paymentEl.on('change', (ev) => {
            const t = (ev && ev.value && ev.value.type) || '';
            setSelectedPmType(String(t || ''));
          });
        } catch (_) {}
        const tryMount = () => {
          if (stripeMountRef.current) {
            paymentEl.mount(stripeMountRef.current);
          } else {
            setTimeout(tryMount, 80);
          }
        };
        tryMount();
        setStripeInstance(sInst);
        setElementsInstance(eInst);
        setPaymentElementInstance(paymentEl);
        setStripeReady(true);
        setCreatingIntent(false);
      } catch (e) {
        if (cancelled) return;
        setStripeError((e && e.message) || 'Payment setup failed.');
        setCreatingIntent(false);
      }
    })();
    return () => { cancelled = true; };
  }, [step, contactReady, checkIn, checkOut, forceFullPaymentMode]);

  // Tear down Stripe Elements so the effect above re-runs and fetches
  // a fresh PaymentIntent (different amount + setup_future_usage).
  // Used by the "Pay full amount" / "Use card instead" prompt below.
  const swapToFullAmountMode = React.useCallback(() => {
    try { if (paymentElementInstance) paymentElementInstance.unmount(); } catch (_) {}
    setPaymentElementInstance(null);
    setElementsInstance(null);
    setStripeInstance(null);
    setStripeReady(false);
    setPendingClientSecret(null);
    setSelectedPmType('');
    setForceFullPaymentMode(true);
  }, [paymentElementInstance]);
  const swapBackToDepositMode = React.useCallback(() => {
    try { if (paymentElementInstance) paymentElementInstance.unmount(); } catch (_) {}
    setPaymentElementInstance(null);
    setElementsInstance(null);
    setStripeInstance(null);
    setStripeReady(false);
    setPendingClientSecret(null);
    setSelectedPmType('');
    setForceFullPaymentMode(false);
  }, [paymentElementInstance]);

  // Off-session-incompatible methods — Klarna / Affirm / Afterpay /
  // Cash App / ACH bank transfer. Picking one of these on a long-lead
  // booking triggers the "pay full now" prompt because the balance
  // can't be auto-charged later off-session.
  const PM_NEEDS_FULL = ['klarna','afterpay_clearpay','affirm','cashapp','us_bank_account','customer_balance'];
  const needsFullAmountSwitch =
    !chargeFullAmount &&
    selectedPmType &&
    PM_NEEDS_FULL.indexOf(selectedPmType) >= 0;

  const roomTotal = property.priceFrom * nights;
  const subtotal = roomTotal;
  const discountAmt = appliedOffer
    ? (appliedOffer._backendCents != null
        ? Math.round(appliedOffer._backendCents / 100)
        : Math.round(subtotal * ((appliedOffer.percent || 0) / 100)))
    : 0;
  const total = subtotal - discountAmt;

  const allChecked = Object.values(agreed).every(Boolean);
  const allAgreed = allChecked && captchaPassed;

  // Track abandoned cart whenever the user enters identifying info or progresses
  React.useEffect(() => {
    if (!window.DyraStore || !DyraStore.upsertAbandonedCart) return;
    if (!pay.email && !pay.name && !pay.phone && step === 0) return; // wait for some signal
    const t = setTimeout(() => {
      DyraStore.upsertAbandonedCart({
        key: cartKey,
        email: pay.email,
        name: pay.name,
        phone: pay.phone,
        propertyId: property.id,
        checkIn,
        checkOut,
        nights,
        adults: 2,
        children: 0,
        total,
        deposit: Math.round(total * 0.3),
        stepReached: step,
        notes: notes || '',
      });
    }, 600); // debounce
    return () => clearTimeout(t);
  }, [pay.email, pay.name, pay.phone, step, total, cartKey, notes, property.id]);

  const handleConfirmPay = async () => {
    if (processing) return;
    if (!stripeInstance || !elementsInstance || !pendingClientSecret) {
      setStripeError('Payment form not ready yet — give it a moment and try again.');
      return;
    }
    setProcessing(true);
    setStripeError('');

    try {
      const { error, paymentIntent } = await stripeInstance.confirmPayment({
        elements: elementsInstance,
        redirect: 'if_required',
      });
      if (error) {
        setProcessing(false);
        setStripeError(error.message || 'Card declined — try a different payment method.');
        if (window.DyraStore && DyraStore.markCartFailed) {
          DyraStore.markCartFailed(cartKey, error.message || 'Card declined');
        }
        return;
      }
      if (!paymentIntent || (paymentIntent.status !== 'succeeded' && paymentIntent.status !== 'processing')) {
        setProcessing(false);
        setStripeError('Unexpected payment status: ' + (paymentIntent ? paymentIntent.status : 'unknown'));
        return;
      }

      // ── Success — build the reservation locally for instant UI feedback. ──
      // Server webhook is the source of truth; this just lets the admin and
      // My Stay views update before /api/m/data refetch.
      // Clear the per-flow checkout request_id so a future booking on
      // the same property starts a fresh dedupe window.
      try { sessionStorage.removeItem(`dyra.checkoutRequestId:${(property && property.id) || 'unknown'}`); } catch (e) {}
      const today = new Date().toISOString().slice(0, 10);
      const adults = (goData && goData.adults) || 2;
      const children = (goData && goData.children) || 0;
      const infants = (goData && goData.infants) || 0;
      const grandTotal = total;
      const deposit = Math.round(grandTotal * 0.3);
      const guestName = (pay.name || '').trim() || 'Guest';
      const guestEmail = (pay.email || '').trim().toLowerCase() || `guest+${Date.now()}@dyra.demo`;
      const phone = pay.phone || '';
      const confirmation = 'DYR-' + (pendingBookingId ? String(pendingBookingId).slice(0, 6).toUpperCase() : Math.floor(100000 + Math.random() * 900000));
      const reservationId = pendingBookingId || ((window.DyraStore && DyraStore.uid) ? DyraStore.uid('res') : 'res-' + Date.now());

      if (window.DyraStore && DyraStore.update) {
        try {
          DyraStore.update(s => {
            s.reservations.unshift({
              id: reservationId,
              status: 'confirmed',
              createdAt: today,
              guestName, email: guestEmail, phone,
              propertyId: property.id,
              checkIn, checkOut, nights,
              adults, children, infants,
              nightlyTotal: property.priceFrom * nights,
              cleaningFee: property.cleaningFee || 0,
              upsells: [],
              depositPaid: deposit,
              balanceDue: null,
              confirmation,
              notes: notes || '',
              kosherPrefs: [],
              source: appliedOffer ? 'recovery-offer' : 'guest-booking',
              offerCode: appliedOffer ? appliedOffer.code : null,
              discountApplied: discountAmt || 0,
            });
            s.payments.unshift({
              id: DyraStore.uid('pmt'), date: today, reservationId,
              kind: 'deposit', amount: deposit, method: 'card', status: 'completed',
              guestName,
            });
            s.notifications.unshift({
              id: DyraStore.uid('ntf'), at: new Date().toISOString(),
              kind: appliedOffer ? 'recovery-booking' : 'booking', read: false,
              title: appliedOffer ? `Recovery booking · ${guestName}` : `New booking · ${guestName}`,
              body: `${property.name} · ${checkIn} → ${checkOut} · $${grandTotal.toLocaleString()}${appliedOffer ? ' · code ' + appliedOffer.code : ''}`,
              link: { screen: 'reservations' },
            });
          });
        } catch (e) { /* admin store not loaded — fine */ }
      }

      if (window.DyraStore && DyraStore.markCartRecovered) {
        DyraStore.markCartRecovered(cartKey, reservationId);
      }
      if (appliedOffer && appliedOffer._backendCents == null && window.DyraStore && DyraStore.redeemDiscountOffer) {
        DyraStore.redeemDiscountOffer(appliedOffer.code);
      }
      try { sessionStorage.removeItem('dyra.cartKey'); } catch (e) {}

      if (window.D1_RESERVATIONS_REGISTER) {
        window.D1_RESERVATIONS_REGISTER(guestEmail, {
          firstName: guestName.split(' ')[0],
          propertyId: property.id,
          property: property.name,
          propertyTagline: property.tagline || '',
          address: property.address || 'Crown Heights, Brooklyn',
          checkIn:  { date: fmtFullDate(checkIn),  time: '4:00pm' },
          checkOut: { date: fmtFullDate(checkOut), time: '11:00am' },
          nights,
          guests: { adults: 2, children: 0, infants: 0 },
          confirmation,
          keyCode: '— sent 24h before arrival —',
          wifi: { name: 'Dyra-' + property.id, password: 'sentOnArrival' },
          upsells: [],
          pricing: { nightlyTotal: property.priceFrom * nights, cleaningFee: property.cleaningFee || 0, depositPaid: deposit, depositDate: today, balanceDue: { date: 'Apr 30, 2026' } },
        });
      }

      setProcessing(false);
      go('confirmation', null, {
        reservationId, confirmation, guestName, guestEmail, phone,
        nights, total: grandTotal, deposit, checkIn, checkOut,
        propertyId: property.id,
      });
    } catch (e) {
      setProcessing(false);
      setStripeError((e && e.message) || 'Payment failed — try again.');
    }
  };

  return (
    <div style={{ padding: '32px 48px 72px', maxWidth: 1200, margin: '0 auto' }}>
      <div style={{ display: 'flex', gap: 6, fontSize: 12, color: p.muted, marginBottom: 24 }}>
        <span onClick={() => go('listing', property.id)} style={{ cursor: 'pointer' }}>← {property.name}</span>
      </div>

      {/* STEPPER */}
      <div style={{ display: 'grid', gridTemplateColumns: `repeat(${steps.length}, 1fr)`, gap: 4, marginBottom: 40 }}>
        {steps.map((s, i) => (
          <div key={i} onClick={() => setStep(i)} style={{
            padding: '14px 0', cursor: 'pointer',
            borderTop: `2px solid ${i <= step ? p.accent : p.line}`,
          }}>
            <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted }}>Step {i+1}</div>
            <div style={{ fontSize: 15, fontWeight: i === step ? 500 : 400, color: i === step ? p.ink : p.muted, marginTop: 2 }}>{s}</div>
          </div>
        ))}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 48 }}>
        <div>
          {step === 0 && (
            <div>
              <h2 style={{ fontFamily: serif, fontSize: 36, fontWeight: 400, letterSpacing: '-0.01em', margin: '0 0 24px' }}>Your dates</h2>

              <BookingDatePicker
                p={p} serif={serif} hebFont={hebFont}
                checkIn={checkIn} checkOut={checkOut}
                onChange={(ci, co) => { setCheckIn(ci); setCheckOut(co); }}
                minNights={minNights}
                propertyId={property.id}
              />

              <h3 style={{ fontFamily: serif, fontSize: 24, fontWeight: 500, margin: '0 0 16px' }}>Guests</h3>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 14, marginBottom: 28 }}>
                <Counter p={p} label="Adults" sub="Ages 13+" v={2}/>
                <Counter p={p} label="Children" sub="Ages 2–12" v={3}/>
                <Counter p={p} label="Infants" sub="Under 2 · crib available" v={1}/>
              </div>
            </div>
          )}

          {step === 1 && (
            <div>
              <h2 style={{ fontFamily: serif, fontSize: 36, fontWeight: 400, letterSpacing: '-0.01em', margin: '0 0 24px' }}>Review & pay</h2>
              <div style={{ background: p.card, border: `1px solid ${p.line}`, borderRadius: 10, padding: 20, marginBottom: 24 }}>
                <div style={{ fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 10 }}>Your stay</div>
                <div style={{ fontSize: 15, marginBottom: 4 }}><strong>{property.name}</strong></div>
                <div style={{ fontSize: 13, color: p.muted }}>{fmtRangeShort(checkIn, checkOut)} · {nights} night{nights === 1 ? '' : 's'} · 2 adults, 3 children, 1 infant</div>
              </div>

              <h3 style={{ fontFamily: serif, fontSize: 22, fontWeight: 500, marginBottom: 14 }}>Who's staying</h3>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 28 }}>
                <Input p={p} label="Full name *" placeholder="Yosef Cohen" value={pay.name} onChange={v => setPay({ ...pay, name: v })}/>
                <Input p={p} label="Email *" placeholder="you@email.com" value={pay.email} onChange={v => setPay({ ...pay, email: v })}/>
                <Input p={p} label="Phone *" placeholder="+1 (718) 555-0100" value={pay.phone} onChange={v => setPay({ ...pay, phone: v })}/>
              </div>

              <h3 style={{ fontFamily: serif, fontSize: 22, fontWeight: 500, marginBottom: 8 }}>Anything else?</h3>
              <p style={{ fontSize: 12, color: p.muted, margin: '0 0 12px', lineHeight: 1.5 }}>
                Allergies, simcha details, special requests, questions for the host.
              </p>
              <textarea
                value={notes}
                onChange={e => setNotes(e.target.value)}
                placeholder="Optional — let us know anything that helps us prepare for your stay…"
                style={{
                  width: '100%', border: `1px solid ${p.line}`, background: p.card, borderRadius: 8, padding: 14,
                  fontFamily: 'inherit', fontSize: 13, minHeight: 90, resize: 'vertical', color: p.ink, boxSizing: 'border-box', marginBottom: 28,
                }}/>

              <h3 style={{ fontFamily: serif, fontSize: 22, fontWeight: 500, marginBottom: 14 }}>Payment</h3>
              <div style={{ marginBottom: 28 }}>
                {!contactReady && (
                  <div style={{ padding: '14px 16px', background: p.card, border: `1px dashed ${p.line}`, borderRadius: 8, fontSize: 13, color: p.muted }}>
                    Add {missingContactLabel} above to load the secure payment form.
                  </div>
                )}
                {contactReady && !stripeReady && !stripeError && (
                  <div style={{ padding: '14px 16px', background: p.card, border: `1px solid ${p.line}`, borderRadius: 8, fontSize: 13, color: p.muted }}>
                    Loading secure payment form…
                  </div>
                )}
                <div ref={stripeMountRef} style={{ display: contactReady && stripeReady ? 'block' : 'none', padding: 14, background: p.card, border: `1px solid ${p.line}`, borderRadius: 8, minHeight: 60 }} />
                {stripeError && (
                  <div style={{ marginTop: 10, padding: '10px 14px', background: '#fdf0ed', border: '1px solid #c0392b', borderRadius: 6, fontSize: 12, color: '#c0392b' }}>{stripeError}</div>
                )}
                {/* BNPL / non-card prompt: shown when guest picks a
                    method (Klarna, Affirm, Afterpay, Cash App, ACH)
                    that we can't auto-charge for the balance later. */}
                {needsFullAmountSwitch && (() => {
                  const totDol = serverTotalCents != null ? Math.round(serverTotalCents / 100) : total;
                  const depDol = serverDepositCents != null ? Math.round(serverDepositCents / 100) : Math.round(total * 0.3);
                  const balDol = Math.max(0, totDol - depDol);
                  return (
                    <div style={{
                      marginTop: 12, padding: '16px 18px',
                      background: '#fff8ec', border: '1px solid #d6a85a',
                      borderRadius: 8,
                    }}>
                      <div style={{ fontSize: 14, fontWeight: 600, color: p.ink, marginBottom: 6 }}>
                        This payment method requires paying the full ${totDol} now
                      </div>
                      <div style={{ fontSize: 12.5, color: p.muted, lineHeight: 1.55, marginBottom: 12 }}>
                        We can't auto-charge the ${balDol} balance on this method 7 days before check-in. To use it, we'll charge the full ${totDol} today instead of the ${depDol} deposit.
                      </div>
                      <div style={{ display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                        <button onClick={swapToFullAmountMode} style={{
                          padding: '10px 18px', borderRadius: 999, border: 'none',
                          background: p.ink, color: p.bg, fontSize: 13, fontWeight: 600, cursor: 'pointer',
                        }}>Pay ${totDol} now</button>
                        <button onClick={swapBackToDepositMode} style={{
                          padding: '10px 18px', borderRadius: 999,
                          border: `1px solid ${p.line}`, background: p.card, color: p.ink,
                          fontSize: 13, fontWeight: 500, cursor: 'pointer',
                        }}>Use a card instead</button>
                      </div>
                    </div>
                  );
                })()}
                {chargeFullAmount && forceFullPaymentMode && (
                  <div style={{ marginTop: 10, fontSize: 12, color: p.muted }}>
                    Switched to full-amount mode. <button onClick={swapBackToDepositMode} style={{ background: 'transparent', border: 'none', color: p.accent, padding: 0, cursor: 'pointer', textDecoration: 'underline' }}>Switch back to a 30% deposit</button>
                  </div>
                )}
                <div style={{ marginTop: 10, fontSize: 11, color: p.muted, display: 'flex', gap: 8, alignItems: 'center' }}>
                  <Ico name="shield" size={12}/>
                  <span>Encrypted by Stripe · we never see or store your payment details.</span>
                </div>
              </div>

              {(() => {
                const totDol = serverTotalCents != null ? Math.round(serverTotalCents / 100) : total;
                const depDol = serverDepositCents != null ? Math.round(serverDepositCents / 100) : Math.round(total * 0.3);
                const balDol = Math.max(0, totDol - depDol);
                return (
                  <div style={{ background: p.ink, color: p.bg, borderRadius: 10, padding: 20, marginBottom: 24 }}>
                    <div style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.7, marginBottom: 14 }}>Payment schedule</div>
                    {chargeFullAmount ? (
                      <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 16, marginBottom: 14 }}>
                        <div style={{ padding: 14, background: 'rgba(255,255,255,0.06)', borderRadius: 6 }}>
                          <div style={{ fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase', opacity: 0.65, marginBottom: 6 }}>Today · paid in full</div>
                          <div style={{ fontFamily: serif, fontSize: 26 }}>${depDol}</div>
                          <div style={{ fontSize: 11, opacity: 0.7, marginTop: 4 }}>No balance due before check-in</div>
                        </div>
                      </div>
                    ) : (
                      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16, marginBottom: 14 }}>
                        <div style={{ padding: 14, background: 'rgba(255,255,255,0.06)', borderRadius: 6 }}>
                          <div style={{ fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase', opacity: 0.65, marginBottom: 6 }}>Today · 30% deposit</div>
                          <div style={{ fontFamily: serif, fontSize: 26 }}>${depDol}</div>
                          <div style={{ fontSize: 11, opacity: 0.7, marginTop: 4 }}>Locks in your dates</div>
                        </div>
                        <div style={{ padding: 14, background: 'rgba(255,255,255,0.06)', borderRadius: 6 }}>
                          <div style={{ fontSize: 10, letterSpacing: '0.12em', textTransform: 'uppercase', opacity: 0.65, marginBottom: 6 }}>1 week before arrival</div>
                          <div style={{ fontFamily: serif, fontSize: 26 }}>${balDol}</div>
                          <div style={{ fontSize: 11, opacity: 0.7, marginTop: 4 }}>Auto-charged · we'll email a reminder</div>
                        </div>
                      </div>
                    )}
                    <div style={{ fontSize: 11, opacity: 0.65, lineHeight: 1.5, paddingTop: 12, borderTop: '1px solid rgba(255,255,255,0.1)' }}>
                      {chargeFullAmount
                        ? 'Dates are reserved only after payment is collected. Free cancellation up to 48h after booking.'
                        : 'Dates are reserved only after the deposit is paid. The remaining balance must be paid no later than 7 days before check-in or the reservation may be released.'}
                    </div>
                  </div>
                );
              })()}

              {/* RENTAL AGREEMENT — required */}
              <div style={{ background: p.card, border: `1px solid ${p.line}`, borderRadius: 10, padding: 24, marginBottom: 16 }}>
                <div style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: p.accent, marginBottom: 8 }}>Rental agreement · required</div>
                <h3 style={{ fontFamily: serif, fontSize: 24, fontWeight: 500, margin: '0 0 6px' }}>Before you book</h3>
                <p style={{ fontSize: 13, color: p.muted, margin: '0 0 20px', lineHeight: 1.6 }}>
                  Please read and acknowledge each item. All boxes must be checked to complete your reservation.
                </p>

                <div style={{ display: 'flex', flexDirection: 'column', gap: 0, border: `1px solid ${p.line}`, borderRadius: 8, overflow: 'hidden', background: p.bg }}>
                  {[
                    { id: 'kosher', label: 'Kosher kitchen', text: 'I will keep the kitchen strictly kosher for the duration of my stay, including separating meat and dairy and using only sealed kosher food and beverages.' },
                    { id: 'respect', label: 'Respect for the property and neighbors', text: 'I will treat the home with care and respect both the property and the neighbors at all times.' },
                    { id: 'quiet', label: 'Quiet hours', text: 'I will keep noise to a minimum between 9:00 PM and 8:00 AM out of consideration for neighbors.' },
                    { id: 'damage', label: 'Responsibility for damage', text: 'I understand I am responsible for any damage caused during my stay and that the security deposit may be applied toward repairs or replacement costs.' },
                    { id: 'terminate', label: "Host's right to terminate", text: 'I understand that the host reserves the right to terminate this reservation immediately, without refund, in the event of significant disturbance, repeated violations of these terms, damage to the property, kashrus violations, or behavior that endangers neighbors or the home. The host\u2019s decision is final.' },
                    { id: 'ack', label: 'Acknowledgment', text: 'I have read, understood, and agree to all of the terms above. I confirm that the information I provided is accurate and honest, including the number of guests staying. I am the named guest and am authorized to make this booking on behalf of all occupants.' },
                  ].map((item, i) => (
                    <label key={item.id} style={{
                      display: 'grid', gridTemplateColumns: '24px 1fr', gap: 14, padding: '16px 18px', cursor: 'pointer',
                      borderTop: i === 0 ? 'none' : `1px solid ${p.line}`,
                      background: agreed[item.id] ? 'rgba(184,107,73,0.04)' : 'transparent',
                    }}>
                      <input
                        type="checkbox"
                        checked={agreed[item.id]}
                        onChange={e => setAgreed({ ...agreed, [item.id]: e.target.checked })}
                        style={{ width: 18, height: 18, marginTop: 2, accentColor: p.accent, cursor: 'pointer' }}
                      />
                      <div>
                        <div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>{item.label}</div>
                        <div style={{ fontSize: 13, color: p.muted, lineHeight: 1.6 }}>{item.text}</div>
                      </div>
                    </label>
                  ))}
                </div>

                <div style={{ marginTop: 20 }}>
                  <div style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: p.accent, marginBottom: 8 }}>Kashrus check</div>
                  <div style={{ fontSize: 13, color: p.muted, marginBottom: 10, lineHeight: 1.6 }}>
                    Enter 4 digit numerical code
                  </div>

                  <div style={{
                    display: 'grid', gridTemplateColumns: '1fr auto', gap: 16, alignItems: 'center',
                    background: p.bg, border: `1px solid ${captchaPassed ? '#3a7a4a' : captchaLocked ? '#c0392b' : p.line}`,
                    borderRadius: 8, padding: '14px 18px', opacity: captchaLocked ? 0.55 : 1,
                  }}>
                    <div style={{ display: 'flex', gap: 18, alignItems: 'center' }}>
                      <div style={{ display: 'flex', gap: 12, fontFamily: hebFont, fontSize: 44, lineHeight: 1, direction: 'rtl', letterSpacing: '0.05em', color: p.ink, userSelect: 'none' }}>
                        {captcha.map((l, i) => <span key={i}>{l.letter}</span>)}
                      </div>
                      <button
                        type="button"
                        disabled={captchaLocked || captchaPassed}
                        onClick={() => { setCaptcha(genCaptcha()); setCaptchaInput(''); }}
                        style={{
                          background: 'transparent', border: `1px solid ${p.line}`, color: p.muted,
                          padding: '6px 12px', borderRadius: 999, fontSize: 11,
                          cursor: (captchaLocked || captchaPassed) ? 'not-allowed' : 'pointer',
                          letterSpacing: '0.06em', opacity: (captchaLocked || captchaPassed) ? 0.5 : 1,
                        }}
                      >↻ New code</button>
                    </div>
                    <input
                      type="text"
                      inputMode="numeric"
                      pattern="[0-9]*"
                      maxLength={4}
                      disabled={captchaLocked || captchaPassed}
                      value={captchaInput}
                      onChange={e => setCaptchaInput(e.target.value.replace(/\D/g, '').slice(0, 4))}
                      placeholder="0000"
                      style={{
                        width: 140, padding: '12px 14px', border: `1px solid ${p.line}`, background: p.card,
                        borderRadius: 8, fontFamily: 'monospace', fontSize: 22, color: p.ink, textAlign: 'center',
                        letterSpacing: '0.25em', boxSizing: 'border-box',
                      }}
                    />
                  </div>
                  {!captchaPassed && !captchaLocked && captchaAttempts > 0 && (
                    <div style={{ fontSize: 12, color: '#c0392b', marginTop: 8 }}>
                      That doesn't match. {3 - captchaAttempts} attempt{3 - captchaAttempts === 1 ? '' : 's'} remaining. Click ↻ for a new code.
                    </div>
                  )}
                  {captchaLocked && (
                    <div style={{ fontSize: 12, color: '#c0392b', marginTop: 8, lineHeight: 1.6 }}>
                      Too many incorrect attempts. This home is reserved for guests who keep kosher. Please contact the host directly to discuss your booking.
                    </div>
                  )}
                  {captchaPassed && (
                    <div style={{ fontSize: 12, color: '#3a7a4a', marginTop: 8 }}>
                      ✓ Verified
                    </div>
                  )}
                </div>
              </div>

              {/* Cancellation policy — surfaced before Pay so the guest sees
                  the refund timeline before committing. Sourced from
                  window.DyraCancellation which mirrors the server policy. */}
              <D1CheckoutCancellationPolicy
                chargedNowCents={(serverDepositCents != null ? serverDepositCents : Math.round((chargeFullAmount ? total : total * 0.3) * 100))}
                p={p}
                serif={serif}
              />
            </div>
          )}

          <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 32 }}>
            <button onClick={() => step > 0 ? setStep(step - 1) : go('listing', property.id)} style={{
              background: 'transparent', color: p.ink, border: `1px solid ${p.line}`, padding: '12px 24px', borderRadius: 999, cursor: 'pointer', fontSize: 13,
            }}>← Back</button>
            {(() => {
              // formValid = contact info + Stripe Element ready. Card validity
              // itself is enforced by the Stripe Element + confirmPayment().
              const formValid = contactReady && stripeReady;
              const ctaMuted = step === 1 && (!allAgreed || !formValid || processing);
              const ctaDisabled = (step === 1 && (!allAgreed || !formValid)) || processing;
              const ctaTitle = step === 1
                ? (!allAgreed
                    ? 'Please complete the agreement, kashrus check, and signature.'
                    : (!formValid ? 'Please fill in all required fields and wait for the card form to load.' : ''))
                : '';
              return (
            <button
              disabled={ctaDisabled}
              onClick={() => step < 1 ? setStep(step + 1) : handleConfirmPay()}
              style={{
                background: ctaMuted ? p.line : p.ink,
                color: ctaMuted ? p.muted : p.bg,
                border: 'none', padding: '12px 28px', borderRadius: 999,
                cursor: ctaMuted ? 'not-allowed' : 'pointer', fontSize: 13,
                fontWeight: ctaMuted ? 400 : 500,
                boxShadow: ctaMuted ? 'none' : '0 1px 2px rgba(0,0,0,0.06)',
                opacity: ctaMuted ? 0.85 : 1,
                display: 'flex', alignItems: 'center', gap: 8,
              }}
              title={ctaTitle}
            >{step === 1
                ? (processing
                    ? 'Processing payment…'
                    : (() => {
                        const amt = serverDepositCents != null
                          ? Math.round(serverDepositCents / 100)
                          : Math.round(total * 0.3);
                        return chargeFullAmount
                          ? `Confirm & pay $${amt}`
                          : `Confirm & pay deposit · $${amt}`;
                      })())
                : 'Continue'} {!processing && <Ico name="arrow" size={14}/>}</button>
              );
            })()}
          </div>
        </div>

        {/* SUMMARY */}
        <div style={{ position: 'sticky', top: 20, alignSelf: 'start' }}>
          <div style={{ background: p.card, border: `1px solid ${p.line}`, borderRadius: 10, padding: 20 }}>
            <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="clay" density="tight" style={{ aspectRatio: '3/2', borderRadius: 6, marginBottom: 16 }}/>
            <div style={{ fontSize: 11, letterSpacing: '0.16em', textTransform: 'uppercase', color: p.accent, marginBottom: 4 }}>{property.tier}</div>
            <div style={{ fontFamily: serif, fontSize: 22, marginBottom: 2 }}>{property.name}</div>
            {property.rating && property.reviews > 0 && (
              <div style={{ fontSize: 12, color: p.muted, marginBottom: 20 }}>★ {property.rating} · {property.reviews} reviews</div>
            )}

            <div style={{ fontSize: 13, lineHeight: 1.8, color: p.muted }}>
              <Row l={`$${property.priceFrom} × ${nights} night${nights === 1 ? '' : 's'}`} r={`$${property.priceFrom * nights}`} p={p}/>
              {appliedOffer && (
                <Row
                  l={appliedOffer._backendCents != null
                      ? (appliedOffer._backendKind === 'percent'
                          ? `Promo · ${appliedOffer.code} (−${appliedOffer.percent}%)`
                          : `Promo · ${appliedOffer.code}`)
                      : `Recovery offer · ${appliedOffer.code} (−${appliedOffer.percent}%)`}
                  r={`−$${discountAmt}`}
                  p={p}
                />
              )}
              <Row l="Total" r={`$${total}`} p={p} total/>
            </div>

            {/* DISCOUNT CODE */}
            <div style={{ marginTop: 14, paddingTop: 14, borderTop: `1px solid ${p.line}` }}>
              <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 6 }}>Have an offer code?</div>
              {appliedOffer ? (
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: '#e8f5ec', border: '1px solid #5b8a6b', borderRadius: 6, fontSize: 12 }}>
                  <span style={{ color: '#2d5239', fontWeight: 500 }}>✓ {appliedOffer.code} · {appliedOffer.percent}% off</span>
                  <button onClick={() => { setAppliedOffer(null); setDiscountCode(''); setDiscountStatus(''); }}
                    style={{ background: 'transparent', border: 'none', color: '#2d5239', fontSize: 11, cursor: 'pointer', textDecoration: 'underline' }}>Remove</button>
                </div>
              ) : (
                <div style={{ display: 'flex', gap: 6 }}>
                  <input value={discountCode} onChange={e => { setDiscountCode(e.target.value.toUpperCase()); setDiscountStatus(''); }}
                    onKeyDown={e => { if (e.key === 'Enter') applyDiscountCode(); }}
                    placeholder="DYRA…" style={{ flex: 1, padding: '8px 10px', border: `1px solid ${discountStatus === 'invalid' ? '#c0392b' : p.line}`, borderRadius: 6, fontSize: 12, fontFamily: 'inherit', textTransform: 'uppercase' }}/>
                  <button onClick={applyDiscountCode} style={{ padding: '8px 14px', background: p.ink, color: p.card, border: 'none', borderRadius: 6, fontSize: 12, cursor: 'pointer' }}>Apply</button>
                </div>
              )}
              {discountStatus === 'invalid' && (
                <div style={{ marginTop: 6, fontSize: 11, color: '#c0392b' }}>Code not found or expired.</div>
              )}
            </div>

            <div style={{ marginTop: 16, padding: 12, background: p.bg, borderRadius: 6, fontSize: 12 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', color: p.ink, fontWeight: 500 }}>
                <span>Due today (30%)</span><span>${Math.round(total * 0.3)}</span>
              </div>
              <div style={{ display: 'flex', justifyContent: 'space-between', color: p.muted, marginTop: 6 }}>
                <span>Due 1 week before arrival</span><span>${total - Math.round(total * 0.3)}</span>
              </div>
            </div>

          </div>
        </div>
      </div>
    </div>
  );
}

// ─── Cancellation policy (checkout) ───────────────────────────────────
// Renders before the Pay button on the desktop checkout review step.
// Numbers come from window.DyraCancellation, which mirrors the server
// policy in src/app/api/guest/cancel/route.ts.
function D1CheckoutCancellationPolicy({ chargedNowCents, p, serif }) {
  const [open, setOpen] = React.useState(false);
  const policy = (window.DyraCancellation && window.DyraCancellation.previewForCheckout(chargedNowCents)) || null;
  if (!policy) return null;
  const fmt = window.DyraCancellation.formatRefundDollars;
  return (
    <div style={{
      background: p.bg, border: `1px solid ${p.line}`, borderRadius: 10,
      padding: 18, marginBottom: 28,
    }}>
      <div style={{ display: 'flex', alignItems: 'flex-start', gap: 12 }}>
        <Ico name="shield" size={18} style={{ color: p.accent, flexShrink: 0, marginTop: 2 }}/>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', color: p.accent, marginBottom: 6 }}>
            Cancellation policy
          </div>
          <div style={{ fontSize: 14, color: p.ink, lineHeight: 1.55 }}>{policy.summary}</div>
          <div style={{ fontSize: 12.5, color: p.muted, marginTop: 6, lineHeight: 1.5 }}>
            If you cancel inside that window we’ll refund {fmt(policy.eligibleRefundCents)} back to your card. {policy.processingNote}
          </div>
          <button
            type="button"
            onClick={() => setOpen(v => !v)}
            style={{
              marginTop: 10, background: 'transparent', border: 'none', padding: 0,
              color: p.accent, fontSize: 12.5, fontWeight: 500,
              cursor: 'pointer', textDecoration: 'underline',
            }}
          >{open ? 'Hide full policy' : 'Show full policy'}</button>
          {open && (
            <div style={{ marginTop: 12, paddingTop: 12, borderTop: `1px solid ${p.line}` }}>
              {policy.tiers.map(tier => (
                <div key={tier.id} style={{
                  display: 'grid', gridTemplateColumns: '12px 1fr', gap: 12,
                  padding: '10px 0', borderBottom: `1px solid ${p.line}`,
                  opacity: tier.activeNow ? 1 : 0.75,
                }}>
                  <div style={{
                    width: 8, height: 8, marginTop: 7, borderRadius: 4,
                    background: tier.activeNow ? p.accent : p.line,
                  }}/>
                  <div>
                    <div style={{ fontFamily: serif, fontSize: 15, color: p.ink, fontWeight: tier.activeNow ? 600 : 500 }}>
                      {tier.label}{tier.activeNow ? ' · Current' : ''}
                    </div>
                    <div style={{ fontSize: 12.5, color: p.muted, marginTop: 4, lineHeight: 1.5 }}>
                      {tier.refundDescription}
                    </div>
                  </div>
                </div>
              ))}
              <div style={{ fontSize: 12, color: p.muted, marginTop: 10, lineHeight: 1.5 }}>
                {policy.processingNote}
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

function BookField({ label, value, sub, p }) {
  return (
    <div style={{ border: `1px solid ${p.line}`, padding: '14px 16px', borderRadius: 8, background: p.card, cursor: 'pointer' }}>
      <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 4 }}>{label}</div>
      <div style={{ fontSize: 15 }}>{value}</div>
      <div style={{ fontSize: 11, color: p.muted }}>{sub}</div>
    </div>
  );
}

function Counter({ p, label, sub, v: initV }) {
  const [v, setV] = React.useState(initV);
  return (
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 16px', border: `1px solid ${p.line}`, borderRadius: 8, background: p.card }}>
      <div>
        <div style={{ fontSize: 14 }}>{label}</div>
        <div style={{ fontSize: 11, color: p.muted }}>{sub}</div>
      </div>
      <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
        <button onClick={() => setV(Math.max(0, v-1))} style={{ width: 30, height: 30, borderRadius: 999, border: `1px solid ${p.line}`, background: 'transparent', cursor: 'pointer' }}><Ico name="minus" size={12}/></button>
        <span style={{ width: 20, textAlign: 'center', fontSize: 14 }}>{v}</span>
        <button onClick={() => setV(v+1)} style={{ width: 30, height: 30, borderRadius: 999, border: `1px solid ${p.line}`, background: 'transparent', cursor: 'pointer' }}><Ico name="plus" size={12}/></button>
      </div>
    </div>
  );
}

function Input({ p, label, placeholder, value, onChange, hebAlign }) {
  return (
    <div>
      <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 6 }}>{label}</div>
      <input value={value || ''} onChange={e => onChange && onChange(e.target.value)} placeholder={placeholder} style={{
        width: '100%', padding: '12px 14px', border: `1px solid ${p.line}`, borderRadius: 8, background: p.card,
        fontFamily: 'inherit', fontSize: 14, color: p.ink, direction: hebAlign ? 'rtl' : 'ltr',
        boxSizing: 'border-box',
      }}/>
    </div>
  );
}

function Row({ l, r, p, total }) {
  return (
    <div style={{
      display: 'flex', justifyContent: 'space-between',
      padding: total ? '14px 0 0' : '4px 0',
      marginTop: total ? 10 : 0, borderTop: total ? `1px solid ${p.line}` : 'none',
      color: total ? p.ink : p.muted, fontWeight: total ? 500 : 400, fontSize: total ? 15 : 13,
    }}>
      <span>{l}</span><span>{r}</span>
    </div>
  );
}

// ─────── UPSELLS CATALOG ───────
function D1Upsells({ p, serif, sans, go }) {
  const cats = ['Shabbos', 'Yom Tov', 'Pantry', 'Family', 'Transport', 'Simcha'];
  const [cat, setCat] = React.useState('Shabbos');
  const visible = window.DYRA.UPSELLS.filter(u => u.category === cat);
  return (
    <div style={{ padding: '48px 48px 72px' }}>
      <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: p.accent, marginBottom: 14 }}>Kosher packages</div>
      <h1 style={{ fontFamily: serif, fontSize: 64, fontWeight: 400, letterSpacing: '-0.02em', margin: '0 0 16px', lineHeight: 0.98 }}>
        Arrive, <em>breathe,</em><br/>light candles.
      </h1>
      <p style={{ fontSize: 15, color: p.muted, maxWidth: 560, lineHeight: 1.7, marginBottom: 36 }}>
        Add any package to any stay. All kosher-certified, prepared by trusted local vendors, delivered before you arrive.
      </p>

      <div style={{ display: 'flex', gap: 6, marginBottom: 36, overflow: 'auto' }}>
        {cats.map(c => (
          <button key={c} onClick={() => setCat(c)} style={{
            background: cat === c ? p.ink : 'transparent', color: cat === c ? p.bg : p.ink,
            border: `1px solid ${cat === c ? p.ink : p.line}`, padding: '9px 20px', borderRadius: 999, cursor: 'pointer', fontSize: 13,
          }}>{c}</button>
        ))}
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(320px, 1fr))', gap: 20 }}>
        {visible.map(u => (
          <div key={u.id} style={{ border: `1px solid ${p.line}`, borderRadius: 10, overflow: 'hidden', background: p.card }}>
            <Placeholder label={`${u.name} · ${u.category}`} tone={u.category === 'Shabbos' ? 'clay' : u.category === 'Yom Tov' ? 'cream' : 'warm'} density="med" style={{ aspectRatio: '4/3' }}/>
            <div style={{ padding: 20 }}>
              <div style={{ fontSize: 10, letterSpacing: '0.16em', textTransform: 'uppercase', color: p.accent, marginBottom: 6 }}>{u.category} · Serves {u.serves}</div>
              <h3 style={{ fontFamily: serif, fontSize: 26, fontWeight: 500, margin: '0 0 8px', letterSpacing: '-0.01em' }}>{u.name}</h3>
              <p style={{ fontSize: 13, color: p.muted, lineHeight: 1.6, margin: '0 0 16px' }}>{u.desc}</p>
              <div style={{ fontSize: 12, color: p.muted, marginBottom: 16, lineHeight: 1.7 }}>
                {u.includes.slice(0, 4).map((x, i) => (
                  <div key={i} style={{ display: 'flex', gap: 8 }}>
                    <Ico name="check" size={12} style={{ color: p.accent, marginTop: 3, flexShrink: 0 }}/>{x}
                  </div>
                ))}
              </div>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', paddingTop: 14, borderTop: `1px solid ${p.line}` }}>
                <span style={{ fontFamily: serif, fontSize: 22, fontWeight: 500 }}>
                  {u.priceFrom === 0 ? 'Complimentary' : `From $${u.priceFrom}`}
                </span>
                <button style={{ background: p.accent, color: p.bg, border: 'none', padding: '8px 16px', borderRadius: 999, fontSize: 12, cursor: 'pointer' }}>Add to stay</button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}

window.D1Booking = D1Booking;
window.D1Upsells = D1Upsells;

// ── Date helpers ───────────────────────────────────────────────────────
// Used by D1Booking + the BookingDatePicker below to format header copy
// and the right-side summary ("May 14 → 17 · 3 nights").
function fmtFullDate(iso) {
  if (!iso) return '';
  const d = new Date(iso + 'T00:00:00');
  return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' });
}
function fmtRangeShort(ci, co) {
  if (!ci || !co) return '';
  const a = new Date(ci + 'T00:00:00'), b = new Date(co + 'T00:00:00');
  const sameMonth = a.getMonth() === b.getMonth() && a.getFullYear() === b.getFullYear();
  const aStr = a.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
  const bStr = sameMonth
    ? b.toLocaleDateString('en-US', { day: 'numeric', year: 'numeric' })
    : b.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
  return `${aStr} – ${bStr}`;
}

// Hebrew calendar — anchored to a known correspondence and walked day by day.
// Anchor: May 14 2026 (Gregorian) ≈ 26 Iyar 5786. Hebrew months in 5786 are
// regular-year-length (we mock leap-year handling — production would use Hebcal).
// This is good enough to show real Hebrew month names and counts as you scroll.
const HEB_MONTHS = [
  // 5786 (regular year): Tishrei→Elul. Lengths in the order Tishrei, Cheshvan, Kislev, Tevet,
  // Shvat, Adar, Nisan, Iyar, Sivan, Tammuz, Av, Elul.
  { 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 HEB_LETTER_DAY = [
  '', // 0 unused
  'א','ב','ג','ד','ה','ו','ז','ח','ט','י',
  'יא','יב','יג','יד','טו','טז','יז','יח','יט','כ',
  'כא','כב','כג','כד','כה','כו','כז','כח','כט','ל',
];
function hebrewDateForGregorian(iso) {
  // Anchor: Gregorian 2026-05-14 = Hebrew 26 Iyar 5786.
  const anchor = new Date('2026-05-14T00:00:00');
  const d = new Date(iso + 'T00:00:00');
  let dayOffset = Math.round((d - anchor) / 86400000);
  // Walk forward/backward through HEB_MONTHS starting at Iyar (index 7), day 26.
  let monthIdx = 7;        // Iyar
  let dayInMonth = 26 + dayOffset;
  // Forward roll-over
  while (dayInMonth > HEB_MONTHS[monthIdx].len) {
    dayInMonth -= HEB_MONTHS[monthIdx].len;
    monthIdx = (monthIdx + 1) % 12;
  }
  // Backward roll-over
  while (dayInMonth < 1) {
    monthIdx = (monthIdx - 1 + 12) % 12;
    dayInMonth += HEB_MONTHS[monthIdx].len;
  }
  return {
    monthName: HEB_MONTHS[monthIdx].name,
    monthIdx,
    day: dayInMonth,
    letterDay: HEB_LETTER_DAY[dayInMonth] || '',
  };
}

// Returns the Hebrew month label that spans a given Gregorian month — usually
// two adjacent months ("Iyar – Sivan 5786") because Gregorian/Hebrew don't align.
function hebrewMonthLabelForGregorian(year, monthIdx) {
  const first = `${year}-${String(monthIdx + 1).padStart(2, '0')}-01`;
  const lastDay = new Date(year, monthIdx + 1, 0).getDate();
  const last = `${year}-${String(monthIdx + 1).padStart(2, '0')}-${String(lastDay).padStart(2, '0')}`;
  const a = hebrewDateForGregorian(first);
  const b = hebrewDateForGregorian(last);
  if (a.monthName === b.monthName) return `${a.monthName} 5786`;
  return `${a.monthName} – ${b.monthName} 5786`;
}

// ── Booking date picker ──────────────────────────────────────────────────
// Real two-month calendar with English + Hebrew labels, range selection,
// minNights enforcement, and free-text date inputs (so the guest can type
// or pick from a date input). Replaces the earlier static mock.
function BookingDatePicker({ p, serif, hebFont, checkIn, checkOut, onChange, minNights, propertyId }) {
  // Walk months forward/backward; default cursor sits on the check-in month.
  const initial = checkIn ? new Date(checkIn + 'T00:00:00') : new Date();
  const [cursor, setCursor] = React.useState({ y: initial.getFullYear(), m: initial.getMonth() });
  // Two-step click model: first click = check-in; second click = check-out.
  const [pickingMode, setPickingMode] = React.useState('checkin'); // 'checkin' | 'checkout'
  const [hover, setHover] = React.useState(null); // hovered ISO during checkout pick
  const [warning, setWarning] = React.useState('');
  const [bookedDates, setBookedDates] = React.useState([]); // ISO strings the property is booked on

  // Pull existing reservations + admin blocks (cleaning, owner stays, yom tov
  // holds) out of the live data shim so already-unavailable nights render as
  // disabled. Source of truth: property.blockedRanges from /api/m/data, which
  // joins bookings (incl. Uplisting-imported) with blackout_dates server-side.
  // We fall back to the legacy DyraStore.reservations expansion when running
  // outside the prototype (e.g. the static design site without the data shim).
  React.useEffect(() => {
    const taken = new Set();
    const fromPropertyRanges = () => {
      const props = (window.DYRA && window.DYRA.PROPERTIES) || (window.DyraStore?.state?.properties) || [];
      const prop = props.find(x => x.id === propertyId || x.slug === propertyId);
      const ranges = prop && Array.isArray(prop.blockedRanges) ? prop.blockedRanges : [];
      ranges.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)) {
          taken.add(d.toISOString().slice(0, 10));
        }
      });
      return ranges.length > 0;
    };
    if (!fromPropertyRanges() && window.DyraStore && window.DyraStore.getState) {
      try {
        const s = window.DyraStore.getState();
        (s.reservations || []).filter(r => r.propertyId === propertyId && r.status !== 'cancelled')
          .forEach(r => {
            const start = new Date(r.checkIn + 'T00:00:00');
            const end   = new Date(r.checkOut + 'T00:00:00');
            for (let d = new Date(start); d < end; d.setDate(d.getDate() + 1)) {
              taken.add(d.toISOString().slice(0, 10));
            }
          });
      } catch (e) {}
    }
    setBookedDates(Array.from(taken));
    if (!window.DyraStore || !window.DyraStore.subscribe) return;
    return window.DyraStore.subscribe(() => {
      // Re-run on any store change so admin-side edits to blocks reflect live.
      const next = new Set();
      const props = (window.DYRA && window.DYRA.PROPERTIES) || (window.DyraStore?.state?.properties) || [];
      const prop = props.find(x => x.id === propertyId || x.slug === propertyId);
      (prop?.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)) {
          next.add(d.toISOString().slice(0, 10));
        }
      });
      setBookedDates(Array.from(next));
    });
  }, [propertyId]);
  const bookedSet = React.useMemo(() => new Set(bookedDates), [bookedDates]);

  const today = new Date(); today.setHours(0, 0, 0, 0);
  const todayISO = today.toISOString().slice(0, 10);

  // Generate the day cells for a given month
  const monthCells = (y, m) => {
    const first = new Date(y, m, 1);
    const last = new Date(y, m + 1, 0);
    const startWeekday = first.getDay();
    const cells = [];
    // Leading blanks
    for (let i = 0; i < startWeekday; i++) cells.push(null);
    for (let d = 1; d <= last.getDate(); d++) {
      const iso = `${y}-${String(m + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`;
      cells.push({ d, iso });
    }
    return cells;
  };

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

  const handleDayClick = (iso) => {
    setWarning('');
    if (iso < todayISO) { setWarning('Pick a date in the future.'); return; }
    if (bookedSet.has(iso)) { setWarning('Those nights are already booked.'); return; }

    if (pickingMode === 'checkin') {
      // Reset checkout — guest is starting a new range
      onChange(iso, '');
      setPickingMode('checkout');
      return;
    }
    // Picking checkout
    if (iso <= checkIn) {
      // Starting over with a new check-in
      onChange(iso, '');
      setPickingMode('checkout');
      return;
    }
    const proposedNights = Math.round((new Date(iso + 'T00:00:00') - new Date(checkIn + 'T00:00:00')) / 86400000);
    if (proposedNights < minNights) {
      setWarning(`Minimum stay is ${minNights} night${minNights === 1 ? '' : 's'}.`);
      return;
    }
    // Verify no booked nights fall inside the chosen range
    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 (bookedSet.has(d.toISOString().slice(0, 10))) {
        setWarning('Some nights inside that range are booked. Try a different range.');
        return;
      }
    }
    onChange(checkIn, iso);
    setPickingMode('checkin');
  };

  // Allow direct keyboard editing of dates via <input type="date">
  const handleManualCheckIn = (val) => {
    setWarning('');
    if (!val) return;
    if (val < todayISO) { setWarning('Pick a date in the future.'); return; }
    if (bookedSet.has(val)) { setWarning('That date is already booked.'); return; }
    // If existing checkout no longer satisfies minNights, recompute it
    let newCheckout = checkOut;
    const minOut = new Date(val + 'T00:00:00'); minOut.setDate(minOut.getDate() + minNights);
    if (!checkOut || new Date(checkOut + 'T00:00:00') < minOut) {
      newCheckout = minOut.toISOString().slice(0, 10);
    }
    onChange(val, newCheckout);
    setPickingMode('checkin');
  };
  const handleManualCheckOut = (val) => {
    setWarning('');
    if (!val) return;
    if (val <= checkIn) { setWarning('Check-out must be after check-in.'); return; }
    const proposedNights = Math.round((new Date(val + 'T00:00:00') - new Date(checkIn + 'T00:00:00')) / 86400000);
    if (proposedNights < minNights) { setWarning(`Minimum stay is ${minNights} night${minNights === 1 ? '' : 's'}.`); return; }
    onChange(checkIn, val);
    setPickingMode('checkin');
  };

  const monthsToShow = [
    { y: cursor.y, m: cursor.m },
    cursor.m === 11 ? { y: cursor.y + 1, m: 0 } : { y: cursor.y, m: cursor.m + 1 },
  ];

  const renderMonth = ({ y, m }) => {
    const cells = monthCells(y, m);
    const monthName = new Date(y, m, 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
    const hebLabel = hebrewMonthLabelForGregorian(y, m);
    return (
      <div key={`${y}-${m}`} style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginBottom: 12 }}>
          <span style={{ fontFamily: serif, fontSize: 20, color: p.ink }}>{monthName}</span>
          <span style={{ fontFamily: hebFont, fontSize: 14, color: p.accent, direction: 'rtl' }}>{hebLabel}</span>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 4 }}>
          {['S','M','T','W','T','F','S'].map((d, i) => (
            <div key={i} style={{ textAlign: 'center', fontSize: 10, letterSpacing: '0.14em', color: p.muted, padding: '6px 0', textTransform: 'uppercase' }}>{d}</div>
          ))}
          {cells.map((c, i) => {
            if (!c) return <div key={i}/>;
            const heb = hebrewDateForGregorian(c.iso);
            const isPast = c.iso < todayISO;
            const isBooked = bookedSet.has(c.iso);
            const isCheckIn = c.iso === checkIn;
            const isCheckOut = c.iso === checkOut;
            const inRange = checkIn && checkOut && c.iso > checkIn && c.iso < checkOut;
            // Hover preview when picking checkout
            const hoverEnd = pickingMode === 'checkout' && hover && hover > checkIn ? hover : null;
            const inHoverRange = hoverEnd && c.iso > checkIn && c.iso <= hoverEnd && !checkOut;
            const dow = new Date(c.iso + 'T00:00:00').getDay();
            const isFriday = dow === 5;
            const isSaturday = dow === 6;
            const disabled = isPast || isBooked;

            // Booked days get a soft red wash + diagonal hatch so they read
            // as unavailable at a glance, alongside the strikethrough on
            // the date number.
            const bookedActive = isBooked && !isPast;
            const bg = isCheckIn || isCheckOut ? p.accent
              : (inRange || inHoverRange) ? p.accentSoft
              : bookedActive
                ? 'repeating-linear-gradient(135deg, rgba(193,72,49,0.10) 0px, rgba(193,72,49,0.10) 5px, rgba(193,72,49,0.20) 5px, rgba(193,72,49,0.20) 10px)'
                : 'transparent';
            const fg = isCheckIn || isCheckOut ? p.bg
              : disabled ? '#bbb1a0'
              : p.ink;

            return (
              <button
                key={i}
                onClick={() => !disabled && handleDayClick(c.iso)}
                onMouseEnter={() => pickingMode === 'checkout' && setHover(c.iso)}
                onMouseLeave={() => setHover(null)}
                disabled={disabled}
                style={{
                  aspectRatio: '1',
                  display: 'flex', flexDirection: 'column', justifyContent: 'center', alignItems: 'center',
                  background: bg, color: fg,
                  border: 'none',
                  borderRadius: isCheckIn ? '6px 0 0 6px' : isCheckOut ? '0 6px 6px 0' : (inRange || inHoverRange) ? 0 : 6,
                  cursor: disabled ? 'not-allowed' : 'pointer',
                  fontFamily: 'inherit',
                  position: 'relative',
                  textDecoration: isBooked ? 'line-through' : 'none',
                  textDecorationColor: isBooked ? '#c14831' : undefined,
                  textDecorationThickness: isBooked ? 2 : undefined,
                  opacity: disabled ? 0.65 : 1,
                  padding: 0,
                }}
                title={isBooked ? 'Already booked' : isPast ? 'In the past' : `${heb.day} ${heb.monthName}`}
              >
                <span style={{ fontSize: 13, fontWeight: isCheckIn || isCheckOut ? 500 : 400, lineHeight: 1 }}>{c.d}</span>
                <span style={{ fontSize: 9, fontFamily: hebFont, direction: 'rtl', marginTop: 2, opacity: isCheckIn || isCheckOut ? 0.92 : 0.55 }}>
                  {heb.letterDay}
                </span>
                {(isFriday || isSaturday) && !isCheckIn && !isCheckOut && !inRange && !inHoverRange && !disabled && (
                  <span style={{ position: 'absolute', bottom: 2, width: 14, height: 1.5, background: p.line }}/>
                )}
              </button>
            );
          })}
        </div>
      </div>
    );
  };

  // Header summary fields — Check-in / Check-out / Nights — all editable
  return (
    <div style={{ marginBottom: 28 }}>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr auto', gap: 12, marginBottom: 16 }}>
        <BookingDateInput
          p={p} serif={serif} hebFont={hebFont}
          label="Check in" iso={checkIn}
          onChange={handleManualCheckIn}
          active={pickingMode === 'checkin'}
        />
        <BookingDateInput
          p={p} serif={serif} hebFont={hebFont}
          label="Check out" iso={checkOut}
          onChange={handleManualCheckOut}
          active={pickingMode === 'checkout'}
        />
        <div style={{
          padding: 14, border: `1px solid ${p.line}`, background: p.card, borderRadius: 8,
          display: 'flex', flexDirection: 'column', justifyContent: 'center', minWidth: 110,
        }}>
          <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 4 }}>Nights</div>
          <div style={{ fontFamily: serif, fontSize: 22, color: p.ink, lineHeight: 1 }}>
            {checkIn && checkOut ? Math.max(1, Math.round((new Date(checkOut + 'T00:00:00') - new Date(checkIn + 'T00:00:00')) / 86400000)) : '—'}
          </div>
          <div style={{ fontSize: 11, color: p.muted, marginTop: 4 }}>min {minNights}</div>
        </div>
      </div>

      <div style={{ border: `1px solid ${p.line}`, borderRadius: 8, background: p.card, padding: 20 }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
          <button onClick={prev} style={{ background: 'none', border: `1px solid ${p.line}`, padding: '6px 12px', borderRadius: 6, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, color: p.muted }}>← Prev</button>
          <div style={{ fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted }}>
            {pickingMode === 'checkin' ? 'Click a day to set check-in' : 'Click a day to set check-out'}
          </div>
          <button onClick={next} style={{ background: 'none', border: `1px solid ${p.line}`, padding: '6px 12px', borderRadius: 6, cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, color: p.muted }}>Next →</button>
        </div>

        <div style={{ display: 'flex', gap: 28 }}>
          {monthsToShow.map(renderMonth)}
        </div>

        {warning && (
          <div style={{ marginTop: 14, padding: '10px 14px', background: '#fdecec', border: '1px solid #e5b7b7', borderRadius: 6, fontSize: 12, color: '#8a3a3a' }}>
            {warning}
          </div>
        )}

        <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap', marginTop: 16, fontSize: 11, color: p.muted, paddingTop: 14, borderTop: `1px solid ${p.line}` }}>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 12, height: 12, background: p.accent, borderRadius: 3 }}/> Your stay
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 12, height: 12, background: p.accentSoft, borderRadius: 3 }}/> In range
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 14, height: 1.5, background: p.line }}/> Friday / Shabbos
          </span>
          <span style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
            <s style={{ color: '#bbb1a0' }}>14</s> Already booked
          </span>
          <span style={{ marginLeft: 'auto', fontFamily: hebFont, fontSize: 13, color: p.accent, direction: 'rtl' }}>
            תאריכים בעברית ובאנגלית
          </span>
        </div>
      </div>
    </div>
  );
}

function BookingDateInput({ p, serif, hebFont, label, iso, onChange, active }) {
  const heb = iso ? hebrewDateForGregorian(iso) : null;
  const display = iso
    ? new Date(iso + 'T00:00:00').toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' })
    : '— pick a date —';
  return (
    <label style={{
      padding: 14,
      border: `1px solid ${active ? p.accent : p.line}`,
      borderRadius: 8,
      background: p.card,
      cursor: 'pointer',
      display: 'block',
      position: 'relative',
      boxShadow: active ? `0 0 0 3px ${p.accent}22` : 'none',
      transition: 'box-shadow 120ms, border-color 120ms',
    }}>
      <div style={{ fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', color: p.muted, marginBottom: 6 }}>{label}</div>
      <div style={{ fontFamily: serif, fontSize: 19, color: iso ? p.ink : p.muted, lineHeight: 1.2 }}>{display}</div>
      {heb && (
        <div style={{ fontSize: 11, color: p.accent, marginTop: 4, fontFamily: hebFont }}>
          {heb.day} {heb.monthName} 5786 · <span style={{ direction: 'rtl' }}>{heb.letterDay} {heb.monthName === 'Cheshvan' ? 'חשון' : heb.monthName === 'Tishrei' ? 'תשרי' : heb.monthName === 'Iyar' ? 'אייר' : heb.monthName === 'Sivan' ? 'סיון' : heb.monthName === 'Tammuz' ? 'תמוז' : heb.monthName === 'Av' ? 'אב' : heb.monthName === 'Elul' ? 'אלול' : heb.monthName === 'Kislev' ? 'כסלו' : heb.monthName === 'Tevet' ? 'טבת' : heb.monthName === 'Shvat' ? 'שבט' : heb.monthName === 'Adar' ? 'אדר' : 'ניסן'}</span>
        </div>
      )}
      {/* Native date input for keyboard / picker entry — visually overlaid */}
      <input
        type="date"
        value={iso || ''}
        onChange={e => onChange(e.target.value)}
        style={{
          position: 'absolute', inset: 0, opacity: 0, cursor: 'pointer',
          width: '100%', height: '100%',
        }}
      />
    </label>
  );
}
