// booking.jsx — Friends SUP, booking flow (3 steps).
// Same chrome / scale / language as the certificate flow.
// Reuses: CardSwitcher, FriendsLogo, fmtRub from certificate.jsx.
//
// Источник данных — backend `/api/routes`. Каталог маршрутов и цены ведётся
// в админке (/admin/routes), фронт получает их при загрузке страницы.
// Список доступных времён на конкретную дату приходит с `/api/availability`
// — в нём уже учтены паттерны и точечные оверрайды (закрытия дня и т.п.).

let BOOKING_ROUTES = [];
const getRoutes = () => BOOKING_ROUTES;

// ─── Helpers ───────────────────────────────────────────────────────
const isWeekend = (iso) => {
  if (!iso) return false;
  const d = new Date(iso);
  if (isNaN(d.getTime())) return false;
  const day = d.getDay();
  return day === 0 || day === 6;
};

const priceFor = (route, iso) => {
  if (!route) return 0;
  return isWeekend(iso) ? route.weekend : route.weekday;
};

// Единая логика расчёта брони — должна совпадать с server.js POST /api/bookings.
// Сервер считает заново и не доверяет фронту, но фронт показывает то же,
// что клиент увидит в итоговой сумме.
//
// promoPercent — реальный процент скидки (0..100), полученный с сервера через
// /api/booking/promo/check. Без проверки на сервере фронт не должен сам
// решать, что промо валидно — `promoPercent === 0` означает «промо нет/не
// применено», и скидка не считается.
const GROUP_DISCOUNT_THRESHOLD = 5;
const GROUP_DISCOUNT_PCT = 0.15;
function calcBookingTotal({ route, date, persons, promoPercent = 0 }) {
  if (!route) return { unit: 0, subtotal: 0, groupDiscount: 0, promoDiscount: 0, discount: 0, total: 0 };
  const unit = priceFor(route, date);
  const subtotal = unit * (persons || 1);
  const groupDiscount = persons >= GROUP_DISCOUNT_THRESHOLD
    ? Math.round(subtotal * GROUP_DISCOUNT_PCT) : 0;
  const afterGroup = subtotal - groupDiscount;
  const promoDiscount = promoPercent > 0
    ? Math.round(afterGroup * (promoPercent / 100)) : 0;
  const discount = groupDiscount + promoDiscount;
  const total = afterGroup - promoDiscount;
  return { unit, subtotal, groupDiscount, promoDiscount, discount, total };
}
if (typeof window !== 'undefined') window.calcBookingTotal = calcBookingTotal;

const formatBookingDate = (iso) => {
  if (!iso) return '—';
  const months = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
  const d = new Date(iso);
  if (isNaN(d.getTime())) return iso;
  return `${d.getDate()} ${months[d.getMonth()]}`;
};

const personsLabel = (n) => {
  if (n === 1) return 'на одну персону';
  if (n >= 6) return `на ${n} персон`;
  if (n < 5) return `на ${n} персоны`;
  return `на ${n} персон`;
};

// Валидаторы — те же, что использует бэк (EMAIL_RE в server.js).
// Телефон считаем валидным при ≥ 11 цифр (российский формат с кодом страны).
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const phoneDigits = (s) => (s || '').replace(/\D/g, '');
const isValidEmail = (s) => !!s && EMAIL_RE.test(String(s).trim());
const isValidPhone = (s) => phoneDigits(s).length >= 11;

// Парсим/печатаем 'YYYY-MM-DD' через UTC, чтобы не ловить TZ-сдвиги (на бэке
// расчёт day-of-week сделан так же).
const isoUtcDate = (d) => {
  const y = d.getUTCFullYear();
  const m = String(d.getUTCMonth() + 1).padStart(2, '0');
  const day = String(d.getUTCDate()).padStart(2, '0');
  return `${y}-${m}-${day}`;
};
const todayIso = () => {
  const n = new Date();
  return isoUtcDate(new Date(Date.UTC(n.getFullYear(), n.getMonth(), n.getDate())));
};
const RU_DAY_SHORT = ['ПН', 'ВТ', 'СР', 'ЧТ', 'ПТ', 'СБ', 'ВС'];
const RU_MONTHS_FULL = ['Январь','Февраль','Март','Апрель','Май','Июнь','Июль','Август','Сентябрь','Октябрь','Ноябрь','Декабрь'];

// ─── DateCalendar ─────────────────────────────────────────────────
// Мини-календарь с дисейбленными днями, на которые на выбранном маршруте
// нет слотов (паттерн отсутствует или есть override 'closed'). На каждое
// открытие месяца тянем availability-range и кэшируем в state.
function DateCalendar({ routeId, value, onChange, compact = false }) {
  // visibleMonth — Date на 1-е число локального месяца.
  const [visible, setVisible] = React.useState(() => {
    if (value) {
      const [y, m] = value.split('-').map(Number);
      return new Date(Date.UTC(y, m - 1, 1));
    }
    const t = new Date();
    return new Date(Date.UTC(t.getFullYear(), t.getMonth(), 1));
  });
  const [availMap, setAvailMap] = React.useState({});
  const [loading, setLoading] = React.useState(false);

  // Окно — 6 недель от первой пнт-недели месяца.
  const days = React.useMemo(() => {
    const firstDow = (new Date(visible).getUTCDay() + 6) % 7; // пн=0
    const start = new Date(visible);
    start.setUTCDate(start.getUTCDate() - firstDow);
    const arr = [];
    for (let i = 0; i < 42; i++) {
      const d = new Date(start);
      d.setUTCDate(d.getUTCDate() + i);
      arr.push(d);
    }
    return arr;
  }, [visible]);

  React.useEffect(() => {
    if (!routeId || !days.length) return;
    let off = false;
    const from = isoUtcDate(days[0]);
    const to = isoUtcDate(days[days.length - 1]);
    setLoading(true);
    fetch(`/api/availability-range?routeId=${encodeURIComponent(routeId)}&from=${from}&to=${to}`)
      .then(r => r.ok ? r.json() : [])
      .then(rows => {
        if (off) return;
        const map = {};
        for (const r of rows) map[r.date] = r;
        setAvailMap(map);
      })
      .catch(() => { if (!off) setAvailMap({}); })
      .finally(() => { if (!off) setLoading(false); });
    return () => { off = true; };
  }, [routeId, days[0] && days[0].toISOString(), days[days.length - 1] && days[days.length - 1].toISOString()]);

  const today = todayIso();
  const visibleMonth = visible.getUTCMonth();
  const visibleYear = visible.getUTCFullYear();
  const goPrev = () => setVisible((d) => { const x = new Date(d); x.setUTCMonth(x.getUTCMonth() - 1); return x; });
  const goNext = () => setVisible((d) => { const x = new Date(d); x.setUTCMonth(x.getUTCMonth() + 1); return x; });

  const cellSize = compact ? 32 : 36;
  const fontSize = compact ? 12 : 13;

  return (
    <div style={{
      background: '#fff', borderRadius: 12, padding: '10px 12px 12px',
      boxShadow: '0 1px 0 rgba(0,0,0,.04)'
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        marginBottom: 8
      }}>
        <button onClick={goPrev} style={calNavBtn} aria-label="prev">‹</button>
        <div style={{
          fontSize: 13, fontWeight: 600, letterSpacing: '-.005em',
          color: '#0A2E1F', textTransform: 'capitalize'
        }}>{RU_MONTHS_FULL[visibleMonth]} {visibleYear}</div>
        <button onClick={goNext} style={calNavBtn} aria-label="next">›</button>
      </div>
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
        gap: 2, marginBottom: 4
      }}>
        {RU_DAY_SHORT.map(d => (
          <div key={d} style={{
            fontSize: 9, fontWeight: 600, color: '#9aa39e',
            letterSpacing: '.14em', textAlign: 'center', padding: '4px 0'
          }}>{d}</div>
        ))}
      </div>
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: 2,
        opacity: loading ? .6 : 1, transition: 'opacity .12s'
      }}>
        {days.map((d) => {
          const iso = isoUtcDate(d);
          const inMonth = d.getUTCMonth() === visibleMonth;
          const past = iso < today;
          const av = availMap[iso];
          const closed = av && av.closed;
          const noSlots = av && !closed && (!av.times || av.times.length === 0);
          const disabled = past || closed || noSlots || !inMonth;
          const selected = value === iso;
          return (
            <button
              key={iso}
              type="button"
              onClick={() => !disabled && onChange(iso)}
              disabled={disabled}
              title={closed ? 'маршрут закрыт' : noSlots ? 'нет слотов' : ''}
              style={{
                appearance: 'none', border: 'none',
                cursor: disabled ? 'not-allowed' : 'pointer',
                background: selected ? '#DD3310' : 'transparent',
                color: selected ? '#fff' : (disabled ? '#c8cdc9' : '#0A2E1F'),
                opacity: !inMonth ? .35 : 1,
                width: '100%', height: cellSize,
                borderRadius: 8, fontSize, fontFamily: 'inherit',
                fontWeight: selected ? 600 : 400,
                fontVariantNumeric: 'tabular-nums',
                transition: 'background .12s, color .12s',
                position: 'relative'
              }}>{d.getUTCDate()}
              {iso === today && !selected && (
                <span style={{
                  position: 'absolute', bottom: 4, left: '50%',
                  transform: 'translateX(-50%)',
                  width: 4, height: 4, borderRadius: '50%',
                  background: disabled ? '#c8cdc9' : '#DD3310'
                }} />
              )}
            </button>
          );
        })}
      </div>
    </div>);
}

const calNavBtn = {
  appearance: 'none', border: 'none', cursor: 'pointer',
  background: 'transparent', color: '#0A2E1F',
  width: 28, height: 28, borderRadius: 8,
  fontSize: 18, fontFamily: 'inherit', lineHeight: 1
};

