// schedule.jsx — modern class-schedule view for Halcyon Pilates.
// Visually cohesive with the booking flow + intro pack + confirmation:
// same cream background, sage accent, Sora display face, rounded cards.
// Influences: airy date strip (Sweat Yoga), Today/Tomorrow grouping
// (Hot Yoga), row-based row density (Studio), capacity awareness
// (Motivate). Designed to feel cleaner and quieter than all of them.

const { useState: useStateSCH, useMemo: useMemoSCH, useEffect: useEffectSCH, useRef: useRefSCH } = React;
const TOTAL_SCH_DAYS = 14;
const INITIAL_VISIBLE_DAYS = 3;

const SCH_BREAKPOINT = 760;
function useSCHMobile() {
  const forced = typeof window !== 'undefined' && window.SCH_FORCE_MOBILE;
  const [m, setM] = useStateSCH(forced || (typeof window !== 'undefined' ? window.innerWidth < SCH_BREAKPOINT : false));
  useEffectSCH(() => {
    if (forced) return undefined;
    const onResize = () => setM(window.innerWidth < SCH_BREAKPOINT);
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, [forced]);
  return m;
}

// ────────────────────────────────────────────────────────────────
// Mock schedule — a week's worth of classes for Halcyon.
// In production this would come from the API.
// ────────────────────────────────────────────────────────────────
const SCH_TODAY = new Date(2026, 4, 20); // Wed May 20, 2026 (deterministic)

const INSTRUCTORS = {
  'Marisol Pena': {
    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. Known for cueing that meets you where your body is that day.',
  },
  'Lena Ortiz': {
    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.',
  },
  'Yasmin Castillo': {
    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. Background in physical therapy. Bring a towel — Yasmin\'s sculpt classes are no joke.',
  },
  'Anika Rao': {
    photo: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?auto=format&fit=crop&w=200&h=200&q=80',
    bio: 'Contemplative, breath-led practice honed through 500-hour yoga training. Anika\'s slow-flow classes are a favorite end to the day.',
  },
};

const LOCATIONS = {
  'Allendale': {
    address: '142 W Allendale Ave, Allendale, NJ 07401',
    notes: 'Free street parking. Entrance on the west side of the building — look for the sage awning.',
  },
  'Wyckoff': {
    address: '385 Franklin Ave Suite 204, Wyckoff, NJ 07481',
    notes: 'Second floor — use the staircase or elevator next to the Whole Foods. Lockers available.',
  },
  'Ho-Ho-Kus': {
    address: '612 N Maple Ave, Ho-Ho-Kus, NJ 07423',
    notes: 'Two-hour lot parking out front. Studio is the back unit — follow signs from the entrance.',
  },
};

const CLASS_IMAGES = {
  'Reformer Flow':      'https://images.unsplash.com/photo-1518611012118-696072aa579a?auto=format&fit=crop&w=200&h=200&q=80',
  'Slow Flow':          'https://images.unsplash.com/photo-1544367567-0f2fcb009e0b?auto=format&fit=crop&w=200&h=200&q=80',
  'Express Reformer':   'https://images.unsplash.com/photo-1599901860904-17e6ed7083a0?auto=format&fit=crop&w=200&h=200&q=80',
  'Reformer Sculpt':    'https://images.unsplash.com/photo-1571019613454-1cb2f99b2d8b?auto=format&fit=crop&w=200&h=200&q=80',
  'Mat Foundations':    'https://images.unsplash.com/photo-1506126613408-eca07ce68773?auto=format&fit=crop&w=200&h=200&q=80',
  'Power Reformer':     'https://images.unsplash.com/photo-1581009146145-b5ef050c2e1e?auto=format&fit=crop&w=200&h=200&q=80',
  'Reformer Privates':  'https://images.unsplash.com/photo-1517836357463-d25dfeac3438?auto=format&fit=crop&w=200&h=200&q=80',
  'Sat Reformer Mix':   'https://images.unsplash.com/photo-1532384748853-8f54a8f476e2?auto=format&fit=crop&w=200&h=200&q=80',
  'Reformer + Mat':     'https://images.unsplash.com/photo-1518310383802-640c2de311b2?auto=format&fit=crop&w=200&h=200&q=80',
  'Sunday Slow Flow':   'https://images.unsplash.com/photo-1554344728-77cf90d9ed26?auto=format&fit=crop&w=200&h=200&q=80',
};

const CLASS_DESCRIPTIONS = {
  'Reformer Flow':      'Flowing reformer class blending strength and stretch. All levels welcome — modifications offered for every spring change.',
  'Slow Flow':          'Slower-paced reformer practice focused on alignment, breath, and deep core engagement. Great recovery day or intro session.',
  'Express Reformer':   'A 45-minute lunch session — full-body burn without the all-day commitment. Lockers and showers on site.',
  'Reformer Sculpt':    'Higher-tempo reformer work with weighted props for sculpt-style conditioning. Expect to break a sweat.',
  'Mat Foundations':    'Classical mat pilates fundamentals — perfect for first-timers or anyone refining technique. No reformer experience required.',
  'Power Reformer':     'Athletic reformer training with heavier springs and dynamic transitions. Best for clients with prior reformer experience.',
  'Reformer Privates':  'One-on-one programming tailored to your body and goals. Ideal for injury return, pre/post-natal, or focused skill work.',
  'Sat Reformer Mix':   'Saturday signature: equal parts reformer flow and reformer sculpt. Cardio, strength, and stretch in one class.',
  'Reformer + Mat':     'Half reformer, half mat work for a full-body session. Develop the kind of core control reformer alone can\'t teach.',
  'Sunday Slow Flow':   'A meditative slow flow on the reformer to start your week. Soft lighting, breathwork, intentional pacing.',
};

// Optional per-page override hook for white-label demos. Set window.SCH_DATA_OVERRIDE
// before this script loads to swap class names, images, descriptions, instructors,
// or locations without forking the schedule data.
const _SCH_OVR = (typeof window !== 'undefined' && window.SCH_DATA_OVERRIDE) || {};
if (_SCH_OVR.classImages)       Object.assign(CLASS_IMAGES, _SCH_OVR.classImages);
if (_SCH_OVR.classDescriptions) Object.assign(CLASS_DESCRIPTIONS, _SCH_OVR.classDescriptions);
if (_SCH_OVR.instructors)       Object.assign(INSTRUCTORS, _SCH_OVR.instructors);
if (_SCH_OVR.locations)         Object.assign(LOCATIONS, _SCH_OVR.locations);
const _CLASS_RENAME      = _SCH_OVR.classRename      || {};
const _INSTRUCTOR_RENAME = _SCH_OVR.instructorRename || {};
const _LOCATION_RENAME   = _SCH_OVR.locationRename   || {};
function _renameClass(n)      { return _CLASS_RENAME[n] || n; }
function _renameInstructor(n) { return _INSTRUCTOR_RENAME[n] || n; }
function _renameLocation(n)   { return _LOCATION_RENAME[n] || n; }

function makeMockSchedule() {
  const make = (dayOffset, time, duration, name, instructor, location, capacityState, spotsLeft) => {
    const renamed = _renameClass(name);
    const renamedInstructor = _renameInstructor(instructor);
    const renamedLocation = _renameLocation(location);
    return {
      id: `${dayOffset}-${time}-${renamed}-${renamedLocation}`,
      date: addDays(SCH_TODAY, dayOffset),
      time, duration,
      name: renamed, instructor: renamedInstructor, location: renamedLocation,
      capacityState,               // 'open' | 'filling' | 'almost-full' | 'waitlist'
      spotsLeft,
      cancelled: false,
    };
  };
  const cancel = (cls, reason) => ({ ...cls, cancelled: true, cancelReason: reason });
  // 14 days of classes, spread across three locations.
  return [
    // Day 0 — today
    make(0, '7:00 AM',  '50 min', 'Reformer Flow',     'Marisol Pena',    'Allendale', 'open', 8),
    make(0, '9:30 AM',  '60 min', 'Slow Flow',         'Lena Ortiz',      'Wyckoff',   'filling', 4),
    make(0, '12:00 PM', '45 min', 'Express Reformer',  'Marisol Pena',    'Allendale', 'almost-full', 2),
    // For waitlist rows, spotsLeft is repurposed to mean "people on the waitlist".
    make(0, '5:30 PM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Ho-Ho-Kus', 'waitlist', 5),
    cancel(make(0, '7:00 PM',  '60 min', 'Mat Foundations',   'Anika Rao',       'Allendale', 'open', 12), 'Instructor out sick'),

    // Day 1
    make(1, '6:30 AM',  '50 min', 'Power Reformer',    'Marisol Pena',    'Allendale', 'open', 6),
    make(1, '8:00 AM',  '60 min', 'Slow Flow',         'Lena Ortiz',      'Wyckoff',   'open', 9),
    make(1, '12:00 PM', '45 min', 'Express Reformer',  'Marisol Pena',    'Ho-Ho-Kus', 'filling', 3),
    make(1, '6:00 PM',  '50 min', 'Reformer Flow',     'Yasmin Castillo', 'Allendale', 'waitlist', 2),

    // Day 2
    make(2, '7:00 AM',  '50 min', 'Reformer Flow',     'Marisol Pena',    'Wyckoff',   'open', 10),
    cancel(make(2, '9:00 AM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Allendale', 'filling', 4), 'Studio maintenance'),
    make(2, '5:30 PM',  '50 min', 'Reformer Sculpt',   'Lena Ortiz',      'Allendale', 'open', 7),

    // Day 3
    make(3, '7:00 AM',  '50 min', 'Reformer Flow',     'Marisol Pena',    'Allendale', 'filling', 4),
    make(3, '9:30 AM',  '60 min', 'Slow Flow',         'Anika Rao',       'Ho-Ho-Kus', 'open', 8),
    make(3, '5:30 PM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Wyckoff',   'open', 9),

    // Day 4
    make(4, '8:00 AM',  '60 min', 'Reformer Privates', 'Lena Ortiz',      'Allendale', 'open', 1),
    make(4, '10:00 AM', '50 min', 'Reformer Flow',     'Marisol Pena',    'Wyckoff',   'filling', 3),
    make(4, '6:00 PM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Ho-Ho-Kus', 'open', 5),

    // Day 5 — Saturday
    make(5, '9:00 AM',  '60 min', 'Sat Reformer Mix',  'Marisol Pena',    'Allendale', 'almost-full', 2),
    make(5, '10:30 AM', '60 min', 'Reformer + Mat',    'Anika Rao',       'Wyckoff',   'open', 9),
    make(5, '12:00 PM', '50 min', 'Reformer Flow',     'Lena Ortiz',      'Ho-Ho-Kus', 'open', 8),

    // Day 6 — Sunday
    make(6, '9:30 AM',  '60 min', 'Sunday Slow Flow',  'Lena Ortiz',      'Allendale', 'open', 11),
    make(6, '11:00 AM', '50 min', 'Reformer Flow',     'Marisol Pena',    'Wyckoff',   'open', 6),

    // Day 7
    make(7, '7:00 AM',  '50 min', 'Reformer Flow',     'Marisol Pena',    'Allendale', 'open', 10),
    make(7, '12:00 PM', '45 min', 'Express Reformer',  'Lena Ortiz',      'Wyckoff',   'open', 7),
    make(7, '5:30 PM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Ho-Ho-Kus', 'filling', 4),

    // Day 8
    make(8, '8:00 AM',  '60 min', 'Slow Flow',         'Anika Rao',       'Allendale', 'open', 9),
    make(8, '6:00 PM',  '50 min', 'Reformer Flow',     'Marisol Pena',    'Wyckoff',   'almost-full', 2),

    // Day 9
    make(9, '7:00 AM',  '50 min', 'Power Reformer',    'Marisol Pena',    'Allendale', 'filling', 5),
    make(9, '9:30 AM',  '60 min', 'Slow Flow',         'Lena Ortiz',      'Ho-Ho-Kus', 'open', 8),
    make(9, '5:30 PM',  '50 min', 'Reformer Sculpt',   'Yasmin Castillo', 'Wyckoff',   'open', 6),

    // Day 10
    make(10, '7:00 AM', '50 min', 'Reformer Flow',     'Marisol Pena',    'Allendale', 'open', 9),
    make(10, '12:00 PM','45 min', 'Express Reformer',  'Lena Ortiz',      'Wyckoff',   'open', 7),

    // Day 11
    make(11, '8:00 AM', '60 min', 'Reformer Privates', 'Lena Ortiz',      'Allendale', 'open', 1),
    make(11, '5:30 PM', '50 min', 'Reformer Flow',     'Yasmin Castillo', 'Ho-Ho-Kus', 'filling', 4),

    // Day 12 — Saturday
    make(12, '9:00 AM', '60 min', 'Sat Reformer Mix',  'Marisol Pena',    'Allendale', 'open', 8),
    make(12, '10:30 AM','60 min', 'Reformer + Mat',    'Anika Rao',       'Wyckoff',   'open', 10),

    // Day 13 — Sunday
    make(13, '9:30 AM', '60 min', 'Sunday Slow Flow',  'Lena Ortiz',      'Allendale', 'open', 11),
  ];
}
function addDays(d, n) { const x = new Date(d); x.setDate(x.getDate() + n); return x; }
function sameDay(a, b) { return a.toDateString() === b.toDateString(); }
function dayDelta(d) {
  const today = SCH_TODAY;
  const days = Math.round((d - today) / 86400000);
  if (days === 0) return 'Today';
  if (days === 1) return 'Tomorrow';
  return null;
}
const WEEKDAYS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const MONTHS_LONG = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
const WEEKDAYS_LONG = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

// ── Month-view (calendar grid) helpers — used when SCH_VIEW_SWITCHER_ENABLED ──
const CAL_GRID_START_HOUR = 6;
const CAL_GRID_END_HOUR   = 21; // exclusive — last label at 8 PM, row to 9 PM
const CAL_HOUR_PX = 52;         // tuned so a 50-min class fully shows time + name
const CAL_TIME_COL_PX = 64;

const CAL_CATEGORY_TINT = {
  'Reformer Flow':      '#4F9DB5',
  'Slow Flow':          '#6FA3A3',
  'Sunday Slow Flow':   '#6FA3A3',
  'Express Reformer':   '#7AA86A',
  'Sat Reformer Mix':   '#7AA86A',
  'Reformer + Mat':     '#7AA86A',
  'Reformer Sculpt':    '#D88B5E',
  'Power Reformer':     '#D88B5E',
  'Reformer Privates':  '#A98AB3',
  'Mat Foundations':    '#A98AB3',
};
const calTintFor = (name, accent) => CAL_CATEGORY_TINT[name] || accent;

function calParseTimeToMin(t) {
  const m = /^(\d{1,2}):(\d{2})\s*(AM|PM)$/i.exec(t.trim());
  if (!m) return 0;
  let h = parseInt(m[1], 10);
  const min = parseInt(m[2], 10);
  if (m[3].toUpperCase() === 'PM' && h !== 12) h += 12;
  if (m[3].toUpperCase() === 'AM' && h === 12) h = 0;
  return h * 60 + min;
}
function calParseDurationMin(d) {
  // Schedule data stores duration as e.g. '50 min'. Parse the leading integer.
  const m = /(\d+)/.exec(String(d || ''));
  return m ? parseInt(m[1], 10) : 60;
}
function calFormatHour(h) {
  const ap = h < 12 ? 'AM' : 'PM';
  const hh = h % 12 === 0 ? 12 : h % 12;
  return `${hh} ${ap}`;
}

const CAL_CAP = {
  'open':        { dot: '#7AA86A', label: 'Open' },
  'filling':     { dot: '#D8A23E', label: 'Filling up' },
  'almost-full': { dot: '#D88B5E', label: 'Almost full' },
  'waitlist':    { dot: '#B05F5F', label: 'Waitlist' },
};
function calCapText(c) {
  if (c.cancelled) return 'Cancelled';
  const s = CAL_CAP[c.capacityState];
  if (c.capacityState === 'waitlist') return `Waitlist · ${c.spotsLeft} ahead`;
  if (!s) return '';
  if (c.capacityState === 'open') return `${c.spotsLeft} spots`;
  return `${s.label} · ${c.spotsLeft} left`;
}

function ScheduleApp({ brandId = 'halcyon' }) {
  const baseBrand = window.BRANDS[brandId];
  const mobile = useSCHMobile();
  const schedule = useMemoSCH(makeMockSchedule, []);

  // ── Tweaks panel integration (opt-in, gated by window.SCH_TWEAKS_ENABLED) ──
  // When enabled, the panel lets the viewer adjust accent, surfaces, fonts,
  // radius and thumbnail source live — useful for the white-label demo.
  const tweaksEnabled = typeof window !== 'undefined' && window.SCH_TWEAKS_ENABLED;
  const defaults = (typeof window !== 'undefined' && window.SCH_TWEAK_DEFAULTS) || {};
  const [t, setTweak] = (tweaksEnabled ? window.useTweaks(defaults) : [defaults, () => {}]);

  // Compose a brand object the rest of the tree consumes. When tweaks are
  // disabled the base brand passes through with panel-text fields defaulted
  // to ink/inkSoft so consumers can always read brand.panelInk.
  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,        // consumed by ClassRow / DateStrip / FilterPill / BookButton
    _thumbnail:    t.thumbnail,     // 'instructor' | 'class'
    _capacityMode: t.capacityMode,  // 'all' | 'threshold' | 'hidden'
    _capacityThreshold: t.capacityThreshold,
    // Per-row hide flags (read by ClassRow / CapacityText)
    _hideDuration:   !!t.hideDuration,
    _hideThumbnail:  !!t.hideThumbnail,
    _hideInstructor: !!t.hideInstructor,
    _hideLocation:   !!t.hideLocation,
    _hideCapacity:   !!t.hideCapacity,
    // Button overrides
    _buttonText:      t.buttonText,
    _buttonTextColor: t.buttonTextColor,
    _fun:             !!t.fun,
  } : {
    ...baseBrand,
    panelInk:     baseBrand.ink,
    panelInkSoft: baseBrand.inkSoft,
  };

  const appBg = tweaksEnabled
    ? (t.background ?? '#FFFFFF')
    : ((typeof window !== 'undefined' && window.SCH_APP_BG) || '#FAFAF7');

  const [activeDate, setActiveDate] = useStateSCH(SCH_TODAY);
  const [classFilter, setClassFilter] = useStateSCH('All classes');
  const [instructorFilter, setInstructorFilter] = useStateSCH('All instructors');
  const [locationFilter, setLocationFilter] = useStateSCH('All locations');
  const [visibleDays, setVisibleDays] = useStateSCH(INITIAL_VISIBLE_DAYS);
  const [monthModalOpen, setMonthModalOpen] = useStateSCH(false);

  // Month-view (calendar grid) state — only consulted when SCH_VIEW_SWITCHER_ENABLED.
  const viewSwitcherEnabled = typeof window !== 'undefined' && window.SCH_VIEW_SWITCHER_ENABLED;
  const [view, setView] = useStateSCH('list'); // 'list' | 'month'
  // Resolve the effective view against the hide flags (list wins ties).
  const effectiveView = (() => {
    if (view === 'list'  && t.hideListView)     return t.hideCalendarView ? 'list'  : 'month';
    if (view === 'month' && t.hideCalendarView) return t.hideListView     ? 'month' : 'list';
    return view;
  })();
  const bothHidden = !!t.hideListView && !!t.hideCalendarView;
  const showSwitcher = viewSwitcherEnabled && !bothHidden && !t.hideListView && !t.hideCalendarView;
  const [weekOffset, setWeekOffset] = useStateSCH(0);
  const [activeCalClass, setActiveCalClass] = useStateSCH(null);
  const calWeekStart = useMemoSCH(() => addDays(SCH_TODAY, weekOffset * 7), [weekOffset]);
  const calWeekEnd = useMemoSCH(() => addDays(calWeekStart, 6), [calWeekStart]);
  const calWeekLabel = useMemoSCH(() => {
    if (calWeekStart.getMonth() === calWeekEnd.getMonth()) {
      return `${MONTHS_LONG[calWeekStart.getMonth()]} ${calWeekStart.getDate()} – ${calWeekEnd.getDate()}`;
    }
    return `${MONTHS_LONG[calWeekStart.getMonth()].slice(0,3)} ${calWeekStart.getDate()} – ${MONTHS_LONG[calWeekEnd.getMonth()].slice(0,3)} ${calWeekEnd.getDate()}`;
  }, [calWeekStart, calWeekEnd]);

  // Build a 7-day strip starting from today.
  const stripDates = useMemoSCH(
    () => Array.from({ length: 7 }, (_, i) => addDays(SCH_TODAY, i)),
    [],
  );

  const allClassNames  = useMemoSCH(() => ['All classes', ...Array.from(new Set(schedule.map(c => c.name)))], [schedule]);
  const allInstructors = useMemoSCH(() => ['All instructors', ...Array.from(new Set(schedule.map(c => c.instructor)))], [schedule]);
  const allLocations   = useMemoSCH(() => ['All locations', ...Object.keys(LOCATIONS)], []);

  const matchesFilters = (c) =>
    (classFilter === 'All classes' || c.name === classFilter) &&
    (instructorFilter === 'All instructors' || c.instructor === instructorFilter) &&
    (locationFilter === 'All locations' || c.location === locationFilter) &&
    (!t.hideCancelled || !c.cancelled);

  // Drop the open calendar card if the active filters now hide it.
  useEffectSCH(() => {
    if (activeCalClass && !matchesFilters(activeCalClass)) setActiveCalClass(null);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [classFilter, instructorFilter, locationFilter]);

  // Per-day refs so date-strip clicks can scroll the right section into view.
  const dayRefs = useRefSCH({});
  const sentinelRef = useRefSCH(null);
  const lastJumpRef = useRefSCH(0);

  // Lazy-load the next batch of days when the sentinel comes into view.
  useEffectSCH(() => {
    if (!sentinelRef.current) return;
    const observer = new IntersectionObserver(([entry]) => {
      if (entry.isIntersecting) {
        setVisibleDays(c => Math.min(c + 2, TOTAL_SCH_DAYS));
      }
    }, { rootMargin: '300px' });
    observer.observe(sentinelRef.current);
    return () => observer.disconnect();
  }, [visibleDays]);

  const jumpToDay = (date) => {
    const offset = Math.round((date - SCH_TODAY) / 86400000);
    setActiveDate(date);
    lastJumpRef.current = Date.now();
    if (offset >= visibleDays) {
      setVisibleDays(Math.min(offset + 1, TOTAL_SCH_DAYS));
    }
    setTimeout(() => {
      const el = dayRefs.current[offset];
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    }, 60);
  };

  // React to a tweak-driven `startDate` — open the widget already focused
  // on a future day. Empty / invalid / out-of-range values are no-ops.
  useEffectSCH(() => {
    if (!t.startDate) return;
    const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(t.startDate);
    if (!m) return;
    const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
    if (isNaN(d.getTime())) return;
    const offset = Math.round((d - SCH_TODAY) / 86400000);
    if (offset < 0 || offset >= TOTAL_SCH_DAYS) return;
    jumpToDay(d);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [t.startDate]);

  const isToday = sameDay(activeDate, SCH_TODAY);
  const visibleDayOffsets = Array.from({ length: visibleDays }, (_, i) => i);

  // Scroll-sync the active date as the user scrolls through day sections.
  // We watch a thin band near the top of the viewport; whichever day section
  // intersects it is "active". `lastJumpRef` suppresses observer firing while
  // a click-driven smooth scroll is in flight (so the click's chosen date wins).
  useEffectSCH(() => {
    const observer = new IntersectionObserver((entries) => {
      if (Date.now() - lastJumpRef.current < 800) return;
      const intersecting = entries.filter(e => e.isIntersecting);
      if (!intersecting.length) return;
      // Pick the topmost section in the band.
      intersecting.sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      const offsetAttr = intersecting[0].target.getAttribute('data-day-offset');
      if (offsetAttr != null) {
        const date = addDays(SCH_TODAY, parseInt(offsetAttr, 10));
        setActiveDate(prev => sameDay(prev, date) ? prev : date);
      }
    }, { rootMargin: '-120px 0px -70% 0px', threshold: 0 });

    visibleDayOffsets.forEach(offset => {
      const el = dayRefs.current[offset];
      if (el) observer.observe(el);
    });
    return () => observer.disconnect();
  }, [visibleDays]);

  // Filter results across the whole loaded schedule (used for empty state).
  const totalMatching = schedule.filter(matchesFilters).length;
  const activeFilters = [
    classFilter !== 'All classes' && classFilter,
    instructorFilter !== 'All instructors' && instructorFilter,
    locationFilter !== 'All locations' && locationFilter,
  ].filter(Boolean);
  const clearAllFilters = () => {
    setClassFilter('All classes');
    setInstructorFilter('All instructors');
    setLocationFilter('All locations');
  };

  const showBrandBar = typeof window !== 'undefined' && window.SCH_SHOW_BRAND_BAR;

  return (
    <div style={{
      minHeight: '100vh',
      background: appBg,
      fontFamily: brand.bodyFont,
      color: brand.ink,
    }}>
      {showBrandBar && <SCHBrandBar brand={brand} mobile={mobile} />}
      <div style={{
        maxWidth: 960,
        margin: '0 auto',
        padding: mobile ? '20px 16px 60px' : '32px 28px 80px',
      }}>
        {/* Header row — title + sign-in + filters */}
        <div style={{
          display: 'flex', alignItems: mobile ? 'flex-start' : 'center',
          justifyContent: 'space-between', flexDirection: mobile ? 'column' : 'row',
          gap: mobile ? 14 : 12, marginBottom: mobile ? 16 : 22,
        }}>
          {!t.hideTitle && (
            <h1 style={{
              margin: 0,
              fontFamily: brand.displayFont,
              fontWeight: brand.displayWeight,
              fontSize: mobile ? 28 : 34,
              letterSpacing: '-0.02em',
              color: brand.ink,
            }}>Book a Class</h1>
          )}
          {viewSwitcherEnabled ? (
            !t.hideSignIn && (
              <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>
            )
          ) : (
            !t.hideFilters && (
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                <FilterPill brand={brand} value={classFilter} options={allClassNames} onChange={setClassFilter} />
                <FilterPill brand={brand} value={instructorFilter} options={allInstructors} onChange={setInstructorFilter} />
                <FilterPill brand={brand} value={locationFilter} options={allLocations} onChange={setLocationFilter} />
              </div>
            )
          )}
        </div>

        {viewSwitcherEnabled && (!t.hideFilters || showSwitcher) && (
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            gap: 12, marginBottom: 16, flexWrap: 'wrap',
          }}>
            {!t.hideFilters ? (
              <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
                <FilterPill brand={brand} value={classFilter} options={allClassNames} onChange={setClassFilter} />
                <FilterPill brand={brand} value={instructorFilter} options={allInstructors} onChange={setInstructorFilter} />
                <FilterPill brand={brand} value={locationFilter} options={allLocations} onChange={setLocationFilter} />
              </div>
            ) : <div />}
            {showSwitcher && <ViewSwitcher brand={brand} value={effectiveView} onChange={setView} />}
          </div>
        )}

        {/* Month-view (calendar grid) — only when SCH_VIEW_SWITCHER_ENABLED + selected */}
        {viewSwitcherEnabled && effectiveView === 'month' ? (
          <div>
            <div style={{
              display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
              gap: 6, marginBottom: 12,
            }}>
              <button
                type="button"
                onClick={() => setWeekOffset(0)}
                disabled={weekOffset === 0}
                style={{
                  background: weekOffset === 0 ? brand.accentSoft : '#F9F9F9',
                  color: weekOffset === 0 ? brand.accentDeep : brand.ink,
                  border: `1px solid ${weekOffset === 0 ? brand.accent : (brand.border || '#ECECEC')}`,
                  borderRadius: 999,
                  padding: '7px 12px',
                  fontSize: 12.5, fontWeight: 600, letterSpacing: '-0.005em',
                  cursor: weekOffset === 0 ? 'default' : 'pointer',
                  fontFamily: brand.bodyFont,
                }}>
                Today
              </button>
              <CalWeekNavBtn brand={brand} onClick={() => setWeekOffset(w => Math.max(0, w - 1))} dir="prev" disabled={weekOffset === 0} />
              <div style={{
                minWidth: 140, textAlign: 'center',
                fontSize: 13.5, fontWeight: 600, color: brand.ink,
                letterSpacing: '-0.005em',
              }}>{calWeekLabel}</div>
              <CalWeekNavBtn brand={brand} onClick={() => setWeekOffset(w => w + 1)} dir="next" disabled={weekOffset >= 1} />
            </div>
            <div style={{
              display: 'grid',
              gridTemplateColumns: activeCalClass ? 'minmax(0, 1fr) 320px' : 'minmax(0, 1fr)',
              gap: 16, alignItems: 'flex-start',
            }}>
              <CalendarGrid
                brand={brand}
                schedule={schedule.filter(matchesFilters)}
                weekStart={calWeekStart}
                activeClass={activeCalClass}
                onSelect={setActiveCalClass}
              />
              {activeCalClass && (
                <CalendarDetailPanel
                  brand={brand}
                  klass={activeCalClass}
                  onClose={() => setActiveCalClass(null)}
                  onBook={() => { /* prototype no-op */ }}
                />
              )}
            </div>
            <div style={{
              marginTop: 20,
              display: 'flex', flexWrap: 'wrap', gap: 14,
              fontSize: 11.5,
            }}>
              <CalLegend brand={brand} dot={CAL_CAP.open.dot}        label="Open" />
              <CalLegend brand={brand} dot={CAL_CAP.filling.dot}     label="Filling up" />
              <CalLegend brand={brand} dot={CAL_CAP['almost-full'].dot} label="Almost full" />
              <CalLegend brand={brand} dot={CAL_CAP.waitlist.dot}    label="Waitlist" />
            </div>
          </div>
        ) : (
        <React.Fragment>
        {/* Sticky date strip — bleeds the page bg behind so content scrolls under cleanly */}
        <div style={{
          position: 'sticky',
          top: 0,
          zIndex: 5,
          background: appBg,
          paddingTop: mobile ? 8 : 12,
          paddingBottom: mobile ? 8 : 12,
          marginTop: -8,
        }}>
          {/* Thin bar above the strip: month label + (when switcher off) sign-in */}
          <div style={{
            display: 'flex', alignItems: 'center', justifyContent: 'space-between',
            gap: 12, marginBottom: 8, padding: '0 2px',
          }}>
            <button
              type="button"
              onClick={() => setMonthModalOpen(true)}
              style={{
                background: 'transparent', border: 0, padding: 0,
                display: 'inline-flex', alignItems: 'center', gap: 6,
                fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
                fontSize: mobile ? 14 : 15,
                color: brand.ink, letterSpacing: '-0.005em',
                cursor: 'pointer',
              }}>
              {MONTHS_LONG[activeDate.getMonth()]} {activeDate.getFullYear()}
              <Caret open={false} brand={brand} />
            </button>
            {!viewSwitcherEnabled && !t.hideSignIn && (
              <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>
          {!t.hideDateStrip && (
            <DateStrip
              brand={brand} mobile={mobile}
              dates={stripDates}
              activeDate={activeDate}
              onSelect={jumpToDay}
              schedule={schedule}
              onJumpToday={() => jumpToDay(SCH_TODAY)}
              showJumpToday={!isToday}
            />
          )}
        </div>

        {totalMatching === 0 ? (
          <FriendlyEmpty
            brand={brand}
            activeFilters={activeFilters}
            onClearAll={clearAllFilters}
          />
        ) : (
          /* Day sections — empty days are skipped so filtered views stay tight */
          <div style={{ marginTop: 16, display: 'flex', flexDirection: 'column', gap: 28 }}>
            {visibleDayOffsets.map(offset => {
              const date = addDays(SCH_TODAY, offset);
              const dayClasses = schedule.filter(c => sameDay(c.date, date) && matchesFilters(c));
              if (dayClasses.length === 0) {
                // Still attach a ref so jump-to-day can scroll near the right spot.
                return (
                  <div
                    key={offset}
                    ref={el => { dayRefs.current[offset] = el; }}
                    data-day-offset={offset}
                    style={{ height: 0, margin: 0, padding: 0 }}
                  />
                );
              }
              const delta = dayDelta(date);
              const long = `${WEEKDAYS_LONG[date.getDay()]}, ${MONTHS_LONG[date.getMonth()]} ${date.getDate()}`;
              const heading = delta ? `${delta}, ${long}` : long;
              return (
                <section
                  key={offset}
                  ref={el => { dayRefs.current[offset] = el; }}
                  data-day-offset={offset}
                  style={{ scrollMarginTop: 96 }}
                >
                  <div style={{
                    display: 'flex', alignItems: 'baseline', justifyContent: 'space-between',
                    gap: 12, flexWrap: 'wrap', marginBottom: 12,
                  }}>
                    <div style={{
                      fontFamily: brand.displayFont, fontWeight: 700,
                      fontSize: mobile ? 17 : 20,
                      color: brand.ink, letterSpacing: '-0.01em',
                    }}>{heading}</div>
                  </div>
                  <div style={{
                    display: 'flex', flexDirection: 'column',
                    gap: (typeof window !== 'undefined' && window.SCH_ROW_STYLE === 'lines') ? 0 : 10,
                  }}>
                    {dayClasses.map(c => (
                      <ClassRow
                        key={c.id}
                        brand={brand}
                        c={c}
                        mobile={mobile}
                        onFilterInstructor={(name) => setInstructorFilter(name)}
                      />
                    ))}
                  </div>
                </section>
              );
            })}
            {visibleDays < TOTAL_SCH_DAYS && (
              <div ref={sentinelRef} style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                <SkeletonRow brand={brand} mobile={mobile} />
                <SkeletonRow brand={brand} mobile={mobile} />
              </div>
            )}
          </div>
        )}
        </React.Fragment>
        )}
        <PoweredByZipper />
      </div>

      {monthModalOpen && (
        <MonthCalendarModal
          brand={brand}
          mobile={mobile}
          schedule={schedule}
          activeDate={activeDate}
          onSelect={(date) => { jumpToDay(date); setMonthModalOpen(false); }}
          onClose={() => setMonthModalOpen(false)}
        />
      )}

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

      {/* Skeleton pulse + book-button confetti keyframes — injected once. */}
      <style>{`
        @keyframes sch-pulse { 0%, 100% { opacity: 0.4; } 50% { opacity: 0.7; } }
        @keyframes sch-confetti {
          0%   { transform: translate(0, 0) rotate(0deg) scale(.4); opacity: 0; }
          12%  { opacity: 1; transform: translate(calc(var(--sch-dx) * .15), calc(var(--sch-dy) * .15)) rotate(calc(var(--sch-rot) * .12)) scale(1); }
          70%  { opacity: 1; }
          100% { transform: translate(var(--sch-dx), calc(var(--sch-dy) + 80px)) rotate(var(--sch-rot)) scale(.9); opacity: 0; }
        }
      `}</style>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Brand bar — same chrome as the booking flow.
// ────────────────────────────────────────────────────────────────
function SCHBrandBar({ brand, mobile }) {
  return (
    <div style={{
      background: '#fff',
      borderBottom: `1px solid ${brand.border}`,
      padding: mobile ? '14px 16px' : '20px 28px',
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      gap: 12,
    }}>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
        <div style={{
          width: mobile ? 32 : 36, height: mobile ? 32 : 36, borderRadius: 8,
          background: brand.accent, color: '#fff',
          display: 'grid', placeItems: 'center',
          fontFamily: brand.displayFont, fontWeight: 800, fontSize: mobile ? 14 : 16,
        }}>{brand.initials}</div>
        <div style={{
          fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
          fontSize: mobile ? 15 : 18, color: brand.ink,
          letterSpacing: '-0.01em',
        }}>{brand.name}</div>
      </div>
      <button style={{
        background: 'transparent', border: 0, padding: '8px 12px',
        fontSize: 13, fontWeight: 700, color: brand.ink,
        cursor: 'pointer',
      }}>Sign in</button>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Filter pill — text label + chevron, opens a tiny native menu.
// ────────────────────────────────────────────────────────────────
function FilterPill({ brand, value, options, onChange }) {
  const isAll = value.startsWith('All');
  return (
    <div style={{ position: 'relative' }}>
      <select
        value={value}
        onChange={e => onChange(e.target.value)}
        style={{
          appearance: 'none',
          WebkitAppearance: 'none',
          MozAppearance: 'none',
          background: isAll ? brand.surface : brand.accentSoft,
          color: isAll ? brand.panelInk : brand.accentDeep,
          border: `1px solid ${isAll ? brand.border : brand.accent}`,
          borderRadius: brand._radius != null ? brand._radius : 999,
          padding: '8px 32px 8px 14px',
          fontSize: 13, fontWeight: 600,
          fontFamily: brand.bodyFont,
          cursor: 'pointer', outline: 'none',
        }}>
        {options.map(o => <option key={o} value={o}>{o}</option>)}
      </select>
      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke={isAll ? brand.panelInkSoft : brand.accentDeep} strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" style={{
        position: 'absolute', right: 12, top: '50%',
        transform: 'translateY(-50%)', pointerEvents: 'none',
      }}>
        <polyline points="6 9 12 15 18 9"/>
      </svg>
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Date strip — 7 days, day name + date number; active gets accent fill.
// Tiny dot under each date if there are classes that day.
// ────────────────────────────────────────────────────────────────
function DateStrip({ brand, mobile, dates, activeDate, onSelect, schedule, onJumpToday, showJumpToday }) {
  const dotFor = (d) => schedule.some(c => sameDay(c.date, d));
  return (
    <div style={{
      background: brand.surface,
      border: `1px solid ${brand.border}`,
      borderRadius: brand._radius != null ? brand._radius : 14,
      padding: mobile ? '10px 6px' : '14px 8px',
      display: 'flex', alignItems: 'center', gap: 4,
      position: 'relative',
    }}>
      <button onClick={() => onSelect(addDays(activeDate, -1))} style={arrowBtn(brand)} aria-label="Previous day">
        <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 ? 2 : 4,
      }}>
        {dates.map(d => {
          const isActive = sameDay(d, activeDate);
          const isToday = sameDay(d, SCH_TODAY);
          return (
            <button
              key={d.toISOString()}
              onClick={() => onSelect(d)}
              style={{
                background: 'transparent',
                border: 0, borderRadius: 0, padding: 0,
                cursor: 'pointer',
                display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
                position: 'relative',
              }}>
              <div style={{
                display: 'flex', flexDirection: 'column', alignItems: 'center',
                padding: mobile ? '6px 8px' : '7px 10px',
                gap: 2,
                borderRadius: brand._radius != null ? brand._radius : 12,
                minWidth: mobile ? 40 : 44,
                background: isActive ? brand.accent : 'transparent',
                transition: 'background .15s ease, color .15s ease',
              }}>
                <div style={{
                  fontSize: 10.5, fontWeight: 700,
                  letterSpacing: '0.1em', textTransform: 'uppercase',
                  color: isActive ? 'rgba(255,255,255,0.9)' : brand.panelInkSoft,
                }}>{WEEKDAYS[d.getDay()]}</div>
                <div style={{
                  fontFamily: brand.displayFont, fontWeight: 700,
                  fontSize: mobile ? 15 : 16,
                  color: isActive ? '#fff' : brand.panelInk,
                }}>{d.getDate()}</div>
              </div>
              <div style={{
                width: 4, height: 4, borderRadius: 999,
                background: dotFor(d) ? (isToday ? brand.accent : brand.border) : 'transparent',
              }}></div>
            </button>
          );
        })}
      </div>

      <button onClick={() => onSelect(addDays(activeDate, 1))} style={arrowBtn(brand)} aria-label="Next day">
        <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>

      {showJumpToday && (
        <button
          onClick={onJumpToday}
          style={{
            position: 'absolute',
            top: -14, right: 10,
            background: '#fff',
            border: `1px solid ${brand.border}`,
            borderRadius: 999,
            padding: '4px 12px',
            fontSize: 11, fontWeight: 700,
            color: brand.accent,
            letterSpacing: '0.06em', textTransform: 'uppercase',
            cursor: 'pointer',
            boxShadow: '0 2px 6px rgba(0,0,0,0.05)',
          }}>
          Today
        </button>
      )}
    </div>
  );
}
function arrowBtn(brand) {
  return {
    flex: '0 0 auto',
    width: 32, height: 32, borderRadius: 999,
    background: 'transparent', color: brand.panelInkSoft,
    border: 0, cursor: 'pointer',
    display: 'grid', placeItems: 'center',
  };
}

// ────────────────────────────────────────────────────────────────
// Class row — time on the left, instructor avatar + class info center,
// capacity + book on right. Class name and instructor name are clickable;
// either expands an accordion below the row with a description.
// ────────────────────────────────────────────────────────────────
function ClassRow({ brand, c, mobile, onFilterInstructor }) {
  const [expanded, setExpanded] = useStateSCH(null); // 'class' | 'instructor' | null
  const isWaitlist = c.capacityState === 'waitlist';
  const isCancelled = !!c.cancelled;
  const buttonLabel = isWaitlist ? 'Join waitlist' : 'Book class';
  const instructor = INSTRUCTORS[c.instructor];
  const description = CLASS_DESCRIPTIONS[c.name];

  const toggle = (kind) => setExpanded(e => e === kind ? null : kind);

  const rowStyle = (typeof window !== 'undefined' && window.SCH_ROW_STYLE) || 'card';
  const containerStyle = rowStyle === 'lines'
    ? {
        background: 'transparent',
        border: 0,
        borderBottom: `1px solid ${brand.border}`,
        borderRadius: 0,
        padding: mobile ? '14px 0' : '16px 2px',
      }
    : {
        background: '#fff',
        border: `1px solid ${brand.border}`,
        borderRadius: 14,
        padding: mobile ? '14px 16px' : '16px 20px',
        transition: 'border-color .15s ease',
      };

  const hideThumb = !!brand._hideThumbnail;
  const hideDur   = !!brand._hideDuration;
  const hideInst  = !!brand._hideInstructor;
  const hideLoc   = !!brand._hideLocation;
  return (
    <div style={containerStyle}>
      <div style={{
        display: 'grid',
        gridTemplateColumns: mobile
          ? (hideThumb ? '1fr' : 'auto 1fr')
          : (hideThumb ? '108px 1fr auto' : '108px 48px 1fr auto'),
        gap: mobile ? 12 : 16,
        alignItems: 'center',
      }}>
        {/* Time + duration */}
        <div style={{
          display: 'flex',
          flexDirection: mobile ? 'row' : 'column',
          alignItems: mobile ? 'baseline' : 'flex-start',
          gridColumn: mobile && !hideThumb ? '1 / 3' : 'auto',
          gap: mobile ? 8 : 0,
        }}>
          <div style={{
            fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
            fontSize: mobile ? 18 : 20,
            color: isCancelled ? brand.inkSoft : brand.ink,
            letterSpacing: '-0.005em',
            textDecoration: isCancelled ? 'line-through' : 'none',
          }}>{c.time}</div>
          {!hideDur && (
            <div style={{
              fontSize: 12.5, color: brand.inkSoft,
              marginTop: mobile ? 0 : 2,
            }}>{c.duration}</div>
          )}
        </div>

        {/* Thumbnail — instructor avatar OR class image depending on tweak */}
        {!hideThumb && (
        <button
          type="button"
          onClick={() => toggle(brand._thumbnail === 'class' ? 'class' : 'instructor')}
          aria-label={brand._thumbnail === 'class' ? `About ${c.name}` : `About ${c.instructor}`}
          style={{
            width: mobile ? 44 : 48, height: mobile ? 44 : 48,
            borderRadius: brand._radius != null ? brand._radius : 999,
            overflow: 'hidden',
            padding: 0, border: 0, background: brand.accentSoft,
            cursor: 'pointer',
            flex: '0 0 auto',
            outline: expanded === 'instructor' ? `2px solid ${brand.accent}` : 'none',
            outlineOffset: 2,
          }}>
          {(() => {
            const showClass = brand._thumbnail === 'class';
            const src = showClass ? CLASS_IMAGES[c.name] : instructor?.photo;
            if (src) return <img src={src} alt={showClass ? c.name : c.instructor} style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />;
            const fallback = showClass ? c.name : c.instructor;
            return (
              <div style={{ width: '100%', height: '100%', display: 'grid', placeItems: 'center', color: brand.accentDeep, fontWeight: 700, fontSize: 14, fontFamily: brand.displayFont }}>
                {fallback.split(' ').map(s => s[0]).slice(0, 2).join('')}
              </div>
            );
          })()}
        </button>
        )}

        {/* Class info */}
        <div style={{ minWidth: 0 }}>
          <button
            type="button"
            onClick={() => toggle('class')}
            style={{
              background: 'transparent', border: 0, padding: 0,
              fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
              fontSize: mobile ? 16 : 17,
              color: isCancelled ? brand.inkSoft : brand.ink,
              letterSpacing: '-0.005em',
              lineHeight: 1.3,
              cursor: 'pointer',
              textAlign: 'left',
              display: 'inline-flex', alignItems: 'center', gap: 6,
              textDecoration: isCancelled ? 'line-through' : 'none',
            }}>
            <span>{c.name}</span>
            <Caret open={expanded === 'class'} brand={brand} />
          </button>
          {(!hideInst || !hideLoc) && (
            <div style={{
              fontSize: 13, color: brand.inkSoft, marginTop: 2,
              lineHeight: 1.5,
            }}>
              {!hideInst && (
                <button
                  type="button"
                  onClick={() => toggle('instructor')}
                  style={{
                    background: 'transparent', border: 0, padding: 0,
                    color: brand.inkSoft, fontSize: 13, fontFamily: brand.bodyFont,
                    cursor: 'pointer',
                    borderBottom: `1px dashed ${brand.border}`,
                  }}>
                  {c.instructor}
                </button>
              )}
              {!hideInst && !hideLoc && ' · '}
              {!hideLoc && (
                <button
                  type="button"
                  onClick={() => toggle('location')}
                  style={{
                    background: 'transparent', border: 0, padding: 0,
                    color: brand.inkSoft, fontSize: 13, fontFamily: brand.bodyFont,
                    cursor: 'pointer',
                    borderBottom: `1px dashed ${brand.border}`,
                  }}>
                  {c.location}
                </button>
              )}
            </div>
          )}
          {mobile && (
            <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
              {isCancelled ? <CancelledNote brand={brand} /> : <CapacityText brand={brand} c={c} />}
              {!isCancelled && <BookButton brand={brand} c={c} label={buttonLabel} />}
            </div>
          )}
        </div>

        {/* Capacity + Book (desktop only) */}
        {!mobile && (
          <div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
            {isCancelled ? <CancelledNote brand={brand} reason={c.cancelReason} /> : <CapacityText brand={brand} c={c} />}
            {!isCancelled && <BookButton brand={brand} c={c} label={buttonLabel} />}
          </div>
        )}
      </div>

      {/* Accordion */}
      {expanded && (
        <div style={{
          marginTop: 14, paddingTop: 14,
          borderTop: `1px solid ${brand.border}`,
        }}>
          {expanded === 'class' && (
            <div>
              <div style={{
                fontSize: 11, fontWeight: 700,
                letterSpacing: '0.1em', textTransform: 'uppercase',
                color: brand.inkSoft, marginBottom: 6,
              }}>About this class</div>
              <div style={{ fontSize: 14, color: brand.ink, lineHeight: 1.55 }}>
                {description || 'Description coming soon.'}
              </div>
            </div>
          )}
          {expanded === 'instructor' && instructor && (
            <div>
              <div style={{
                fontSize: 11, fontWeight: 700,
                letterSpacing: '0.1em', textTransform: 'uppercase',
                color: brand.inkSoft, marginBottom: 6,
              }}>About {c.instructor}</div>
              <div style={{ fontSize: 14, color: brand.ink, lineHeight: 1.55 }}>
                {instructor.bio}
              </div>
              {onFilterInstructor && (
                <button
                  type="button"
                  onClick={() => { onFilterInstructor(c.instructor); setExpanded(null); }}
                  style={{
                    display: 'inline-flex', alignItems: 'center', gap: 6,
                    marginTop: 12,
                    background: 'transparent', border: 0, padding: 0,
                    fontSize: 12.5, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase',
                    color: brand.accent, fontFamily: brand.bodyFont,
                    cursor: 'pointer',
                  }}>
                  See all of {c.instructor.split(' ')[0]}'s classes
                  <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                    <line x1="5" y1="12" x2="19" y2="12"/>
                    <polyline points="12 5 19 12 12 19"/>
                  </svg>
                </button>
              )}
            </div>
          )}
          {expanded === 'location' && LOCATIONS[c.location] && (
            <div>
              <div style={{
                fontSize: 11, fontWeight: 700,
                letterSpacing: '0.1em', textTransform: 'uppercase',
                color: brand.inkSoft, marginBottom: 6,
              }}>{c.location}</div>
              <div style={{ fontSize: 14, color: brand.ink, lineHeight: 1.55 }}>
                {LOCATIONS[c.location].address}
              </div>
              <div style={{ fontSize: 13, color: brand.inkSoft, lineHeight: 1.55, marginTop: 4 }}>
                {LOCATIONS[c.location].notes}
              </div>
              <a
                href={`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(LOCATIONS[c.location].address)}`}
                target="_blank" rel="noopener noreferrer"
                style={{
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                  marginTop: 10,
                  fontSize: 12.5, fontWeight: 700, letterSpacing: '0.06em', textTransform: 'uppercase',
                  color: brand.accent, textDecoration: 'none',
                }}>
                Get directions
                <svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                  <line x1="5" y1="12" x2="19" y2="12"/>
                  <polyline points="12 5 19 12 12 19"/>
                </svg>
              </a>
            </div>
          )}
        </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 SchedulingTweaks({ t, setTweak }) {
  const {
    TweaksPanel: Panel, TweakSection: Section, TweakColor: Color,
    TweakSlider: Slider, TweakSelect: Select, TweakRadio: Radio,
    TweakButton: Button, TweakToggle: Toggle, TweakText: Text, TweakRow: Row,
  } = window;
  const resetAll = () => {
    const defaults = (typeof window !== 'undefined' && window.SCH_TWEAK_DEFAULTS) || {};
    Object.keys(defaults).forEach(k => setTweak(k, defaults[k]));
  };
  // The shared TweaksPanel stays closed until it receives an
  // `__activate_edit_mode` window message (it's normally driven by a host
  // editor). For this standalone demo we send the message ourselves so the
  // panel is visible by default; the user can still dismiss it.
  useEffectSCH(() => {
    if (!Panel) return;
    const id = window.setTimeout(() => {
      window.postMessage({ type: '__activate_edit_mode' }, '*');
    }, 0);
    return () => window.clearTimeout(id);
  }, [Panel]);
  if (!Panel) return null; // tweaks-panel.jsx not loaded
  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="Corner radius" value={t.radius} min={0} max={28} step={1} unit="px"
              onChange={(v) => setTweak('radius', v)} />

      <Section label="Class thumbnail" />
      <Radio  label="Image"
              value={t.thumbnail}
              options={['instructor', 'class']}
              onChange={(v) => setTweak('thumbnail', v)} />

      <Section label="Capacity display" />
      <Select label="Mode"
              value={t.capacityMode}
              options={[
                { value: 'all',       label: 'Show all remaining' },
                { value: 'threshold', label: 'Show only if low' },
                { value: 'hidden',    label: 'Do not show' },
              ]}
              onChange={(v) => setTweak('capacityMode', v)} />
      {t.capacityMode === 'threshold' && (
        <Slider label="Threshold" value={t.capacityThreshold} min={1} max={20} step={1} unit=" or fewer"
                onChange={(v) => setTweak('capacityThreshold', v)} />
      )}

      <Section label="Hide" />
      <Toggle label="Class Schedule title"  value={!!t.hideTitle}      onChange={(v) => setTweak('hideTitle', v)} />
      <Toggle label="Sign in link"          value={!!t.hideSignIn}     onChange={(v) => setTweak('hideSignIn', v)} />
      <Toggle label="Filters"               value={!!t.hideFilters}    onChange={(v) => setTweak('hideFilters', v)} />
      <Toggle label="Week calendar strip"   value={!!t.hideDateStrip}  onChange={(v) => setTweak('hideDateStrip', v)} />
      <Toggle label="List view option"      value={!!t.hideListView}     onChange={(v) => setTweak('hideListView', v)} />
      <Toggle label="Calendar view option"  value={!!t.hideCalendarView} onChange={(v) => setTweak('hideCalendarView', v)} />
      <Toggle label="Class duration"        value={!!t.hideDuration}   onChange={(v) => setTweak('hideDuration', v)} />
      <Toggle label="Class thumbnail image" value={!!t.hideThumbnail}  onChange={(v) => setTweak('hideThumbnail', v)} />
      <Toggle label="Instructor name"       value={!!t.hideInstructor} onChange={(v) => setTweak('hideInstructor', v)} />
      <Toggle label="Location name"         value={!!t.hideLocation}   onChange={(v) => setTweak('hideLocation', v)} />
      <Toggle label="Capacity count"        value={!!t.hideCapacity}   onChange={(v) => setTweak('hideCapacity', v)} />
      <Toggle label="All cancelled classes" value={!!t.hideCancelled}  onChange={(v) => setTweak('hideCancelled', v)} />

      <Section label="Other" />
      <Row label="Start at future date">
        <input
          type="date"
          className="twk-field"
          value={t.startDate || ''}
          onChange={(e) => setTweak('startDate', e.target.value)}
        />
      </Row>
      <Text label="Button text"
            value={t.buttonText || ''} placeholder="Book class"
            onChange={(v) => setTweak('buttonText', v)} />

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

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

function MonthCalendarModal({ brand, mobile, schedule, activeDate, onSelect, onClose }) {
  const [viewMonth, setViewMonth] = useStateSCH(
    new Date(activeDate.getFullYear(), activeDate.getMonth(), 1)
  );

  // Days in view: 6-week grid starting from the Sunday on/before the 1st.
  const grid = (() => {
    const first = new Date(viewMonth.getFullYear(), viewMonth.getMonth(), 1);
    const start = new Date(first);
    start.setDate(first.getDate() - first.getDay()); // back to Sunday
    return Array.from({ length: 42 }, (_, i) => {
      const d = new Date(start);
      d.setDate(start.getDate() + i);
      return d;
    });
  })();

  // Bookable window: TODAY through TOTAL_SCH_DAYS - 1
  const lastBookable = addDays(SCH_TODAY, TOTAL_SCH_DAYS - 1);
  const isBookable = (d) =>
    d.getTime() >= new Date(SCH_TODAY.getFullYear(), SCH_TODAY.getMonth(), SCH_TODAY.getDate()).getTime() &&
    d.getTime() <= new Date(lastBookable.getFullYear(), lastBookable.getMonth(), lastBookable.getDate()).getTime();
  const hasClasses = (d) => schedule.some(c => sameDay(c.date, d));

  const monthLabel = `${MONTHS_LONG[viewMonth.getMonth()]} ${viewMonth.getFullYear()}`;
  const stepMonth = (delta) =>
    setViewMonth(new Date(viewMonth.getFullYear(), viewMonth.getMonth() + delta, 1));

  // Close on ESC
  useEffectSCH(() => {
    const onKey = (e) => { if (e.key === 'Escape') onClose(); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [onClose]);

  return (
    <div
      role="dialog" aria-modal="true" aria-label="Month view"
      onClick={onClose}
      style={{
        position: 'fixed', inset: 0,
        background: 'rgba(20, 22, 18, 0.45)',
        backdropFilter: 'blur(2px)',
        display: 'grid', placeItems: 'center',
        padding: 16, zIndex: 50,
      }}>
      <div
        onClick={(e) => e.stopPropagation()}
        style={{
          background: brand.surface,
          border: `1px solid ${brand.border}`,
          borderRadius: 0,
          width: 'min(440px, 100%)',
          padding: mobile ? '18px 16px 12px' : '22px 22px 16px',
          fontFamily: brand.bodyFont,
          color: brand.panelInk,
        }}>
        {/* Header: prev / month label / next, plus close */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 14 }}>
          <button onClick={() => stepMonth(-1)} aria-label="Previous month"
            style={arrowBtn(brand)}>
            <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, textAlign: 'center',
            fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
            fontSize: mobile ? 18 : 20,
            color: brand.panelInk, letterSpacing: '-0.005em',
          }}>{monthLabel}</div>
          <button onClick={() => stepMonth(1)} aria-label="Next month"
            style={arrowBtn(brand)}>
            <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>
          <button onClick={onClose} aria-label="Close"
            style={{ ...arrowBtn(brand), color: brand.panelInkSoft }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
          </button>
        </div>

        {/* Weekday header */}
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
          fontSize: 10.5, fontWeight: 700, letterSpacing: '0.12em',
          textTransform: 'uppercase', color: brand.panelInkSoft,
          textAlign: 'center', marginBottom: 6,
        }}>
          {WEEKDAYS.map(w => <div key={w}>{w}</div>)}
        </div>

        {/* Day grid */}
        <div style={{
          display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)',
          gap: 4,
        }}>
          {grid.map((d, i) => {
            const inMonth = d.getMonth() === viewMonth.getMonth();
            const bookable = isBookable(d);
            const isActive = sameDay(d, activeDate);
            const isTdy = sameDay(d, SCH_TODAY);
            const hasC = hasClasses(d);
            return (
              <button
                key={i}
                onClick={() => bookable && onSelect(d)}
                disabled={!bookable}
                style={{
                  position: 'relative',
                  aspectRatio: '1 / 1',
                  background: isActive ? brand.accent : 'transparent',
                  color: isActive ? '#fff'
                       : !bookable ? 'rgba(0,0,0,0.18)'
                       : inMonth ? brand.panelInk
                       : brand.panelInkSoft,
                  border: isTdy && !isActive ? `1px solid ${brand.accent}` : `1px solid transparent`,
                  borderRadius: 0,
                  padding: 0,
                  fontFamily: brand.bodyFont,
                  fontSize: 13, fontWeight: 600,
                  cursor: bookable ? 'pointer' : 'default',
                  outline: 'none',
                }}>
                <span>{d.getDate()}</span>
                {hasC && bookable && (
                  <span style={{
                    position: 'absolute', left: '50%', bottom: 4,
                    transform: 'translateX(-50%)',
                    width: 4, height: 4, borderRadius: 999,
                    background: isActive ? '#fff' : brand.accent,
                  }} />
                )}
              </button>
            );
          })}
        </div>

        {/* Legend */}
        <div style={{
          display: 'flex', alignItems: 'center', gap: 14,
          marginTop: 14, paddingTop: 12, borderTop: `1px solid ${brand.border}`,
          fontSize: 11, color: brand.panelInkSoft,
          letterSpacing: '0.04em', textTransform: 'uppercase', fontWeight: 600,
        }}>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 4, height: 4, borderRadius: 999, background: brand.accent }} /> Classes
          </span>
          <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6 }}>
            <span style={{ width: 10, height: 10, border: `1px solid ${brand.accent}` }} /> Today
          </span>
        </div>
      </div>
    </div>
  );
}

function CancelledNote({ brand }) {
  return (
    <span style={{
      fontSize: 12.5, fontWeight: 700,
      color: brand.ink,
      letterSpacing: '0.04em', textTransform: 'uppercase',
      whiteSpace: 'nowrap',
    }}>Cancelled</span>
  );
}

function CapacityText({ brand, c }) {
  if (brand._hideCapacity) return null;
  if (c.capacityState === 'waitlist') {
    const n = c.spotsLeft; // repurposed: # on waitlist
    return (
      <span style={{
        fontSize: 12.5, fontWeight: 500,
        color: brand.inkSoft,
        whiteSpace: 'nowrap',
      }}>{n > 0 ? `${n} waiting` : 'Waitlist only'}</span>
    );
  }
  const mode = brand._capacityMode || 'all';
  if (mode === 'hidden') return null;
  if (mode === 'threshold' && c.spotsLeft > (brand._capacityThreshold ?? 5)) return null;
  return (
    <span style={{
      fontSize: 12.5, fontWeight: 600,
      color: brand.ink,
      whiteSpace: 'nowrap',
    }}>{c.spotsLeft} left</span>
  );
}

function BookButton({ brand, c, label }) {
  const isWaitlist = c.capacityState === 'waitlist';
  // Studios can override the primary CTA copy. Waitlist label stays — it
  // describes a different action (joining a queue vs booking a spot).
  const text = isWaitlist ? label : (brand._buttonText || label);
  const textColor = brand._buttonTextColor || '#fff';
  const fun = !!brand._fun;
  const [bursts, setBursts] = useStateSCH([]);
  const burstSeq = useRefSCH(0);

  const handleClick = () => {
    if (fun) {
      const id = ++burstSeq.current;
      // Bright, mixed palette so confetti pops on any background. Mix in the
      // brand accent so the burst still feels on-brand.
      const palette = [brand.accent, '#FFC857', '#FF6B6B', '#4ECDC4',
                       '#A06CD5', '#FFD166', '#06D6A0', '#EF476F'];
      const N = 22;
      const particles = Array.from({ length: N }, (_, i) => {
        // Bias spread upward — 220° fan from -200° to +20° (pointing up-ish)
        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);
    }
    window.setTimeout(
      () => alert(`${text}: ${c.name} · ${c.time}`),
      fun ? 650 : 0,
    );
  };

  return (
    <span style={{ position: 'relative', display: 'inline-block' }}>
      <button
        onClick={handleClick}
        style={{
          background: isWaitlist ? '#fff' : brand.accent,
          color: isWaitlist ? brand.accent : textColor,
          border: isWaitlist ? `1.5px solid ${brand.accent}` : 0,
          borderRadius: brand._radius != null ? brand._radius : 10,
          padding: '10px 18px',
          fontSize: 13.5, fontWeight: 700,
          fontFamily: brand.displayFont,
          letterSpacing: '-0.005em',
          cursor: 'pointer',
          whiteSpace: 'nowrap',
        }}>
        {text}
      </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,
                '--sch-dx': `${p.dx}px`,
                '--sch-dy': `${p.dy}px`,
                '--sch-rot': `${p.rot}deg`,
                animation: `sch-confetti ${p.duration}ms cubic-bezier(.22,.61,.36,1) ${p.delay}ms forwards`,
              }} />
            ))
          )}
        </span>
      )}
    </span>
  );
}

