// appointments.jsx — the Halcyon Pilates appointment booking widget.
// Two-screen flow:
//   Step 1 — Lines-style service list with a location card on top.
//   Step 2 — On-page booking with three numbered milestone cards
//            (instructor → date → time) plus an optional add-ons section.
//            Date auto-selects the soonest opening for the current
//            instructor on entry, and re-jumps if the user picks an
//            instructor whose schedule doesn't cover the active date.
//   Step 3 — Page-level success confirmation.

const { useState: useStateAPT, useMemo: useMemoAPT, useEffect: useEffectAPT, useRef: useRefAPT } = React;

const APT_BREAKPOINT = 760;
function useAPTMobile() {
  const [m, setM] = useStateAPT(typeof window !== 'undefined' ? window.innerWidth < APT_BREAKPOINT : false);
  useEffectAPT(() => {
    const onResize = () => setM(window.innerWidth < APT_BREAKPOINT);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);
  return m;
}

const APT_TODAY = new Date(2026, 4, 20); // Wed May 20, 2026
function addDaysAPT(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
function sameDayAPT(a, b) { return a.toDateString() === b.toDateString(); }
function fmtMoney(n) { return `$${n.toFixed(2)}`; }
function fmtDuration(min) {
  if (min < 60) return `${min} min`;
  const h = Math.floor(min / 60), r = min % 60;
  return r ? `${h}h ${r}m` : `${h} hr`;
}
const APT_WEEKDAYS_SHORT = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const APT_WEEKDAYS_LONG  = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const APT_MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];

// ── Catalog ──────────────────────────────────────────────────────
const APT_LOCATION = {
  shortName: 'Allendale',
  fullName:  'Halcyon Pilates — Allendale',
  address:   '142 W Allendale Ave, Allendale, NJ 07401',
  details:   'Free street parking. Entrance on the west side of the building — look for the sage awning. Hours: Mon–Fri 6am–9pm, Sat 8am–4pm, Sun 9am–2pm.',
};

const APT_ADD_ONS = {
  stickySocks: {
    name: 'Sticky socks', price: 8, duration: 0,
    description: 'Grippy socks to wear on the reformer or mat. Yours to keep after the session — bring them back next time.',
    thumbnail: 'https://images.unsplash.com/photo-1586350977771-2a1dba9b6a52?auto=format&fit=crop&w=240&h=240&q=80',
  },
  bottledWater: {
    name: 'Bottled water', price: 3, duration: 0,
    description: 'A chilled glass bottle waiting in your room. Pairs well with not running out of breath.',
    thumbnail: 'https://images.unsplash.com/photo-1523362628745-0c100150b504?auto=format&fit=crop&w=240&h=240&q=80',
  },
};
const APT_ALL_ADD_ON_IDS = Object.keys(APT_ADD_ONS);
const APT_SERVICES = [
  {
    id: 'normatec', category: 'Recovery',
    name: 'Normatec Boots', duration: 30, price: 45,
    description: 'Sequential compression boots that flush the legs, knees, and hips. Settle in with a podcast — the system runs through five zones across the session.',
    thumbnail: 'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'sauna', category: 'Recovery',
    name: 'Infrared Sauna', duration: 30, price: 35,
    description: 'A single-person infrared cabin at 140°F. Recommended after lifting or a long run. Towels, water, and cold rinse included.',
    thumbnail: 'https://images.unsplash.com/photo-1554504318-1bbb8d4e0470?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'cold-plunge', category: 'Recovery',
    name: 'Cold Plunge', duration: 15, price: 25,
    description: 'A guided plunge in a 50°F tub. Three rounds with breathwork between — built for nervous-system reset, not endurance.',
    thumbnail: 'https://images.unsplash.com/photo-1530210124550-912dc1381cb8?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'red-light', category: 'Recovery',
    name: 'Red Light Therapy', duration: 20, price: 30,
    description: 'Full-body panel session combining red and near-infrared wavelengths. Quiet, warm, and easy to layer onto another service.',
    thumbnail: 'https://images.unsplash.com/photo-1540206395-68808572332f?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'swedish-45', category: 'Massage',
    name: 'Swedish Massage', duration: 45, price: 95,
    description: 'A flowing, full-body relaxation massage. Medium pressure throughout, with extra attention on the neck, shoulders, and lower back.',
    thumbnail: 'https://images.unsplash.com/photo-1544161515-4ab6ce6db874?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'deep-tissue-45', category: 'Massage',
    name: 'Deep Tissue Massage', duration: 45, price: 110,
    description: 'Targeted, firm pressure on chronically tight areas. Best when you can name the two or three spots that need work going in.',
    thumbnail: 'https://images.unsplash.com/photo-1519823551278-64ac92734fb1?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
  {
    id: 'swedish-60', category: 'Massage',
    name: 'Swedish Massage', duration: 60, price: 120,
    description: 'The long version of our most-booked service. The extra fifteen minutes goes to a slower opening sequence and a deeper close.',
    thumbnail: 'https://images.unsplash.com/photo-1600334129128-685c5582fd35?auto=format&fit=crop&w=480&h=320&q=80',
    addOns: APT_ALL_ADD_ON_IDS,
  },
];
const APT_GENERIC_PROVIDERS = [
  { id: 'any',  name: 'First Available',     generic: true,
    bio: 'We\'ll match you with the earliest instructor who has the slot you pick.' },
  { id: 'anyF', name: 'Any Female Instructor', generic: true, gender: 'F',
    bio: 'No preference on the individual — we\'ll book the soonest female instructor available.' },
  { id: 'anyM', name: 'Any Male Instructor',   generic: true, gender: 'M',
    bio: 'No preference on the individual — we\'ll book the soonest male instructor available.' },
];
const APT_PROVIDERS = [
  { id: 'marisol', name: 'Marisol Pena',  gender: 'F',
    photo: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'STOTT-certified instructor with 12 years teaching reformer + mat across NYC studios. Marisol\'s cueing meets you where your body is that day.' },
  { id: 'lena',    name: 'Lena Ortiz',    gender: 'F',
    photo: 'https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'Halcyon founder. A decade of classical pilates and dance training before opening the studio in 2019. Specializes in privates and pre/post-natal work.' },
  { id: 'yasmin',  name: 'Yasmin Castillo',gender: 'F',
    photo: 'https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'Athletic reformer programming and post-rehab work, with a background in physical therapy. Bring a towel.' },
  { id: 'anika',   name: 'Anika Rao',     gender: 'F',
    photo: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'A contemplative, breath-led practice honed through 500-hour yoga training. Anika\'s slow privates are a favorite end to the day.' },
  { id: 'dylan',   name: 'Dylan Greene',  gender: 'M',
    photo: 'https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'Sports-oriented reformer work, strong on programming for runners and lifters. Twelve years of teaching experience.' },
  { id: 'peter',   name: 'Peter Lim',     gender: 'M',
    photo: 'https://images.unsplash.com/photo-1463453091185-61582044d556?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'Classical pilates lineage with extra training in clinical bodywork. Gentle but precise — great for first-timers.' },
];

// ── Availability sim ─────────────────────────────────────────────
function aptHash(s) { let h = 0; for (let i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) | 0; } return Math.abs(h); }
const APT_ALL_SLOTS = {
  morning:   ['9:00 AM', '9:30 AM', '10:00 AM', '10:15 AM', '10:30 AM', '11:00 AM', '11:30 AM', '11:45 AM'],
  afternoon: ['12:30 PM', '1:00 PM', '1:30 PM', '2:00 PM', '2:15 PM', '2:30 PM', '2:45 PM', '3:00 PM', '4:00 PM'],
  evening:   ['5:30 PM', '6:00 PM', '6:30 PM', '7:00 PM'],
};
function slotsForProviderDay(providerId, date) {
  const dow = date.getDay();
  const all = [...APT_ALL_SLOTS.morning, ...APT_ALL_SLOTS.afternoon, ...APT_ALL_SLOTS.evening];
  const out = { morning: [], afternoon: [], evening: [] };
  const seed = aptHash(`${providerId}|${date.toDateString()}`);
  const baseRate = (seed % 100) / 100;
  const dowMult = dow === 0 ? 0.15 : (dow === 6 ? 0.45 : 0.75);
  const isGeneric = providerId.startsWith('any');
  const targetRate = isGeneric ? Math.min(0.95, baseRate * 1.6) : baseRate * dowMult;
  all.forEach((slot) => {
    const h = aptHash(`${providerId}|${date.toDateString()}|${slot}`);
    const want = (h % 1000) / 1000;
    if (want < targetRate) {
      if (APT_ALL_SLOTS.morning.includes(slot)) out.morning.push(slot);
      else if (APT_ALL_SLOTS.afternoon.includes(slot)) out.afternoon.push(slot);
      else out.evening.push(slot);
    }
  });
  return out;
}
function totalSlotsCount(s) { return s.morning.length + s.afternoon.length + s.evening.length; }
// `findFirstAvailableDate` includes today as a candidate (unlike
// `findNextAvailableDate`, which always advances) — used for the auto-pick
// that fires when entering step 2 so we don't skip past today's openings.
function findFirstAvailableDate(providerId, fromDate) {
  if (totalSlotsCount(slotsForProviderDay(providerId, fromDate)) > 0) return fromDate;
  return findNextAvailableDate(providerId, fromDate);
}
function findNextAvailableDate(providerId, fromDate) {
  for (let i = 1; i <= 30; i++) {
    const d = addDaysAPT(fromDate, i);
    if (totalSlotsCount(slotsForProviderDay(providerId, d)) > 0) return d;
  }
  return null;
}

// ────────────────────────────────────────────────────────────────
// Root
// ────────────────────────────────────────────────────────────────
function AppointmentApp({ brandId = 'halcyon' }) {
  const baseBrand = window.BRANDS[brandId];
  const mobile = useAPTMobile();

  const tweaksEnabled = typeof window !== 'undefined' && window.APT_TWEAKS_ENABLED;
  const defaults = (typeof window !== 'undefined' && window.APT_TWEAK_DEFAULTS) || {};
  const [t, setTweak] = (tweaksEnabled ? window.useTweaks(defaults) : [defaults, () => {}]);

  const brand = tweaksEnabled ? {
    ...baseBrand,
    accent:        t.accent     ?? baseBrand.accent,
    accentDeep:    t.accent     ?? baseBrand.accentDeep,
    accentSoft:    baseBrand.accentSoft,
    surface:       t.panel      ?? baseBrand.surface,
    border:        t.lineColor  ?? baseBrand.border,
    ink:           t.headline   ?? baseBrand.ink,
    inkSoft:       t.details    ?? baseBrand.inkSoft,
    panelInk:      t.panelInk     ?? t.headline ?? baseBrand.ink,
    panelInkSoft:  t.panelInkSoft ?? t.details  ?? baseBrand.inkSoft,
    displayFont:   t.font       ?? baseBrand.displayFont,
    bodyFont:      t.font       ?? baseBrand.bodyFont,
    displayWeight: t.weight     ?? baseBrand.displayWeight,
    _radius:       t.radius,
    _thumbRadius:  t.thumbnailRadius,
    _fun:          !!t.fun,
    _buttonTextColor: t.buttonTextColor,
  } : {
    ...baseBrand,
    panelInk:     baseBrand.ink,
    panelInkSoft: baseBrand.inkSoft,
  };
  const appBg = tweaksEnabled ? (t.background ?? '#FFFFFF') : '#FFFFFF';

  const [step, setStep] = useStateAPT(1);
  const [serviceId, setServiceId] = useStateAPT(null);
  const [addOnIds, setAddOnIds] = useStateAPT([]);
  const [providerId, setProviderId] = useStateAPT('any');
  const [date, setDate] = useStateAPT(null);
  const [time, setTime] = useStateAPT(null);

  const service = useMemoAPT(
    () => APT_SERVICES.find(s => s.id === serviceId) || null,
    [serviceId],
  );
  const provider = useMemoAPT(() => {
    return APT_GENERIC_PROVIDERS.find(p => p.id === providerId)
        || APT_PROVIDERS.find(p => p.id === providerId)
        || APT_GENERIC_PROVIDERS[0];
  }, [providerId]);

  const openService = (id) => {
    setServiceId(id);
    setAddOnIds([]);
    setProviderId('any');
    // Auto-select the soonest available date for the default instructor so
    // the user can immediately see times instead of an empty placeholder.
    setDate(findFirstAvailableDate('any', APT_TODAY));
    setTime(null);
    setStep(2);
  };
  const goBackToList = () => {
    setStep(1); setServiceId(null);
    setAddOnIds([]); setProviderId('any'); setDate(null); setTime(null);
  };
  const onBook = () => setStep(3);

  const subtotal = useMemoAPT(() => {
    let sum = service ? service.price : 0;
    addOnIds.forEach(id => { sum += (APT_ADD_ONS[id]?.price ?? 0); });
    return sum;
  }, [service, addOnIds]);

  // Changing the instructor invalidates the chosen time. If the active date
  // is fully booked for the new instructor, auto-jump to the next opening so
  // the user never lands on an empty Times section.
  const onPickProvider = (id) => {
    setProviderId(id);
    setTime(null);
    if (date && totalSlotsCount(slotsForProviderDay(id, date)) === 0) {
      setDate(findFirstAvailableDate(id, APT_TODAY));
    }
  };
  const onPickDate = (d) => { setDate(d); setTime(null); };

  const canBook = !!(date && time);

  return (
    <div style={{
      minHeight: '100vh', background: appBg,
      fontFamily: brand.bodyFont, color: brand.ink,
    }}>
      <BrandBar brand={brand} mobile={mobile} hideMyAccount={!!t.hideMyAccount} />
      <div style={{
        maxWidth: 960, margin: '0 auto',
        padding: mobile ? '20px 16px 32px' : '32px 28px 32px',
        paddingBottom: (step === 2 ? (mobile ? 110 : 100) : (mobile ? 40 : 64)),
      }}>
        {step === 1 && (
          <Step1Services
            brand={brand} mobile={mobile}
            services={APT_SERVICES}
            onSelect={openService}
            hidePrices={!!t.hidePrices}
            hideThumbnail={!!t.hideThumbnail}
            hideLocation={!!t.hideLocation}
            hideMyAccount={!!t.hideMyAccount}
            collapseCategories={!!t.collapseCategories}
          />
        )}
        {step === 2 && service && (
          <Step2Booking
            brand={brand} mobile={mobile}
            service={service}
            providerId={providerId} setProviderId={onPickProvider}
            date={date} setDate={onPickDate}
            time={time} setTime={setTime}
            addOnIds={addOnIds} setAddOnIds={setAddOnIds}
            onBack={goBackToList}
            showAvatars={t.thumbnail !== 'no-avatar'}
            showGenerics={!t.hideGenericProviders}
            hidePrices={!!t.hidePrices}
            hideAddOnDetails={!!t.hideAddOnDetails}
            hideThumbnail={!!t.hideThumbnail}
          />
        )}
        {step === 3 && service && (
          <Step3Success
            brand={brand} mobile={mobile}
            service={service} provider={provider}
            date={date} time={time} addOnIds={addOnIds}
            subtotal={subtotal}
            onDone={goBackToList}
            hidePrices={!!t.hidePrices}
          />
        )}
        <PoweredByZipper />
      </div>

      {step === 2 && (
        <StickyFooter
          brand={brand} mobile={mobile}
          subtotal={subtotal} canBook={canBook}
          onBook={onBook}
          hidePrices={!!t.hidePrices}
        />
      )}

      {tweaksEnabled && <AppointmentTweaks t={t} setTweak={setTweak} />}

      <style>{`
        @keyframes apt-confetti {
          0%   { transform: translate(0, 0) rotate(0deg) scale(.4); opacity: 0; }
          12%  { opacity: 1; transform: translate(calc(var(--apt-dx) * .15), calc(var(--apt-dy) * .15)) rotate(calc(var(--apt-rot) * .12)) scale(1); }
          70%  { opacity: 1; }
          100% { transform: translate(var(--apt-dx), calc(var(--apt-dy) + 80px)) rotate(var(--apt-rot)) scale(.9); opacity: 0; }
        }
        @keyframes apt-fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
      `}</style>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// BrandBar
// ────────────────────────────────────────────────────────────────
function BrandBar({ brand, mobile, hideMyAccount }) {
  return (
    <div style={{
      borderBottom: `1px solid ${brand.border}`,
      background: '#fff',
      padding: mobile ? '14px 16px' : '18px 32px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      gap: 12,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{
          width: mobile ? 30 : 34, height: mobile ? 30 : 34,
          borderRadius: 8,
          background: brand.accent, color: '#fff',
          display: 'grid', placeItems: 'center',
          fontFamily: brand.displayFont, fontWeight: 800,
          fontSize: mobile ? 13 : 15, letterSpacing: '0.02em',
        }}>H</div>
        <div style={{
          fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
          fontSize: mobile ? 16 : 18, color: brand.ink, letterSpacing: '-0.01em',
        }}>Halcyon Pilates</div>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// STEP 1 — Location card on top, then lines-style service list.
// ────────────────────────────────────────────────────────────────
function Step1Services({ brand, mobile, services, onSelect, hidePrices, hideThumbnail, hideLocation, hideMyAccount, collapseCategories }) {
  // Group services by category in insertion order. Services without an
  // explicit category fall back to a single "Services" bucket so we don't
  // need to special-case the un-categorized renderer.
  const groups = useMemoAPT(() => {
    const map = new Map();
    services.forEach((s) => {
      const cat = s.category || 'Services';
      if (!map.has(cat)) map.set(cat, []);
      map.get(cat).push(s);
    });
    return Array.from(map.entries());
  }, [services]);

  // Categories with a single bucket and no explicit category get the
  // collapsible header suppressed — there's nothing to organize.
  const showCategoryHeaders = groups.length > 1 || (services[0] && services[0].category);

  const [collapsed, setCollapsed] = useStateAPT(() => {
    const init = {};
    if (collapseCategories) groups.forEach(([name]) => { init[name] = true; });
    return init;
  });
  // Reset whenever the tweak toggles, so the panel acts as a "collapse all" /
  // "expand all" affordance rather than just a first-render default.
  useEffectAPT(() => {
    const init = {};
    groups.forEach(([name]) => { init[name] = !!collapseCategories; });
    setCollapsed(init);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [collapseCategories]);

  const toggle = (name) => setCollapsed(prev => ({ ...prev, [name]: !prev[name] }));

  return (
    <div>
      <div style={{
        margin: mobile ? '2px 0 14px' : '8px 0 18px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12,
      }}>
        <h1 style={{
          margin: 0,
          fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
          fontSize: mobile ? 26 : 32, letterSpacing: '-0.02em',
          color: brand.ink,
        }}>Book a session</h1>
        {!hideMyAccount && (
          <button type="button" style={{
            background: 'transparent', border: 0, padding: '4px 0',
            display: 'inline-flex', alignItems: 'center', gap: 6,
            fontFamily: brand.bodyFont, fontSize: 12, fontWeight: 700,
            letterSpacing: '0.08em', textTransform: 'uppercase', color: brand.accent,
            cursor: 'pointer',
          }}>
            <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"/>
              <polyline points="10 17 15 12 10 7"/>
              <line x1="15" y1="12" x2="3" y2="12"/>
            </svg>
            Sign in
          </button>
        )}
      </div>

      <div style={{ display: 'flex', flexDirection: 'column' }}>
        {groups.map(([catName, items], gi) => (
          <CategorySection
            key={catName}
            brand={brand} mobile={mobile}
            name={catName} items={items}
            showHeader={showCategoryHeaders}
            isFirstGroup={gi === 0}
            collapsed={!!collapsed[catName]}
            onToggle={() => toggle(catName)}
            onSelect={onSelect}
            hidePrices={hidePrices}
            hideThumbnail={hideThumbnail}
            hideLocation={hideLocation}
          />
        ))}
      </div>
    </div>
  );
}

// CategorySection — typographic group header + collapsible body. The header
// uses the same uppercase, accent-tinted treatment as the row-accordion
// labels, so it reads as part of the lines aesthetic rather than a separate
// component family. A thin border under the header doubles as the top border
// of the first row, avoiding a double divider.
function CategorySection({
  brand, mobile, name, items, showHeader, isFirstGroup,
  collapsed, onToggle, onSelect, hidePrices, hideThumbnail, hideLocation,
}) {
  if (!showHeader) {
    return (
      <div style={{ display: 'flex', flexDirection: 'column' }}>
        {items.map((s, i) => (
          <ServiceRow key={s.id} brand={brand} mobile={mobile}
                      service={s} onSelect={() => onSelect(s.id)}
                      isFirst={i === 0}
                      hidePrices={hidePrices}
                      hideThumbnail={hideThumbnail}
                      hideLocation={hideLocation} />
        ))}
      </div>
    );
  }
  return (
    <div style={{ marginTop: isFirstGroup ? 0 : (mobile ? 22 : 28) }}>
      <button
        type="button"
        onClick={onToggle}
        aria-expanded={!collapsed}
        style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          width: '100%', background: 'transparent', border: 0,
          padding: mobile ? '10px 0 10px' : '12px 0 12px',
          cursor: 'pointer', textAlign: 'left',
          borderBottom: `1px solid ${brand.border}`,
        }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, minWidth: 0 }}>
          <span style={{
            fontFamily: brand.displayFont,
            fontSize: mobile ? 18 : 22, fontWeight: brand.displayWeight,
            letterSpacing: '-0.01em',
            color: brand.ink,
          }}>{name}</span>
          <span style={{
            fontFamily: brand.bodyFont, fontSize: 13,
            color: brand.inkSoft,
          }}>{items.length} {items.length === 1 ? 'option' : 'options'}</span>
        </div>
        <Caret open={!collapsed} brand={brand} />
      </button>
      {!collapsed && (
        <div style={{
          display: 'flex', flexDirection: 'column',
          animation: 'apt-fadeIn .15s ease',
        }}>
          {items.map((s, i) => (
            <ServiceRow key={s.id} brand={brand} mobile={mobile}
                        service={s} onSelect={() => onSelect(s.id)}
                        isFirst={i === 0}
                        hidePrices={hidePrices}
                        hideThumbnail={hideThumbnail}
                        hideLocation={hideLocation} />
          ))}
        </div>
      )}
    </div>
  );
}

// ServiceRow — lines-style row with two independent accordions:
// (a) "About this session" triggered by the thumbnail or title (click again
//     to close); (b) "Location" triggered by the underlined location link
//     in the secondary text. Only one accordion is open at a time, mirroring
//     the Class Schedule Widget's instructor/location pattern.
function ServiceRow({ brand, mobile, service, onSelect, isFirst, hidePrices, hideThumbnail, hideLocation }) {
  // null | 'service' | 'location'
  const [expanded, setExpanded] = useStateAPT(null);
  const toggle = (key) => setExpanded(prev => (prev === key ? null : key));
  const radius = brand._radius != null ? brand._radius : 8;
  const thumbRadius = brand._thumbRadius != null ? brand._thumbRadius : 999;
  const indentPx = hideThumbnail ? 0 : (mobile ? 68 : 80);
  return (
    <div style={{
      borderTop: isFirst ? 'none' : `1px solid ${brand.border}`,
      padding: mobile ? '14px 0' : '20px 4px',
    }}>
      <div style={{
        display: 'flex', alignItems: 'center',
        gap: mobile ? 12 : 16,
      }}>
        {!hideThumbnail && (
          <button
            type="button"
            onClick={() => toggle('service')}
            aria-label={`About ${service.name}`}
            style={{
              padding: 0, border: 0, background: brand.accentSoft,
              borderRadius: thumbRadius,
              width: mobile ? 56 : 64, height: mobile ? 56 : 64,
              flexShrink: 0, overflow: 'hidden', cursor: 'pointer',
              outline: expanded === 'service' ? `2px solid ${brand.accent}` : 'none',
              outlineOffset: 2,
            }}>
            {service.thumbnail
              ? <img src={service.thumbnail} alt=""
                     style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
              : <div style={{
                  width: '100%', height: '100%', display: 'grid', placeItems: 'center',
                  color: brand.accentDeep, fontFamily: brand.displayFont,
                  fontWeight: 700, fontSize: 16,
                }}>{service.name.slice(0, 1)}</div>
            }
          </button>
        )}

        <div style={{ flex: 1, minWidth: 0 }}>
          <button
            type="button" onClick={() => toggle('service')}
            style={{
              background: 'transparent', border: 0, padding: 0,
              display: 'inline-flex', alignItems: 'center', gap: 6,
              fontFamily: brand.displayFont, fontWeight: 700,
              fontSize: mobile ? 16 : 18, letterSpacing: '-0.01em',
              color: brand.ink, cursor: 'pointer', textAlign: 'left',
            }}>
            {service.name}
            <Caret open={expanded === 'service'} brand={brand} />
          </button>
          <div style={{
            marginTop: 2,
            fontFamily: brand.bodyFont, fontSize: 13, color: brand.inkSoft,
            display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap',
          }}>
            <span>{fmtDuration(service.duration)}</span>
            {!hidePrices && (
              <>
                <span style={{ opacity: 0.5 }}>·</span>
                <span>{fmtMoney(service.price)}</span>
              </>
            )}
          </div>
        </div>

        <SelectButton brand={brand} onClick={onSelect} />
      </div>

      {expanded === 'service' && service.description && (
        <AccordionPanel brand={brand} mobile={mobile}
          indentPx={indentPx} label="About this session">
          {service.description}
        </AccordionPanel>
      )}

      {expanded === 'location' && (
        <AccordionPanel brand={brand} mobile={mobile}
          indentPx={indentPx} label="Location">
          <div style={{
            fontFamily: brand.displayFont, fontWeight: 700, fontSize: 14.5,
            color: brand.panelInk,
          }}>{APT_LOCATION.fullName}</div>
          <div style={{
            marginTop: 2,
            fontFamily: brand.bodyFont, fontSize: 13.5, color: brand.panelInkSoft,
          }}>{APT_LOCATION.address}</div>
          <div style={{
            marginTop: 8,
            fontFamily: brand.bodyFont, fontSize: 13.5, lineHeight: 1.55,
            color: brand.panelInkSoft,
          }}>{APT_LOCATION.details}</div>
        </AccordionPanel>
      )}
    </div>
  );
}

// Shared row-accordion shell — same surface treatment, uppercase tracked
// label, inline animation, so service-description and location accordions
// look like siblings.
function AccordionPanel({ brand, mobile, indentPx, label, children }) {
  const radius = brand._radius != null ? brand._radius : 8;
  return (
    <div style={{
      marginTop: 12,
      marginLeft: indentPx,
      padding: mobile ? '12px 12px' : '14px 16px',
      background: brand.surface,
      border: `1px solid ${brand.border}`,
      borderRadius: radius,
      animation: 'apt-fadeIn .15s ease',
    }}>
      <div style={{
        fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
        textTransform: 'uppercase',
        color: brand.panelInkSoft, marginBottom: 6,
      }}>{label}</div>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 14, lineHeight: 1.55,
        color: brand.panelInk,
      }}>{children}</div>
    </div>
  );
}

function Caret({ open, brand }) {
  return (
    <svg width="11" height="11" viewBox="0 0 24 24" fill="none"
         stroke={brand.inkSoft} strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round"
         style={{ transform: open ? 'rotate(180deg)' : 'rotate(0deg)', transition: 'transform .15s ease' }}>
      <polyline points="6 9 12 15 18 9" />
    </svg>
  );
}

function SelectButton({ brand, onClick }) {
  const textColor = brand._buttonTextColor || '#fff';
  return (
    <button
      type="button" onClick={onClick}
      style={{
        background: brand.accent, color: textColor,
        border: 0,
        borderRadius: brand._radius != null ? brand._radius : 10,
        padding: '10px 18px',
        fontFamily: brand.displayFont, fontWeight: 700, fontSize: 13.5,
        letterSpacing: '-0.005em', cursor: 'pointer', whiteSpace: 'nowrap',
      }}>
      Select
    </button>
  );
}

// ────────────────────────────────────────────────────────────────
// STEP 2 — Three numbered cards (instructor / date / time) plus
// an optional add-ons section.
// ────────────────────────────────────────────────────────────────
function Step2Booking({
  brand, mobile, service,
  providerId, setProviderId,
  date, setDate, time, setTime,
  addOnIds, setAddOnIds,
  onBack,
  showAvatars, showGenerics, hidePrices, hideAddOnDetails, hideThumbnail,
}) {
  const radius = brand._radius != null ? brand._radius : 12;
  const thumbRadius = brand._thumbRadius != null ? brand._thumbRadius : 999;
  // Anchor the strip on whichever week contains the active date, so the
  // auto-picked next-available day is in view on first render even when it
  // sits days from now.
  const initialAnchor = date
    ? addDaysAPT(date, -((date.getDay() - APT_TODAY.getDay() + 7) % 7))
    : APT_TODAY;
  const [stripAnchor, setStripAnchor] = useStateAPT(initialAnchor);
  const stripDates = useMemoAPT(
    () => Array.from({ length: 7 }, (_, i) => addDaysAPT(stripAnchor, i)),
    [stripAnchor],
  );
  const dayHasSlots = (d) => totalSlotsCount(slotsForProviderDay(providerId, d)) > 0;
  const slots = useMemoAPT(() => date ? slotsForProviderDay(providerId, date) : null, [providerId, date]);
  const fullyBooked = date && slots && totalSlotsCount(slots) === 0;
  const nextAvail = fullyBooked ? findNextAvailableDate(providerId, date) : null;

  // Keep the strip anchor in sync when the user picks a date outside the
  // visible week (e.g. via the "next available" jump button).
  useEffectAPT(() => {
    if (!date) return;
    const inView = stripDates.some(d => sameDayAPT(d, date));
    if (!inView) {
      setStripAnchor(addDaysAPT(date, -((date.getDay() - stripAnchor.getDay() + 7) % 7)));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [date]);

  return (
    <div>
      <button
        type="button" onClick={onBack}
        style={{
          background: 'transparent', border: 0, padding: '4px 0',
          fontFamily: brand.bodyFont, fontSize: 12, fontWeight: 700,
          letterSpacing: '0.08em', textTransform: 'uppercase', color: brand.accent,
          display: 'inline-flex', alignItems: 'center', gap: 6,
          cursor: 'pointer',
          marginBottom: mobile ? 10 : 14,
        }}>
        <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
          <line x1="19" y1="12" x2="5" y2="12" />
          <polyline points="12 19 5 12 12 5" />
        </svg>
        Back to sessions
      </button>

      {/* Service summary */}
      <div style={{
        border: `1px solid ${brand.border}`,
        borderRadius: radius,
        padding: mobile ? '14px 14px' : '18px 20px',
        background: brand.surface,
        display: 'flex', alignItems: 'center', gap: mobile ? 12 : 16,
        marginBottom: mobile ? 20 : 26,
      }}>
        {service.thumbnail && !hideThumbnail && (
          <img src={service.thumbnail} alt=""
            style={{
              width: mobile ? 56 : 64, height: mobile ? 56 : 64,
              borderRadius: thumbRadius, objectFit: 'cover', flexShrink: 0,
            }} />
        )}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: brand.displayFont, fontWeight: 700,
            fontSize: mobile ? 18 : 20, letterSpacing: '-0.005em',
            color: brand.panelInk,
          }}>{service.name}</div>
          <div style={{
            marginTop: 4,
            fontFamily: brand.bodyFont, fontSize: 13.5, color: brand.panelInkSoft,
          }}>
            {fmtDuration(service.duration)}{!hidePrices ? ` · From ${fmtMoney(service.price)}` : ''}
          </div>
        </div>
      </div>

      <NumberedStep brand={brand} mobile={mobile} number={1} title="Choose staff">
        <InstructorChips brand={brand}
          providers={[
            ...(showGenerics ? APT_GENERIC_PROVIDERS : []),
            ...APT_PROVIDERS,
          ]}
          activeId={providerId} onSelect={setProviderId}
          showAvatars={showAvatars} />
      </NumberedStep>

      <NumberedStep brand={brand} mobile={mobile} number={2} title="Pick a date">
        <DateStripCompact brand={brand} mobile={mobile}
          dates={stripDates} activeDate={date}
          onSelect={setDate}
          onPrev={() => setStripAnchor(addDaysAPT(stripAnchor, -7))}
          onNext={() => setStripAnchor(addDaysAPT(stripAnchor, 7))}
          dayHasSlots={dayHasSlots} />
      </NumberedStep>

      <NumberedStep brand={brand} mobile={mobile} number={3}
        title={date
          ? `Pick a time — ${APT_WEEKDAYS_LONG[date.getDay()]}, ${APT_MONTHS_LONG[date.getMonth()]} ${date.getDate()}`
          : 'Pick a time'}>
        {!date ? (
          <div style={{
            padding: '12px 14px',
            borderRadius: brand._radius != null ? brand._radius : 8,
            background: brand.surface,
            fontFamily: brand.bodyFont, fontSize: 13.5, color: brand.panelInkSoft,
          }}>Pick a date first to see times.</div>
        ) : fullyBooked ? (
          <FullyBookedNotice brand={brand} nextDate={nextAvail}
            onJump={() => { if (nextAvail) { setDate(nextAvail); } }} />
        ) : (
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            {['morning', 'afternoon', 'evening'].map(part => (
              <TimeGroup key={part} brand={brand}
                label={part === 'morning' ? 'Morning' : part === 'afternoon' ? 'Afternoon' : 'Evening'}
                slots={slots[part]} selected={time} onSelect={setTime} />
            ))}
          </div>
        )}
      </NumberedStep>

      {service.addOns && service.addOns.length > 0 && (
        <OptionalExtras brand={brand} mobile={mobile}
          addOns={service.addOns}
          selected={addOnIds}
          onToggle={(id) => setAddOnIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id])}
          hidePrices={hidePrices}
          hideDetails={hideAddOnDetails} />
      )}
    </div>
  );
}

function NumberedStep({ brand, mobile, number, title, children }) {
  const radius = brand._radius != null ? brand._radius : 12;
  return (
    <div style={{
      border: `1px solid ${brand.border}`,
      borderRadius: radius,
      padding: mobile ? '18px 16px 16px' : '22px 22px 20px',
      background: brand.surface,
      marginBottom: mobile ? 14 : 16,
      animation: 'apt-fadeIn .2s ease',
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 12,
        marginBottom: mobile ? 14 : 16,
      }}>
        <div style={{
          width: mobile ? 30 : 34, height: mobile ? 30 : 34,
          borderRadius: 999,
          background: brand.accent, color: brand._buttonTextColor || '#fff',
          display: 'grid', placeItems: 'center',
          fontFamily: brand.displayFont, fontWeight: 800, fontSize: mobile ? 14 : 15,
          letterSpacing: '0.02em', flexShrink: 0,
        }}>{number}</div>
        <h3 style={{
          margin: 0, minWidth: 0, flex: 1,
          fontFamily: brand.displayFont, fontWeight: 700,
          fontSize: mobile ? 17 : 19, letterSpacing: '-0.01em',
          color: brand.panelInk, lineHeight: 1.3,
        }}>{title}</h3>
      </div>
      {children}
    </div>
  );
}

// ── OptionalExtras — each add-on is its own row with a toggle + name +
// price + a Details accordion that reveals the description. The accordion
// (and its chevron) can be hidden via the `hideDetails` tweak.
function OptionalExtras({ brand, mobile, addOns, selected, onToggle, hidePrices, hideDetails }) {
  return (
    <div style={{
      marginTop: mobile ? 8 : 14,
      paddingTop: mobile ? 14 : 18,
      borderTop: `1px dashed ${brand.border}`,
    }}>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 12, fontWeight: 700,
        letterSpacing: '0.1em', textTransform: 'uppercase', color: brand.inkSoft,
        marginBottom: 4,
      }}>Optional extras</div>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 13.5, color: brand.inkSoft,
        marginBottom: 12,
      }}>Add a little something to your session.</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {addOns.map(id => {
          const ao = APT_ADD_ONS[id]; if (!ao) return null;
          return (
            <AddOnRow key={id} brand={brand} mobile={mobile}
                      ao={ao} isOn={selected.includes(id)}
                      onToggle={() => onToggle(id)}
                      hidePrices={hidePrices} hideDetails={hideDetails} />
          );
        })}
      </div>
    </div>
  );
}