// ─── DateField ───────────────────────────────────────────────────
// Узкое поле «📅 + дата». При клике открывается компактный popover с
// DateCalendar над полем; повторный клик / клик вне поповера / Escape — закрывают.
function DateField({ routeId, value, onChange, placeholder = 'Выберите дату' }) {
  const [open, setOpen] = React.useState(false);
  const wrapRef = React.useRef(null);

  React.useEffect(() => {
    if (!open) return;
    const onDoc = (e) => {
      if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('mousedown', onDoc);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('mousedown', onDoc);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  // В заголовке поля показываем дату с годом — «15 апреля 2026», чтобы
  // нельзя было перепутать с датой следующего/прошлого года при долгом
  // выборе. В тикете и рекапах год не нужен (там контекст уже понятен).
  const months = ['января','февраля','марта','апреля','мая','июня','июля','августа','сентября','октября','ноября','декабря'];
  const fmtWithYear = (iso) => {
    if (!iso) return placeholder;
    const [y, m, d] = iso.split('-').map(Number);
    return `${d} ${months[m - 1]} ${y}`;
  };
  const display = value ? fmtWithYear(value) : placeholder;

  return (
    <div ref={wrapRef} style={{ position: 'relative' }}>
      <button
        type="button"
        onClick={() => setOpen((o) => !o)}
        style={{
          appearance: 'none', border: 'none', cursor: 'pointer',
          width: '100%', textAlign: 'left',
          background: '#fff', borderRadius: 10, padding: '11px 13px',
          boxShadow: open ? '0 0 0 1.5px #0A2E1F' : '0 1px 0 rgba(0,0,0,.04)',
          display: 'flex', alignItems: 'center', gap: 8,
          fontFamily: 'inherit', fontSize: 13,
          color: value ? '#0A2E1F' : '#9aa39e',
          transition: 'box-shadow .15s'
        }}>
        <span style={{ fontSize: 14 }}>📅</span>
        <span style={{ flex: 1, fontVariantNumeric: 'tabular-nums' }}>{display}</span>
        <span style={{ fontSize: 10, color: '#9aa39e', transform: open ? 'rotate(180deg)' : 'none', transition: 'transform .15s' }}>▾</span>
      </button>
      {open && (
        <div style={{
          position: 'absolute', top: 'calc(100% + 6px)', left: 0,
          width: 244, zIndex: 50,
          boxShadow: '0 18px 40px -12px rgba(10,46,31,.28), 0 1px 0 rgba(0,0,0,.04)',
          borderRadius: 12, background: '#fff'
        }}>
          <DateCalendar
            routeId={routeId}
            value={value}
            onChange={(v) => { onChange(v); setOpen(false); }}
            compact />
        </div>
      )}
    </div>);
}

// Делаем компоненты доступными мобильному bundle (booking-mobile.jsx внутри
// собственного IIFE-скоупа), чтобы не дублировать тот же код.
if (typeof window !== 'undefined') {
  window.DateCalendar = DateCalendar;
  window.DateField = DateField;
}

// ─── Ticket card (replaces gift certificate) ───────────────────────
function TicketCard({ route, date, time, persons }) {
  const fmtRub = window.fmtRub;
  const weekend = isWeekend(date);
  const price = priceFor(route, date);
  // Билет показывает финальную «к оплате» с уже учтённой групповой скидкой.
  // Промо (если есть) применится в Step3 — здесь его не учитываем (билет
  // одинаков на всех шагах визарда).
  const { total: ticketTotal } = calcBookingTotal({ route, date, persons });

  return (
    <div style={{
      position: 'relative',
      width: '100%',
      aspectRatio: '4 / 5',
      borderRadius: 18,
      background: '#FAF7EF',
      boxShadow: '0 30px 60px -20px rgba(10,46,31,.28), 0 4px 12px rgba(0,0,0,.06)',
      overflow: 'hidden',
      color: '#0A2E1F',
      fontFamily: 'inherit',
      display: 'flex', flexDirection: 'column'
    }}>
      <div style={{ position: 'absolute', inset: 0, boxShadow: 'inset 0 0 0 1px rgba(10,46,31,.06)', borderRadius: 18, pointerEvents: 'none', zIndex: 3 }} />

      {/* Notch (ticket perforation) — centered ON the tear-off line */}
      <div style={{
        position: 'absolute', left: -10, top: 'calc(66.666% - 10px)', width: 20, height: 20,
        borderRadius: '50%', background: '#F4F1EA', zIndex: 2,
        boxShadow: 'inset 0 0 0 1px rgba(10,46,31,.06)'
      }} />
      <div style={{
        position: 'absolute', right: -10, top: 'calc(66.666% - 10px)', width: 20, height: 20,
        borderRadius: '50%', background: '#F4F1EA', zIndex: 2,
        boxShadow: 'inset 0 0 0 1px rgba(10,46,31,.06)'
      }} />

      {/* TOP: photo */}
      <div style={{
        height: '66.666%', position: 'relative', overflow: 'hidden',
        background: '#1d2a26'
      }}>
        <img
          src={route.photo}
          alt=""
          style={{
            position: 'absolute', inset: 0,
            width: '100%', height: '100%',
            objectFit: 'cover',
            objectPosition: 'center 60%',
            display: 'block'
          }} />
        <div style={{
          position: 'absolute', inset: 0,
          background: 'linear-gradient(rgba(0,0,0,0) 55%, rgba(0,0,0,.28) 100%)'
        }} />
        {/* Logo only */}
        <div style={{
          position: 'absolute', top: 16, left: 16, right: 16,
          display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start',
          gap: 10
        }}>
          <img
            src={window.__resources.logoOrange}
            alt="Friends SUP"
            style={{
              height: 'clamp(18px, 3.2cqw, 28px)', width: 'auto'
            }} />
        </div>

        {/* Bottom-left: route name overlaid on photo */}
        <div style={{
          position: 'absolute', bottom: 18, left: 18, right: 18,
          color: '#fff'
        }}>
          <div style={{
            fontSize: 'clamp(8px, 1.05cqw, 10px)',
            letterSpacing: '.22em', textTransform: 'uppercase',
            fontWeight: 500, opacity: .85, marginBottom: 4
          }}>Билет на прогулку</div>
          <div style={{
            fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
            fontSize: 'clamp(20px, 3.6cqw, 32px)',
            fontWeight: 800, letterSpacing: '-.01em', lineHeight: 1.05,
            textShadow: '0 2px 14px rgba(0,0,0,.35)'
          }}>{route.name}</div>
        </div>
      </div>

      {/* Divider — aligned with notch centers (the tear-off line) */}
      <div style={{
        position: 'absolute', left: 18, right: 18, top: '66.666%',
        borderTop: '1.5px dashed rgba(10,46,31,.22)',
        zIndex: 4, pointerEvents: 'none'
      }} />

      {/* BOTTOM: details */}
      <div style={{
        // padding-top увеличен — нужно ощутимое расстояние от пунктирной
        // перфорации билета до подписей «ДАТА»/«ВРЕМЯ», иначе они
        // визуально с ней соприкасаются.
        height: '33.333%', padding: '44px 20px 14px',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        background: '#fff'
      }}>
        <div style={{
          display: 'grid', gridTemplateColumns: '1fr 1fr auto', columnGap: 16, rowGap: 14,
          alignItems: 'start', flex: 1, minHeight: 0
        }}>
          <TicketField label="Дата" value={formatBookingDate(date)} sub={date ? (weekend ? 'выходной' : 'будни') : null} />
          <TicketField label="Время" value={time || '—'} mono />
          <a href="#" onClick={(e) => e.preventDefault()} style={{
            width: 'clamp(112px, 19cqw, 156px)',
            aspectRatio: '1 / 1',
            background: '#fff', borderRadius: 10, padding: 6,
            flexShrink: 0, alignSelf: 'center',
            display: 'block', boxShadow: '0 0 0 1px rgba(10,46,31,.08)',
            gridRow: '1 / span 2'
          }} title="taplink">
            <img src={window.__resources.qrSvg} alt="QR" style={{ width: '100%', height: '100%', display: 'block' }} />
          </a>
          <TicketField label="Персон" value={persons || '—'} mono />
          <TicketField label="К оплате" value={date ? fmtRub(ticketTotal) : '—'} accent mono />
        </div>

        <div style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          fontSize: 'clamp(9px, 1.05cqw, 10px)', color: '#5a6660',
          paddingTop: 10, borderTop: '1px solid rgba(10,46,31,.08)',
          gap: 10
        }}>
          <div>
            <span style={{ opacity: .55, textTransform: 'uppercase', letterSpacing: '.14em' }}>№ </span>
            <span style={{ fontVariantNumeric: 'tabular-nums', color: '#0A2E1F', fontWeight: 500, letterSpacing: '.08em' }}>*******</span>
          </div>
          <div style={{ color: '#0A2E1F', fontWeight: 500 }}>friends-sup.ru</div>
        </div>
      </div>
    </div>);
}

function TicketField({ label, value, mono, accent, sub }) {
  return (
    <div style={{ minWidth: 0 }}>
      <div style={{
        fontSize: 'clamp(8px, 1.05cqw, 10px)', color: '#5a6660',
        letterSpacing: '.18em', textTransform: 'uppercase', fontWeight: 500,
        marginBottom: 6
      }}>{label}</div>
      <div style={{
        fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
        fontSize: 'clamp(22px, 3.6cqw, 32px)',
        fontWeight: accent ? 800 : 700,
        color: accent ? '#DD3310' : '#0A2E1F',
        letterSpacing: '-.015em', lineHeight: 1.05,
        fontVariantNumeric: mono ? 'tabular-nums' : 'normal',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
      }}>{value}</div>
      {sub && (
        <div style={{
          fontSize: 'clamp(10px, 1.25cqw, 12px)',
          color: '#5a6660', marginTop: 4,
          letterSpacing: '.02em', fontWeight: 400
        }}>{sub}</div>
      )}
    </div>);
}

// ─── Shared chrome (booking-flavoured) ─────────────────────────────
function BookingShell({ stepNumber, children }) {
  return (
    <div style={{
      position: 'relative', width: '100%', height: '100%',
      background: '#fff', borderRadius: 18, overflow: 'hidden',
      boxShadow: '0 1px 0 rgba(0,0,0,.04), 0 30px 80px -30px rgba(0,0,0,.18)',
      fontFamily: '"NT Somic", system-ui, -apple-system, sans-serif',
      color: '#0A2E1F', display: 'flex', flexDirection: 'column'
    }}>
      <header style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '22px 32px', borderBottom: '1px solid rgba(10,46,31,.06)', flexShrink: 0
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <window.FriendsLogo size={22} />
          <div style={{ width: 1, height: 22, background: 'rgba(10,46,31,.12)' }} />
          <span style={{ fontSize: 14, color: '#3a4a44' }}>Бронирование прогулки на сапах</span>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 12, color: '#5a6660' }}>
          {[
            { n: stepNumber > 1 ? '✓' : '1', label: 'Маршрут',     done: stepNumber > 1, active: stepNumber === 1 },
            { n: stepNumber > 2 ? '✓' : '2', label: 'Детали',      done: stepNumber > 2, active: stepNumber === 2 },
            { n: '3', label: 'Подтверждение', done: false, active: stepNumber === 3 }
          ].map((s, i) => (
            <React.Fragment key={i}>
              {i > 0 && <span style={{ width: 14, height: 1, background: 'rgba(10,46,31,.16)' }} />}
              <span style={{
                display: 'flex', alignItems: 'center', gap: 6,
                color: s.active ? '#0A2E1F' : '#5a6660',
                fontWeight: s.active ? 500 : 400,
                opacity: !s.active && !s.done ? .5 : 1
              }}>
                <span style={{
                  width: 18, height: 18, borderRadius: '50%',
                  background: s.active ? '#DD3310' : s.done ? '#0A2E1F' : 'rgba(10,46,31,.12)',
                  color: s.active || s.done ? '#fff' : '#0A2E1F',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 10, fontWeight: 600
                }}>{s.n}</span>
                {s.label}
              </span>
            </React.Fragment>
          ))}
        </div>
      </header>

      <div style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
        {children}
      </div>

      <footer style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '14px 32px', borderTop: '1px solid rgba(10,46,31,.06)', flexShrink: 0
      }}>
        <div style={{ fontSize: 13, color: '#5a6660', letterSpacing: '.04em' }}>friends-sup.ru</div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{
            width: 30, height: 30, borderRadius: '50%',
            background: '#0A2E1F', display: 'flex', alignItems: 'center', justifyContent: 'center',
            color: '#fff', fontSize: 14
          }}>?</div>
          <span style={{ fontSize: 12, fontWeight: 600, letterSpacing: '.16em', color: '#0A2E1F' }}>ПОДДЕРЖКА</span>
        </div>
        <a href="#" onClick={(e) => e.preventDefault()} style={{ fontSize: 13, color: '#5a6660', textDecoration: 'none' }}>Правила</a>
      </footer>
    </div>);
}