// ────────────────────────────────────────────────────────────────
// Empty state — friendly, on-brand.
// ────────────────────────────────────────────────────────────────
// ────────────────────────────────────────────────────────────────
// Skeleton row + keyframe — used while lazy-loading more days.
// ────────────────────────────────────────────────────────────────
function SkeletonRow({ brand, mobile }) {
  const cell = (w, h) => ({
    width: w, height: h, borderRadius: 6,
    background: brand.border, opacity: 0.55,
    animation: 'sch-pulse 1.4s ease-in-out infinite',
  });
  return (
    <div style={{
      background: '#fff', border: `1px solid ${brand.border}`, borderRadius: 14,
      padding: mobile ? '14px 16px' : '16px 20px',
      display: 'grid',
      gridTemplateColumns: mobile ? 'auto 1fr' : '108px 48px 1fr auto',
      gap: mobile ? 12 : 16,
      alignItems: 'center',
    }}>
      <div style={{
        gridColumn: mobile ? '1 / 3' : 'auto',
        display: 'flex', flexDirection: mobile ? 'row' : 'column',
        gap: mobile ? 8 : 6,
      }}>
        <div style={cell(56, 18)} />
        <div style={cell(40, 12)} />
      </div>
      <div style={{ width: mobile ? 44 : 48, height: mobile ? 44 : 48, borderRadius: 999, background: brand.border, opacity: 0.55, animation: 'sch-pulse 1.4s ease-in-out infinite' }} />
      <div>
        <div style={cell(160, 16)} />
        <div style={{ height: 6 }} />
        <div style={cell(220, 12)} />
      </div>
      {!mobile && <div style={cell(110, 36)} />}
    </div>
  );
}