function AddOnRow({ brand, mobile, ao, isOn, onToggle, hidePrices, hideDetails }) {
  const [expanded, setExpanded] = useStateAPT(false);
  const radius = brand._radius != null ? brand._radius : 8;
  return (
    <div style={{
      border: `1.5px solid ${isOn ? brand.accent : brand.border}`,
      background: isOn ? brand.accentSoft : brand.surface,
      borderRadius: radius,
      transition: 'border-color .15s ease, background .15s ease',
      overflow: 'hidden',
    }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 8,
        padding: mobile ? '10px 12px' : '12px 14px',
      }}>
        <button type="button" onClick={onToggle}
          style={{
            background: 'transparent', border: 0, padding: 0,
            flex: 1, minWidth: 0, textAlign: 'left',
            display: 'flex', alignItems: 'center', gap: 10,
            cursor: 'pointer',
          }}>
          <span aria-hidden="true" style={{
            width: 18, height: 18, borderRadius: 999,
            border: `1.5px solid ${isOn ? brand.accent : brand.border}`,
            background: isOn ? brand.accent : 'transparent',
            display: 'grid', placeItems: 'center', flexShrink: 0,
          }}>
            {isOn && (
              <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="#fff"
                   strokeWidth="3.5" strokeLinecap="round" strokeLinejoin="round">
                <polyline points="20 6 9 17 4 12" />
              </svg>
            )}
          </span>
          {ao.thumbnail && (
            <span aria-hidden="true" style={{
              width: mobile ? 36 : 40, height: mobile ? 36 : 40,
              borderRadius: brand._thumbRadius != null ? brand._thumbRadius : 8,
              backgroundImage: `url(${ao.thumbnail})`,
              backgroundSize: 'cover', backgroundPosition: 'center',
              flexShrink: 0,
              border: `1px solid ${brand.border}`,
            }} />
          )}
          <span style={{
            fontFamily: brand.bodyFont, fontWeight: 600, fontSize: 14,
            color: brand.panelInk, flex: 1, minWidth: 0,
          }}>{ao.name}</span>
          {!hidePrices && (
            <span style={{
              fontFamily: brand.bodyFont, fontWeight: 700, fontSize: 13,
              color: brand.panelInk, marginRight: hideDetails ? 0 : 8,
            }}>+{fmtMoney(ao.price)}</span>
          )}
        </button>
        {!hideDetails && (
          <button type="button"
            onClick={() => setExpanded(v => !v)}
            aria-label={expanded ? `Hide ${ao.name} details` : `Show ${ao.name} details`}
            style={{
              background: 'transparent', border: 0, padding: '4px 8px',
              cursor: 'pointer', color: brand.accent,
              display: 'inline-flex', alignItems: 'center', gap: 4,
              fontFamily: brand.bodyFont, fontSize: 12, fontWeight: 700,
              letterSpacing: '0.04em',
            }}>
            {expanded ? 'Hide' : 'Details'}
            <Caret open={expanded} brand={brand} />
          </button>
        )}
      </div>
      {expanded && !hideDetails && (
        <div style={{
          padding: mobile
            ? (ao.thumbnail ? '0 12px 12px 86px' : '0 12px 12px 40px')
            : (ao.thumbnail ? '0 14px 12px 92px' : '0 14px 12px 42px'),
          fontFamily: brand.bodyFont, fontSize: 13, lineHeight: 1.55, color: brand.panelInkSoft,
          animation: 'apt-fadeIn .15s ease',
        }}>{ao.description}</div>
      )}
    </div>
  );
}