function BookingTwoCol({ left, right }) {
  return (
    <div style={{
      flex: 1,
      display: 'grid', gridTemplateColumns: '0.85fr 1fr', gap: 0,
      background: '#F4F1EA', minHeight: 0
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: '20px 14px 20px 28px'
      }}>
        <div style={{ width: '100%', maxWidth: 440, containerType: 'inline-size' }}>{left}</div>
      </div>
      <div style={{
        padding: '20px 28px 20px 16px',
        display: 'flex', flexDirection: 'column', minHeight: 0
      }}>
        {right}
      </div>
    </div>);
}

// ─── STEP 1: Route + date + time + persons ─────────────────────────
function BookingStep1({
  initialRouteId = 'new-holland',
  initialDate = '',
  initialTime = '',
  initialPersons = 2,
  descriptionMode = 'tooltip',
  initialInfoRouteId = null,
  onContinue
}) {
  const fmtRub = window.fmtRub;
  const CardSwitcher = window.CardSwitcher;

  const [routeId, setRouteId] = React.useState(initialRouteId);
  const [date, setDate] = React.useState(initialDate);
  const [time, setTime] = React.useState(initialTime);
  const [persons, setPersons] = React.useState(initialPersons);
  const [infoRouteId, setInfoRouteId] = React.useState(initialInfoRouteId);

  const route = BOOKING_ROUTES.find(r => r.id === routeId) || BOOKING_ROUTES[0];
  const infoRoute = BOOKING_ROUTES.find(r => r.id === infoRouteId);
  const pricing = calcBookingTotal({ route, date, persons });
  const price = pricing.unit;
  const subtotal = pricing.subtotal;
  const groupDiscount = pricing.groupDiscount;
  const total = pricing.total;

  // Доступные времена приходят с бэка с учётом паттернов и оверрайдов.
  // Пока маршрут+дата не выбраны — показываем дефолтные слоты как «скелет»,
  // но кликнуть по ним нельзя (canContinue требует выбранную дату).
  const [availTimes, setAvailTimes] = React.useState([]);
  const [availClosed, setAvailClosed] = React.useState(false);
  const [availLoading, setAvailLoading] = React.useState(false);
  const [slotRemaining, setSlotRemaining] = React.useState({});

  React.useEffect(() => {
    if (!routeId || !date) { setAvailTimes([]); setAvailClosed(false); setSlotRemaining({}); return; }
    let off = false;
    setAvailLoading(true);
    fetch(`/api/availability?routeId=${encodeURIComponent(routeId)}&date=${encodeURIComponent(date)}`)
      .then(r => r.ok ? r.json() : { times: [], closed: false, remaining: {} })
      .then(d => {
        if (!off) {
          setAvailTimes(d.times || []);
          setAvailClosed(!!d.closed);
          setSlotRemaining(d.remaining || {});
          // Если выбрано время и кол-во превышает остаток — корректируем
          const maxNow = (time && d.remaining && d.remaining[time] != null) ? d.remaining[time] : 20;
          setPersons(prev => Math.min(prev, maxNow));
        }
      })
      .catch(() => { if (!off) { setAvailTimes([]); setAvailClosed(false); } })
      .finally(() => { if (!off) setAvailLoading(false); });
    return () => { off = true; };
  }, [routeId, date]);

  // Если выбранное время больше не входит в доступные — сбрасываем.
  React.useEffect(() => {
    if (time && availTimes.length && !availTimes.includes(time)) setTime('');
  }, [availTimes]);

  const TIMES = availTimes.length ? availTimes : ['09:00','11:00','13:00','15:00','17:00','19:00'];
  const timesDisabled = !date || availClosed;
  const canContinue = !!date && !!time && availTimes.includes(time);

  return (
    <BookingShell stepNumber={1}>
      <BookingTwoCol
        left={
          <CardSwitcher amountKey={`book1-${routeId}-${date}-${time}-${persons}`}>
            <TicketCard route={route} date={date} time={time} persons={persons} />
          </CardSwitcher>
        }
        right={
          <>
            <StepHeader stepNumber={1} title="Выберите маршрут" />

            {/* Route grid 2×3 */}
            <div style={{
              display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10,
              // Между подшагами (маршрут / дата+время / число человек) делаем
              // явный воздух, чтобы они визуально читались как отдельные группы.
              marginBottom: 26
            }}>
              {BOOKING_ROUTES.map(r => {
                const active = routeId === r.id;
                const showInfo = infoRouteId === r.id;
                return (
                  <div key={r.id} style={{ position: 'relative' }}>
                    <button
                      onClick={() => setRouteId(r.id)}
                      style={{
                        appearance: 'none', border: 'none', cursor: 'pointer',
                        background: active ? '#DD3310' : '#fff',
                        color: active ? '#fff' : '#0A2E1F',
                        borderRadius: 12,
                        // Высота больше + симметричный паддинг по бокам, чтобы
                        // «БУДНИ/ВЫХ» доходил до правого края (info-кнопка
                        // позиционируется абсолютно поверх правого верхнего угла).
                        padding: '10px 14px',
                        minHeight: 62,
                        display: 'flex', flexDirection: 'column',
                        justifyContent: 'space-between',
                        gap: 6, textAlign: 'left', width: '100%',
                        fontFamily: 'inherit',
                        boxShadow: active ? '0 8px 22px -10px rgba(221,51,16,.55)' : '0 1px 0 rgba(0,0,0,.04)',
                        transition: 'all .15s', minWidth: 0
                      }}>
                      <div style={{
                        fontSize: 14, fontWeight: 500, letterSpacing: '-.005em',
                        // Резервируем место под info-кнопку (22 + 8) — текст ровно
                        // обрезается в эллипсис, не наезжая на «i».
                        paddingRight: 30,
                        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
                      }}>{r.name}</div>
                      <div style={{
                        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', gap: 8
                      }}>
                        <span style={{
                          fontSize: 12,
                          color: active ? 'rgba(255,255,255,.78)' : '#5a6660',
                          fontVariantNumeric: 'tabular-nums'
                        }}>{r.weekday === r.weekend ? fmtRub(r.weekday) : `${fmtRub(r.weekday)} / ${fmtRub(r.weekend)}`}</span>
                        <span style={{
                          fontSize: 9, fontWeight: 600, letterSpacing: '.14em',
                          color: active ? 'rgba(255,255,255,.55)' : '#9aa39e',
                          textTransform: 'uppercase'
                        }}>будни/вых</span>
                      </div>
                    </button>
                    {/* Info trigger */}
                    <button
                      type="button"
                      onMouseEnter={() => setInfoRouteId(r.id)}
                      onMouseLeave={() => setInfoRouteId(prev => prev === r.id ? null : prev)}
                      onClick={(e) => {
                        e.stopPropagation();
                        setInfoRouteId(showInfo ? null : r.id);
                      }}
                      style={{
                        position: 'absolute', top: 8, right: 8,
                        width: 22, height: 22, borderRadius: '50%',
                        appearance: 'none', cursor: 'pointer',
                        border: active ? '1px solid rgba(255,255,255,.4)' : '1px solid rgba(10,46,31,.18)',
                        background: showInfo
                          ? (active ? 'rgba(255,255,255,.2)' : '#0A2E1F')
                          : 'transparent',
                        color: showInfo
                          ? (active ? '#fff' : '#fff')
                          : (active ? '#fff' : '#0A2E1F'),
                        fontFamily: '"RF Dewi Extended", "NT Somic", serif',
                        fontSize: 11, fontWeight: 700, fontStyle: 'italic',
                        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                        transition: 'all .15s', lineHeight: 1
                      }}
                      title="О маршруте">i</button>

                    {/* Tooltip */}
                    {showInfo && (
                      <div style={{
                        position: 'absolute', top: 'calc(100% + 8px)', left: 0, right: 0,
                        background: 'rgba(10, 46, 31, .72)',
                        backdropFilter: 'blur(14px)',
                        WebkitBackdropFilter: 'blur(14px)',
                        color: '#fff',
                        borderRadius: 10, padding: '12px 14px',
                        fontSize: 12, lineHeight: 1.45,
                        boxShadow: '0 16px 36px -10px rgba(10,46,31,.35)',
                        border: '1px solid rgba(255,255,255,.08)',
                        zIndex: 20, pointerEvents: 'none'
                      }}>
                        <div style={{
                          position: 'absolute', top: -6, right: 11,
                          width: 12, height: 12, background: 'rgba(10,46,31,.72)',
                          backdropFilter: 'blur(14px)',
                          WebkitBackdropFilter: 'blur(14px)',
                          transform: 'rotate(45deg)', borderRadius: 2
                        }} />
                        <div style={{
                          fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
                          fontSize: 13, fontWeight: 700, marginBottom: 4
                        }}>{r.name}</div>
                        <div style={{ opacity: .9, marginBottom: 6 }}>{r.description}</div>
                        <div style={{
                          fontSize: 11, opacity: .7,
                          letterSpacing: '.06em', textTransform: 'uppercase'
                        }}>Продолжительность · {r.duration}</div>
                      </div>
                    )}
                  </div>);
              })}
            </div>

            {/* Date + time row — поле «Дата» компактное, при клике
                открывается календарь-поповер. Доступные дни считаются
                на бэке через /api/availability-range. */}
            <div style={{
              display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12,
              marginBottom: 26
            }}>
              <div>
                <div style={{ fontSize: 11, color: '#5a6660', letterSpacing: '.18em', textTransform: 'uppercase', marginBottom: 6, fontWeight: 500 }}>Дата</div>
                <DateField routeId={routeId} value={date} onChange={setDate} />
              </div>
              <div>
                <div style={{ fontSize: 11, color: '#5a6660', letterSpacing: '.18em', textTransform: 'uppercase', marginBottom: 6, fontWeight: 500 }}>Время</div>
                {availClosed ? (
                  <div style={{
                    background: 'rgba(221,51,16,.08)', color: '#DD3310',
                    borderRadius: 10, padding: '12px 14px',
                    fontSize: 12, lineHeight: 1.4
                  }}>В этот день маршрут закрыт. Пожалуйста, выберите другую дату.</div>
                ) : (
                  <div style={{
                    background: '#fff', borderRadius: 10, padding: '6px 6px',
                    boxShadow: '0 1px 0 rgba(0,0,0,.04)',
                    display: 'grid',
                    gridTemplateColumns: `repeat(${Math.max(TIMES.length, 1)}, 1fr)`,
                    gap: 2
                  }}>
                    {TIMES.map(t => {
                      const active = time === t;
                      const disabled = timesDisabled || !availTimes.includes(t);
                      return (
                        <button
                          key={t}
                          onClick={() => !disabled && setTime(t)}
                          disabled={disabled}
                          style={{
                            appearance: 'none', border: 'none',
                            cursor: disabled ? 'not-allowed' : 'pointer',
                            background: active ? '#0A2E1F' : 'transparent',
                            color: active ? '#fff' : '#0A2E1F',
                            opacity: disabled ? .35 : 1,
                            borderRadius: 8, padding: '6px 0',
                            fontSize: 11, fontWeight: 500, fontFamily: 'inherit',
                            fontVariantNumeric: 'tabular-nums',
                            transition: 'all .15s'
                          }}>{t}</button>);
                    })}
                  </div>
                )}
                {date && !availClosed && availLoading && (
                  <div style={{ fontSize: 11, color: '#5a6660', marginTop: 6 }}>загружаем расписание…</div>
                )}
                {date && !availClosed && !availLoading && availTimes.length === 0 && (
                  <div style={{ fontSize: 11, color: '#5a6660', marginTop: 6 }}>на этот день расписания нет — выберите другую дату.</div>
                )}
              </div>
            </div>

            {/* Persons */}
            <div style={{ fontSize: 11, color: '#5a6660', letterSpacing: '.18em', textTransform: 'uppercase', marginBottom: 10, fontWeight: 500 }}>Количество человек</div>
            <div style={{
              display: 'flex', alignItems: 'center', gap: 10, marginBottom: 22
            }}>
              {(() => {
                const maxPersons = (time && slotRemaining[time] != null) ? slotRemaining[time] : 20;
                return (
                  <>
                    <button
                      onClick={() => setPersons(Math.max(1, persons - 1))}
                      style={stepperBtn}>−</button>
                    <div style={{
                      background: '#fff', borderRadius: 10, padding: '11px 0',
                      width: 64, textAlign: 'center', fontSize: 16, fontWeight: 500,
                      fontVariantNumeric: 'tabular-nums',
                      boxShadow: '0 1px 0 rgba(0,0,0,.04)'
                    }}>{persons}</div>
                    <button
                      onClick={() => setPersons(Math.min(maxPersons, persons + 1))}
                      disabled={persons >= maxPersons}
                      style={{ ...stepperBtn, ...(persons >= maxPersons ? { opacity: 0.35, cursor: 'not-allowed' } : {}) }}>+</button>
                  </>
                );
              })()}
              <div style={{ fontSize: 12, color: '#5a6660', marginLeft: 6 }}>
                {persons === 1 ? 'один человек' : persons < 5 ? `${persons} человека` : `${persons} человек`}
              </div>
            </div>

            {/* Подсказка о групповой скидке: до 5 человек — призыв,
                от 5 — подтверждение что скидка применена. */}
            <div style={{
              fontSize: 12, lineHeight: 1.45, marginBottom: 14,
              color: groupDiscount > 0 ? '#0B5D3B' : '#5a6660'
            }}>
              {groupDiscount > 0
                ? `✓ Групповая скидка 15% применена (−${fmtRub(groupDiscount)})`
                : 'От 5 человек — скидка 15% на бронь.'}
            </div>

            <div style={{ flex: 1, minHeight: 6 }} />

            {/* VPN warning */}
            <div style={{
              fontSize: 11, color: '#5a6660', lineHeight: 1.45,
              padding: '10px 14px', background: 'rgba(10,46,31,.04)',
              borderRadius: 10, marginBottom: 12
            }}>
              Пожалуйста, проверьте, выключен ли у вас VPN, прежде чем перейти на следующий шаг.
            </div>

            {/* Total + Next */}
            <div style={{
              display: 'flex', alignItems: 'center', gap: 12,
              background: '#fff', borderRadius: 14, padding: '10px 12px 10px 18px',
              boxShadow: '0 1px 0 rgba(0,0,0,.04)'
            }}>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 11, color: '#5a6660', textTransform: 'uppercase', letterSpacing: '.14em' }}>К оплате</div>
                <div style={{
                  display: 'flex', alignItems: 'baseline', gap: 8,
                  fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
                  letterSpacing: '-.01em',
                  fontVariantNumeric: 'tabular-nums'
                }}>
                  {date && groupDiscount > 0 && (
                    <span style={{
                      fontSize: 14, fontWeight: 400,
                      color: '#9aa39e', textDecoration: 'line-through',
                      textDecorationThickness: '1px'
                    }}>{fmtRub(subtotal)}</span>
                  )}
                  <span style={{
                    fontSize: 22, fontWeight: 500,
                    color: date && groupDiscount > 0 ? '#DD3310' : '#0A2E1F'
                  }}>{date ? fmtRub(total) : '—'}</span>
                </div>
              </div>
              <button
                onClick={() => canContinue && onContinue && onContinue({ routeId, date, time, persons })}
                disabled={!canContinue}
                style={{
                  appearance: 'none', border: 'none',
                  cursor: canContinue ? 'pointer' : 'not-allowed',
                  background: canContinue ? '#0A2E1F' : 'rgba(10,46,31,.12)',
                  color: canContinue ? '#fff' : '#9aa39e',
                  borderRadius: 12, padding: '14px 22px',
                  display: 'inline-flex', alignItems: 'center', gap: 12,
                  fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
                  boxShadow: canContinue ? '0 12px 28px -12px rgba(10,46,31,.5)' : 'none',
                  transition: 'all .15s'
                }}>
                Далее
                <span style={{
                  width: 26, height: 26, borderRadius: '50%',
                  background: canContinue ? '#DD3310' : 'rgba(10,46,31,.08)',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 13, color: canContinue ? '#fff' : '#9aa39e'
                }}>→</span>
              </button>
            </div>
          </>
        } />
    </BookingShell>);
}