function EmptyState({ brand, subtle }) {
  return (
    <div style={{
      background: subtle ? 'transparent' : '#fff',
      border: `1px dashed ${brand.border}`,
      borderRadius: 14,
      padding: subtle ? '20px 18px' : '34px 22px',
      textAlign: 'center',
    }}>
      <div style={{
        fontFamily: brand.displayFont, fontWeight: 700,
        fontSize: subtle ? 14 : 17, color: brand.ink, marginBottom: subtle ? 0 : 4,
      }}>No classes match these filters</div>
      {!subtle && (
        <div style={{ fontSize: 13.5, color: brand.inkSoft }}>
          Try clearing a filter or jumping to another day.
        </div>
      )}
    </div>
  );
}

// Friendly empty state shown when *no* class anywhere in the loaded
// schedule matches the active filters.
function FriendlyEmpty({ brand, activeFilters, onClearAll }) {
  return (
    <div style={{
      background: '#fff',
      border: `1px solid ${brand.border}`,
      borderRadius: 14,
      padding: '40px 28px',
      textAlign: 'center',
      marginTop: 8,
    }}>
      <div style={{
        width: 56, height: 56, borderRadius: 999,
        background: brand.accentSoft, color: brand.accentDeep,
        display: 'inline-grid', placeItems: 'center',
        marginBottom: 14,
      }}>
        <svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="11" cy="11" r="7"/>
          <line x1="21" y1="21" x2="16.65" y2="16.65"/>
        </svg>
      </div>
      <div style={{
        fontFamily: brand.displayFont, fontWeight: 700,
        fontSize: 22, color: brand.ink,
        letterSpacing: '-0.01em', marginBottom: 6,
      }}>Nothing on the schedule for that yet</div>
      <div style={{
        fontSize: 14, color: brand.inkSoft, lineHeight: 1.55,
        maxWidth: 420, margin: '0 auto',
      }}>
        Try a different instructor, a different location, or clear your filters to see everything we've got coming up.
      </div>
      {activeFilters.length > 0 && (
        <button
          onClick={onClearAll}
          style={{
            marginTop: 18,
            background: brand.accent, color: '#fff',
            border: 0, borderRadius: 10,
            padding: '10px 20px',
            fontSize: 13.5, fontWeight: 700,
            fontFamily: brand.displayFont,
            letterSpacing: '-0.005em',
            cursor: 'pointer',
          }}>
          Clear all filters
        </button>
      )}
    </div>
  );
}