// ── Compact date strip
function DateStripCompact({ brand, mobile, dates, activeDate, onSelect, onPrev, onNext, dayHasSlots }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
      <button onClick={onPrev} style={stripArrowBtn(brand)} aria-label="Previous week">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="15 18 9 12 15 6" />
        </svg>
      </button>
      <div style={{
        flex: 1, display: 'grid',
        gridTemplateColumns: 'repeat(7, 1fr)',
        gap: mobile ? 4 : 6,
      }}>
        {dates.map(d => {
          const isActive = activeDate && sameDayAPT(d, activeDate);
          const isToday = sameDayAPT(d, APT_TODAY);
          const isPast = d < APT_TODAY && !isToday;
          const hasSlots = !isPast && dayHasSlots(d);
          return (
            <button
              key={d.toISOString()}
              onClick={() => !isPast && onSelect(d)}
              disabled={isPast}
              style={{
                background: isActive ? brand.accent : brand.surface,
                color: isActive ? '#fff' : (isPast ? brand.panelInkSoft : brand.panelInk),
                border: `1.5px solid ${isActive ? brand.accent : (isToday ? brand.accent : brand.border)}`,
                borderRadius: brand._radius != null ? brand._radius : 14,
                padding: mobile ? '6px 0' : '8px 0',
                cursor: isPast ? 'default' : 'pointer',
                opacity: isPast ? 0.45 : 1,
                fontFamily: brand.bodyFont,
                display: 'flex', flexDirection: 'column', alignItems: 'center',
                gap: 2,
                position: 'relative',
              }}>
              <span style={{
                fontSize: 10.5, fontWeight: 700, letterSpacing: '0.08em', textTransform: 'uppercase',
                color: isActive ? 'rgba(255,255,255,0.88)' : brand.panelInkSoft,
              }}>{APT_WEEKDAYS_SHORT[d.getDay()]}</span>
              <span style={{
                fontFamily: brand.displayFont, fontWeight: 700,
                fontSize: mobile ? 15 : 17,
                color: isActive ? '#fff' : brand.panelInk,
              }}>{d.getDate()}</span>
              {hasSlots && !isActive && (
                <span style={{
                  position: 'absolute', bottom: 4, left: '50%',
                  transform: 'translateX(-50%)',
                  width: 4, height: 4, borderRadius: 999, background: brand.accent,
                }} />
              )}
            </button>
          );
        })}
      </div>
      <button onClick={onNext} style={stripArrowBtn(brand)} aria-label="Next week">
        <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="9 18 15 12 9 6" />
        </svg>
      </button>
    </div>
  );
}
function stripArrowBtn(brand) {
  return {
    flex: '0 0 auto',
    width: 30, height: 30, borderRadius: 999,
    background: 'transparent', color: brand.inkSoft,
    border: 0, cursor: 'pointer',
    display: 'grid', placeItems: 'center',
  };
}