// ─── STEP 2: Booking details ───────────────────────────────────────
function BookingStep2({
  routeId = 'new-holland',
  date = '2026-05-11',
  time = '14:00',
  persons = 2,
  initialFirstName = '',
  initialLastName = '',
  initialPhone = '',
  initialEmail = '',
  initialAdult = false,
  onBack, onContinue
}) {
  const fmtRub = window.fmtRub;
  const CardSwitcher = window.CardSwitcher;

  const [firstName, setFirstName] = React.useState(initialFirstName);
  const [lastName, setLastName] = React.useState(initialLastName);
  const [phone, setPhone] = React.useState(initialPhone);
  const [email, setEmail] = React.useState(initialEmail);
  const [adult, setAdult] = React.useState(initialAdult);
  const [phoneTouched, setPhoneTouched] = React.useState(!!initialPhone);
  const [emailTouched, setEmailTouched] = React.useState(!!initialEmail);

  const route = BOOKING_ROUTES.find(r => r.id === routeId) || BOOKING_ROUTES[0];
  const pricing2 = calcBookingTotal({ route, date, persons });
  const total = pricing2.total;
  const subtotal = pricing2.subtotal;
  const groupDiscount = pricing2.groupDiscount;

  const phoneError = phoneTouched && !!phone && !isValidPhone(phone) ? 'Полный номер: +7 и 10 цифр' : null;
  const emailError = emailTouched && !!email && !isValidEmail(email) ? 'В email нужны @ и точка, например name@mail.ru' : null;
  const canContinue =
    firstName.trim() && lastName.trim() &&
    isValidPhone(phone) && isValidEmail(email) && adult;

  return (
    <BookingShell stepNumber={2}>
      <BookingTwoCol
        left={
          <CardSwitcher amountKey={`book2-${routeId}-${date}-${time}-${persons}`}>
            <TicketCard route={route} date={date} time={time} persons={persons} />
          </CardSwitcher>
        }
        right={
          <>
            <StepHeader stepNumber={2} title="Детали бронирования" onBack={onBack} />

            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12, marginBottom: 12 }}>
              <BookingField label="Имя*" value={firstName} onChange={setFirstName} placeholder="Имя" />
              <BookingField label="Фамилия*" value={lastName} onChange={setLastName} placeholder="Фамилия" />
              <BookingField
                label="Номер телефона*"
                value={phone}
                onChange={(v) => setPhone(window.formatRussianPhone ? window.formatRussianPhone(v) : v)}
                onBlur={() => setPhoneTouched(true)}
                placeholder="+7 (___) ___-__-__"
                prefix="🇷🇺"
                type="tel"
                error={phoneError} />
              <BookingField
                label="Электронная почта*"
                value={email}
                onChange={setEmail}
                onBlur={() => setEmailTouched(true)}
                placeholder="name@mail.ru"
                type="email"
                error={emailError} />
            </div>

            <label style={{
              display: 'flex', alignItems: 'flex-start', gap: 10,
              fontSize: 13, color: '#0A2E1F',
              padding: '12px 14px', background: '#fff', borderRadius: 12,
              boxShadow: '0 1px 0 rgba(0,0,0,.04)',
              cursor: 'pointer', marginBottom: 10
            }}>
              <Checkbox checked={adult} onChange={setAdult} />
              <span style={{ lineHeight: 1.4 }}>Подтверждаю, что всем участникам больше 18 лет</span>
            </label>

            <div style={{
              fontSize: 12, color: '#5a6660', lineHeight: 1.5,
              padding: '0 4px', marginBottom: 12
            }}>
              Если участникам меньше 18 лет, запись производится через&nbsp;
              <a href="#" onClick={(e) => e.preventDefault()} style={{ color: '#DD3310', textDecoration: 'none', borderBottom: '1px solid rgba(221,51,16,.4)' }}>Telegram</a>
              , так как на некоторых маршрутах действуют ограничения.
            </div>

            <div style={{ flex: 1, minHeight: 6 }} />

            <BookingTotal route={route} date={date} persons={persons} total={total} subtotal={subtotal} groupDiscount={groupDiscount} />

            <div style={{ display: 'flex', gap: 10 }}>
              <button onClick={onBack} style={backBtn}>Назад</button>
              <button
                onClick={() => canContinue && onContinue && onContinue({ firstName, lastName, phone, email })}
                disabled={!canContinue}
                style={{
                  flex: 1, appearance: 'none', border: 'none',
                  cursor: canContinue ? 'pointer' : 'not-allowed',
                  background: canContinue ? '#0A2E1F' : 'rgba(10,46,31,.12)',
                  color: canContinue ? '#fff' : '#9aa39e',
                  borderRadius: 12, padding: '14px 22px',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'space-between',
                  fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
                  boxShadow: canContinue ? '0 12px 28px -12px rgba(10,46,31,.5)' : 'none'
                }}>
                Далее
                <span style={{
                  width: 26, height: 26, borderRadius: '50%',
                  background: canContinue ? '#DD3310' : 'rgba(10,46,31,.08)',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 13, color: canContinue ? '#fff' : '#9aa39e'
                }}>→</span>
              </button>
            </div>
          </>
        } />
    </BookingShell>);
}