// ────────────────────────────────────────────────────────────────
// Month-view calendar grid + detail panel (opt-in via SCH_VIEW_SWITCHER_ENABLED)
// ────────────────────────────────────────────────────────────────
function CalendarGrid({ brand, schedule, weekStart, activeClass, onSelect }) {
  const days = useMemoSCH(() => Array.from({ length: 7 }, (_, i) => addDays(weekStart, i)), [weekStart]);
  const totalHours = CAL_GRID_END_HOUR - CAL_GRID_START_HOUR;
  const gridHeight = totalHours * CAL_HOUR_PX;
  const surface  = brand.surface || '#FFFFFF';
  const panel    = '#F9F9F9';
  const edge     = brand.border || '#ECECEC';
  const todayWash = `${brand.accent}10`;
  const gridLine  = edge;
  const gridLineSoft = `${edge}80`;

  return (
    <div style={{
      background: surface,
      border: `1px solid ${edge}`,
      borderRadius: 12, overflow: 'hidden',
    }}>
      {/* Day header */}
      <div style={{
        display: 'grid',
        gridTemplateColumns: `${CAL_TIME_COL_PX}px repeat(7, 1fr)`,
        borderBottom: `1px solid ${edge}`,
        background: panel,
      }}>
        <div />
        {days.map((d, i) => {
          const isToday = sameDay(d, SCH_TODAY);
          return (
            <div key={i} style={{
              padding: '10px 8px 12px',
              textAlign: 'center',
              borderLeft: i === 0 ? 'none' : `1px solid ${edge}`,
              background: isToday ? todayWash : 'transparent',
            }}>
              <div style={{
                fontSize: 10.5, fontWeight: 700, letterSpacing: '0.1em',
                textTransform: 'uppercase',
                color: isToday ? brand.accent : (brand.inkSoft || '#8C9690'),
              }}>{WEEKDAYS[d.getDay()]}</div>
              <div style={{
                marginTop: 4,
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                width: 28, height: 28, borderRadius: 999,
                background: isToday ? brand.accent : 'transparent',
                color: isToday ? '#FFFFFF' : brand.ink,
                fontSize: 15, fontWeight: 600,
                fontFamily: brand.displayFont,
              }}>{d.getDate()}</div>
            </div>
          );
        })}
      </div>

      {/* Body */}
      <div style={{
        display: 'grid',
        gridTemplateColumns: `${CAL_TIME_COL_PX}px repeat(7, 1fr)`,
        position: 'relative',
      }}>
        {/* Time gutter */}
        <div style={{ position: 'relative', height: gridHeight, background: surface }}>
          {Array.from({ length: totalHours }, (_, i) => CAL_GRID_START_HOUR + i).map((h, i) => (
            <div key={h} style={{
              position: 'absolute',
              top: i * CAL_HOUR_PX, right: 8,
              transform: 'translateY(-50%)',
              fontSize: 10.5, fontWeight: 600, letterSpacing: '0.04em',
              color: brand.inkSoft || '#8C9690',
              textTransform: 'uppercase',
              display: i === 0 ? 'none' : 'block',
            }}>{calFormatHour(h)}</div>
          ))}
        </div>

        {/* Day columns */}
        {days.map((d, dayIdx) => {
          const isToday = sameDay(d, SCH_TODAY);
          const dayClasses = schedule.filter(c => sameDay(c.date, d));
          return (
            <div key={dayIdx} style={{
              position: 'relative',
              height: gridHeight,
              borderLeft: `1px solid ${edge}`,
              background: isToday ? todayWash : 'transparent',
            }}>
              {Array.from({ length: totalHours }, (_, i) => (
                <div key={i} style={{
                  position: 'absolute', left: 0, right: 0, top: i * CAL_HOUR_PX,
                  borderTop: i === 0 ? 'none' : `1px solid ${i % 2 === 0 ? gridLine : gridLineSoft}`,
                }} />
              ))}
              {isToday && <CalNowLine brand={brand} />}
              {dayClasses.map(c => {
                const startMin = calParseTimeToMin(c.time);
                const durMin = calParseDurationMin(c.duration);
                const top = ((startMin - CAL_GRID_START_HOUR * 60) / 60) * CAL_HOUR_PX;
                const height = Math.max(28, (durMin / 60) * CAL_HOUR_PX - 2);
                const tint = calTintFor(c.name, brand.accent);
                const isActive = activeClass && activeClass.id === c.id;
                return (
                  <button
                    key={c.id}
                    type="button"
                    onClick={() => onSelect(c)}
                    style={{
                      position: 'absolute',
                      left: 4, right: 4, top, height,
                      textAlign: 'left',
                      background: c.cancelled ? '#FAFAFA' : '#FFFFFF',
                      border: `1px solid ${c.cancelled ? '#D5D5D5' : tint}`,
                      borderRadius: 8,
                      padding: '5px 8px',
                      cursor: 'pointer',
                      overflow: 'hidden',
                      boxShadow: isActive ? `0 4px 14px ${tint}33` : '0 1px 0 rgba(20,30,25,0.02)',
                      transition: 'box-shadow .12s ease, border-color .12s ease, transform .12s ease',
                      fontFamily: brand.bodyFont,
                      color: brand.ink,
                      opacity: c.cancelled ? 0.7 : 1,
                    }}
                    onMouseEnter={e => { e.currentTarget.style.boxShadow = `0 4px 14px ${tint}33`; }}
                    onMouseLeave={e => {
                      if (!isActive) e.currentTarget.style.boxShadow = '0 1px 0 rgba(20,30,25,0.02)';
                    }}>
                    <div style={{
                      fontSize: 11, fontWeight: 600, color: brand.inkSoft || '#516056',
                      letterSpacing: '-0.005em',
                      textDecoration: c.cancelled ? 'line-through' : 'none',
                    }}>{c.time}</div>
                    <div style={{
                      fontSize: 12.5, fontWeight: 600, color: brand.ink,
                      letterSpacing: '-0.005em',
                      marginTop: 1,
                      whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                      textDecoration: c.cancelled ? 'line-through' : 'none',
                    }}>{c.name}</div>
                  </button>
                );
              })}
            </div>
          );
        })}
      </div>
    </div>
  );
}