function FullyBookedNotice({ brand, nextDate, onJump }) {
  const radius = brand._radius != null ? brand._radius : 10;
  return (
    <div style={{
      padding: '14px 16px',
      borderRadius: radius,
      background: brand.surface,
    }}>
      <div style={{
        fontFamily: brand.displayFont, fontWeight: 700, fontSize: 14.5,
        color: brand.panelInk, marginBottom: 4,
      }}>Fully booked for this day</div>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 13, color: brand.panelInkSoft, marginBottom: 12,
      }}>Try another day above, or jump to the next opening.</div>
      {nextDate && (
        <button type="button" onClick={onJump}
          style={{
            background: '#fff', border: `1px solid ${brand.border}`,
            borderRadius: 999, padding: '8px 16px',
            fontFamily: brand.bodyFont, fontWeight: 600, fontSize: 13,
            color: brand.ink, cursor: 'pointer',
          }}>
          Go to {APT_WEEKDAYS_SHORT[nextDate.getDay()]} {APT_MONTHS_LONG[nextDate.getMonth()]} {nextDate.getDate()}
        </button>
      )}
    </div>
  );
}

function TimeGroup({ brand, label, slots, selected, onSelect }) {
  if (slots.length === 0) return null;
  return (
    <div>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 12, fontWeight: 700,
        letterSpacing: '0.06em', textTransform: 'uppercase',
        color: brand.panelInkSoft, marginBottom: 6,
      }}>{label}</div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
        {slots.map(s => {
          const isOn = selected === s;
          return (
            <button key={s} type="button" onClick={() => onSelect(s)}
              style={{
                appearance: 'none',
                background: isOn ? brand.accent : brand.surface,
                color: isOn ? '#fff' : brand.panelInk,
                border: `1px solid ${isOn ? brand.accent : brand.border}`,
                borderRadius: brand._radius != null ? brand._radius : 8,
                padding: '8px 14px',
                fontFamily: brand.bodyFont, fontWeight: 600, fontSize: 13.5,
                cursor: 'pointer',
                transition: 'background .12s ease, color .12s ease, border-color .12s ease',
              }}>
              {s}
            </button>
          );
        })}
      </div>
    </div>
  );
}