// ─── STEP 3: Confirmation + payment ────────────────────────────────
function BookingStep3({
  routeId = 'new-holland',
  date = '2026-05-11',
  time = '14:00',
  persons = 2,
  firstName = 'Михаил',
  lastName = 'Шатенев',
  phone = '+79121123121',
  email = 'shatenev.mikhail@yandex.com',
  initialPromo = '',
  initialPromoApplied = false,
  initialComment = '',
  initialAgreements = { personal: false, oferta: false, cancel: false },
  onBack,
  onBooked,
  paymentResult
}) {
  const fmtRub = window.fmtRub;
  const CardSwitcher = window.CardSwitcher;

  // Способ оплаты выбирается на стороне Paykeeper'а (карта/СБП/СберПэй) —
  // дублировать выбор у себя не нужно, поэтому payment-state удалён.
  const [promo, setPromo] = React.useState(initialPromo);
  const [promoPercent, setPromoPercent] = React.useState(initialPromoApplied ? 10 : 0);
  const [promoError, setPromoError] = React.useState(null);
  const [promoChecking, setPromoChecking] = React.useState(false);
  const promoApplied = promoPercent > 0;
  const [comment, setComment] = React.useState(initialComment);
  const [agreements, setAgreements] = React.useState(initialAgreements);

  const applyPromo = async () => {
    if (!promo.trim() || promoChecking) return;
    setPromoChecking(true);
    setPromoError(null);
    try {
      const res = await fetch('/api/booking/promo/check', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ code: promo.trim() })
      });
      const data = await res.json();
      if (data.valid) {
        setPromoPercent(data.discountPercent);
      } else {
        setPromoPercent(0);
        setPromoError(data.error || 'Промокод не подошёл');
      }
    } catch {
      setPromoError('Ошибка сети');
    } finally {
      setPromoChecking(false);
    }
  };

  const route = BOOKING_ROUTES.find(r => r.id === routeId) || BOOKING_ROUTES[0];
  const pricing3 = calcBookingTotal({ route, date, persons, promoPercent });
  const price = pricing3.unit;
  const subtotal = pricing3.subtotal;
  const groupDiscount = pricing3.groupDiscount;
  const discount = pricing3.discount;
  const total = pricing3.total;
  const canBook = agreements.personal && agreements.oferta && agreements.cancel;

  const [submitting, setSubmitting] = React.useState(false);
  const [submitError, setSubmitError] = React.useState(null);

  const handleSubmit = async () => {
    if (!canBook || submitting) return;
    setSubmitting(true);
    setSubmitError(null);
    try {
      const res = await fetch('/api/bookings', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          routeId, date, time, persons,
          firstName, lastName, phone, email,
          comment, adultConfirmed: true,
          // Сервер сам валидирует промо и пересчитывает скидку, передаём только код.
          promoCode: promoApplied ? promo : null,
          agreements
        })
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) {
        setSubmitError(
          data.error === 'validation'
            ? 'Проверьте, что выбраны дата, время и заполнены все поля.'
            : 'Не удалось создать заявку. Попробуйте ещё раз.'
        );
        return;
      }
      // Передаём наверх — BookingApp решит:
      //  · paymentUrl есть → открыть iframe-фрейм поверх визарда (Paykeeper);
      //  · paymentUrl нет → простой success-screen (например, ручная бронь).
      if (typeof onBooked === 'function') {
        onBooked({ bookingId: data.bookingId, paymentUrl: data.paymentUrl, total: data.total });
      }
    } catch (err) {
      setSubmitError('Ошибка сети. Проверьте интернет.');
    } finally {
      setSubmitting(false);
    }
  };

  return (
    <BookingShell stepNumber={3}>
      <BookingTwoCol
        left={
          <CardSwitcher amountKey={`book3-${routeId}-${date}-${time}-${persons}`}>
            <TicketCard route={route} date={date} time={time} persons={persons} />
          </CardSwitcher>
        }
        right={
          <>
            <StepHeader stepNumber={3} title="Подтверждение" onBack={onBack} />

            {/* Recap grid */}
            <div style={{
              background: '#fff', borderRadius: 12, padding: '14px 16px',
              boxShadow: '0 1px 0 rgba(0,0,0,.04)',
              display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px 18px',
              marginBottom: 12
            }}>
              <RecapBlock label="Дата и время начала" value={`${formatBookingDate(date)}, ${time}`} sub={route.name} />
              <RecapBlock label="Электронная почта" value={email} />
              <RecapBlock label="Имя" value={firstName} />
              <RecapBlock label="Фамилия" value={lastName} />
              <RecapBlock label="Номер телефона" value={phone} />
              <RecapBlock label="Кол-во человек" value={persons} />
              <RecapBlock label="Стоимость" value={fmtRub(subtotal)} accent />
              <RecapBlock label="Тариф" value={isWeekend(date) ? 'Выходной' : 'Будни'} />
            </div>

            {/* Promo */}
            <div style={{ marginBottom: 10 }}>
              <div style={{ fontSize: 11, color: '#5a6660', letterSpacing: '.14em', textTransform: 'uppercase', marginBottom: 5, fontWeight: 500 }}>Промокод</div>
                {promoApplied ? (
                  <div style={{
                    background: 'linear-gradient(90deg, #0B5D3B 0%, #0A2E1F 100%)', color: '#fff',
                    borderRadius: 10, padding: '8px 10px',
                    display: 'flex', alignItems: 'center', gap: 8,
                    fontSize: 12
                  }}>
                    <span style={{
                      width: 20, height: 20, borderRadius: '50%',
                      background: 'rgba(255,255,255,.18)', display: 'inline-flex',
                      alignItems: 'center', justifyContent: 'center', fontSize: 11
                    }}>✓</span>
                    <div style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 12, fontWeight: 500, letterSpacing: '.04em' }}>{promo.toUpperCase()} · −{promoPercent}%</div>
                      <div style={{ fontSize: 10, opacity: .8 }}>−{fmtRub(pricing3.promoDiscount)}</div>
                    </div>
                    <button
                      onClick={() => { setPromoPercent(0); setPromo(''); setPromoError(null); }}
                      style={{
                        appearance: 'none', border: 'none', cursor: 'pointer',
                        background: 'transparent', color: 'rgba(255,255,255,.7)',
                        fontSize: 11, fontFamily: 'inherit'
                      }}>×</button>
                  </div>
                ) : (
                  <div>
                    <div style={{
                      background: '#fff', borderRadius: 10, padding: '5px 6px 5px 13px',
                      boxShadow: '0 1px 0 rgba(0,0,0,.04)',
                      display: 'flex', alignItems: 'center', gap: 6
                    }}>
                      <input
                        value={promo}
                        onChange={(e) => { setPromo(e.target.value); setPromoError(null); }}
                        onKeyDown={(e) => { if (e.key === 'Enter') applyPromo(); }}
                        placeholder="Промокод"
                        style={{
                          flex: 1, border: 'none', outline: 'none', background: 'transparent',
                          fontFamily: 'inherit', fontSize: 13, color: '#0A2E1F',
                          letterSpacing: '.04em', textTransform: 'uppercase', minWidth: 0
                        }} />
                      <button
                        onClick={applyPromo}
                        disabled={!promo || promoChecking}
                        style={{
                          appearance: 'none', border: 'none',
                          cursor: (promo && !promoChecking) ? 'pointer' : 'default',
                          background: 'rgba(10,46,31,.06)', color: '#0A2E1F',
                          padding: '6px 10px', borderRadius: 999,
                          fontSize: 10, fontWeight: 600, letterSpacing: '.08em',
                          textTransform: 'uppercase',
                          fontFamily: 'inherit', opacity: (promo && !promoChecking) ? 1 : .5
                        }}>{promoChecking ? '…' : 'Применить'}</button>
                    </div>
                    {promoError && (
                      <div style={{ fontSize: 11, color: '#DD3310', marginTop: 4 }}>{promoError}</div>
                    )}
                  </div>
                )}
            </div>

            {/* Comment */}
            <div style={{ marginBottom: 10 }}>
              <div style={{ fontSize: 11, color: '#5a6660', letterSpacing: '.14em', textTransform: 'uppercase', marginBottom: 5, fontWeight: 500 }}>Комментарий</div>
              <textarea
                value={comment}
                onChange={(e) => setComment(e.target.value)}
                rows={1}
                maxLength={300}
                placeholder="Оставьте комментарий, например если у вас или ваших друзей День Рождения в день прогулки :)"
                style={{
                  width: '100%', boxSizing: 'border-box',
                  background: '#fff', border: 'none', borderRadius: 10,
                  padding: '10px 12px',
                  fontSize: 13, lineHeight: 1.45, color: '#0A2E1F',
                  fontFamily: 'inherit', resize: 'none', outline: 'none',
                  boxShadow: '0 1px 0 rgba(0,0,0,.04)'
                }} />
            </div>

            {/* Agreements */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
              <AgreementRow checked={agreements.personal}
                onChange={(v) => setAgreements({...agreements, personal: v})}
                label="Согласен на обработку персональных данных" />
              <AgreementRow checked={agreements.oferta}
                onChange={(v) => setAgreements({...agreements, oferta: v})}
                label="Согласен с публичной офертой" />
              <AgreementRow checked={agreements.cancel}
                onChange={(v) => setAgreements({...agreements, cancel: v})}
                label="Согласен с условиями отмены и переноса" />
            </div>

            <div style={{ flex: 1, minHeight: 4 }} />

            <BookingTotal route={route} date={date} persons={persons} total={total} subtotal={subtotal} discount={discount} groupDiscount={groupDiscount} />

            {/* Памятка + VPN — одной плотной строкой, чтобы не ломать высоту шага. */}
            <div style={{
              fontSize: 11, color: '#5a6660', lineHeight: 1.45,
              padding: '8px 12px', background: 'rgba(10,46,31,.04)',
              borderRadius: 8, marginBottom: 10
            }}>
              После оплаты пришлём памятку на email · VPN перед оплатой выключите.
            </div>

            {paymentResult && paymentResult !== 'paid' && (
              <div style={{
                background: 'rgba(221,51,16,.08)', color: '#DD3310',
                borderRadius: 10, padding: '10px 14px',
                fontSize: 12, lineHeight: 1.4, marginBottom: 10
              }}>
                {paymentResult === 'canceled'
                  ? 'Оплата была отменена. Можно попробовать снова.'
                  : paymentResult === 'expired'
                    ? 'Время на оплату истекло. Можно попробовать снова.'
                    : 'Оплата не прошла. Можно попробовать снова.'}
              </div>
            )}
            {submitError && (
              <div style={{
                background: 'rgba(221,51,16,.08)', color: '#DD3310',
                borderRadius: 10, padding: '10px 14px',
                fontSize: 12, lineHeight: 1.4, marginBottom: 10
              }}>{submitError}</div>
            )}

            <div style={{ display: 'flex', gap: 10 }}>
              <button onClick={onBack} disabled={submitting} style={backBtn}>Назад</button>
              <button
                onClick={handleSubmit}
                disabled={!canBook || submitting}
                style={{
                  flex: 1, appearance: 'none', border: 'none',
                  cursor: (canBook && !submitting) ? 'pointer' : 'not-allowed',
                  background: (canBook && !submitting) ? '#DD3310' : 'rgba(10,46,31,.12)',
                  color: (canBook && !submitting) ? '#fff' : '#9aa39e',
                  borderRadius: 12, padding: '14px 22px',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'space-between',
                  fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
                  boxShadow: (canBook && !submitting) ? '0 12px 28px -12px rgba(221,51,16,.55)' : 'none'
                }}>
                {submitting ? 'Отправляем…' : `Забронировать ${canBook ? fmtRub(total) : ''}`}
                <span style={{
                  width: 26, height: 26, borderRadius: '50%',
                  background: (canBook && !submitting) ? 'rgba(255,255,255,.2)' : 'rgba(10,46,31,.08)',
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                  fontSize: 13, color: (canBook && !submitting) ? '#fff' : '#9aa39e'
                }}>→</span>
              </button>
            </div>
          </>
        } />
    </BookingShell>);
}