function CalNowLine({ brand }) {
  // Pin "now" at 9:42 AM so the marker is visible during morning landings (same as Calendar prototype).
  const NOW_MIN = 9 * 60 + 42;
  const top = ((NOW_MIN - CAL_GRID_START_HOUR * 60) / 60) * CAL_HOUR_PX;
  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, top, zIndex: 2,
      pointerEvents: 'none',
    }}>
      <div style={{
        position: 'absolute', left: -5, top: -5,
        width: 10, height: 10, borderRadius: '50%',
        background: brand.accent,
        boxShadow: `0 0 0 3px ${brand.accent}2E`,
      }} />
      <div style={{ height: 1.5, background: brand.accent }} />
    </div>
  );
}

function CalendarDetailPanel({ brand, klass, onClose, onBook }) {
  const tint = calTintFor(klass.name, brand.accent);
  const dateStr = `${WEEKDAYS_LONG[klass.date.getDay()]}, ${MONTHS_LONG[klass.date.getMonth()]} ${klass.date.getDate()}`;
  const edge = brand.border || '#ECECEC';
  return (
    <div style={{
      background: '#FFFFFF',
      border: `1px solid ${edge}`,
      borderRadius: 12,
      padding: 16,
      display: 'flex', flexDirection: 'column', gap: 14,
      position: 'sticky', top: 16,
    }}>
      <div style={{ display: 'flex', gap: 10, alignItems: 'flex-start' }}>
        <div style={{ width: 4, alignSelf: 'stretch', background: tint, borderRadius: 2 }} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{
            fontSize: 11, fontWeight: 700, letterSpacing: '0.08em',
            textTransform: 'uppercase', color: brand.inkSoft || '#8C9690',
          }}>{dateStr} · {klass.time}</div>
          <div style={{
            marginTop: 4,
            fontFamily: brand.displayFont, fontWeight: brand.displayWeight,
            fontSize: 22, color: brand.ink, letterSpacing: '-0.01em',
            textDecoration: klass.cancelled ? 'line-through' : 'none',
          }}>{klass.name}</div>
        </div>
        <button
          type="button" onClick={onClose} aria-label="Close"
          style={{ background: 'transparent', border: 0, cursor: 'pointer', color: brand.inkSoft || '#8C9690', padding: 4, lineHeight: 0 }}>
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>
          </svg>
        </button>
      </div>

      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, minmax(0, 1fr))', gap: 10 }}>
        <CalDetail brand={brand} label="Instructor" value={klass.instructor} />
        <CalDetail brand={brand} label="Duration" value={klass.duration} />
        <CalDetail brand={brand} label="Location" value={klass.location} />
        <CalDetail
          brand={brand}
          label="Capacity"
          value={calCapText(klass)}
          dot={!klass.cancelled ? CAL_CAP[klass.capacityState]?.dot : undefined}
        />
      </div>

      {klass.cancelled ? (
        <div style={{
          padding: '10px 12px', borderRadius: 8,
          background: '#FBEEEC', color: '#9F4438',
          fontSize: 13, fontWeight: 500,
        }}>
          This class was cancelled — {klass.cancelReason || 'no reason provided'}.
        </div>
      ) : (
        <button
          type="button"
          onClick={() => onBook(klass)}
          style={{
            background: brand.accent, color: '#FFFFFF',
            border: 0, borderRadius: 8,
            padding: '12px 16px', cursor: 'pointer',
            fontSize: 14, fontWeight: 600, letterSpacing: '-0.005em',
            fontFamily: brand.bodyFont,
          }}>
          {klass.capacityState === 'waitlist' ? 'Join waitlist' : 'Book class'}
        </button>
      )}
    </div>
  );
}