function InstructorChips({ brand, providers, activeId, onSelect, showAvatars }) {
  return (
    <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
      {providers.map(p => {
        const isOn = activeId === p.id;
        return (
          <button key={p.id} type="button" onClick={() => onSelect(p.id)}
            style={{
              appearance: 'none',
              background: isOn ? brand.accent : brand.surface,
              color: isOn ? (brand._buttonTextColor || '#fff') : brand.panelInk,
              border: `1.5px solid ${isOn ? brand.accent : brand.border}`,
              borderRadius: brand._radius != null ? brand._radius : 999,
              padding: '4px 14px 4px 4px',
              fontFamily: brand.bodyFont, fontWeight: 600, fontSize: 13.5,
              cursor: 'pointer',
              display: 'inline-flex', alignItems: 'center', gap: 8,
              transition: 'background .15s ease, border-color .15s ease',
            }}>
            <Avatar brand={brand} provider={p} showAvatar={showAvatars} size={28} />
            <span>{p.name}</span>
          </button>
        );
      })}
    </div>
  );
}

function Avatar({ brand, provider, showAvatar, size }) {
  const thumbRadius = brand._thumbRadius != null ? brand._thumbRadius : 999;
  if (provider.generic) {
    return (
      <div style={{
        width: size, height: size, borderRadius: thumbRadius,
        background: brand.surface, display: 'grid', placeItems: 'center', flexShrink: 0,
        color: brand.panelInkSoft,
      }}>
        <svg width={size * 0.55} height={size * 0.55} viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
          <circle cx="9" cy="7" r="4" />
          <path d="M22 21v-2a4 4 0 0 0-3-3.87" />
          <path d="M16 3.13a4 4 0 0 1 0 7.75" />
        </svg>
      </div>
    );
  }
  if (showAvatar && provider.photo) {
    return (
      <img src={provider.photo} alt={provider.name}
        style={{ width: size, height: size, borderRadius: thumbRadius, objectFit: 'cover', flexShrink: 0 }} />
    );
  }
  return (
    <div style={{
      width: size, height: size, borderRadius: thumbRadius,
      background: brand.accentSoft, color: brand.accentDeep,
      display: 'grid', placeItems: 'center', flexShrink: 0,
      fontFamily: brand.displayFont, fontWeight: 700, fontSize: size * 0.42,
    }}>{(provider.name || '?').slice(0, 1).toUpperCase()}</div>
  );
}