// ─── Sub-components ────────────────────────────────────────────────
// Кнопка «Назад» сверху убрана по запросу — навигация назад остаётся
// только через кнопку внизу формы (см. backBtn в Step2/Step3). Поэтому
// onBack пропсу больше не используется в шапке, но сама подпись «Шаг N из 3»
// остаётся на месте.
function StepHeader({ stepNumber, title }) {
  return (
    <>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 10,
        fontSize: 11, letterSpacing: '.18em', textTransform: 'uppercase',
        color: '#5a6660', fontWeight: 500, marginBottom: 10
      }}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
          <span style={{
            width: 22, height: 22, borderRadius: '50%',
            background: '#DD3310', color: '#fff',
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 11, fontWeight: 600
          }}>{stepNumber}</span>
          Шаг {stepNumber} из 3
        </span>
      </div>
      <div style={{
        fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
        fontSize: 22, fontWeight: 500,
        letterSpacing: '-.01em', color: '#0A2E1F', marginBottom: 14
      }}>{title}</div>
    </>);
}

function BookingField({ label, value, onChange, placeholder, prefix, type, error, onBlur }) {
  const [focused, setFocused] = React.useState(false);
  const ring = error ? '#DD3310' : (focused ? '#0A2E1F' : null);
  return (
    <div>
      <label style={{ display: 'block', fontSize: 12, color: '#5a6660', marginBottom: 5 }}>{label}</label>
      <div style={{
        background: '#fff', borderRadius: 10,
        boxShadow: ring ? `0 0 0 1.5px ${ring}` : '0 1px 0 rgba(0,0,0,.04)',
        display: 'flex', alignItems: 'center',
        padding: '11px 13px', gap: 8, transition: 'box-shadow .15s'
      }}>
        {prefix && <span style={{ fontSize: 13 }}>{prefix}</span>}
        <input
          type={type || 'text'}
          inputMode={type === 'tel' ? 'tel' : type === 'email' ? 'email' : undefined}
          value={value}
          onChange={(e) => onChange(e.target.value)}
          placeholder={placeholder}
          onFocus={() => setFocused(true)}
          onBlur={() => { setFocused(false); if (onBlur) onBlur(); }}
          style={{
            flex: 1, border: 'none', outline: 'none', background: 'transparent',
            fontFamily: 'inherit', fontSize: 13, color: '#0A2E1F', minWidth: 0
          }} />
      </div>
      {error && (
        <div style={{ fontSize: 11, color: '#DD3310', marginTop: 4 }}>{error}</div>
      )}
    </div>);
}