function CalDetail({ brand, label, value, dot }) {
  return (
    <div style={{ background: '#F9F9F9', borderRadius: 8, padding: '8px 10px' }}>
      <div style={{
        fontSize: 10, fontWeight: 700, letterSpacing: '0.08em',
        textTransform: 'uppercase', color: brand.inkSoft || '#8C9690',
      }}>{label}</div>
      <div style={{
        marginTop: 3,
        fontSize: 13, fontWeight: 500, color: brand.ink,
        display: 'inline-flex', alignItems: 'center', gap: 6,
      }}>
        {dot && <span style={{ width: 7, height: 7, borderRadius: '50%', background: dot }} />}
        {value}
      </div>
    </div>
  );
}

function CalWeekNavBtn({ brand, onClick, dir, disabled }) {
  return (
    <button
      type="button" onClick={onClick} disabled={disabled}
      aria-label={dir === 'prev' ? 'Previous week' : 'Next week'}
      style={{
        width: 32, height: 32,
        background: '#F9F9F9',
        border: `1px solid ${brand.border || '#ECECEC'}`,
        borderRadius: 999,
        display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
        cursor: disabled ? 'not-allowed' : 'pointer',
        color: disabled ? (brand.inkSoft || '#8C9690') : brand.ink,
        opacity: disabled ? 0.5 : 1,
      }}>
      <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
        {dir === 'prev' ? <polyline points="15 18 9 12 15 6"/> : <polyline points="9 18 15 12 9 6"/>}
      </svg>
    </button>
  );
}