// ────────────────────────────────────────────────────────────────
// Sticky footer
// ────────────────────────────────────────────────────────────────
function StickyFooter({ brand, mobile, subtotal, canBook, onBook, hidePrices }) {
  return (
    <div style={{
      position: 'fixed', left: 0, right: 0, bottom: 0,
      background: '#fff',
      borderTop: `1px solid ${brand.border}`,
      boxShadow: '0 -4px 18px rgba(20, 22, 18, 0.06)',
      zIndex: 30,
    }}>
      <div style={{
        maxWidth: 960, margin: '0 auto',
        padding: mobile ? '12px 16px' : '14px 28px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        gap: 12,
      }}>
        <div>
          {!hidePrices ? (
            <>
              <div style={{
                fontFamily: brand.bodyFont, fontSize: 11, fontWeight: 700,
                letterSpacing: '0.1em', textTransform: 'uppercase', color: brand.inkSoft,
              }}>Subtotal</div>
              <div style={{
                fontFamily: brand.displayFont, fontWeight: 700,
                fontSize: mobile ? 18 : 20, letterSpacing: '-0.01em',
                color: brand.ink,
              }}>{fmtMoney(subtotal)}</div>
            </>
          ) : <span />}
        </div>
        <PrimaryButton brand={brand} disabled={!canBook} confetti onClick={onBook}>
          Book appointment
        </PrimaryButton>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// STEP 3 — Success
// ────────────────────────────────────────────────────────────────
function Step3Success({ brand, mobile, service, provider, date, time, addOnIds, subtotal, onDone, hidePrices }) {
  return (
    <div style={{
      maxWidth: 560, margin: '0 auto',
      textAlign: 'center',
      padding: mobile ? '12px 4px 0' : '32px 4px 0',
    }}>
      <div style={{
        width: 56, height: 56, borderRadius: 999,
        background: brand.accentSoft, color: brand.accentDeep,
        display: 'grid', placeItems: 'center',
        margin: '0 auto 16px',
      }}>
        <svg width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor"
             strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
          <polyline points="20 6 9 17 4 12" />
        </svg>
      </div>
      <h2 style={{
        margin: '0 0 8px',
        fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
        fontSize: mobile ? 26 : 32, letterSpacing: '-0.02em', color: brand.ink,
      }}>You&apos;re booked.</h2>
      <div style={{
        fontFamily: brand.bodyFont, fontSize: 14.5, color: brand.inkSoft,
        marginBottom: 20,
      }}>A confirmation is on its way to your inbox. Plan to arrive a few minutes early to settle in.</div>

      <div style={{
        margin: '0 auto', maxWidth: 460,
        padding: '16px 18px',
        borderRadius: brand._radius != null ? brand._radius : 10,
        background: brand.surface,
        textAlign: 'left',
        display: 'flex', flexDirection: 'column', gap: 4,
        fontFamily: brand.bodyFont, fontSize: 13.5, color: brand.panelInk, lineHeight: 1.55,
      }}>
        <div style={{ fontFamily: brand.displayFont, fontWeight: 700, fontSize: 16 }}>
          {service.name} – {fmtDuration(service.duration)}
        </div>
        {date && time && (
          <div style={{ color: brand.panelInkSoft }}>
            {APT_WEEKDAYS_SHORT[date.getDay()]}, {APT_MONTHS_LONG[date.getMonth()]} {date.getDate()}, {date.getFullYear()} at {time}
          </div>
        )}
        {provider && <div style={{ color: brand.panelInkSoft }}>with {provider.name}</div>}
        {!hidePrices && addOnIds.length > 0 && (
          <div style={{ color: brand.panelInkSoft }}>
            + {addOnIds.map(id => APT_ADD_ONS[id]?.name).filter(Boolean).join(' · ')}
          </div>
        )}
        {!hidePrices && (
          <div style={{
            marginTop: 6, fontFamily: brand.displayFont, fontWeight: 700,
            color: brand.panelInk, fontSize: 15,
          }}>Total {fmtMoney(subtotal)}</div>
        )}
      </div>

      <div style={{ marginTop: 24 }}>
        <PrimaryButton brand={brand} onClick={onDone}>Book another</PrimaryButton>
      </div>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// PrimaryButton — confetti pop wired to brand._fun.
// ────────────────────────────────────────────────────────────────
function PrimaryButton({ brand, onClick, disabled, confetti, children }) {
  const fun = !!brand._fun;
  const textColor = brand._buttonTextColor || '#fff';
  const [bursts, setBursts] = useStateAPT([]);
  const seqRef = useRefAPT(0);

  const fireBurst = () => {
    const id = ++seqRef.current;
    const palette = [brand.accent, '#FFC857', '#FF6B6B', '#4ECDC4',
                     '#A06CD5', '#FFD166', '#06D6A0', '#EF476F'];
    const N = 22;
    const particles = Array.from({ length: N }, (_, i) => {
      const t = i / (N - 1);
      const angle = (-Math.PI * 1.1) + t * Math.PI * 1.2 + (Math.random() - 0.5) * 0.25;
      const dist = 70 + Math.random() * 70;
      return {
        id: i, color: palette[(i + Math.floor(Math.random() * palette.length)) % palette.length],
        dx: Math.cos(angle) * dist, dy: Math.sin(angle) * dist,
        rot: (Math.random() * 720 - 360) | 0,
        size: 5 + Math.random() * 6,
        round: Math.random() > 0.55,
        delay: (Math.random() * 60) | 0,
        duration: 700 + (Math.random() * 250) | 0,
      };
    });
    setBursts(prev => [...prev, { id, particles }]);
    window.setTimeout(() => setBursts(prev => prev.filter(b => b.id !== id)), 1100);
  };

  const handleClick = () => {
    if (disabled) return;
    if (fun && confetti) { fireBurst(); window.setTimeout(() => onClick && onClick(), 600); }
    else { onClick && onClick(); }
  };
  return (
    <span style={{ position: 'relative', display: 'inline-block' }}>
      <button
        type="button" onClick={handleClick}
        disabled={disabled}
        style={{
          background: disabled ? brand.surface : brand.accent,
          color: disabled ? brand.inkSoft : textColor,
          border: disabled ? `1px solid ${brand.border}` : 0,
          borderRadius: brand._radius != null ? brand._radius : 10,
          padding: '12px 22px',
          fontFamily: brand.displayFont, fontWeight: 700, fontSize: 13.5,
          letterSpacing: '-0.005em', cursor: disabled ? 'default' : 'pointer',
        }}>
        {children}
      </button>
      {bursts.length > 0 && (
        <span aria-hidden="true" style={{
          position: 'absolute', left: '50%', top: '50%',
          width: 0, height: 0, pointerEvents: 'none', zIndex: 50,
        }}>
          {bursts.flatMap(b => b.particles.map(p => (
            <i key={`${b.id}-${p.id}`} style={{
              position: 'absolute',
              left: -p.size / 2, top: -p.size / 2,
              width: p.size, height: p.size,
              background: p.color,
              borderRadius: p.round ? '50%' : 2,
              opacity: 0,
              '--apt-dx': `${p.dx}px`,
              '--apt-dy': `${p.dy}px`,
              '--apt-rot': `${p.rot}deg`,
              animation: `apt-confetti ${p.duration}ms cubic-bezier(.22,.61,.36,1) ${p.delay}ms forwards`,
            }} />
          )))}
        </span>
      )}
    </span>
  );
}

// ────────────────────────────────────────────────────────────────
// Tweaks panel
// ────────────────────────────────────────────────────────────────
function AppointmentTweaks({ t, setTweak }) {
  const {
    TweaksPanel: Panel, TweakSection: Section, TweakColor: Color,
    TweakSlider: Slider, TweakSelect: Select, TweakToggle: Toggle,
    TweakButton: Button,
  } = window;
  const resetAll = () => {
    const defaults = (typeof window !== 'undefined' && window.APT_TWEAK_DEFAULTS) || {};
    Object.keys(defaults).forEach(k => setTweak(k, defaults[k]));
  };
  useEffectAPT(() => {
    if (!Panel) return;
    const id = window.setTimeout(() => window.postMessage({ type: '__activate_edit_mode' }, '*'), 0);
    return () => window.clearTimeout(id);
  }, [Panel]);
  if (!Panel) return null;
  const FONTS = [
    "'Poppins', system-ui, sans-serif",
    "'Inter', system-ui, sans-serif",
    "'Sora', system-ui, sans-serif",
    "'Open Sans', system-ui, sans-serif",
    "'DM Sans', system-ui, sans-serif",
    "'Manrope', system-ui, sans-serif",
    "'Bebas Neue', sans-serif",
    "'Playfair Display', serif",
  ];
  const fontLabel = (f) => f.replace(/['"]/g, '').split(',')[0];
  return (
    <Panel title="Studio settings" noDeckControls={true}>
      <Section label="Surfaces" />
      <Color label="Accent"      value={t.accent}     onChange={(v) => setTweak('accent', v)} />
      <Color label="Background"  value={t.background} onChange={(v) => setTweak('background', v)} />
      <Color label="Panel"       value={t.panel}      onChange={(v) => setTweak('panel', v)} />
      <Color label="Line color"  value={t.lineColor || '#ECECEC'}
                                 onChange={(v) => setTweak('lineColor', v)} />

      <Section label="Text on background" />
      <Color label="Primary"     value={t.headline}   onChange={(v) => setTweak('headline', v)} />
      <Color label="Secondary"   value={t.details}    onChange={(v) => setTweak('details', v)} />

      <Section label="Text on panel" />
      <Color label="Primary"     value={t.panelInk}     onChange={(v) => setTweak('panelInk', v)} />
      <Color label="Secondary"   value={t.panelInkSoft} onChange={(v) => setTweak('panelInkSoft', v)} />

      <Section label="Text on accent" />
      <Color label="Button text" value={t.buttonTextColor || '#FFFFFF'}
                                 onChange={(v) => setTweak('buttonTextColor', v)} />

      <Section label="Typography" />
      <Select label="Font family" value={t.font}
              options={FONTS.map(f => ({ value: f, label: fontLabel(f) }))}
              onChange={(v) => setTweak('font', v)} />
      <Slider label="Headline weight" value={t.weight} min={300} max={800} step={100}
              onChange={(v) => setTweak('weight', v)} />

      <Section label="Shape" />
      <Slider label="Panels &amp; buttons" value={t.radius} min={0} max={28} step={1} unit="px"
              onChange={(v) => setTweak('radius', v)} />
      <Slider label="Thumbnails" value={t.thumbnailRadius} min={0} max={32} step={1} unit="px"
              onChange={(v) => setTweak('thumbnailRadius', v)} />

      <Section label="Hide" />
      <Toggle label="Prices"               value={!!t.hidePrices}           onChange={(v) => setTweak('hidePrices', v)} />
      <Toggle label="Service thumbnails"   value={!!t.hideThumbnail}        onChange={(v) => setTweak('hideThumbnail', v)} />
      <Toggle label="Instructor thumbnails" value={t.thumbnail === 'no-avatar'} onChange={(v) => setTweak('thumbnail', v ? 'no-avatar' : 'photo')} />
      <Toggle label="Location"            value={!!t.hideLocation}         onChange={(v) => setTweak('hideLocation', v)} />
      <Toggle label="Add-on details"      value={!!t.hideAddOnDetails}     onChange={(v) => setTweak('hideAddOnDetails', v)} />
      <Toggle label="Generic instructors" value={!!t.hideGenericProviders} onChange={(v) => setTweak('hideGenericProviders', v)} />
      <Toggle label="My Account link"     value={!!t.hideMyAccount}        onChange={(v) => setTweak('hideMyAccount', v)} />

      <Section label="Layout" />
      <Toggle label="Collapse categories" value={!!t.collapseCategories}   onChange={(v) => setTweak('collapseCategories', v)} />

      <Section label="Fun" />
      <Toggle label="Confetti on Book appointment" value={!!t.fun}
              onChange={(v) => setTweak('fun', v)} />

      <Section label="" />
      <Button label="Restore default settings" secondary onClick={resetAll} />
    </Panel>
  );
}

function PoweredByZipper() {
  return (
    <div style={{
      padding: '36px 0 8px',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      gap: 10,
      fontSize: 10.5, fontWeight: 600, letterSpacing: '0.1em',
      textTransform: 'uppercase', color: '#9CA3AF',
    }}>
      <span>Powered by</span>
      <img src="zipper-logo-grey.svg" alt="Zipper" height="18"
           style={{ display: 'block', height: 18, width: 'auto' }} />
    </div>
  );
}

// Mount
const aptRoot = ReactDOM.createRoot(document.getElementById('root'));
aptRoot.render(<AppointmentApp brandId="halcyon" />);