function Checkbox({ checked, onChange }) {
  return (
    <span
      onClick={() => onChange(!checked)}
      style={{
        width: 18, height: 18, borderRadius: 5, flexShrink: 0,
        background: checked ? '#DD3310' : '#fff',
        boxShadow: checked ? 'none' : 'inset 0 0 0 1.5px rgba(10,46,31,.18)',
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        color: '#fff', fontSize: 11, fontWeight: 700,
        marginTop: 1, transition: 'all .15s'
      }}>{checked ? '✓' : ''}</span>);
}

function AgreementRow({ checked, onChange, label }) {
  return (
    <label style={{
      display: 'flex', alignItems: 'center', gap: 10,
      fontSize: 12, color: '#0A2E1F', cursor: 'pointer'
    }}>
      <Checkbox checked={checked} onChange={onChange} />
      <span style={{
        color: '#1c5db5', textDecoration: 'underline', textUnderlineOffset: 2,
        textDecorationColor: 'rgba(28,93,181,.4)'
      }}>{label}</span>
    </label>);
}

function RecapBlock({ label, value, sub, accent }) {
  return (
    <div style={{ minWidth: 0 }}>
      <div style={{
        fontSize: 10, color: '#5a6660', letterSpacing: '.14em',
        textTransform: 'uppercase', marginBottom: 3, fontWeight: 500
      }}>{label}</div>
      <div style={{
        fontSize: 13, fontWeight: 500,
        color: accent ? '#DD3310' : '#0A2E1F',
        whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
        fontVariantNumeric: 'tabular-nums'
      }}>{value}</div>
      {sub && (
        <div style={{ fontSize: 11, color: '#5a6660', marginTop: 1 }}>{sub}</div>
      )}
    </div>);
}

function BookingTotal({ route, date, persons, total, subtotal, discount = 0, groupDiscount = 0 }) {
  const fmtRub = window.fmtRub;
  const price = priceFor(route, date);
  const tariff = isWeekend(date) ? 'выходной' : 'будни';
  const hasDiscount = (discount || groupDiscount) > 0;
  // Подпись о причине скидки. Если применены обе — показываем обе.
  const discountNote = (() => {
    const parts = [];
    if (groupDiscount > 0) parts.push('групповое бронирование −15%');
    if (discount > groupDiscount) parts.push('промокод −10%');
    return parts.join(' · ');
  })();
  return (
    <div style={{
      background: '#fff', borderRadius: 14, padding: '12px 18px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      gap: 16, marginBottom: 12,
      boxShadow: '0 1px 0 rgba(0,0,0,.04)'
    }}>
      <div>
        <div style={{ fontSize: 11, color: '#5a6660', textTransform: 'uppercase', letterSpacing: '.14em' }}>Итого</div>
        <div style={{ fontSize: 12, color: '#5a6660', marginTop: 3, fontVariantNumeric: 'tabular-nums' }}>
          {fmtRub(price)} ({tariff}) × {persons} {persons === 1 ? 'человек' : 'чел.'}{discountNote ? ` · ${discountNote}` : ''}
        </div>
      </div>
      <div style={{
        display: 'flex', alignItems: 'baseline', gap: 8,
        fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
        letterSpacing: '-.01em', fontVariantNumeric: 'tabular-nums'
      }}>
        {hasDiscount && subtotal != null && (
          <span style={{
            fontSize: 14, fontWeight: 400,
            color: '#9aa39e', textDecoration: 'line-through',
            textDecorationThickness: '1px'
          }}>{fmtRub(subtotal)}</span>
        )}
        <span style={{
          fontSize: 24, fontWeight: 500,
          color: hasDiscount ? '#DD3310' : '#0A2E1F'
        }}>{fmtRub(total)}</span>
      </div>
    </div>);
}

const stepperBtn = {
  appearance: 'none', border: 'none', cursor: 'pointer',
  background: '#fff', color: '#0A2E1F',
  width: 40, height: 40, borderRadius: 10,
  fontSize: 18, fontWeight: 500, fontFamily: 'inherit',
  boxShadow: '0 1px 0 rgba(0,0,0,.04)',
  display: 'inline-flex', alignItems: 'center', justifyContent: 'center'
};

const backBtn = {
  appearance: 'none', border: 'none', cursor: 'pointer',
  background: '#DD3310', color: '#fff',
  borderRadius: 12, padding: '14px 22px',
  fontFamily: 'inherit', fontSize: 14, fontWeight: 500,
  boxShadow: '0 12px 28px -12px rgba(221,51,16,.55)'
};

window.BOOKING_ROUTES = BOOKING_ROUTES;
window.TicketCard = TicketCard;
window.BookingStep1 = BookingStep1;
window.BookingStep2 = BookingStep2;
window.BookingStep3 = BookingStep3;

// ─── BookingPaymentFrame ──────────────────────────────────────────
// Iframe с инвойсом Paykeeper'а внутри визарда (без полного редиректа,
// так же как в сертификатах). Поллит /api/bookings/:id/status каждые 1.5 с
// и силой триггерит refresh при iframe.onload (Paykeeper навигировался —
// значит, пользователь дошёл до результата).
function BookingPaymentFrame({ bookingId, invoiceUrl, onCancel, onPaid, onFailed }) {
  const isMobile = useIsMobile();
  const stoppedRef = React.useRef(false);
  const initialIframeLoadRef = React.useRef(true);

  const fetchStatus = React.useCallback(async () => {
    if (stoppedRef.current) return null;
    try {
      const res = await fetch(`/api/bookings/${bookingId}/status`);
      if (!res.ok) return null;
      return res.json();
    } catch { return null; }
  }, [bookingId]);

  const forceRefresh = React.useCallback(async () => {
    if (stoppedRef.current) return null;
    try {
      const res = await fetch(`/api/bookings/${bookingId}/refresh`, { method: 'POST' });
      if (!res.ok) return null;
      return res.json();
    } catch { return null; }
  }, [bookingId]);

  const applyStatus = React.useCallback((data, intervalId) => {
    if (!data) return false;
    if (data.paymentStatus === 'paid') {
      stoppedRef.current = true;
      if (intervalId) clearInterval(intervalId);
      onPaid && onPaid(data);
      return true;
    }
    if (data.paymentStatus === 'expired' || data.paymentStatus === 'canceled') {
      stoppedRef.current = true;
      if (intervalId) clearInterval(intervalId);
      onFailed && onFailed(data.paymentStatus);
      return true;
    }
    return false;
  }, [onPaid, onFailed]);

  const handleIframeLoad = React.useCallback(async () => {
    if (initialIframeLoadRef.current) {
      initialIframeLoadRef.current = false;
      return;
    }
    if (stoppedRef.current) return;
    const data = await forceRefresh();
    applyStatus(data);
  }, [forceRefresh, applyStatus]);

  React.useEffect(() => {
    stoppedRef.current = false;
    let intervalId;
    const tick = async () => {
      const data = await fetchStatus();
      applyStatus(data, intervalId);
    };
    tick();
    intervalId = setInterval(tick, 1500);
    return () => { stoppedRef.current = true; clearInterval(intervalId); };
  }, [bookingId, fetchStatus, applyStatus]);

  const handleCancel = async () => {
    const data = await fetchStatus();
    if (data && data.paymentStatus === 'paid') { onPaid && onPaid(data); return; }
    if (data && (data.paymentStatus === 'expired' || data.paymentStatus === 'canceled')) {
      onFailed && onFailed(data.paymentStatus); return;
    }
    onCancel && onCancel();
  };

  const statusBar = (
    <div style={{
      flexShrink: 0,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: isMobile ? '10px 16px' : '12px 28px',
      background: '#FAF7EF',
      borderBottom: '1px solid rgba(10,46,31,.06)',
      gap: 12
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        fontSize: 12, color: '#3a4a44', letterSpacing: '.02em', minWidth: 0
      }}>
        <span style={{
          width: 8, height: 8, borderRadius: '50%', flexShrink: 0,
          background: '#0A2E1F',
          boxShadow: '0 0 0 0 rgba(10,46,31,.4)',
          animation: 'pkPulse 1.4s ease-in-out infinite'
        }} />
        <span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          Ждём подтверждение оплаты · <span style={{ fontVariantNumeric: 'tabular-nums', color: '#0A2E1F', fontWeight: 500 }}>{bookingId.slice(0, 8)}</span>
        </span>
      </div>
      <button onClick={handleCancel} style={{
        appearance: 'none', border: '1px solid rgba(10,46,31,.22)',
        background: 'transparent', color: '#0A2E1F',
        fontSize: 12, padding: '6px 12px', borderRadius: 999,
        cursor: 'pointer', fontFamily: 'inherit', flexShrink: 0
      }}>✕ Отменить</button>
    </div>);

  const iframeEl = (
    <iframe
      src={invoiceUrl}
      title="Оплата"
      onLoad={handleIframeLoad}
      style={{ flex: 1, border: 'none', width: '100%', minHeight: 0, background: '#F0EEE9' }}
      allow="payment" />);

  const pulseStyles = (
    <style>{`
      @keyframes pkPulse {
        0%   { box-shadow: 0 0 0 0 rgba(10,46,31,.4); }
        70%  { box-shadow: 0 0 0 8px rgba(10,46,31,0); }
        100% { box-shadow: 0 0 0 0 rgba(10,46,31,0); }
      }
    `}</style>);

  if (!isMobile) {
    // Десктоп — оборачиваем в стандартный BookingShell со step=3 (та же шапка
    // и breadcrumb «Маршрут → Детали → Подтверждение», что и на других шагах).
    return (
      <BookingShell stepNumber={3}>
        {statusBar}
        {iframeEl}
        {pulseStyles}
      </BookingShell>);
  }
  // Мобайл — собственная плоская оболочка, чтобы iframe занял всё.
  return (
    <div style={{
      height: '100%', minHeight: '100%',
      display: 'flex', flexDirection: 'column',
      background: '#F4F1EA',
      fontFamily: '"NT Somic", system-ui, -apple-system, sans-serif',
      color: '#0A2E1F',
      overflow: 'hidden'
    }}>
      <header style={{
        padding: 'calc(env(safe-area-inset-top, 0px) + 14px) 20px 14px',
        background: '#fff',
        borderBottom: '1px solid rgba(10,46,31,.06)',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        flexShrink: 0
      }}>
        {window.FriendsLogo ? <window.FriendsLogo size={20} /> : <span />}
        <span style={{ fontSize: 12, color: '#5a6660' }}>Оплата</span>
      </header>
      {statusBar}
      {iframeEl}
      {pulseStyles}
    </div>);
}