function ViewSwitcher({ brand, value, onChange }) {
  const opts = [
    { id: 'list',  label: 'List View' },
    { id: 'month', label: 'Month View' },
  ];
  return (
    <div style={{
      display: 'inline-flex', alignItems: 'center',
      background: '#F9F9F9',
      border: `1px solid ${brand.border || '#ECECEC'}`,
      borderRadius: 999, padding: 3, gap: 2,
    }}>
      {opts.map(o => {
        const active = value === o.id;
        return (
          <button
            key={o.id}
            type="button"
            onClick={() => onChange(o.id)}
            style={{
              appearance: 'none', cursor: 'pointer',
              background: active ? '#FFFFFF' : 'transparent',
              border: active ? `1px solid ${brand.accent}` : '1px solid transparent',
              color: active ? brand.accentDeep : brand.ink,
              padding: '6px 12px', borderRadius: 999,
              fontSize: 12.5, fontWeight: 600, letterSpacing: '-0.005em',
              fontFamily: brand.bodyFont,
              boxShadow: active ? '0 1px 0 rgba(20,30,25,0.04)' : 'none',
            }}>
            {o.label}
          </button>
        );
      })}
    </div>
  );
}

function CalLegend({ dot, label, brand }) {
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, color: brand.inkSoft || '#516056' }}>
      <span style={{ width: 8, height: 8, borderRadius: '50%', background: dot }} />
      {label}
    </span>
  );
}

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>
  );
}

window.ScheduleApp = ScheduleApp;
const _schRoot = document.getElementById('root');
if (_schRoot && !_schRoot.dataset.framed) {
  ReactDOM.createRoot(_schRoot).render(<ScheduleApp />);
}