// ─── BookingSuccess ────────────────────────────────────────────────
// Заглушка экрана-благодарности. Тексты — пока минимальные; финальный
// копирайт + интеграция оплаты (Paykeeper) и писем добавим после
// согласования.
function BookingSuccess({ bookingId, total, onAgain }) {
  return (
    // Оборачиваем в стандартный BookingShell — чтобы шапка с лого и breadcrumb
    // (1 ✓ → 2 ✓ → 3 ✓) и нижний футер (friends-sup.ru / поддержка / правила)
    // оставались на экране, как на остальных шагах визарда.
    <BookingShell stepNumber={3}>
      <div style={{
        flex: 1, minHeight: 0,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        padding: '40px 24px', background: '#F4F1EA'
      }}>
        <div style={{ maxWidth: 460, textAlign: 'center' }}>
          <div style={{
            width: 64, height: 64, borderRadius: '50%',
            background: '#DD3310',
            color: '#fff', fontSize: 28,
            display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
            marginBottom: 18
          }}>✓</div>
          <h2 style={{
            fontFamily: '"RF Dewi Extended", "NT Somic", sans-serif',
            fontSize: 26, fontWeight: 800, letterSpacing: '-.015em',
            margin: '0 0 10px'
          }}>Заявка принята!</h2>
          <p style={{
            fontSize: 15, lineHeight: 1.5, color: '#5a6660',
            margin: '0 0 22px'
          }}>
            Подтверждение отправили на email. Памятка с деталями (что взять
            с собой, точка сбора, время) придёт отдельным письмом.
          </p>
          <div style={{
            background: '#fff', borderRadius: 10,
            padding: '10px 14px', fontSize: 12, color: '#5a6660',
            fontFamily: 'ui-monospace, monospace', marginBottom: 22,
            letterSpacing: '.04em', display: 'inline-block'
          }}>номер заявки: {bookingId}</div>
          <div style={{ display: 'flex', gap: 10, justifyContent: 'center' }}>
            <button onClick={() => location.href = '/'} style={{
              appearance: 'none', border: '1px solid rgba(10,46,31,.12)',
              background: '#fff', color: '#0A2E1F', cursor: 'pointer',
              borderRadius: 10, padding: '11px 18px',
              fontFamily: 'inherit', fontSize: 14, fontWeight: 500
            }}>На главную</button>
            <button onClick={onAgain} style={{
              appearance: 'none', border: 'none', cursor: 'pointer',
              background: '#0A2E1F', color: '#fff',
              borderRadius: 10, padding: '11px 18px',
              fontFamily: 'inherit', fontSize: 14, fontWeight: 500
            }}>Забронировать ещё</button>
          </div>
        </div>
      </div>
    </BookingShell>);
}

function BookingLoadingShell({ message }) {
  return (
    <div style={{
      width: '100%', height: '100%',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      color: '#5a6660', fontSize: 14
    }}>{message || 'Загрузка…'}</div>);
}

// Хук определения мобильной/десктопной верстки. <768 — мобила (тот же
// брейкпоинт, что и в @media в booking.html).
function useIsMobile() {
  const [m, setM] = React.useState(typeof window !== 'undefined' ? window.innerWidth < 768 : false);
  React.useEffect(() => {
    const onR = () => setM(window.innerWidth < 768);
    window.addEventListener('resize', onR);
    return () => window.removeEventListener('resize', onR);
  }, []);
  return m;
}

function BookingApp() {
  const [routesLoading, setRoutesLoading] = React.useState(true);
  const [routesError, setRoutesError] = React.useState(null);
  const [, forceRender] = React.useState(0);

  const isMobile = useIsMobile();

  const [step, setStep] = React.useState(1);
  const [routeId, setRouteId] = React.useState(null);
  const [date, setDate] = React.useState('');
  const [time, setTime] = React.useState('');
  const [persons, setPersons] = React.useState(2);
  const [contact, setContact] = React.useState({ firstName: '', lastName: '', phone: '', email: '' });
  const [success, setSuccess] = React.useState(null);
  // Сессия оплаты — пока не null, рендерим iframe с Paykeeper'ом поверх визарда
  // (как в сертификатах). Когда пользователь оплатил/отменил/время вышло —
  // фрейм сам поймёт это через poll и вызовет соответствующий колбек.
  const [paymentSession, setPaymentSession] = React.useState(null);
  const [paymentResult, setPaymentResult] = React.useState(null);

  React.useEffect(() => {
    let off = false;
    fetch('/api/routes')
      .then(r => r.ok ? r.json() : Promise.reject(r.status))
      .then((data) => {
        if (off) return;
        // Маппинг бэкенд-формата на формат, ожидаемый компонентами
        // (унаследован от исходного дизайна): weekend/weekday + URL фото.
        BOOKING_ROUTES = data.map(r => ({
          id: r.id,
          name: r.name,
          subtitle: r.subtitle || '',
          duration: r.duration || '',
          description: r.description || '',
          weekend: r.price_weekend,
          weekday: r.price_weekday,
          photo: 'assets/' + r.photo
        }));
        // Кладём ссылку на window — мобильный bundle (booking-mobile.jsx)
        // читает каталог маршрутов отсюда (он в собственном IIFE-скоупе).
        window.BOOKING_ROUTES_DATA = BOOKING_ROUTES;
        if (!routeId && BOOKING_ROUTES.length) setRouteId(BOOKING_ROUTES[0].id);
        forceRender(n => n + 1);
      })
      .catch((err) => { if (!off) setRoutesError(String(err)); })
      .finally(() => { if (!off) setRoutesLoading(false); });
    return () => { off = true; };
  }, []);

  React.useEffect(() => { document.documentElement.scrollTop = 0; }, [step]);

  if (routesLoading) return <BookingLoadingShell message="Загрузка маршрутов…" />;
  if (routesError || !BOOKING_ROUTES.length) {
    return <BookingLoadingShell message="Маршруты пока не настроены. Зайдите чуть позже." />;
  }
  if (success) {
    return (
      <BookingSuccess
        bookingId={success.bookingId}
        total={success.total}
        onAgain={() => {
          setSuccess(null); setStep(1);
          setDate(''); setTime(''); setPersons(2);
          setContact({ firstName: '', lastName: '', phone: '', email: '' });
          setPaymentResult(null);
        }} />);
  }
  // Iframe-оплата поверх визарда. После paid → success-screen; после
  // отмены/expired/canceled — возвращаемся на Step3, чтобы пользователь
  // мог попробовать ещё раз (новая попытка создаёт новый инвойс).
  if (paymentSession) {
    return (
      <BookingPaymentFrame
        bookingId={paymentSession.bookingId}
        invoiceUrl={paymentSession.invoiceUrl}
        onPaid={(d) => {
          setPaymentSession(null);
          setPaymentResult('paid');
          setSuccess({ bookingId: paymentSession.bookingId, total: null });
        }}
        onFailed={(reason) => {
          setPaymentSession(null);
          setPaymentResult(reason);
        }}
        onCancel={() => {
          setPaymentSession(null);
          setPaymentResult('canceled');
        }} />);
  }

  // Выбираем десктоп- или мобильный-вариант шага. Mobile-компоненты
  // живут в booking-mobile.jsx и регистрируются на window после загрузки.
  const Step1 = isMobile && window.MobileBookingStep1 ? window.MobileBookingStep1 : BookingStep1;
  const Step2 = isMobile && window.MobileBookingStep2 ? window.MobileBookingStep2 : BookingStep2;
  const Step3 = isMobile && window.MobileBookingStep3 ? window.MobileBookingStep3 : BookingStep3;

  if (step === 1) {
    return (
      <Step1
        initialRouteId={routeId || BOOKING_ROUTES[0].id}
        initialDate={date}
        initialTime={time}
        initialPersons={persons}
        onContinue={({ routeId: r, date: d, time: t, persons: p }) => {
          setRouteId(r); setDate(d); setTime(t); setPersons(p);
          setStep(2);
        }} />);
  }
  if (step === 2) {
    return (
      <Step2
        routeId={routeId}
        date={date}
        time={time}
        persons={persons}
        initialFirstName={contact.firstName}
        initialLastName={contact.lastName}
        initialPhone={contact.phone}
        initialEmail={contact.email}
        onBack={() => setStep(1)}
        onContinue={(c) => { setContact(c); setStep(3); }} />);
  }
  return (
    <Step3
      routeId={routeId}
      date={date}
      time={time}
      persons={persons}
      firstName={contact.firstName}
      lastName={contact.lastName}
      phone={contact.phone}
      email={contact.email}
      onBack={() => setStep(2)}
      paymentResult={paymentResult}
      onBooked={(s) => {
        // Бэк создал инвойс — открываем его в iframe поверх визарда.
        if (s && s.paymentUrl) setPaymentSession({ bookingId: s.bookingId, invoiceUrl: s.paymentUrl });
        // Без paymentUrl (например, ручное создание) — старый success-screen.
        else setSuccess(s);
      }} />);
}

window.BookingApp = BookingApp;
