// Main app screens (Home, Progress, Settings) — share the same theme +
// accent props so we can host them inside the iOS frame, an Android
// frame, or a desktop web layout.

// ── Profile switcher ─────────────────────────────────────────────────
// Shown inside Settings below the account banner when the user is signed
// in. Reads the roster + active id from window.BFProfiles, subscribes to
// changes, and supports switching / renaming / creating / archiving.
//
// Gating: any action that grows the roster calls
// BFEntitlement.requireOrPaywall('profiles.multi'), so free / plus users
// see the Plus/Family paywall instead of a silent failure.
function BFProfileSwitcher({ theme, accentH }) {
  const [roster,   setRoster]   = React.useState(() => (window.BFProfiles ? window.BFProfiles.list() : []));
  const [activeId, setActiveId] = React.useState(() => {
    const c = window.BFProfiles && window.BFProfiles.current();
    return c ? c.id : null;
  });

  React.useEffect(() => {
    if (!window.BFProfiles) return;
    // Kick off a fresh load in case auth just resolved.
    window.BFProfiles.load();
    const off = window.BFProfiles.subscribe((snap) => {
      setRoster(snap.list.filter((p) => !p.archived_at));
      setActiveId(snap.activeId);
    });
    return off;
  }, []);

  // PIN status. BFKids.hasPin() is sync but only valid after hydration;
  // we poll twice (now + 150ms) to pick up the hydrated value from storage
  // without blocking render. refreshPin() is called after set/clear
  // actions to bring the badge in sync.
  const [pinSet, setPinSet] = React.useState(false);
  const refreshPin = React.useCallback(() => {
    if (window.BFKids) setPinSet(window.BFKids.hasPin());
  }, []);
  React.useEffect(() => {
    refreshPin();
    const t = setTimeout(refreshPin, 150);
    return () => clearTimeout(t);
  }, [refreshPin]);

  const activeProfile = roster.find((p) => p.id === activeId) || null;
  const canMulti = !!(window.BFEntitlement && window.BFEntitlement.can && window.BFEntitlement.can('profiles.multi'));
  const atCap    = roster.length >= 6;
  const hasKidProfile = roster.some((p) => p.kind === 'kid');

  // PIN management — visible only when the family has at least one kid
  // profile so the row doesn't clutter adult-only setups. Setting a PIN
  // while one already exists silently overwrites it after the two-step
  // confirm flow; clearing requires a confirm to prevent thumb-slips.
  const onSetOrChangePin = async () => {
    if (!window.BFKids) return;
    const ok = await window.BFKids.promptSetPin({
      title: pinSet ? 'Change parent PIN' : 'Set parent PIN',
      body:  pinSet
        ? 'Enter a new 4-digit PIN, then confirm it.'
        : 'Choose a 4-digit PIN. Kids will need this to exit Kids mode or open Settings.',
    });
    refreshPin();
  };
  const onClearPin = async () => {
    if (!window.BFKids || !window.BFDialog) return;
    const ok = await window.BFDialog.confirm({
      title: 'Clear parent PIN?',
      body:  'Kids will be able to exit Kids mode and open Settings without a PIN.',
      confirmLabel: 'Clear', cancelLabel: 'Keep',
      danger: true,
    });
    if (!ok) return;
    await window.BFKids.clearPin();
    refreshPin();
    if (window.BFToast) BFToast.info('PIN cleared', 'No parent PIN is set.');
  };

  // Switching profiles. Kid ↔ kid is free (siblings share the device). Kid
  // → adult requires the parent PIN — the whole point of Kids mode is that
  // a child can't tap their way back into the adult library or Settings.
  // Adult → kid asks for a quick confirm so the parent knows what they're
  // entering (Kids mode = playful UI, limited library, PIN needed to exit).
  const onPick = async (id) => {
    if (!window.BFProfiles) return;
    if (id === activeId) return;
    const from = activeProfile;
    const to   = roster.find((p) => p.id === id);
    const kidEntering = to && to.kind === 'kid' && (!from || from.kind !== 'kid');
    const needsPin    = from && from.kind === 'kid' && to && to.kind !== 'kid';
    if (kidEntering && window.BFDialog) {
      const ok = await window.BFDialog.confirm({
        title: 'Switch to Kids mode for ' + to.name + '?',
        body:  'You\u2019ll see the playful UI, a shorter pattern library, and a balloon visualizer. You\u2019ll need the parent PIN to switch back.',
        confirmLabel: 'Yes, switch',
        cancelLabel:  'Keep here',
      });
      if (!ok) return;
    }
    if (needsPin && window.BFKids) {
      const ok = await window.BFKids.requirePin({ reason: 'exit' });
      if (!ok) return;
    }
    await window.BFProfiles.setActive(id);
  };

  // Add a profile. For Family-tier users we ask for both a name and a
  // kind (adult | kid) — kind='kid' auto-enters Kids mode when switched to.
  // The two-step prompt keeps the UI lightweight (two BFDialog rounds vs a
  // custom form sheet); can be upgraded later.
  const onAdd = async () => {
    if (!canMulti) {
      // Paywall fires; if the user taps "Yes, enable it", onGrant re-runs
      // onAdd so they go straight into the name prompt without needing a
      // second tap on "+ Add profile".
      if (window.BFEntitlement) {
        window.BFEntitlement.requireOrPaywall('profiles.multi', {
          reason:  'profile-add',
          onGrant: () => { setTimeout(onAdd, 0); },
        });
      }
      return;
    }
    if (atCap) {
      if (window.BFToast) BFToast.info('Profile cap reached', 'Family plan supports up to 6 profiles.');
      return;
    }
    if (!window.BFDialog) return;
    const name = await window.BFDialog.prompt({
      title: 'Add profile', placeholder: 'Name',
      confirmLabel: 'Next', cancelLabel: 'Cancel',
    });
    if (!name) return;
    // Ask whether this is an adult or kid profile. confirm() returns a
    // boolean; repurpose it as a two-option picker where true = kid.
    const isKid = window.BFDialog.confirm
      ? await window.BFDialog.confirm({
          title: 'Who is this profile for?',
          body:  'Kid profiles use the Kids mode skin (balloon visualizer, gentler patterns). Adults see the full library.',
          confirmLabel: 'Kid',
          cancelLabel:  'Adult',
        })
      : false;
    // Avatar picker. Kid + adult share the dialog but see different emoji
    // sets so a girl doesn't end up with the generic meditator and a boy
    // doesn't end up with a unicorn by default. Cancelling the picker
    // keeps the profile without an avatar — render falls back to a kind-
    // default emoji downstream.
    const AVATAR_OPTIONS_KID = [
      { value: '\uD83E\uDDD2', label: 'Kid' },     // 🧒
      { value: '\uD83D\uDC67', label: 'Girl' },     // 👧
      { value: '\uD83D\uDC66', label: 'Boy' },      // 👦
      { value: '\uD83D\uDD84', label: 'Unicorn' },  // 🦄
      { value: '\uD83D\uDC09', label: 'Dragon' },   // 🐉
      { value: '\uD83C\uDF88', label: 'Balloon' },  // 🎈
      { value: '\u2B50',        label: 'Star' },    // ⭐
      { value: '\uD83C\uDF38', label: 'Flower' },   // 🌸
    ];
    const AVATAR_OPTIONS_ADULT = [
      { value: '\uD83E\uDDD8', label: 'Calm' },       // 🧘
      { value: '\uD83E\uDDD8\u200D\u2640\uFE0F', label: 'Calm (F)' },  // 🧘‍♀️
      { value: '\uD83E\uDDD8\u200D\u2642\uFE0F', label: 'Calm (M)' },  // 🧘‍♂️
      { value: '\uD83C\uDF3F', label: 'Leaf' },     // 🌿
      { value: '\uD83C\uDF0A', label: 'Wave' },     // 🌊
      { value: '\uD83C\uDF11', label: 'Moon' },     // 🌑
      { value: '\u2600\uFE0F',  label: 'Sun' },     // ☀️
      { value: '\uD83C\uDFD4\uFE0F', label: 'Peak' }, // 🏔️
    ];
    const avatar = window.BFDialog.choose
      ? await window.BFDialog.choose({
          title: 'Pick an avatar',
          body:  'Tap one that feels right. You can change it later.',
          options: isKid ? AVATAR_OPTIONS_KID : AVATAR_OPTIONS_ADULT,
          cancelLabel: 'Skip',
        })
      : null;
    try {
      await window.BFProfiles.create({
        name:   name,
        kind:   isKid ? 'kid' : 'adult',
        avatar: avatar || null,
      });
      if (window.BFToast) BFToast.success('Profile added', name);
      // First-run PIN setup: as soon as there's at least one kid profile
      // and no PIN configured yet, prompt the parent to set one. The PIN
      // gate on kid → adult switches assumes a PIN exists; without this
      // nudge a family could create a kid profile, never set a PIN, and
      // then the exit prompt would fall back to setup at exit time.
      if (isKid && window.BFKids && !window.BFKids.hasPin()) {
        await window.BFKids.promptSetPin({
          title: 'Set a parent PIN',
          body:  'This PIN keeps kids from tapping their way out of Kids mode or into Settings. You can reset it by signing out and back in.',
        });
        refreshPin();
      }
    } catch (e) {
      if (e && e.message !== 'locked' && window.BFToast) BFToast.error('Add failed', e.message || String(e));
    }
  };

  const onRename = async () => {
    if (!activeProfile || !window.BFDialog) return;
    const next = await window.BFDialog.prompt({
      title: 'Rename profile', defaultValue: activeProfile.name,
      placeholder: 'Name', confirmLabel: 'Save', cancelLabel: 'Cancel',
    });
    if (!next || next === activeProfile.name) return;
    try {
      await window.BFProfiles.rename(activeProfile.id, next);
    } catch (e) {
      if (window.BFToast) BFToast.error('Rename failed', e.message || String(e));
    }
  };

  // Remove flow. Two-step: first ask "Archive or Erase permanently?" so the
  // user picks their level of finality, then route to the appropriate
  // confirm + backend call. Archive is the safe default (history stays,
  // recoverable via support). Erase hard-deletes the row + cascaded
  // history; chosen only by users who actively want the data gone.
  const onArchive = async () => {
    if (!activeProfile || !window.BFDialog) return;
    if (activeProfile.is_primary) {
      if (window.BFToast) BFToast.info('Primary profile', 'The primary profile can\u2019t be removed.');
      return;
    }
    const mode = window.BFDialog.choose
      ? await window.BFDialog.choose({
          title: 'Remove ' + activeProfile.name + '?',
          body:  'Archive keeps their history (recoverable via support). Erase deletes the profile and its sessions forever.',
          options: [
            { value: 'archive', emoji: '\uD83D\uDCE6', label: 'Archive' },    // 📦
            { value: 'erase',   emoji: '\uD83D\uDDD1\uFE0F', label: 'Erase' }, // 🗑️
          ],
          cancelLabel: 'Keep',
        })
      : 'archive';
    if (!mode) return;
    if (mode === 'archive') {
      const ok = await window.BFDialog.confirm({
        title: 'Archive ' + activeProfile.name + '?',
        body:  'Their history stays saved. You can restore later via support.',
        confirmLabel: 'Archive', cancelLabel: 'Keep', danger: true,
      });
      if (!ok) return;
      try {
        await window.BFProfiles.archive(activeProfile.id);
      } catch (e) {
        if (window.BFToast) BFToast.error('Archive failed', e.message || String(e));
      }
    } else if (mode === 'erase') {
      // Two-step confirm for irreversible action — first ask, then require
      // them to type the name (or just a second confirm for speed).
      const ok = await window.BFDialog.confirm({
        title: 'Erase ' + activeProfile.name + ' permanently?',
        body:  'This deletes their profile and every session they logged. It cannot be undone.',
        confirmLabel: 'Erase forever', cancelLabel: 'Keep', danger: true,
      });
      if (!ok) return;
      try {
        if (typeof window.BFProfiles.destroy === 'function') {
          await window.BFProfiles.destroy(activeProfile.id);
        } else {
          // Older BFProfiles build without destroy — fall back to archive so
          // the user gets *something* rather than a silent failure.
          await window.BFProfiles.archive(activeProfile.id);
        }
        if (window.BFToast) BFToast.success('Erased', activeProfile.name + ' and their history are gone.');
      } catch (e) {
        if (window.BFToast) BFToast.error('Erase failed', e.message || String(e));
      }
    }
  };

  if (!roster.length) return null;

  const kindBadge = (kind) => (kind === 'kid' ? '🧒' : '🌿');

  return (
    <>
      <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>
        Profiles
      </div>
      <BFCard theme={theme} style={{ padding: 14, marginBottom: 22 }}>
        <div style={{
          display: 'flex', gap: 6, overflowX: 'auto', paddingBottom: 8,
          marginBottom: 10, scrollbarWidth: 'thin',
        }}>
          {roster.map((p) => {
            const active = p.id === activeId;
            return (
              <button
                key={p.id} onClick={() => onPick(p.id)}
                style={{
                  flexShrink: 0,
                  padding: '8px 14px', borderRadius: 999, cursor: 'pointer',
                  background: active ? bfAccentSolid(accentH) : theme.cardSoft,
                  color:      active ? bfAccentSolidFg() : theme.fgMuted,
                  border: 'none',
                  fontFamily: BF_FONTS.sans, fontSize: 12, fontWeight: 500,
                  display: 'inline-flex', alignItems: 'center', gap: 6,
                }}
              >
                <span style={{ fontSize: 13 }}>{p.avatar || kindBadge(p.kind)}</span>
                <span>{p.name}</span>
                {p.is_primary && (
                  <span style={{
                    fontSize: 9, letterSpacing: 0.5,
                    padding: '1px 5px', borderRadius: 5,
                    background: active ? 'rgba(0,0,0,0.15)' : 'rgba(255,255,255,0.08)',
                    color: active ? 'rgba(0,0,0,0.6)' : theme.fgFaint,
                    textTransform: 'uppercase',
                  }}>Primary</span>
                )}
              </button>
            );
          })}
          <button onClick={onAdd} style={{
            flexShrink: 0,
            padding: '8px 14px', borderRadius: 999, cursor: 'pointer',
            background: 'transparent', color: theme.fgMuted,
            border: `1px dashed ${theme.line}`,
            fontFamily: BF_FONTS.sans, fontSize: 12, fontWeight: 500,
          }}>+ Add profile</button>
        </div>

        {activeProfile && (
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={onRename} style={{
              padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
              background: 'transparent', color: theme.fgMuted,
              border: `1px solid ${theme.line}`,
              fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 500,
            }}>Rename</button>
            {!activeProfile.is_primary && (
              <button onClick={onArchive} style={{
                padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
                background: 'transparent', color: bfDangerPill(theme).fg,
                border: `1px solid ${bfDangerPill(theme).border}`,
                fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 500,
              }}>Remove</button>
            )}
          </div>
        )}
        {hasKidProfile && window.BFKids && (
          <div style={{
            marginTop: 12, paddingTop: 12,
            borderTop: `1px solid ${theme.line}`,
            display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap',
            fontFamily: BF_FONTS.sans,
          }}>
            <div style={{
              fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3,
              flex: 1, minWidth: 0,
            }}>
              {pinSet
                ? 'Parent PIN is set. Required to exit Kids mode or open Settings.'
                : 'No parent PIN. Kids can freely exit Kids mode and open Settings.'}
            </div>
            <button onClick={onSetOrChangePin} style={{
              padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
              background: 'transparent', color: theme.fgMuted,
              border: `1px solid ${theme.line}`,
              fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 500,
            }}>{pinSet ? 'Change PIN' : 'Set PIN'}</button>
            {pinSet && (
              <button onClick={onClearPin} style={{
                padding: '6px 12px', borderRadius: 999, cursor: 'pointer',
                background: 'transparent', color: bfDangerPill(theme).fg,
                border: `1px solid ${bfDangerPill(theme).border}`,
                fontFamily: BF_FONTS.sans, fontSize: 11, fontWeight: 500,
              }}>Clear</button>
            )}
          </div>
        )}
        {!canMulti && (
          <div style={{
            marginTop: 10, fontSize: 11, color: theme.fgFaint, lineHeight: 1.5,
            fontFamily: BF_FONTS.mono, letterSpacing: 0.2,
          }}>
            Family plan unlocks up to 6 profiles — kids mode, shared streaks, family dashboard.
          </div>
        )}
      </BFCard>
    </>
  );
}

// ── Favorites storage ────────────────────────────────────────────────
// Per-profile set of pattern ids. Each profile keeps its own taste — a kid
// profile shouldn't surface adult-favorited patterns and vice versa.
// Storage key shape: `favorites.<profileId>`. Falls back to a guest bucket
// (`favorites`) when no profile is active (signed-out / pre-load).
const BF_FAVS_LEGACY_KEY = 'favorites';
function bfFavsKey() {
  try {
    const p = window.BFProfiles && window.BFProfiles.current && window.BFProfiles.current();
    if (p && p.id) return 'favorites.' + p.id;
  } catch (e) {}
  return BF_FAVS_LEGACY_KEY;
}
async function bfFavsGet() {
  const key = bfFavsKey();
  const cur = await BFStorage.getJSON(key);
  if (cur) return cur;
  // One-time migration: legacy `favorites` (pre-per-profile) belongs to
  // the primary adult. Other profiles start empty.
  if (key !== BF_FAVS_LEGACY_KEY) {
    try {
      const p = window.BFProfiles && window.BFProfiles.current && window.BFProfiles.current();
      if (p && p.is_primary && p.kind === 'adult') {
        const legacy = await BFStorage.getJSON(BF_FAVS_LEGACY_KEY);
        if (legacy && Array.isArray(legacy) && legacy.length) {
          await BFStorage.setJSON(key, legacy);
          await BFStorage.setJSON(BF_FAVS_LEGACY_KEY, []);
          return legacy;
        }
      }
    } catch (e) {}
  }
  return [];
}
async function bfFavsToggle(id) {
  const key = bfFavsKey();
  const cur = (await BFStorage.getJSON(key)) || [];
  const next = cur.includes(id) ? cur.filter(x => x !== id) : [...cur, id];
  await BFStorage.setJSON(key, next);
  return next;
}

function useFavorites() {
  const [favs, setFavs] = React.useState([]);
  React.useEffect(() => {
    let cancelled = false;
    const reload = () => bfFavsGet().then((v) => { if (!cancelled) setFavs(v); });
    reload();
    let unsub = null;
    if (window.BFProfiles && window.BFProfiles.subscribe) {
      unsub = window.BFProfiles.subscribe(reload);
    }
    return () => { cancelled = true; if (unsub) try { unsub(); } catch (e) {} };
  }, []);
  const toggle = async (id) => { setFavs(await bfFavsToggle(id)); };
  return { favs, toggle, isFav: (id) => favs.includes(id) };
}

// ── My patterns (user-saved named customs) ──────────────────────────
const BF_MY_PATTERNS_KEY = 'my-patterns';
// Migrate legacy progression field names from the editor's UI schema
// (startMin / capSec) to the engine's schema (startAfterSec / maxAdd).
// Early saves wrote UI names straight through; the engine only reads
// startAfterSec + maxAdd, so those customs were silently running with
// defaults (bump at t=60, cap at +4s). This rewrites on read once so
// existing users get the config they actually configured without a
// re-save. Patterns already in engine schema are untouched.
function bfMigrateProgression(p) {
  const g = p && p.progression;
  if (!g || g.enabled !== true) return p;
  if ('startAfterSec' in g && 'maxAdd' in g) return p;
  const base = Math.max(p.inhale || 0, p.exhale || 0);
  const next = {
    enabled: true,
    stepEvery: Math.max(10, g.stepEvery || 60),
    stepBy:    Math.max(1,  g.stepBy    || 1),
    startAfterSec: ('startAfterSec' in g)
      ? Math.max(0, g.startAfterSec)
      : Math.max(0, (g.startMin || 0) * 60),
    maxAdd: ('maxAdd' in g)
      ? Math.max(0, g.maxAdd)
      : Math.max(0, (g.capSec || 0) - base),
  };
  return { ...p, progression: next };
}
async function bfMyPatternsGet() {
  const raw = (await BFStorage.getJSON(BF_MY_PATTERNS_KEY)) || [];
  let migrated = false;
  const out = raw.map((p) => {
    const m = bfMigrateProgression(p);
    if (m !== p) migrated = true;
    return m;
  });
  if (migrated) {
    try { await BFStorage.setJSON(BF_MY_PATTERNS_KEY, out); } catch (e) {}
  }
  return out;
}
async function bfMyPatternsAdd(p) {
  const cur = await bfMyPatternsGet();
  const next = [...cur, p];
  await BFStorage.setJSON(BF_MY_PATTERNS_KEY, next);
  return next;
}
async function bfMyPatternsRemove(id) {
  const cur = await bfMyPatternsGet();
  const next = cur.filter(x => x.id !== id);
  await BFStorage.setJSON(BF_MY_PATTERNS_KEY, next);
  return next;
}
function useMyPatterns() {
  const [list, setList] = React.useState([]);
  React.useEffect(() => { bfMyPatternsGet().then(setList); }, []);
  return {
    list,
    add: async (p) => { setList(await bfMyPatternsAdd(p)); },
    remove: async (id) => { setList(await bfMyPatternsRemove(id)); },
    reload: async () => { setList(await bfMyPatternsGet()); },
  };
}

// ── Home ─────────────────────────────────────────────────────────────
function BFHome({ theme, accentH, onStart, onOpenPreset, onOpenFamily, onOpenFamilyDash, session, haptics, goLibrary, activeProfile = null }) {
  // Filter the master pattern list by the active profile's audience. Kid
  // profiles see 'all' + 'kids' patterns; adult profiles see 'all' + 'adult'.
  // Falls back to BF_PATTERNS (= 'all' + 'adult' + 'kids') when BFProfiles
  // hasn't loaded yet.
  const visiblePatterns = (typeof bfPatternsForProfile === 'function')
    ? bfPatternsForProfile(activeProfile)
    : BF_PATTERNS;
  const kidsMode = !!(activeProfile && activeProfile.kind === 'kid');
  const hour = new Date().getHours();
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const greeting = hour < 5 ? t('home.good_evening')
                 : hour < 12 ? t('home.good_morning')
                 : hour < 18 ? t('home.good_afternoon')
                 : t('home.good_evening');
  const { favs } = useFavorites();
  const mine = useMyPatterns();
  // Featured (For today) is always the kind default:
  //   - Kid:   Balloon (signature Kids-mode pattern, 4·2·4·2).
  //   - Adult: Box     (4·4·4·4, predictable staple).
  // Favorites surface separately as a strip below — they don't override the
  // For today card so the daily anchor stays predictable.
  const featured = kidsMode
    ? (visiblePatterns.find(p => p.id === 'balloon') || visiblePatterns[0])
    : (visiblePatterns.find(p => p.id === 'box')     || visiblePatterns[0]);
  const favPatterns = [
    ...visiblePatterns.filter(p => favs.includes(p.id) && (!featured || p.id !== featured.id)),
    ...mine.list.filter(p => favs.includes(p.id) && (!featured || p.id !== featured.id)),
  ];
  const tapHaptic = () => { if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light(); };
  // Re-render when the profile roster changes (e.g. family-tier user lands on
  // Home before BFProfiles finishes its async load). Without this, the Family
  // session tile stays hidden even after the roster populates, because we read
  // window.BFProfiles.list() inline from render.
  const [, _profileTick] = React.useState(0);
  React.useEffect(() => {
    if (!window.BFProfiles || !window.BFProfiles.subscribe) return undefined;
    return window.BFProfiles.subscribe(() => _profileTick((n) => n + 1));
  }, []);
  // Same for entitlement — tier can upgrade mid-session (trial → family) and
  // we want the tile to appear without a full reload.
  const [, _entTick] = React.useState(0);
  React.useEffect(() => {
    if (!window.BFEntitlement || !window.BFEntitlement.subscribe) return undefined;
    return window.BFEntitlement.subscribe(() => _entTick((n) => n + 1));
  }, []);
  // Home profile switcher. Needed specifically so kids can exit Kids mode
  // without having to find Settings (kid UI hides the settings tab). Also
  // useful for adult households where the device changes hands. Shown when
  // the roster has 2+ active profiles; tapping opens a BFDialog.choose with
  // the roster, and the kid→adult edge re-uses the same PIN gate as the
  // Settings switcher.
  const roster = (window.BFProfiles && typeof window.BFProfiles.list === 'function')
    ? window.BFProfiles.list() : [];
  const showSwitcher = roster.length >= 2;
  const onSwitchProfile = async () => {
    if (!window.BFDialog || !window.BFDialog.choose || !window.BFProfiles) return;
    const options = roster.map((p) => ({
      value: p.id,
      emoji: p.avatar || (p.kind === 'kid' ? '\uD83E\uDDD2' : '\uD83C\uDF3F'),
      label: p.name + (p.id === (activeProfile && activeProfile.id) ? ' \u2713' : ''),
    }));
    const pickedId = await window.BFDialog.choose({
      title: 'Switch profile',
      body:  kidsMode
        ? 'Switching to a parent profile needs the PIN.'
        : 'Pick who\u2019s using the app right now.',
      options: options,
      cancelLabel: 'Cancel',
    });
    if (!pickedId || pickedId === (activeProfile && activeProfile.id)) return;
    const to = roster.find((p) => p.id === pickedId);
    // Same gating rules as the Settings switcher: kid→adult needs PIN;
    // adult→kid asks for confirm so the parent knows they're entering
    // Kids mode.
    const kidEntering = to && to.kind === 'kid' && (!activeProfile || activeProfile.kind !== 'kid');
    const needsPin    = activeProfile && activeProfile.kind === 'kid' && to && to.kind !== 'kid';
    if (kidEntering && window.BFDialog) {
      const ok = await window.BFDialog.confirm({
        title: 'Switch to Kids mode for ' + to.name + '?',
        body:  'Playful UI, shorter pattern library, balloon visualizer. Parent PIN needed to switch back.',
        confirmLabel: 'Yes, switch', cancelLabel: 'Keep here',
      });
      if (!ok) return;
    }
    if (needsPin && window.BFKids) {
      const ok = await window.BFKids.requirePin({ reason: 'exit' });
      if (!ok) return;
    }
    await window.BFProfiles.setActive(pickedId);
  };

  return (
    <div style={{ padding: '64px 20px calc(110px + env(safe-area-inset-bottom, 0px))', color: theme.fg, fontFamily: BF_FONTS.sans }}>
      {/* Greeting + profile chip. Chip sits top-right so a kid can always
          see their way out of Kids mode without hunting for Settings. */}
      <div style={{ marginBottom: 28, display: 'flex', alignItems: 'flex-start', gap: 12 }}>
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13, color: theme.fgFaint, letterSpacing: 0.3 }}>
            {greeting}
            {activeProfile && activeProfile.name ? ', ' + activeProfile.name : ''}
            {kidsMode ? ' \u2B50' : ''}
          </div>
          <div style={{
            fontFamily: BF_FONTS.serif, fontSize: 38, lineHeight: 1.05, fontStyle: 'italic',
            color: theme.fg, marginTop: 6, letterSpacing: -0.5, textWrap: 'pretty', whiteSpace: 'pre-line',
          }}>{t('home.tagline')}</div>
        </div>
        {showSwitcher && (
          <button onClick={onSwitchProfile} aria-label="Switch profile" style={{
            flexShrink: 0, width: 40, height: 40, borderRadius: 999, cursor: 'pointer',
            background: theme.cardSoft, color: theme.fgMuted,
            border: `1px solid ${theme.line}`,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
            fontSize: 18, lineHeight: 1,
          }}>
            {(activeProfile && activeProfile.avatar) ||
              (kidsMode ? '\uD83E\uDDD2' : '\uD83C\uDF3F')}
          </button>
        )}
      </div>

      {/* Streak + quick stat */}
      <div style={{ display: 'flex', gap: 10, marginBottom: 22 }}>
        <BFCard theme={theme} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px' }}>
          <div style={{ width: 32, height: 32, borderRadius: 999, background: bfAccentTint(theme, 45, 0.15), color: bfAccentText(theme, 45), display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <BFIcon name="flame" size={18} />
          </div>
          <div>
            <div style={{ fontFamily: BF_FONTS.mono, fontSize: 20, lineHeight: 1 }}>{session.streak}</div>
            <div style={{ fontSize: 11, color: theme.fgFaint, marginTop: 3, letterSpacing: 0.2 }}>{t('home.day_streak')}</div>
          </div>
        </BFCard>
        <BFCard theme={theme} style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px' }}>
          <div style={{ width: 32, height: 32, borderRadius: 999, background: bfAccentTint(theme, accentH, 0.15), color: bfAccentText(theme, accentH), display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <BFIcon name="clock" size={18} />
          </div>
          <div>
            <div style={{ fontFamily: BF_FONTS.mono, fontSize: 20, lineHeight: 1 }}>{session.totalMinutes}m</div>
            <div style={{ fontSize: 11, color: theme.fgFaint, marginTop: 3, letterSpacing: 0.2 }}>{t('home.this_week')}</div>
          </div>
        </BFCard>
      </div>

      {/* Featured session card — opens the preset detail so the user can
          set duration / edit phases before committing to a session. */}
      <div onClick={() => { tapHaptic(); onOpenPreset(featured); }} style={{
        position: 'relative', borderRadius: 26, overflow: 'hidden', cursor: 'pointer',
        marginBottom: 28,
        background: bfFeaturedGradient(theme, accentH),
        border: `1px solid ${theme.line}`,
        padding: 22,
      }}>
        <div style={{ position: 'absolute', right: -20, top: -20, opacity: 0.7 }}>
          <BreathingVisualizer style="circle" amplitude={0.8} accentH={accentH} size={180} theme={theme} />
        </div>
        <BFPatternTag tag={t('home.for_today')} accentH={accentH} />
        <div style={{ fontFamily: BF_FONTS.serif, fontSize: 28, fontStyle: 'italic', marginTop: 12, letterSpacing: -0.3 }}>{bfPatternName(featured)}</div>
        <div style={{ fontSize: 13, color: theme.fgMuted, marginTop: 6, maxWidth: 220, textWrap: 'pretty' }}>
          {bfPatternDesc(featured)}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginTop: 20 }}>
          <div style={{
            padding: '10px 16px', borderRadius: 999,
            background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
            fontSize: 13, fontWeight: 600, display: 'inline-flex', alignItems: 'center', gap: 6,
          }}>
            {t('home.open')} <BFIcon name="chevron" size={12} color={bfAccentSolidFg()} />
          </div>
          <div style={{ fontFamily: BF_FONTS.mono, fontSize: 11, color: theme.fgFaint }}>
            {featured.inhale}·{featured.holdIn}·{featured.exhale}·{featured.holdOut}
          </div>
        </div>
      </div>

      {/* Family session CTA — shown to family-tier accounts when there are
          2+ active profiles. Kids don't see this tile (they can't initiate
          a family session, and the picker includes a parent PIN gate
          anyway for the kid→adult parts of the UI). */}
      {!kidsMode && (() => {
        const canFamily = !!(window.BFEntitlement && window.BFEntitlement.can && window.BFEntitlement.can('family.dashboard'));
        const rosterLen = (window.BFProfiles && typeof window.BFProfiles.list === 'function')
          ? window.BFProfiles.list().length : 0;
        if (!canFamily || rosterLen < 2 || typeof onOpenFamily !== 'function') return null;
        return (
          <BFCard theme={theme} onClick={() => { tapHaptic(); onOpenFamily(); }} style={{
            display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', cursor: 'pointer',
            marginBottom: 22,
            background: bfFeaturedSoftGradient(theme, accentH),
            borderColor: bfAccentBorder(theme, accentH, 0.25),
          }}>
            <div style={{
              width: 40, height: 40, borderRadius: 12,
              background: bfAccentTint(theme, accentH, 0.18),
              color:      bfAccentText(theme, accentH),
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}>
              <BFIcon name="user" size={18} />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 500 }}>Family session</div>
              <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 2 }}>
                Breathe together — one session, logged for each person.
              </div>
            </div>
            <BFIcon name="chevron" size={16} color={theme.fgFaint} />
          </BFCard>
        );
      })()}

      {/* Family progress dashboard link — same gating as Family session
          tile. Takes the user to a per-profile stats view; read-only. */}
      {!kidsMode && (() => {
        const canFamily = !!(window.BFEntitlement && window.BFEntitlement.can && window.BFEntitlement.can('family.dashboard'));
        const rosterLen = (window.BFProfiles && typeof window.BFProfiles.list === 'function')
          ? window.BFProfiles.list().length : 0;
        if (!canFamily || rosterLen < 2 || typeof onOpenFamilyDash !== 'function') return null;
        return (
          <BFCard theme={theme} onClick={() => { tapHaptic(); onOpenFamilyDash(); }} style={{
            display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', cursor: 'pointer',
            marginBottom: 22,
          }}>
            <div style={{
              width: 40, height: 40, borderRadius: 12,
              background: bfAccentTint(theme, accentH, 0.12),
              color:      bfAccentText(theme, accentH),
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}>
              <BFIcon name="chart" size={18} />
            </div>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 14, fontWeight: 500 }}>Family progress</div>
              <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 2 }}>
                See streaks and minutes for everyone in your family.
              </div>
            </div>
            <BFIcon name="chevron" size={16} color={theme.fgFaint} />
          </BFCard>
        );
      })()}

      {/* Favorites — pinned patterns, hearted from Library */}
      {favPatterns.length > 0 && (
        <>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 12 }}>
            <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic' }}>{t('home.favorites')}</div>
            <div style={{ fontSize: 11, color: theme.fgFaint }}>{t('home.saved_count', { n: favPatterns.length })}</div>
          </div>
          <div style={{ display: 'flex', gap: 10, overflowX: 'auto', margin: '0 -20px 22px', padding: '0 20px', scrollbarWidth: 'none' }}>
            {favPatterns.map(p => (
              <div key={p.id} onClick={() => { tapHaptic(); onOpenPreset(p); }} style={{
                flexShrink: 0, width: 148, padding: 12, borderRadius: 18, cursor: 'pointer',
                background: theme.card, border: `1px solid ${theme.line}`,
                display: 'flex', flexDirection: 'column', gap: 8,
              }}>
                <BreathingVisualizer style={bfDefaultViz(p)} amplitude={0.75} accentH={accentH} size={56} theme={theme} />
                <div style={{ fontSize: 13, fontWeight: 500 }}>{bfPatternName(p)}</div>
                <div style={{ fontFamily: BF_FONTS.mono, fontSize: 10, color: theme.fgFaint }}>
                  {p.inhale}·{p.holdIn}·{p.exhale}·{p.holdOut}
                </div>
              </div>
            ))}
          </div>
        </>
      )}

      {/* Browse library CTA — moves the full list into the Library tab */}
      <BFCard theme={theme} onClick={() => { tapHaptic(); if (goLibrary) goLibrary(); }} style={{
        display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', cursor: 'pointer',
      }}>
        <div style={{ width: 40, height: 40, borderRadius: 12, background: bfAccentTint(theme, accentH, 0.14), color: bfAccentText(theme, accentH), display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
          <BFIcon name="flow" size={18} />
        </div>
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 14, fontWeight: 500 }}>{t('home.browse_library')}</div>
          <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 2 }}>{t('home.browse_sub')}</div>
        </div>
        <BFIcon name="chevron" size={16} color={theme.fgFaint} />
      </BFCard>
    </div>
  );
}

// ── Preset detail modal ─────────────────────────────────────────────
//
// Beyond showing the preset, this screen now lets the user *tweak* each
// phase (+1s / −1s). The base preset stays read-only — edits live in
// component state until Begin session, which fires the edited pattern
// upward. Limits: 1..20s per phase (0 allowed only on the holds, so you
// can trim a 4-4-4-4 into a 4-0-4-0 rhythmic flow).
function BFPresetDetail({ pattern, theme, accentH, onStart, onClose, haptics, onSavedAsNew }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const defaults = { inhale: pattern.inhale, holdIn: pattern.holdIn, exhale: pattern.exhale, holdOut: pattern.holdOut };
  const [duration, setDuration] = React.useState(180);
  const [phases, setPhases] = React.useState(defaults);
  const [savedPhases, setSavedPhases] = React.useState(defaults);
  const [justSaved, setJustSaved] = React.useState(false);
  // Progression config — only meaningful for custom patterns. Stored on
  // the saved pattern itself (attached at Save time). For built-ins we
  // ignore it (they keep their designer-authored phase lengths).
  // Defaults model the user's ask: "start at min 2, +1s every min, cap 7".
  const DEFAULT_PROG = { enabled: false, startMin: 2, stepEvery: 60, stepBy: 1, capSec: 7 };
  const [prog, setProg] = React.useState(() => {
    const g = pattern.progression;
    if (!g || typeof g !== 'object') return DEFAULT_PROG;
    // Saved patterns carry the engine's schema (startAfterSec / maxAdd).
    // Translate back into the editor's schema (startMin / capSec) so the
    // UI stepper reflects what the user actually configured.
    const base = Math.max(pattern.inhale || 0, pattern.exhale || 0);
    return {
      enabled:   g.enabled === true,
      startMin:  ('startAfterSec' in g)
        ? Math.round((g.startAfterSec || 0) / 60)
        : (g.startMin != null ? g.startMin : DEFAULT_PROG.startMin),
      stepEvery: g.stepEvery != null ? g.stepEvery : DEFAULT_PROG.stepEvery,
      stepBy:    g.stepBy    != null ? g.stepBy    : DEFAULT_PROG.stepBy,
      capSec:    ('maxAdd' in g)
        ? (base + (g.maxAdd || 0))
        : (g.capSec != null ? g.capSec : DEFAULT_PROG.capSec),
    };
  });
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      const saved = await BFStorage.getJSON('phases.' + pattern.id);
      if (cancelled) return;
      if (saved && typeof saved.inhale === 'number') {
        const s = { inhale: saved.inhale, holdIn: saved.holdIn, exhale: saved.exhale, holdOut: saved.holdOut };
        setPhases(s); setSavedPhases(s);
      } else {
        setPhases(defaults); setSavedPhases(defaults);
      }
    })();
    return () => { cancelled = true; };
  }, [pattern.id]);
  const options = [60, 180, 300, 600, 900];
  const isHold = (k) => k === 'holdIn' || k === 'holdOut';
  const bump = (k, d) => setPhases(p => {
    const min = isHold(k) ? 0 : 1;
    const max = 20;
    const raw = (p[k] || 0) + d;
    const next = Math.max(min, Math.min(max, Math.round(raw)));
    return { ...p, [k]: next };
  });
  const isCustom = pattern.id === 'custom' || (pattern.id || '').startsWith('my-');
  // Effective pattern for the session. For customs with progression on,
  // attach progression config so the engine can expand inhale/exhale as
  // elapsed time passes. Built-ins don't carry progression.
  const effective = (() => {
    const base = { ...pattern, ...phases, name: pattern.name };
    if (isCustom && prog.enabled) {
      const maxAdd = Math.max(0, (prog.capSec || 0) - Math.max(phases.inhale || 0, phases.exhale || 0));
      base.progression = {
        enabled: true,
        stepEvery: Math.max(10, prog.stepEvery || 60),
        stepBy:    Math.max(1,  prog.stepBy    || 1),
        maxAdd:    Math.max(0,  maxAdd),
        startAfterSec: Math.max(0, (prog.startMin || 0) * 60),
      };
    }
    return base;
  })();
  const edited = (phases.inhale !== defaults.inhale || phases.holdIn !== defaults.holdIn || phases.exhale !== defaults.exhale || phases.holdOut !== defaults.holdOut);
  const dirty = (phases.inhale !== savedPhases.inhale || phases.holdIn !== savedPhases.holdIn || phases.exhale !== savedPhases.exhale || phases.holdOut !== savedPhases.holdOut);
  const doSave = async () => {
    if (isCustom) {
      const suggested = `${t('preset.my_pattern_prefix')} \u00B7 ${phases.inhale}\u00B7${phases.holdIn}\u00B7${phases.exhale}\u00B7${phases.holdOut}`;
      let raw;
      if (typeof BFDialog !== 'undefined') {
        raw = await BFDialog.prompt({
          title: t('preset.name_title'),
          body:  t('preset.name_body'),
          placeholder: suggested,
          defaultValue: pattern.id && pattern.id.startsWith('my-') ? pattern.name : '',
          confirmLabel: t('common.save'),
        });
      } else {
        raw = (typeof window !== 'undefined' && window.prompt) ? window.prompt(t('preset.name_title'), suggested) : suggested;
      }
      if (raw === null) return;
      const name = (raw || '').trim() || suggested;
      const entry = {
        id: pattern.id && pattern.id.startsWith('my-') ? pattern.id : 'my-' + Date.now().toString(36),
        name, tag: 'Custom',
        inhale: phases.inhale, holdIn: phases.holdIn, exhale: phases.exhale, holdOut: phases.holdOut,
        desc: 'Your saved pattern · edit or delete from Library.',
        progression: prog.enabled ? (() => {
          // Persist the engine's schema, not the editor's. BFProgression only
          // reads startAfterSec + maxAdd; writing the UI names (startMin /
          // capSec) here made saved patterns silently fall back to defaults.
          const base = Math.max(phases.inhale || 0, phases.exhale || 0);
          return {
            enabled: true,
            stepEvery:     Math.max(10, prog.stepEvery || 60),
            stepBy:        Math.max(1,  prog.stepBy    || 1),
            startAfterSec: Math.max(0, (prog.startMin || 0) * 60),
            maxAdd:        Math.max(0, (prog.capSec || 0) - base),
          };
        })() : null,
      };
      await bfMyPatternsAdd(entry);
      if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light();
      if (typeof onSavedAsNew === 'function') onSavedAsNew(entry);
      if (typeof BFToast !== 'undefined') BFToast.info(t('preset.saved_toast'), name);
      onClose();
      return;
    }
    BFStorage.setJSON('phases.' + pattern.id, phases).catch(() => {});
    setSavedPhases(phases);
    setJustSaved(true);
    setTimeout(() => setJustSaved(false), 1400);
    if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light();
  };
  const doReset = () => {
    setPhases(defaults);
    if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light();
  };
  const stepBtn = (onClick, label, disabled) => (
    <button onClick={onClick} disabled={disabled} style={{
      width: 26, height: 26, borderRadius: 999, border: `1px solid ${theme.line}`, cursor: disabled ? 'default' : 'pointer',
      background: disabled ? 'transparent' : theme.cardSoft,
      color: disabled ? theme.fgFaint : theme.fg,
      fontFamily: BF_FONTS.mono, fontSize: 14, lineHeight: '22px',
      opacity: disabled ? 0.4 : 1,
    }}>{label}</button>
  );
  return (
    <div style={{ position: 'absolute', inset: 0, background: theme.bg, color: theme.fg, fontFamily: BF_FONTS.sans, overflow: 'auto' }}>
      <div style={{ padding: 'calc(20px + env(safe-area-inset-top, 40px)) 20px 20px' }}>
        <button onClick={onClose} style={{ background: 'rgba(255,255,255,0.05)', border: 'none', width: 40, height: 40, borderRadius: 999, color: theme.fg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <BFIcon name="close" size={18} />
        </button>
      </div>
      <div style={{ padding: '0 20px 18px', display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
        <BreathingVisualizer style={bfDefaultViz(effective)} amplitude={0.85} accentH={accentH} size={140} theme={theme} />
        <div style={{ marginTop: 6 }}><BFPatternTag tag={pattern.tag} accentH={accentH} /></div>
        <div style={{ fontFamily: BF_FONTS.serif, fontSize: 24, fontStyle: 'italic', marginTop: 6, textAlign: 'center' }}>{bfPatternName(pattern)}</div>
        <div style={{ fontSize: 12, color: theme.fgMuted, textAlign: 'center', marginTop: 6, maxWidth: 300, textWrap: 'pretty', lineHeight: 1.4 }}>{bfPatternDesc(pattern)}</div>
      </div>

      {/* Pattern editor — tap ± to tune each phase */}
      <div style={{ margin: '0 20px 14px', padding: 14, background: theme.card, borderRadius: 18, border: `1px solid ${theme.line}` }}>
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 10, gap: 8 }}>
          <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, flex: 1, minWidth: 0 }}>{t('preset.editor_hint')}</div>
          <div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
            <button onClick={edited ? doReset : undefined} disabled={!edited} style={{
              background: 'transparent', border: `1px solid ${theme.line}`, borderRadius: 999,
              padding: '4px 10px', color: edited ? theme.fgMuted : theme.fgFaint,
              cursor: edited ? 'pointer' : 'default', opacity: edited ? 1 : 0.4,
              fontFamily: BF_FONTS.mono, fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase',
            }}>{t('preset.reset')}</button>
            {(() => {
              const canSave = isCustom ? true : dirty;
              const label = isCustom ? t('preset.save_as') : (justSaved ? t('preset.saved') : t('common.save'));
              return (
                <button onClick={canSave ? doSave : undefined} disabled={!canSave} style={{
                  background: canSave ? bfAccentTint(theme, accentH, 0.18) : 'transparent',
                  border: `1px solid ${canSave ? bfAccentBorder(theme, accentH, 0.5) : theme.line}`, borderRadius: 999,
                  padding: '4px 10px', color: canSave ? bfAccentText(theme, accentH) : theme.fgFaint,
                  cursor: canSave ? 'pointer' : 'default', opacity: canSave ? 1 : 0.4,
                  fontFamily: BF_FONTS.mono, fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase',
                }}>{label}</button>
              );
            })()}
          </div>
        </div>
        {/* 2×2 grid so all four phases fit on narrow phones (a single row
            clipped the 4th column at ~390px CSS width). */}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', rowGap: 10, columnGap: 8 }}>
          {[
            { k: 'inhale',  label: t('player.inhale') },
            { k: 'holdIn',  label: t('player.hold')   },
            { k: 'exhale',  label: t('player.exhale') },
            { k: 'holdOut', label: t('player.hold')   },
          ].map(({ k, label }) => {
            const v = phases[k];
            const min = isHold(k) ? 0 : 1;
            return (
              <div key={k} style={{ textAlign: 'center' }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
                  {stepBtn(() => bump(k, -1), '−', v <= min)}
                  <div style={{ fontFamily: BF_FONTS.serif, fontSize: 28, fontStyle: 'italic', color: v === 0 ? theme.fgFaint : theme.fg, minWidth: 28 }}>{v || '—'}</div>
                  {stepBtn(() => bump(k, +1), '+', v >= 20)}
                </div>
                <div style={{ fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: theme.fgFaint, marginTop: 4 }}>{label}</div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Progression config — only for custom patterns. The built-ins keep
          their designer-authored phase lengths. Controls: enable toggle,
          "start after" minutes, cap seconds. Stepping is +1s per minute
          fixed (keeps the UI surface small; advanced users can still tune
          underlying inhale/exhale to choose the starting point). */}
      {isCustom && (
        <div style={{ margin: '0 20px 14px', padding: 14, background: theme.card, borderRadius: 18, border: `1px solid ${theme.line}` }}>
          <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: prog.enabled ? 12 : 0, gap: 10 }}>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint }}>{t('preset.progression')}</div>
              <div style={{ fontSize: 12, color: theme.fgMuted, marginTop: 3, textWrap: 'pretty', lineHeight: 1.35 }}>
                {t('preset.progression_desc')}
              </div>
            </div>
            <button
              onClick={() => setProg(p => ({ ...p, enabled: !p.enabled }))}
              aria-pressed={prog.enabled}
              style={{
                width: 46, height: 26, borderRadius: 999, border: 'none', cursor: 'pointer',
                background: prog.enabled ? bfAccentSolid(accentH) : bfToggleOffBg(theme),
                position: 'relative', flexShrink: 0,
              }}
            >
              <div style={{
                position: 'absolute', top: 3, left: prog.enabled ? 23 : 3,
                width: 20, height: 20, borderRadius: 999, background: '#fff',
                transition: 'left .2s', boxShadow: '0 1px 3px rgba(0,0,0,.3)',
              }}/>
            </button>
          </div>
          {prog.enabled && (() => {
            const baseMax = Math.max(phases.inhale || 0, phases.exhale || 0);
            const capMin = Math.max(baseMax + 1, 2);
            const capClamp = (n) => Math.max(capMin, Math.min(20, n));
            const startClamp = (n) => Math.max(0, Math.min(30, n));
            const bumpCap = (d) => setProg(p => ({ ...p, capSec: capClamp((p.capSec || baseMax + 3) + d) }));
            const bumpStart = (d) => setProg(p => ({ ...p, startMin: startClamp((p.startMin || 0) + d) }));
            // Quick preview: at start min, we're still at base. After start,
            // every 60s adds 1s up to cap. "Full speed" reached at
            // startMin + (cap - baseMax) minutes.
            const ramp = Math.max(0, (prog.capSec || baseMax) - baseMax);
            const fullAt = (prog.startMin || 0) + ramp;
            return (
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
                <div style={{ textAlign: 'center', padding: '10px 8px', background: theme.cardSoft, borderRadius: 12 }}>
                  <div style={{ fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 6 }}>{t('preset.start_after')}</div>
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
                    {stepBtn(() => bumpStart(-1), '−', (prog.startMin || 0) <= 0)}
                    <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', minWidth: 40 }}>{t('preset.minutes_short', { n: prog.startMin || 0 })}</div>
                    {stepBtn(() => bumpStart(+1), '+', (prog.startMin || 0) >= 30)}
                  </div>
                </div>
                <div style={{ textAlign: 'center', padding: '10px 8px', background: theme.cardSoft, borderRadius: 12 }}>
                  <div style={{ fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 6 }}>{t('preset.cap_at')}</div>
                  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 10 }}>
                    {stepBtn(() => bumpCap(-1), '−', (prog.capSec || capMin) <= capMin)}
                    <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', minWidth: 40 }}>{t('preset.seconds_short', { n: prog.capSec || capMin })}</div>
                    {stepBtn(() => bumpCap(+1), '+', (prog.capSec || capMin) >= 20)}
                  </div>
                </div>
                <div style={{ gridColumn: '1 / -1', fontSize: 11, color: theme.fgFaint, textAlign: 'center', lineHeight: 1.4 }}>
                  {t('preset.ramp_summary', { base: baseMax, cap: prog.capSec || capMin, ramp: ramp, full: fullAt })}
                </div>
              </div>
            );
          })()}
        </div>
      )}

      {/* Duration selector */}
      <div style={{ padding: '0 20px 14px' }}>
        <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('flow.duration')}</div>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {options.map(d => (
            <button key={d} onClick={() => setDuration(d)} style={{
              padding: '8px 12px', borderRadius: 999, cursor: 'pointer',
              background: duration === d ? bfAccentSolid(accentH) : theme.card,
              color: duration === d ? bfAccentSolidFg() : theme.fg,
              border: `1px solid ${duration === d ? 'transparent' : theme.line}`,
              fontFamily: BF_FONTS.mono, fontSize: 12, fontWeight: 500,
            }}>{bfFmt(d)}</button>
          ))}
        </div>
      </div>

      {/* Start */}
      <div style={{ padding: '0 20px calc(20px + env(safe-area-inset-bottom, 0px))' }}>
        <button onClick={() => {
          if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.medium();
          onStart(effective, duration);
        }} style={{
          width: '100%', height: 52, borderRadius: 999, border: 'none', cursor: 'pointer',
          background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
          fontFamily: BF_FONTS.sans, fontSize: 15, fontWeight: 600,
          boxShadow: `0 8px 24px oklch(0.78 0.1 ${accentH} / 0.3)`,
        }}>{edited ? t('preset.begin_session_custom') : t('preset.begin_session')}</button>
      </div>
    </div>
  );
}

// ── Family session picker ───────────────────────────────────────────
// Modal that lets a parent start a breathing session with multiple
// participants on the same device — a phone passed around, one session,
// N history rows (one per selected profile; wired in Phase 3b via the
// `family_session_id` column added in migration 0005).
//
// Audience filtering: the pattern strip intersects the audiences of
// every selected participant. If any kid is selected, the list hides
// adult-only patterns. User-saved customs ('all' audience) always
// qualify. The visible set is recomputed as the user toggles profiles.
//
// The hard safety check in BFApp.startSession re-validates the final
// pattern against every participant so this layer is a UX filter, not
// a security boundary.
function BFFamilyPicker({ theme, accentH, onClose, onStart, haptics }) {
  // Live-subscribe to the roster + entitlement so the picker reacts if
  // the user adds/archives a profile in another tab mid-flow.
  const [roster, setRoster] = React.useState(() =>
    (window.BFProfiles && typeof window.BFProfiles.list === 'function')
      ? window.BFProfiles.list() : []
  );
  React.useEffect(() => {
    if (!window.BFProfiles || !window.BFProfiles.subscribe) return;
    const off = window.BFProfiles.subscribe((snap) => {
      setRoster(snap.list.filter((p) => !p.archived_at));
    });
    return off;
  }, []);

  // Pre-select every profile on open — the common case is "everyone in
  // the room is joining". The user can de-select anyone who's sitting
  // this one out. Requires >= 2 to actually start.
  const [selected, setSelected] = React.useState(() => new Set(roster.map((p) => p.id)));
  // Keep selection in sync if the roster changes while the modal is open
  // (e.g. a new profile appeared): add new ids, drop archived ones.
  React.useEffect(() => {
    setSelected((prev) => {
      const valid = new Set(roster.map((p) => p.id));
      const next = new Set();
      prev.forEach((id) => { if (valid.has(id)) next.add(id); });
      // Any brand-new profiles auto-enrol (matches the "pre-select all"
      // mental model the user opened the picker with).
      roster.forEach((p) => { if (!prev.has(p.id) && !next.has(p.id)) next.add(p.id); });
      return next;
    });
  }, [roster.length]);

  const selectedProfiles = roster.filter((p) => selected.has(p.id));
  const anyKid = selectedProfiles.some((p) => p.kind === 'kid');
  // The audience-intersection shortcut: if any participant is a kid, the
  // common viewable set is bfPatternsForProfile({ kind:'kid' }). Otherwise
  // everyone is adult → use bfPatternsForProfile({ kind:'adult' }). This
  // matches the per-profile gate in BFHome / BFFlow.
  const audienceProbe = anyKid ? { kind: 'kid' } : { kind: 'adult' };
  const availablePatterns = (typeof bfPatternsForProfile === 'function')
    ? bfPatternsForProfile(audienceProbe)
    : BF_PATTERNS;

  const [pattern, setPattern]   = React.useState(null);
  const [duration, setDuration] = React.useState(180);
  // Shorter options than solo — keeps the passed-around format crisp.
  const durations = [60, 180, 300, 600];

  // If a kid joins after a pattern was picked and the pattern is now
  // adult-only, drop it and force the user to re-pick. Prevents starting
  // with a stale invalid selection.
  React.useEffect(() => {
    if (pattern && !availablePatterns.find((p) => p.id === pattern.id)) {
      setPattern(null);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [anyKid]);

  const toggle = (pid) => {
    if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light();
    setSelected((prev) => {
      const next = new Set(prev);
      if (next.has(pid)) next.delete(pid); else next.add(pid);
      return next;
    });
  };

  const canStart = selected.size >= 2 && !!pattern;
  const doStart = () => {
    if (!canStart) return;
    if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light();
    onStart(pattern, duration, Array.from(selected));
  };

  return (
    <div style={{ position: 'absolute', inset: 0, background: theme.bg, color: theme.fg, fontFamily: BF_FONTS.sans, overflow: 'auto' }}>
      <div style={{ padding: 'calc(20px + env(safe-area-inset-top, 40px)) 20px 10px', display: 'flex', alignItems: 'center', gap: 12 }}>
        <button onClick={onClose} aria-label="Close" style={{ background: 'rgba(255,255,255,0.05)', border: 'none', width: 40, height: 40, borderRadius: 999, color: theme.fg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <BFIcon name="close" size={18} />
        </button>
        <div>
          <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, textTransform: 'uppercase' }}>Family session</div>
          <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', marginTop: 2 }}>Breathe together</div>
        </div>
      </div>

      {/* Participants */}
      <div style={{ padding: '12px 20px 20px' }}>
        <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, marginBottom: 10, textTransform: 'uppercase' }}>Who's joining</div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(130px, 1fr))', gap: 10 }}>
          {roster.map((p) => {
            const active = selected.has(p.id);
            return (
              <button key={p.id} onClick={() => toggle(p.id)} style={{
                padding: '14px 12px', borderRadius: 18, cursor: 'pointer', textAlign: 'left',
                background: active ? bfAccentTint(theme, accentH, 0.16) : theme.card,
                border: `1px solid ${active ? bfAccentBorder(theme, accentH, 0.5) : theme.line}`,
                color: theme.fg, fontFamily: BF_FONTS.sans,
                display: 'flex', flexDirection: 'column', gap: 6,
                transition: 'background .12s ease, border-color .12s ease',
              }}>
                <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
                  <div style={{ fontSize: 22, lineHeight: 1 }}>{p.avatar || (p.kind === 'kid' ? '🧒' : '🧘')}</div>
                  {active && (
                    <div style={{ width: 18, height: 18, borderRadius: 999, background: bfAccentSolid(accentH), color: bfAccentSolidFg(), display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
                      <BFIcon name="check" size={12} color={bfAccentSolidFg()} strokeWidth={2.4} />
                    </div>
                  )}
                </div>
                <div style={{ fontSize: 14, fontWeight: 500 }}>{p.name}</div>
                <div style={{ fontSize: 10, color: theme.fgFaint, letterSpacing: 0.3, textTransform: 'uppercase' }}>{p.kind === 'kid' ? 'Kid' : 'Adult'}{p.is_primary ? ' · primary' : ''}</div>
              </button>
            );
          })}
        </div>
        {selected.size < 2 && (
          <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 10 }}>
            Select at least two profiles to start.
          </div>
        )}
      </div>

      {/* Pattern picker */}
      <div style={{ padding: '0 20px 16px' }}>
        <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, marginBottom: 10, textTransform: 'uppercase' }}>
          Pattern{anyKid && <span style={{ color: theme.fgMuted, textTransform: 'none', letterSpacing: 0, marginLeft: 6 }}>· kid-safe only</span>}
        </div>
        <div style={{ display: 'flex', gap: 10, overflowX: 'auto', margin: '0 -20px', padding: '0 20px 4px', scrollbarWidth: 'none' }}>
          {availablePatterns.map((p) => {
            const active = pattern && pattern.id === p.id;
            return (
              <div key={p.id} onClick={() => { if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light(); setPattern(p); }} style={{
                flexShrink: 0, width: 148, padding: 12, borderRadius: 18, cursor: 'pointer',
                background: active ? bfAccentTint(theme, accentH, 0.18) : theme.card,
                border: `1px solid ${active ? bfAccentBorder(theme, accentH, 0.6) : theme.line}`,
                display: 'flex', flexDirection: 'column', gap: 8,
                transition: 'background .12s ease, border-color .12s ease',
              }}>
                <BreathingVisualizer style={bfDefaultViz(p)} amplitude={0.75} accentH={accentH} size={56} theme={theme} />
                <div style={{ fontSize: 13, fontWeight: 500 }}>{bfPatternName(p)}</div>
                <div style={{ fontFamily: BF_FONTS.mono, fontSize: 10, color: theme.fgFaint }}>
                  {p.inhale}·{p.holdIn}·{p.exhale}·{p.holdOut}
                </div>
              </div>
            );
          })}
        </div>
      </div>

      {/* Duration chips */}
      <div style={{ padding: '0 20px 140px' }}>
        <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, marginBottom: 10, textTransform: 'uppercase' }}>Duration</div>
        <div style={{ display: 'flex', gap: 8, flexWrap: 'wrap' }}>
          {durations.map((sec) => {
            const active = duration === sec;
            const label = sec >= 60 ? `${Math.round(sec / 60)} min` : `${sec}s`;
            return (
              <button key={sec} onClick={() => { if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light(); setDuration(sec); }} style={{
                padding: '10px 16px', borderRadius: 999, cursor: 'pointer',
                background: active ? bfAccentSolid(accentH) : theme.card,
                color:      active ? bfAccentSolidFg() : theme.fg,
                border: `1px solid ${active ? bfAccentSolid(accentH) : theme.line}`,
                fontFamily: BF_FONTS.sans, fontSize: 13, fontWeight: active ? 600 : 500,
              }}>{label}</button>
            );
          })}
        </div>
      </div>

      {/* Sticky Start CTA */}
      <div style={{
        position: 'sticky', bottom: 0, left: 0, right: 0,
        padding: '12px 20px calc(16px + env(safe-area-inset-bottom, 0px))',
        background: `linear-gradient(to top, ${theme.bg} 70%, transparent)`,
      }}>
        <button onClick={doStart} disabled={!canStart} style={{
          width: '100%', padding: '14px 18px', borderRadius: 999, cursor: canStart ? 'pointer' : 'default',
          background: canStart ? bfAccentSolid(accentH) : theme.cardSoft,
          color:      canStart ? bfAccentSolidFg() : theme.fgFaint,
          border: 'none', fontSize: 15, fontWeight: 600,
          boxShadow: canStart ? `0 8px 24px oklch(0.78 0.1 ${accentH} / 0.3)` : 'none',
          opacity: canStart ? 1 : 0.6,
        }}>
          {canStart
            ? `Start family session · ${selected.size} ${selected.size === 1 ? 'person' : 'people'}`
            : (selected.size < 2 ? 'Pick at least 2 people' : 'Pick a pattern to start')}
        </button>
      </div>
    </div>
  );
}

// ── Family progress dashboard ─────────────────────────────────────────
// Per-profile cards showing each participant's streak, sessions, minutes,
// plus a small weekly spark of activity. Gated behind the `family.dashboard`
// entitlement (family tier). Sources data via BFSessions.statsByProfile.
//
// This modal is read-only — no row editing. For writing, users go through
// the normal player flow (solo or family picker), which stamps profileId +
// familySessionId correctly.
function BFFamilyDashboard({ theme, accentH, onClose, haptics }) {
  // Live roster — same pattern as BFFamilyPicker so adding/archiving a
  // profile in another tab updates the dashboard without a reload.
  const [roster, setRoster] = React.useState(() =>
    (window.BFProfiles && typeof window.BFProfiles.list === 'function')
      ? window.BFProfiles.list() : []
  );
  React.useEffect(() => {
    if (!window.BFProfiles || !window.BFProfiles.subscribe) return;
    return window.BFProfiles.subscribe((snap) => {
      setRoster(snap.list.filter((p) => !p.archived_at));
    });
  }, []);

  // Per-profile stats map: { [profileId]: stats }. Recomputed on mount and
  // whenever the roster changes. Stats are cheap to compute locally (scan
  // of the sessions array) — no debouncing needed at the scale we expect.
  const [statsByPid, setStatsByPid] = React.useState({});
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      if (typeof BFSessions === 'undefined' || !BFSessions.statsByProfile) return;
      const next = {};
      for (const p of roster) {
        try { next[p.id] = await BFSessions.statsByProfile(p.id); }
        catch (e) { next[p.id] = null; }
      }
      if (!cancelled) setStatsByPid(next);
    })();
    return () => { cancelled = true; };
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [roster.map((p) => p.id).join('|')]);

  // Family totals = sum across distinct family sessions (not per-row sums,
  // which would double-count a 5-min family run as 5×N minutes). We pull
  // all rows once and dedupe by familySessionId for totals, falling back
  // to row-sum for solo rows.
  const [familyTotals, setFamilyTotals] = React.useState(null);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      if (typeof BFSessions === 'undefined' || !BFSessions.all) return;
      try {
        const rows = await BFSessions.all();
        const seenFamilyIds = new Set();
        let totalSessions = 0;
        let totalSeconds = 0;
        for (const r of rows) {
          if (r.familySessionId) {
            if (seenFamilyIds.has(r.familySessionId)) continue;
            seenFamilyIds.add(r.familySessionId);
            totalSessions += 1;
            totalSeconds += (r.durationSec || 0);
          } else {
            totalSessions += 1;
            totalSeconds += (r.durationSec || 0);
          }
        }
        if (!cancelled) setFamilyTotals({
          familyRuns: seenFamilyIds.size,
          totalSessions, totalMinutes: Math.floor(totalSeconds / 60),
        });
      } catch (e) { if (!cancelled) setFamilyTotals(null); }
    })();
    return () => { cancelled = true; };
  }, [roster.map((p) => p.id).join('|')]);

  const tapHaptic = () => { if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light(); };
  const days = ['M','T','W','T','F','S','S'];

  return (
    <div style={{ position: 'absolute', inset: 0, background: theme.bg, color: theme.fg, fontFamily: BF_FONTS.sans, overflow: 'auto' }}>
      <div style={{ padding: 'calc(20px + env(safe-area-inset-top, 40px)) 20px 10px', display: 'flex', alignItems: 'center', gap: 12 }}>
        <button onClick={() => { tapHaptic(); onClose(); }} aria-label="Close" style={{ background: 'rgba(255,255,255,0.05)', border: 'none', width: 40, height: 40, borderRadius: 999, color: theme.fg, cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
          <BFIcon name="close" size={18} />
        </button>
        <div>
          <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, textTransform: 'uppercase' }}>Family progress</div>
          <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', marginTop: 2 }}>How we&apos;re breathing</div>
        </div>
      </div>

      {/* Family totals */}
      {familyTotals && (
        <div style={{ padding: '12px 20px 8px' }}>
          <BFCard theme={theme} style={{
            padding: '14px 16px', display: 'flex', alignItems: 'center', gap: 14,
            background: bfFeaturedSoftGradient(theme, accentH),
            borderColor: bfAccentBorder(theme, accentH, 0.25),
          }}>
            <div style={{ flex: 1 }}>
              <div style={{ fontSize: 11, color: theme.fgFaint, letterSpacing: 0.3, textTransform: 'uppercase' }}>Together</div>
              <div style={{ fontFamily: BF_FONTS.serif, fontSize: 24, fontStyle: 'italic', marginTop: 2, lineHeight: 1.1 }}>
                {familyTotals.totalMinutes}m across {familyTotals.totalSessions} session{familyTotals.totalSessions === 1 ? '' : 's'}
              </div>
              {familyTotals.familyRuns > 0 && (
                <div style={{ fontSize: 12, color: theme.fgMuted, marginTop: 4 }}>
                  {familyTotals.familyRuns} family run{familyTotals.familyRuns === 1 ? '' : 's'}
                </div>
              )}
            </div>
          </BFCard>
        </div>
      )}

      {/* Per-profile cards */}
      <div style={{ padding: '4px 20px 40px', display: 'flex', flexDirection: 'column', gap: 12 }}>
        {roster.length === 0 && (
          <div style={{ fontSize: 13, color: theme.fgFaint, padding: '24px 4px' }}>
            No profiles yet. Add family members from Settings &rarr; Profiles.
          </div>
        )}
        {roster.map((p) => {
          const st = statsByPid[p.id];
          const week = (st && Array.isArray(st.weekMinutes)) ? st.weekMinutes : [0,0,0,0,0,0,0];
          const weekMax = Math.max(...week, 1);
          const streak = st ? st.streak : 0;
          const sessions = st ? st.totalSessions : 0;
          const minutes = st ? st.totalMinutesAllTime : 0;
          return (
            <BFCard key={p.id} theme={theme} style={{ padding: 16 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 12, marginBottom: 14 }}>
                <div style={{
                  width: 40, height: 40, borderRadius: 999,
                  background: bfAccentTint(theme, accentH, 0.15), color: bfAccentText(theme, accentH),
                  display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 22,
                }}>
                  {p.avatar || (p.kind === 'kid' ? '🧒' : '🧘')}
                </div>
                <div style={{ flex: 1 }}>
                  <div style={{ fontSize: 15, fontWeight: 500 }}>{p.name}</div>
                  <div style={{ fontSize: 10, color: theme.fgFaint, letterSpacing: 0.3, textTransform: 'uppercase', marginTop: 2 }}>
                    {p.kind === 'kid' ? 'Kid' : 'Adult'}{p.is_primary ? ' · primary' : ''}
                  </div>
                </div>
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, marginBottom: 14 }}>
                {[
                  { k: 'Streak',   v: streak,   unit: streak === 1 ? 'day'     : 'days' },
                  { k: 'Sessions', v: sessions, unit: 'lifetime' },
                  { k: 'Minutes',  v: minutes,  unit: 'lifetime' },
                ].map((x, i) => (
                  <div key={i} style={{
                    background: theme.cardSoft || 'rgba(255,255,255,0.03)',
                    border: `1px solid ${theme.line}`, borderRadius: 14,
                    padding: '10px 8px', textAlign: 'center',
                  }}>
                    <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', lineHeight: 1 }}>{x.v}</div>
                    <div style={{ fontSize: 9, letterSpacing: 0.4, textTransform: 'uppercase', color: theme.fgFaint, marginTop: 6 }}>{x.k}</div>
                    <div style={{ fontSize: 10, color: theme.fgMuted, marginTop: 1 }}>{x.unit}</div>
                  </div>
                ))}
              </div>
              {/* Weekly spark — same visual grammar as BFProgress */}
              <div style={{ display: 'flex', alignItems: 'flex-end', gap: 6, height: 48 }}>
                {week.map((v, i) => {
                  const h = Math.max(3, Math.round((v / weekMax) * 44));
                  return (
                    <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4 }}>
                      <div style={{
                        width: '100%', height: h, borderRadius: 4,
                        background: v > 0 ? bfAccentTint(theme, accentH, 0.7) : theme.line,
                      }} />
                      <div style={{ fontSize: 9, color: theme.fgFaint }}>{days[i]}</div>
                    </div>
                  );
                })}
              </div>
            </BFCard>
          );
        })}
      </div>
    </div>
  );
}

// ── Flow (library + custom builder) ──────────────────────────────────
function BFFlow({ theme, accentH, onOpenPreset, haptics, myPatternsVer = 0, activeProfile = null }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const [filter, setFilter] = React.useState('All');
  // Source list is audience-filtered by the active profile. Kid profiles
  // see only 'all' + 'kids' patterns; adults see 'all' + 'adult'. Guarded
  // for early mount when BFProfiles hasn't loaded.
  const visiblePatterns = (typeof bfPatternsForProfile === 'function')
    ? bfPatternsForProfile(activeProfile)
    : BF_PATTERNS;
  const kidsMode = !!(activeProfile && activeProfile.kind === 'kid');
  // Tag chips. 'Play' surfaces only in Kids mode (Balloon + Dragon) since
  // it would always be empty for adults.
  const tags = [
    { id: 'All',     label: t('flow.all') },
    { id: 'Focus',   label: t('flow.tag_focus') },
    { id: 'Calm',    label: t('flow.tag_calm') },
    { id: 'Sleep',   label: t('flow.tag_sleep') },
    { id: 'Balance', label: t('flow.tag_balance') },
    { id: 'Energy',  label: t('flow.tag_energy') },
    ...(kidsMode ? [{ id: 'Play', label: t('flow.tag_play') }] : []),
  ];
  const filtered = filter === 'All' ? visiblePatterns : visiblePatterns.filter(p => p.tag === filter);
  const { favs, toggle: toggleFav, isFav } = useFavorites();
  const mine = useMyPatterns();
  React.useEffect(() => { if (myPatternsVer > 0) mine.reload(); }, [myPatternsVer]);
  const tapHaptic = () => { if (haptics && typeof bfHaptic !== 'undefined') bfHaptic.light(); };

  // Delete with an in-app confirm (BFDialog) — never the native window.confirm
  // which bleeds the site URL into the prompt and looks out of place in the app.
  const askDelete = async (p) => {
    if (typeof BFDialog === 'undefined') { mine.remove(p.id); return; }
    const ok = await BFDialog.confirm({
      title: 'Delete pattern?',
      body:  `"${bfPatternName(p)}" will be removed from My patterns.`,
      confirmLabel: 'Delete',
      cancelLabel:  'Keep',
      danger: true,
    });
    if (ok) { tapHaptic(); mine.remove(p.id); if (typeof BFToast !== 'undefined') BFToast.info('Deleted', bfPatternName(p)); }
  };

  return (
    <div style={{ padding: '64px 20px calc(110px + env(safe-area-inset-bottom, 0px))', color: theme.fg, fontFamily: BF_FONTS.sans }}>
      <div style={{ fontSize: 13, color: theme.fgFaint }}>{t('flow.library_title')}</div>
      <div style={{ fontFamily: BF_FONTS.serif, fontSize: 34, fontStyle: 'italic', marginTop: 4, letterSpacing: -0.4 }}>{t('flow.choose_flow')}</div>

      {/* My patterns — user-saved named customs.
          (Favorites strip lives on Home only — cleaner separation of concerns.) */}
      {mine.list.length > 0 && (
        <>
          <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', margin: '22px 0 10px' }}>
            <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint }}>{t('flow.my_patterns')}</div>
            <div style={{ fontSize: 11, color: theme.fgFaint }}>{t('flow.saved_count', { n: mine.list.length })}</div>
          </div>
          <div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 6 }}>
            {mine.list.map(p => (
              <BFCard key={p.id} theme={theme} onClick={() => { tapHaptic(); onOpenPreset(p); }} style={{ display: 'flex', gap: 14, padding: '12px 14px', alignItems: 'center' }}>
                <div style={{ width: 42, height: 42, borderRadius: 14, background: theme.cardSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
                  <BreathingVisualizer style={bfDefaultViz(p)} amplitude={0.6} accentH={accentH} size={36} theme={theme} />
                </div>
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 15, fontWeight: 500, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{bfPatternName(p)}</div>
                  <div style={{ fontFamily: BF_FONTS.mono, fontSize: 11, color: theme.fgFaint, marginTop: 3 }}>
                    {p.inhale}·{p.holdIn}·{p.exhale}·{p.holdOut}
                  </div>
                </div>
                {/* Heart — pin a custom pattern to Home's Favorites strip. */}
                <button
                  onClick={(e) => { e.stopPropagation(); tapHaptic(); toggleFav(p.id); }}
                  title={isFav(p.id) ? t('flow.remove_fav') : t('flow.add_fav')}
                  style={{
                    background: 'transparent', border: 'none', cursor: 'pointer',
                    color: isFav(p.id) ? bfHeartColor(theme) : theme.fgFaint,
                    padding: 6, display: 'flex', alignItems: 'center', flexShrink: 0,
                  }}
                >
                  <BFIcon name={isFav(p.id) ? 'heart-filled' : 'heart'} size={16} />
                </button>
                <button
                  onClick={(e) => { e.stopPropagation(); askDelete(p); }}
                  title={t('flow.delete_pattern')}
                  style={{
                    background: 'transparent', border: 'none', cursor: 'pointer',
                    color: theme.fgFaint, padding: 6, display: 'flex', alignItems: 'center', flexShrink: 0,
                  }}
                >
                  <BFIcon name="close" size={14} />
                </button>
              </BFCard>
            ))}
          </div>
        </>
      )}

      {/* Filters */}
      <div style={{ display: 'flex', gap: 6, overflowX: 'auto', margin: '20px -20px 18px', padding: '0 20px', scrollbarWidth: 'none' }}>
        {tags.map(tag => (
          <button key={tag.id} onClick={() => setFilter(tag.id)} style={{
            padding: '8px 14px', borderRadius: 999, cursor: 'pointer',
            background: filter === tag.id ? theme.fg : theme.card,
            color: filter === tag.id ? theme.bg : theme.fgMuted,
            border: `1px solid ${filter === tag.id ? 'transparent' : theme.line}`,
            fontFamily: BF_FONTS.sans, fontSize: 12, fontWeight: 500,
            whiteSpace: 'nowrap', flexShrink: 0,
          }}>{tag.label}</button>
        ))}
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        {filtered.map(p => (
          <BFCard key={p.id} theme={theme} onClick={() => onOpenPreset(p)} style={{ display: 'flex', gap: 14, padding: '14px 14px', alignItems: 'center' }}>
            <div style={{ width: 54, height: 54, borderRadius: 16, background: theme.cardSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}>
              <BreathingVisualizer style={bfDefaultViz(p)} amplitude={0.65} accentH={accentH} size={44} theme={theme} />
            </div>
            <div style={{ flex: 1, minWidth: 0 }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}><div style={{ fontSize: 15, fontWeight: 500 }}>{bfPatternName(p)}</div><BFPatternTag tag={p.tag} accentH={accentH} /></div>
              <div style={{ fontSize: 12, color: theme.fgMuted, marginTop: 4, textWrap: 'pretty' }}>{bfPatternDesc(p)}</div>
            </div>
            {/* Hide the heart on the Home-featured pattern (Box for adults,
                Balloon for kids) so it doesn't appear twice. */}
            {!((kidsMode && p.id === 'balloon') || (!kidsMode && p.id === 'box')) && (
              <button
                onClick={(e) => { e.stopPropagation(); tapHaptic(); toggleFav(p.id); }}
                title={isFav(p.id) ? t('flow.remove_fav') : t('flow.add_fav')}
                style={{
                  background: 'transparent', border: 'none', cursor: 'pointer',
                  color: isFav(p.id) ? bfHeartColor(theme) : theme.fgFaint,
                  padding: 6, display: 'flex', alignItems: 'center', flexShrink: 0,
                }}
              >
                <BFIcon name={isFav(p.id) ? 'heart-filled' : 'heart'} size={16} />
              </button>
            )}
          </BFCard>
        ))}

        {/* Build your own — opens the preset editor on a blank custom pattern */}
        <BFCard theme={theme} onClick={() => onOpenPreset(BF_CUSTOM_PATTERN)} style={{
          marginTop: 16, padding: 18, display: 'flex', gap: 14, alignItems: 'center',
          borderStyle: 'dashed', borderColor: theme.line, background: 'transparent',
        }}>
          <div style={{ width: 54, height: 54, borderRadius: 16, background: bfAccentTint(theme, accentH, 0.12), display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0, color: bfAccentText(theme, accentH) }}>
            <BFIcon name="plus" size={22} />
          </div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 14, fontWeight: 500 }}>{t('flow.build_your_own')}</div>
            <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 3 }}>{t('flow.build_sub')}</div>
          </div>
          <BFIcon name="chevron" size={16} color={theme.fgFaint} />
        </BFCard>
      </div>
    </div>
  );
}

// ── Progress ─────────────────────────────────────────────────────────
function BFProgress({ theme, accentH, session }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const days = ['M','T','W','T','F','S','S'];
  // Real week activity (Mon..Sun minutes) from BFSessions. Fall back to
  // zeroed array if the stats payload is still loading.
  const activity = (session && Array.isArray(session.weekMinutes) && session.weekMinutes.length === 7)
    ? session.weekMinutes
    : [0, 0, 0, 0, 0, 0, 0];
  const max = Math.max(...activity, 1);
  // Pull real milestone state from BFSessions. Refreshes whenever `session`
  // changes (i.e. every time the parent re-fetches stats after a session).
  const [achievements, setAchievements] = React.useState([]);
  React.useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        if (typeof BFSessions === 'undefined') return;
        const list = await BFSessions.milestonesWithState(session || {});
        if (!cancelled) setAchievements(list);
      } catch (e) { /* keep last good */ }
    })();
    return () => { cancelled = true; };
  }, [session]);
  const hasActivity = activity.some(v => v > 0);
  return (
    <div style={{ padding: '64px 20px calc(110px + env(safe-area-inset-bottom, 0px))', color: theme.fg, fontFamily: BF_FONTS.sans }}>
      <div style={{ fontSize: 13, color: theme.fgFaint }}>{t('progress.title')}</div>
      <div style={{ fontFamily: BF_FONTS.serif, fontSize: 34, fontStyle: 'italic', marginTop: 4, letterSpacing: -0.4 }}>{t('progress.your_flow')}</div>

      {/* Headline stats */}
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 8, margin: '22px 0' }}>
        {[
          { k: t('progress.streak'),   v: session.streak,               unit: t('progress.days') },
          { k: t('progress.total'),    v: session.totalSessions,        unit: t('progress.sessions_lc') },
          { k: t('progress.minutes'),  v: session.totalMinutesAllTime,  unit: t('progress.lifetime') },
        ].map((x, i) => (
          <BFCard key={i} theme={theme} style={{ padding: '14px 12px', textAlign: 'center' }}>
            <div style={{ fontFamily: BF_FONTS.serif, fontSize: 30, fontStyle: 'italic', color: theme.fg, lineHeight: 1 }}>{x.v}</div>
            <div style={{ fontSize: 10, letterSpacing: 0.5, textTransform: 'uppercase', color: theme.fgFaint, marginTop: 6 }}>{x.k}</div>
            <div style={{ fontSize: 11, color: theme.fgMuted, marginTop: 1 }}>{x.unit}</div>
          </BFCard>
        ))}
      </div>

      {/* Weekly activity */}
      <BFCard theme={theme} style={{ padding: 18, marginBottom: 18, position: 'relative' }}>
        <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between' }}>
          <div style={{ fontSize: 13, color: theme.fgMuted }}>{t('progress.this_week')}</div>
          <div style={{ fontFamily: BF_FONTS.mono, fontSize: 12, color: theme.fgFaint }}>{activity.reduce((a,b)=>a+b,0)} {t('progress.min_short')}</div>
        </div>
        <div style={{ display: 'flex', alignItems: 'flex-end', gap: 8, height: 100, marginTop: 16 }}>
          {activity.map((v, i) => (
            <div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
              <div style={{ flex: 1, width: '100%', display: 'flex', alignItems: 'flex-end' }}>
                <div style={{
                  width: '100%', height: `${(v / max) * 100}%`,
                  background: v > 0 ? `linear-gradient(180deg, ${bfAccentSolid(accentH)} 0%, oklch(0.62 0.08 ${accentH}) 100%)` : theme.cardSoft,
                  borderRadius: 6, minHeight: 4,
                }} />
              </div>
              <div style={{ fontSize: 10, color: theme.fgFaint, fontFamily: BF_FONTS.mono }}>{days[i]}</div>
            </div>
          ))}
        </div>
        {!hasActivity && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
            background: `linear-gradient(180deg, transparent 30%, ${theme.card} 90%)`, borderRadius: 'inherit',
            pointerEvents: 'none',
          }}>
            <div style={{ fontSize: 12, color: theme.fgMuted, textAlign: 'center', padding: '0 20px' }}>
              {t('progress.first_session')}
            </div>
          </div>
        )}
      </BFCard>

      {/* Achievements — grouped by category (streak / volume / depth / cadence / variety).
          Category headings only render if the group has any milestones, so
          future catalog changes don't leave orphaned headers. */}
      <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', marginBottom: 10 }}>{t('progress.milestones')}</div>
      {(() => {
        const groups = (typeof BFSessions !== 'undefined' && BFSessions.MILESTONE_GROUPS)
          ? BFSessions.MILESTONE_GROUPS
          : ['streak', 'volume', 'depth', 'cadence', 'variety'];
        const renderCard = (a, i) => (
          <BFCard key={a.id || i} theme={theme} style={{ padding: 12, display: 'flex', alignItems: 'center', gap: 10, opacity: a.unlocked ? 1 : 0.45 }}>
            <div style={{
              width: 36, height: 36, borderRadius: 999,
              background: a.unlocked ? bfAccentTint(theme, accentH, 0.18) : theme.cardSoft,
              color: a.unlocked ? bfAccentText(theme, accentH) : theme.fgFaint,
              display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}>
              <BFIcon name={a.unlocked ? a.icon : 'lock'} size={16} />
            </div>
            <div style={{ fontSize: 12, fontWeight: 500, lineHeight: 1.2 }}>{bfMilestoneName(a)}</div>
          </BFCard>
        );
        // Bucket achievements by group (older cached entries without a `group`
        // field land in a trailing 'uncategorized' bucket so they still render).
        const buckets = {};
        for (const a of achievements) {
          const g = a.group || 'variety';
          (buckets[g] = buckets[g] || []).push(a);
        }
        const renderedGroups = groups.filter(g => buckets[g] && buckets[g].length > 0);
        return renderedGroups.map((g, gi) => (
          <div key={g} style={{ marginBottom: gi === renderedGroups.length - 1 ? 0 : 16 }}>
            <div style={{
              fontSize: 11, textTransform: 'uppercase', letterSpacing: 0.6,
              color: theme.fgMuted, marginBottom: 8, fontFamily: BF_FONTS.mono,
            }}>
              {t('milestone.group.' + g)}
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8 }}>
              {buckets[g].map(renderCard)}
            </div>
          </div>
        ));
      })()}

      {/* Streak tiers — merged from the former Rewards tab */}
      {(() => {
        // Locale-agnostic "Nd" suffix for the right-hand counter. Keeps the
        // column short on narrow phones; full names live in the body.
        const dayShort = (n) => `${n}${t('progress.day_short')}`;
        const tiers = [
          { name: t('progress.tier_bronze'),   req: 3,   desc: t('progress.tier_3_day') },
          { name: t('progress.tier_silver'),   req: 7,   desc: t('progress.tier_one_week') },
          { name: t('progress.tier_gold'),     req: 21,  desc: t('progress.tier_21_day') },
          { name: t('progress.tier_platinum'), req: 60,  desc: t('progress.tier_60_day') },
          { name: t('progress.tier_diamond'),  req: 180, desc: t('progress.tier_180_day') },
        ];
        return (
          <>
            <div style={{ fontFamily: BF_FONTS.serif, fontSize: 22, fontStyle: 'italic', margin: '22px 0 10px' }}>{t('progress.tiers')}</div>
            <BFCard theme={theme} style={{ padding: 18 }}>
              {tiers.map((tier, i) => {
                const active = session.streak >= tier.req;
                return (
                  <div key={tier.name} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '10px 0', borderBottom: i < tiers.length - 1 ? `1px solid ${theme.line}` : 'none' }}>
                    <div style={{
                      width: 40, height: 40, borderRadius: 999, flexShrink: 0,
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      background: active ? bfAccentTint(theme, accentH, 0.2) : theme.cardSoft,
                      color: active ? bfAccentText(theme, accentH) : theme.fgFaint,
                    }}>
                      <BFIcon name={active ? 'trophy' : 'lock'} size={18} />
                    </div>
                    <div style={{ flex: 1 }}>
                      <div style={{ fontSize: 14, fontWeight: 500, color: active ? theme.fg : theme.fgMuted }}>{tier.name}</div>
                      <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 2 }}>{tier.desc}</div>
                    </div>
                    <div style={{ fontFamily: BF_FONTS.mono, fontSize: 12, color: theme.fgFaint }}>{dayShort(tier.req)}</div>
                  </div>
                );
              })}
            </BFCard>
          </>
        );
      })()}
    </div>
  );
}

// Curated list of 8 system voices (4 accents × M/F). Windows ships most of
// these as "Microsoft <Name>"; "<Name> Online (Natural)" is the new neural
// variant. We match loosely so any installed voice in the target locale that
// hits our name hint wins; if no name matches, we fall back to ANY voice in
// the locale with the right gender hint, then to ANY voice in the locale.
// This keeps the list stable across Windows / macOS / Chrome / Safari even
// though each OS names its voices differently.
const BF_CURATED_VOICES = [
  { id: 'us-m', label: 'American',  gender: 'M', lang: 'en-US', name: /(^|\b)(David|Mark|Guy|Eric|Andrew)(\b|Online|$)/i, genderHint: /(David|Mark|Guy|Eric|Andrew|male)/i },
  { id: 'us-f', label: 'American',  gender: 'F', lang: 'en-US', name: /(^|\b)(Zira|Aria|Jenny|Ava|Emma|Samantha)(\b|Online|$)/i, genderHint: /(Zira|Aria|Jenny|Ava|Emma|Samantha|female)/i },
  { id: 'gb-m', label: 'British',   gender: 'M', lang: 'en-GB', name: /(^|\b)(George|Ryan|Oliver|Thomas|Daniel)(\b|Online|$)/i, genderHint: /(George|Ryan|Oliver|Thomas|Daniel|male)/i },
  { id: 'gb-f', label: 'British',   gender: 'F', lang: 'en-GB', name: /(^|\b)(Sonia|Libby|Hazel|Susan|Kate|Serena)(\b|Online|$)/i, genderHint: /(Sonia|Libby|Hazel|Susan|Kate|Serena|female)/i },
  { id: 'au-m', label: 'Australian',gender: 'M', lang: 'en-AU', name: /(^|\b)(William|James|Lee)(\b|Online|$)/i, genderHint: /(William|James|Lee|male)/i },
  { id: 'au-f', label: 'Australian',gender: 'F', lang: 'en-AU', name: /(^|\b)(Natasha|Catherine|Karen)(\b|Online|$)/i, genderHint: /(Natasha|Catherine|Karen|female)/i },
  { id: 'in-m', label: 'Indian',    gender: 'M', lang: 'en-IN', name: /(^|\b)(Prabhat|Ravi|Rishi)(\b|Online|$)/i, genderHint: /(Prabhat|Ravi|Rishi|male)/i },
  { id: 'in-f', label: 'Indian',    gender: 'F', lang: 'en-IN', name: /(^|\b)(Neerja|Heera|Isha)(\b|Online|$)/i, genderHint: /(Neerja|Heera|Isha|female)/i },
];

// Given a curated target and the live voice list, find the best voice to
// represent this card. Returns the Voice object or null when nothing can
// confidently be matched to the requested (locale × gender). Falling back
// to "any voice in the locale" would mis-assign a female voice to a male
// card (or vice versa), so we prefer greying the card out over lying.
function bfResolveCuratedVoice(entry, voices) {
  if (!voices || !voices.length) return null;
  const pool = voices.filter(v => v.lang && v.lang.toLowerCase().startsWith(entry.lang.toLowerCase()));
  if (!pool.length) return null;
  const byName = pool.find(v => entry.name.test(v.name));
  if (byName) return byName;
  const byGender = pool.find(v => entry.genderHint.test(v.name));
  return byGender || null;
}

// Reusable themed dropdown. We use this instead of a native <select> because
// native <option>/<optgroup> popups inherit OS chrome (white background in
// dark mode on Chromium/Safari). `groups` is a list of { label?, options: [{value,label}] };
// pass a single group without `label` for a flat list. `compact` uses a small
// pill-sized button for inline rows (e.g. language picker).
function BFDropdown({ theme, accentH, value, onChange, groups, compact, ariaLabel }) {
  const [open, setOpen] = React.useState(false);
  const [rect, setRect] = React.useState(null);
  const wrapRef = React.useRef(null);
  const btnRef = React.useRef(null);
  const menuRef = React.useRef(null);

  // Recompute the popup's viewport coords whenever it opens, and keep it
  // in sync while the user scrolls / resizes the viewport.
  React.useEffect(() => {
    if (!open || !btnRef.current) return;
    const place = () => {
      if (!btnRef.current) return;
      const r = btnRef.current.getBoundingClientRect();
      setRect({ top: r.bottom + 4, left: r.left, right: r.right, width: r.width });
    };
    place();
    window.addEventListener('resize', place);
    window.addEventListener('scroll', place, true);
    return () => {
      window.removeEventListener('resize', place);
      window.removeEventListener('scroll', place, true);
    };
  }, [open]);

  React.useEffect(() => {
    if (!open) return;
    const onDown = (e) => {
      const inBtn = btnRef.current && btnRef.current.contains(e.target);
      const inMenu = menuRef.current && menuRef.current.contains(e.target);
      if (!inBtn && !inMenu) setOpen(false);
    };
    const onKey = (e) => { if (e.key === 'Escape') setOpen(false); };
    document.addEventListener('pointerdown', onDown);
    document.addEventListener('keydown', onKey);
    return () => {
      document.removeEventListener('pointerdown', onDown);
      document.removeEventListener('keydown', onKey);
    };
  }, [open]);

  // Find the currently selected option's label
  let currentLabel = '';
  for (const g of (groups || [])) {
    for (const o of (g.options || [])) {
      if (o.value === value) { currentLabel = o.label; break; }
    }
    if (currentLabel) break;
  }

  const size = compact
    ? { padding: '6px 30px 6px 10px', fontSize: 12, radius: 999, minWidth: 140 }
    : { padding: '8px 30px 8px 12px', fontSize: 13, radius: 10, minWidth: 0 };

  // Popup positioning: right-align to the button for compact pills, full-width for normal.
  const popupStyle = rect ? (compact ? {
    position: 'fixed',
    top: rect.top,
    left: Math.max(8, rect.right - 220),
    minWidth: 220,
  } : {
    position: 'fixed',
    top: rect.top,
    left: rect.left,
    width: rect.width,
  }) : { display: 'none' };

  return (
    <div ref={wrapRef} style={{ position: 'relative', width: compact ? 'auto' : '100%', fontFamily: BF_FONTS.sans }}>
      <button
        ref={btnRef}
        type="button"
        aria-haspopup="listbox"
        aria-expanded={open}
        aria-label={ariaLabel}
        onClick={() => setOpen(!open)}
        style={{
          width: compact ? 'auto' : '100%',
          minWidth: size.minWidth,
          padding: size.padding,
          borderRadius: size.radius,
          background: 'rgba(255,255,255,0.04)',
          border: `1px solid ${theme.line}`,
          color: theme.fg,
          fontFamily: BF_FONTS.sans,
          fontSize: size.fontSize,
          cursor: 'pointer',
          textAlign: 'left',
          display: 'flex', alignItems: 'center', gap: 8,
          position: 'relative',
          appearance: 'none', WebkitAppearance: 'none',
          backgroundImage: `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%23999' d='M0 0h10L5 6z'/></svg>")`,
          backgroundRepeat: 'no-repeat', backgroundPosition: 'right 10px center',
        }}
      >
        <span style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
          {currentLabel}
        </span>
      </button>
      {open && (
        <div
          ref={menuRef}
          role="listbox"
          style={{
            ...popupStyle,
            maxHeight: 320,
            overflowY: 'auto',
            background: theme.card,
            border: `1px solid ${theme.line}`,
            borderRadius: 10,
            boxShadow: '0 10px 30px rgba(0,0,0,0.45)',
            zIndex: 9999,
            padding: 4,
          }}
        >
          {(groups || []).map((g, gi) => (
            <div key={gi}>
              {g.label && (
                <div style={{
                  padding: '8px 10px 4px', fontSize: 10,
                  letterSpacing: 0.6, textTransform: 'uppercase',
                  color: theme.fgFaint, fontWeight: 600,
                }}>{g.label}</div>
              )}
              {(g.options || []).map((o) => {
                const active = o.value === value;
                return (
                  <button
                    key={o.value}
                    type="button"
                    role="option"
                    aria-selected={active}
                    onClick={() => { onChange(o.value); setOpen(false); }}
                    style={{
                      display: 'block', width: '100%', textAlign: 'left',
                      padding: '8px 10px',
                      border: 'none',
                      background: active ? bfAccentTint(theme, accentH, 0.16) : 'transparent',
                      color: active ? bfAccentText(theme, accentH) : theme.fg,
                      fontFamily: BF_FONTS.sans,
                      fontSize: size.fontSize,
                      borderRadius: 6,
                      cursor: 'pointer',
                    }}
                    onMouseEnter={(e) => { if (!active) e.currentTarget.style.background = 'rgba(255,255,255,0.05)'; }}
                    onMouseLeave={(e) => { if (!active) e.currentTarget.style.background = 'transparent'; }}
                  >
                    {o.label}
                  </button>
                );
              })}
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

// Voice pack picker. Two tiers:
//   1. Narrator packs, grouped by locale (English · Drew, Spanish · María /
//      Carlos, Russian · Анастасия / Дмитрий, Ukrainian · Олена / Андрій).
//      Packs whose mp3s haven't been generated yet still work — they route
//      through BFAudio.speak() in the pack's locale so the cue is spoken
//      in the right language regardless.
//   2. System voice, which surfaces the curated grid of 8 browser voices
//      (4 accents × M/F) when picked. The grid only renders on desktop
//      browsers that expose installed voices.
//
// The list of system voices loads asynchronously on Chrome, so we subscribe
// to BFAudio.onVoicesChanged and re-render when the list populates.
function BFVoicePackRow({ theme, accentH, voicePack, setVoicePack, ttsVoiceName, setTtsVoiceName }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const rawPack = voicePack || 'en-drew';
  const pack = (window.BFVoicePacks && window.BFVoicePacks.migrateLegacyPackId)
    ? window.BFVoicePacks.migrateLegacyPackId(rawPack)
    : rawPack;
  const [voices, setVoices] = React.useState(() => {
    try { return window.BFAudio ? window.BFAudio.listVoices() : []; } catch (e) { return []; }
  });
  React.useEffect(() => {
    if (!window.BFAudio || typeof window.BFAudio.onVoicesChanged !== 'function') return;
    setVoices(window.BFAudio.listVoices());
    return window.BFAudio.onVoicesChanged(() => setVoices(window.BFAudio.listVoices()));
  }, []);

  // Locale → narrator packs, pulled from the voice-packs manifest so new
  // packs appear automatically. Order of locales matches BFVoicePacks.locales.
  const packs = window.BFVoicePacks;
  const LOCALE_NAMES = {
    en: 'English', es: 'Español', ru: 'Русский', uk: 'Українська',
  };
  const groups = (packs ? packs.locales : ['en']).map(loc => ({
    locale: loc,
    label: LOCALE_NAMES[loc] || loc,
    packs: packs ? packs.packsForLocale(loc) : [],
  })).filter(g => g.packs.length > 0);

  // Pick a pack id and tell BFAudio about the right language right away.
  const pick = (id) => {
    if (setVoicePack) setVoicePack(id);
    try {
      const def = (window.BF_VOICE_PACKS || {})[id];
      if (def && def.ttsLang) {
        const v = window.BFAudio && window.BFAudio.findVoiceForLang(def.ttsLang);
        if (v) window.BFAudio.setPreferredVoice(v.name);
      }
      if (packs) {
        const locale = def ? def.locale : 'en';
        window.BFAudio && window.BFAudio.speak(packs.cueText('inhale', locale), def && def.ttsLang ? { lang: def.ttsLang } : undefined).catch(() => {});
      }
    } catch (e) {}
  };

  // Build grouped options for our themed BFDropdown. We avoid native <select>
  // because the OS-rendered popup ignores our dark theme (white background
  // on Chromium/Safari).
  const recordedSuffix = t('voice.suffix_recorded');
  const ddGroups = groups.map((g) => ({
    label: g.label,
    options: g.packs.map((p) => ({
      value: p.id,
      label: `${p.name}${p.gender ? ` · ${p.gender}` : ''}${p.source === 'recorded' ? ' ' + recordedSuffix : ''}`,
    })),
  }));
  ddGroups.push({ label: 'System', options: [{ value: 'system', label: t('voice.browser_fallback') }] });

  return (
    <div style={{ padding: '10px 16px 14px', borderBottom: `1px solid ${theme.line}` }}>
      <BFDropdown
        theme={theme}
        accentH={accentH}
        value={pack}
        onChange={pick}
        groups={ddGroups}
        ariaLabel="Voice narrator"
      />
      {pack === 'system' && (
        voices.length === 0 ? (
          <div style={{ fontSize: 11, color: theme.fgFaint, fontFamily: BF_FONTS.sans }}>
            Loading system voices… try again in a moment.
          </div>
        ) : (
          <div>
            <div style={{
              display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6,
            }}>
              {BF_CURATED_VOICES.map((entry) => {
                const v = bfResolveCuratedVoice(entry, voices);
                const available = !!v;
                const on = available && ttsVoiceName === v.name;
                return (
                  <button
                    key={entry.id}
                    disabled={!available}
                    onClick={() => {
                      if (!available) return;
                      if (setTtsVoiceName) setTtsVoiceName(v.name);
                      try { window.BFAudio && window.BFAudio.setPreferredVoice(v.name); } catch (err) {}
                      // Immediate feedback using the just-picked voice.
                      try { window.BFAudio && window.BFAudio.speak('Inhale').catch(() => {}); } catch (err) {}
                    }}
                    title={available ? v.name : 'Not installed on this device'}
                    style={{
                      padding: '8px 10px', borderRadius: 10, cursor: available ? 'pointer' : 'not-allowed',
                      background: on ? bfAccentTint(theme, accentH, 0.14) : 'transparent',
                      border: `1px solid ${on ? bfAccentBorder(theme, accentH, 0.45) : theme.line}`,
                      color: on ? bfAccentText(theme, accentH) : (available ? theme.fgMuted : theme.fgFaint),
                      opacity: available ? 1 : 0.45,
                      fontFamily: BF_FONTS.sans, textAlign: 'left',
                      display: 'flex', alignItems: 'center', gap: 8,
                    }}
                  >
                    <span style={{
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      width: 26, height: 26, borderRadius: 999, flexShrink: 0,
                      background: bfAccentTint(theme, accentH, 0.14),
                      color: bfAccentText(theme, accentH),
                      fontSize: 10, fontWeight: 700, letterSpacing: 0.5,
                    }}>{entry.gender}</span>
                    <span style={{ flex: 1, minWidth: 0 }}>
                      <div style={{ fontSize: 12, fontWeight: 600 }}>{entry.label}</div>
                      <div style={{
                        fontSize: 10, letterSpacing: 0.2, color: theme.fgFaint, marginTop: 2,
                        overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
                      }}>
                        {available ? v.name.replace(/^Microsoft /, '').split(' - ')[0] : 'Not available'}
                      </div>
                    </span>
                  </button>
                );
              })}
            </div>
            <button
              onClick={() => {
                if (setTtsVoiceName) setTtsVoiceName(null);
                try { window.BFAudio && window.BFAudio.setPreferredVoice(null); } catch (err) {}
                try { window.BFAudio && window.BFAudio.speak('Inhale').catch(() => {}); } catch (err) {}
              }}
              style={{
                marginTop: 8, padding: '6px 12px', borderRadius: 999,
                background: 'transparent', border: `1px solid ${theme.line}`,
                color: !ttsVoiceName ? bfAccentText(theme, accentH) : theme.fgFaint,
                fontFamily: BF_FONTS.sans, fontSize: 10, letterSpacing: 0.5,
                textTransform: 'uppercase', cursor: 'pointer',
              }}
            >{!ttsVoiceName ? '\u2713 ' : ''}Browser default</button>
          </div>
        )
      )}
    </div>
  );
}

// UI language picker — compact native <select>. Pulls available locales
// from BFI18n.locales so adding a new UI locale in src/i18n.js makes it
// appear here automatically. Each option shows the native name in the
// target script (e.g. "Русский") so a user who can't read the current
// UI still recognises their language.
function BFLanguageRow({ theme, accentH, uiLocale, setUiLocale }) {
  const locales = (window.BFI18n && window.BFI18n.locales) || [{ id: 'en', name: 'English', native: 'English' }];
  const active = uiLocale || 'en';
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const ddGroups = [{
    options: locales.map((loc) => ({ value: loc.id, label: loc.native })),
  }];
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 16px', borderBottom: `1px solid ${theme.line}` }}>
      <div style={{ width: 32, height: 32, borderRadius: 10, background: theme.cardSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', color: theme.fgMuted, flexShrink: 0 }}>
        <BFIcon name="chat" size={16} />
      </div>
      <div style={{ flex: 1, fontSize: 14 }}>{t('settings.language')}</div>
      <BFDropdown
        theme={theme}
        accentH={accentH}
        value={active}
        onChange={(v) => { if (setUiLocale) setUiLocale(v); }}
        groups={ddGroups}
        compact
        ariaLabel={t('settings.language')}
      />
    </div>
  );
}

// ── Reminders card ───────────────────────────────────────────────────
// Self-contained component that drives the BFReminders client module.
// Reads/writes Supabase via BFReminders; no state is cached in the parent.

// Format an hour (0-23) using the user's locale conventions. English-language
// locales (en-US/CA/AU/NZ/IN/PH/GB/IE) get "8 AM" / "8 PM"; everywhere else
// gets zero-padded "08" / "20". Explicit locale check beats Intl auto-detect
// because the auto-detect path was returning hour12=false on Android Chrome
// even for North American users — a known device-settings quirk.
function bfPrefersHour12() {
  try {
    const appLocale = (window.BFI18n && window.BFI18n.getLocale && window.BFI18n.getLocale()) || '';
    if (appLocale.startsWith('en')) return true;
    const navLocale = (navigator.language || '').toLowerCase();
    if (navLocale.startsWith('en')) return true;
  } catch (e) {}
  return false;
}
function bfFormatHour(h) {
  if (bfPrefersHour12()) {
    const period = h < 12 ? 'AM' : 'PM';
    const h12 = h === 0 ? 12 : (h > 12 ? h - 12 : h);
    return h12 + ' ' + period;
  }
  return String(h).padStart(2, '0');
}

function BFRemindersCard({ theme, accentH, signedIn }) {
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const [status, setStatus]   = React.useState(null);
  const [busy, setBusy]       = React.useState(false);
  const [hour, setHour]       = React.useState(20);
  const [minute, setMinute]   = React.useState(0);
  const [mode, setMode]       = React.useState('daily');

  const refresh = React.useCallback(async () => {
    if (!window.BFReminders) return;
    try {
      const s = await window.BFReminders.status();
      setStatus(s);
      if (s && s.prefs) {
        if (typeof s.prefs.hour_local   === 'number') setHour(s.prefs.hour_local);
        if (typeof s.prefs.minute_local === 'number') setMinute(s.prefs.minute_local);
        if (s.prefs.mode && s.prefs.mode !== 'off')   setMode(s.prefs.mode);
      }
    } catch (e) { console.warn('[BFRemindersCard] status', e); }
  }, []);

  React.useEffect(() => { refresh(); }, [refresh, signedIn]);

  const supported  = status && status.supported;
  const permission = status ? status.permission : 'default';
  const subscribed = status && status.subscribed && status.prefs && status.prefs.mode !== 'off';
  const blocked    = permission === 'denied';

  const onToggle = async () => {
    if (busy || !window.BFReminders) return;
    setBusy(true);
    try {
      if (subscribed) {
        await window.BFReminders.disable();
        try { BFToast.info(t('reminders.off_title'), t('reminders.off_body')); } catch (e) {}
      } else {
        if (!signedIn) {
          try { BFToast.info(t('reminders.signin_title'), t('reminders.signin_body')); } catch (e) {}
          return;
        }
        // Permission priming: if the OS hasn't been asked yet, show our own
        // styled explainer first. The browser's native prompt is unstylable
        // and feels jarring — softening it with a one-liner ahead of time
        // sets the expectation. Skip when permission is already granted or
        // already denied (browser won't re-prompt anyway).
        if (permission === 'default' && window.BFDialog && window.BFDialog.confirm) {
          const ok = await window.BFDialog.confirm({
            title:        t('reminders.prime_title'),
            body:         t('reminders.prime_body'),
            confirmLabel: t('reminders.prime_confirm'),
            cancelLabel:  t('reminders.prime_cancel'),
          });
          if (!ok) return;
        }
        await window.BFReminders.enable({ hour, minute, mode });
        try { BFToast.success(t('reminders.on_title'), t('reminders.on_body')); } catch (e) {}
      }
      await refresh();
    } catch (e) {
      const code = e && e.code;
      if (code === 'permission_denied')       { try { BFToast.error(t('reminders.err_denied_title'),       t('reminders.err_denied_body'));       } catch (_) {} }
      else if (code === 'not_signed_in')      { try { BFToast.info (t('reminders.signin_title'),            t('reminders.signin_body'));            } catch (_) {} }
      else if (code === 'no_vapid')           { try { BFToast.error(t('reminders.err_unconfigured_title'),  t('reminders.err_unconfigured_body'));  } catch (_) {} }
      else if (code === 'unsupported')        { try { BFToast.error(t('reminders.err_unsupported_title'),   t('reminders.err_unsupported_body'));   } catch (_) {} }
      else                                     { try { BFToast.error(t('reminders.err_generic_title'),      (e && e.message) || '');                } catch (_) {} }
    } finally { setBusy(false); }
  };

  const onSaveTime = async () => {
    if (!subscribed || busy || !window.BFReminders) return;
    setBusy(true);
    try {
      await window.BFReminders.updatePrefs({ hour, minute, mode });
      try { BFToast.success(t('reminders.saved_title'), t('reminders.saved_body')); } catch (e) {}
      await refresh();
    } catch (e) {
      try { BFToast.error(t('reminders.err_generic_title'), (e && e.message) || ''); } catch (_) {}
    } finally { setBusy(false); }
  };

  const Toggle = ({ on, onClick }) => (
    <button onClick={(e) => { e.stopPropagation(); onClick(); }} style={{
      width: 46, height: 26, borderRadius: 999, border: 'none', cursor: busy ? 'default' : 'pointer',
      background: on ? bfAccentSolid(accentH) : bfToggleOffBg(theme),
      position: 'relative', transition: 'background .2s', opacity: busy ? 0.6 : 1,
    }}>
      <div style={{ position: 'absolute', top: 3, left: on ? 23 : 3, width: 20, height: 20, borderRadius: 999, background: '#fff', transition: 'left .2s', boxShadow: '0 1px 3px rgba(0,0,0,.3)' }}/>
    </button>
  );

  const rowStyle = {
    display: 'flex', alignItems: 'center', gap: 14,
    padding: '14px 16px', borderBottom: `1px solid ${theme.line}`,
  };
  const iconWrap = {
    width: 32, height: 32, borderRadius: 10, background: theme.cardSoft,
    display: 'flex', alignItems: 'center', justifyContent: 'center', color: theme.fgMuted, flexShrink: 0,
  };
  const selectStyle = {
    padding: '6px 10px', borderRadius: 8, border: `1px solid ${theme.line}`,
    background: theme.cardSoft, color: theme.fg,
    fontFamily: BF_FONTS.sans, fontSize: 13, cursor: busy ? 'default' : 'pointer',
  };

  if (!supported) {
    return (
      <>
        <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('reminders.section')}</div>
        <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 18 }}>
          <div style={rowStyle}>
            <div style={iconWrap}><BFIcon name="bell" size={16} /></div>
            <div style={{ flex: 1, fontSize: 13, color: theme.fgFaint, lineHeight: 1.4 }}>
              {t('reminders.unsupported_hint')}
            </div>
          </div>
        </BFCard>
      </>
    );
  }

  return (
    <>
      <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('reminders.section')}</div>
      <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 18 }}>
        {/* Enable/disable */}
        <div style={rowStyle}>
          <div style={iconWrap}><BFIcon name="bell" size={16} /></div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 14 }}>{t('reminders.daily')}</div>
            {!signedIn && (
              <div style={{ fontSize: 11, color: theme.fgFaint, marginTop: 2 }}>{t('reminders.signin_hint')}</div>
            )}
            {blocked && (
              <div style={{ fontSize: 11, color: bfDangerText(theme), marginTop: 2 }}>{t('reminders.blocked_hint')}</div>
            )}
          </div>
          <Toggle on={!!subscribed} onClick={onToggle} />
        </div>

        {/* Time picker + mode (only when enabled) */}
        {subscribed && (
          <>
            {/* Time picker — labels honour the user's locale (12hr w/ AM/PM
                in en-US et al, 24hr elsewhere). Stored value is always 0-23. */}
            <div style={rowStyle}>
              <div style={iconWrap}><BFIcon name="moon" size={16} /></div>
              <div style={{ flex: 1, fontSize: 14 }}>{t('reminders.time')}</div>
              <select value={hour} onChange={(e) => setHour(parseInt(e.target.value, 10))} style={selectStyle} disabled={busy}>
                {Array.from({ length: 24 }).map((_, h) => (
                  <option key={h} value={h}>{bfFormatHour(h)}</option>
                ))}
              </select>
              <span style={{ color: theme.fgFaint, fontSize: 14 }}>:</span>
              <select value={minute} onChange={(e) => setMinute(parseInt(e.target.value, 10))} style={selectStyle} disabled={busy}>
                {[0, 15, 30, 45].map((m) => (
                  <option key={m} value={m}>{String(m).padStart(2, '0')}</option>
                ))}
              </select>
            </div>
            <div style={rowStyle}>
              <div style={iconWrap}><BFIcon name="sparkle" size={16} /></div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 14 }}>{t('reminders.mode')}</div>
                <div style={{ fontSize: 11, color: theme.fgFaint, marginTop: 2 }}>
                  {t('reminders.mode_' + mode + '_desc')}
                </div>
              </div>
              <div style={{ display: 'flex', gap: 4, background: theme.cardSoft, borderRadius: 999, padding: 3 }}>
                {['daily', 'smart'].map((m) => (
                  <button key={m} onClick={() => setMode(m)} disabled={busy} style={{
                    padding: '5px 10px', borderRadius: 999, border: 'none', cursor: busy ? 'default' : 'pointer',
                    background: mode === m ? bfAccentSolid(accentH) : 'transparent',
                    color: mode === m ? bfAccentSolidFg() : theme.fgMuted,
                    fontSize: 11, fontWeight: 500,
                  }}>{t('reminders.mode_' + m)}</button>
                ))}
              </div>
            </div>
            <div style={{ padding: '10px 16px', display: 'flex', justifyContent: 'flex-end' }}>
              <button onClick={onSaveTime} disabled={busy} style={{
                padding: '8px 16px', borderRadius: 999, border: 'none', cursor: busy ? 'default' : 'pointer',
                background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
                fontFamily: BF_FONTS.sans, fontSize: 12, fontWeight: 500, opacity: busy ? 0.6 : 1,
              }}>{t('reminders.save')}</button>
            </div>
          </>
        )}
      </BFCard>
    </>
  );
}

// ── Settings / Profile ───────────────────────────────────────────────
function BFSettings({ theme, accentH, setAccent, vizStyle, setVizStyle, voice, setVoice, voicePack, setVoicePack, ttsVoiceName, setTtsVoiceName, uiLocale, setUiLocale, sound, setSound, darkMode, setDarkMode, signedIn, setSignedIn, user, resetOnboarded, fontPairing, setFontPairing, ambientId, setAmbientId, ambientVol, setAmbientVol, haptics, setHaptics, onOpenCustom, onOpenAdmin }) {
  // Ambient preview should never outlive Settings. The previous version
  // started the engine on pick and never stopped it, so ambient played
  // across the whole app. Stop on unmount.
  React.useEffect(() => () => { try { bfAmbient.stop(); } catch (e) {} }, []);
  // "What is BreatheFlow" routes to the BFHelp full-screen sheet (same
  // component as the floating ? button). Kept local so opening it from
  // Settings doesn't need to plumb state through the parent shell.
  const [helpOpen, setHelpOpen] = React.useState(false);
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  const displayName = (user && (user.user_metadata?.full_name || user.user_metadata?.name || user.email)) || (signedIn ? t('settings.signed_in') : t('settings.guest'));
  const subline = signedIn ? (user && user.email ? user.email : t('settings.synced')) : t('settings.guest_sub');
  const Row = ({ icon, label, children, onClick, disabled, hint }) => (
    <div onClick={disabled ? undefined : onClick} style={{
      display: 'flex', alignItems: 'center', gap: 14,
      padding: '14px 16px', cursor: disabled ? 'default' : (onClick ? 'pointer' : 'default'),
      borderBottom: `1px solid ${theme.line}`, opacity: disabled ? 0.4 : 1,
    }}>
      <div style={{ width: 32, height: 32, borderRadius: 10, background: theme.cardSoft, display: 'flex', alignItems: 'center', justifyContent: 'center', color: theme.fgMuted, flexShrink: 0 }}>
        <BFIcon name={icon} size={16} />
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 14 }}>{label}</div>
        {hint ? <div style={{ fontSize: 11, color: theme.fgFaint, marginTop: 2, lineHeight: 1.3 }}>{hint}</div> : null}
      </div>
      {disabled ? <span style={{ fontSize: 10, color: theme.fgFaint, letterSpacing: 0.5, textTransform: 'uppercase', fontFamily: BF_FONTS.mono }}>{t('common.soon')}</span> : children}
    </div>
  );
  const Toggle = ({ on, onClick }) => (
    <button onClick={(e) => { e.stopPropagation(); onClick(); }} style={{
      width: 46, height: 26, borderRadius: 999, border: 'none', cursor: 'pointer',
      background: on ? bfAccentSolid(accentH) : bfToggleOffBg(theme),
      position: 'relative', transition: 'background .2s',
    }}>
      <div style={{ position: 'absolute', top: 3, left: on ? 23 : 3, width: 20, height: 20, borderRadius: 999, background: '#fff', transition: 'left .2s', boxShadow: '0 1px 3px rgba(0,0,0,.3)' }}/>
    </button>
  );
  return (
    <div style={{ padding: '64px 20px calc(110px + env(safe-area-inset-bottom, 0px))', color: theme.fg, fontFamily: BF_FONTS.sans }}>
      <div style={{ fontSize: 13, color: theme.fgFaint }}>{t('settings.account')}</div>
      <div style={{ fontFamily: BF_FONTS.serif, fontSize: 34, fontStyle: 'italic', marginTop: 4, letterSpacing: -0.4, marginBottom: 18 }}>{t('settings.title')}</div>

      {/* Account banner */}
      <BFCard theme={theme} style={{ padding: 16, marginBottom: 22, display: 'flex', alignItems: 'center', gap: 14 }}>
        {user && user.user_metadata?.avatar_url ? (
          <img src={user.user_metadata.avatar_url} alt="" referrerPolicy="no-referrer" style={{ width: 44, height: 44, borderRadius: 999, objectFit: 'cover' }} />
        ) : (
          <div style={{ width: 44, height: 44, borderRadius: 999, background: bfAccentTint(theme, accentH, 0.2), color: bfAccentText(theme, accentH), display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <BFIcon name={signedIn ? 'check' : 'user'} size={20} />
          </div>
        )}
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 15, fontWeight: 500, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{displayName}</div>
          <div style={{ fontSize: 12, color: theme.fgFaint, marginTop: 2, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{subline}</div>
        </div>
        <button onClick={() => setSignedIn(!signedIn)} style={{
          padding: '8px 14px', borderRadius: 999, border: 'none', cursor: 'pointer',
          background: signedIn ? theme.cardSoft : bfAccentSolid(accentH),
          color: signedIn ? theme.fgMuted : bfAccentSolidFg(),
          fontFamily: BF_FONTS.sans, fontSize: 12, fontWeight: 500,
          display: 'inline-flex', alignItems: 'center', gap: 6,
          flexShrink: 0,
        }}>
          {!signedIn && (
            <svg viewBox="0 0 24 24" width="14" height="14" aria-hidden="true">
              <path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
              <path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
              <path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l3.66-2.84z"/>
              <path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
            </svg>
          )}
          {signedIn ? t('settings.sign_out') : t('settings.sign_in_google')}
        </button>
      </BFCard>

      {/* Profile switcher — only surfaces when signed in. For free/plus
          users the roster is always exactly one primary profile; the
          switcher still renders so they can see who they're practising as
          and tap "Add profile" to hit the paywall. Family users get full
          roster management. */}
      {signedIn && <BFProfileSwitcher theme={theme} accentH={accentH} />}

      <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('settings.practice')}</div>
      <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 18 }}>
        <Row icon="flow" label={t('settings.visualizer')}>
          <div style={{ display: 'flex', gap: 4, background: theme.cardSoft, borderRadius: 999, padding: 3 }}>
            {['circle','geometric','flower'].map(v => (
              <button key={v} onClick={() => setVizStyle(v)} style={{
                padding: '5px 10px', borderRadius: 999, border: 'none', cursor: 'pointer',
                background: vizStyle === v ? bfAccentSolid(accentH) : 'transparent',
                color: vizStyle === v ? bfAccentSolidFg() : theme.fgMuted,
                fontSize: 11, fontWeight: 500,
              }}>{t('settings.viz_' + v)}</button>
            ))}
          </div>
        </Row>
        <Row icon="volume" label={t('settings.voice_guidance')}>
          <button
            onClick={(e) => {
              e.stopPropagation();
              // Preview the currently selected pack through the manifest:
              // plays the pack's 'inhale' mp3 if present, otherwise speaks
              // the cue in the pack's locale so Russian/Spanish/Ukrainian
              // users hear the right language even before mp3s are shipped.
              const packs = window.BFVoicePacks;
              const rawId = voicePack || 'en-drew';
              const packId = packs ? packs.migrateLegacyPackId(rawId) : rawId;
              const pack = (window.BF_VOICE_PACKS || {})[packId];
              const locale = pack ? pack.locale : 'en';
              const ttsLang = pack ? pack.ttsLang : null;
              const ttsText = packs ? packs.cueText('inhale', locale) : 'Inhale';
              const speak = () => BFAudio.speak(ttsText, ttsLang ? { lang: ttsLang } : undefined).catch(() => {});
              const url = packs ? packs.cueUrl(packId, 'inhale') : null;
              if (url) {
                BFAudio.preload('voice.inhale', url, { volume: 0.9 })
                  .then(() => BFAudio.play('voice.inhale', { restart: true }).catch(speak))
                  .catch(speak);
              } else {
                speak();
              }
            }}
            title="Preview the selected voice"
            style={{
              padding: '4px 10px', borderRadius: 999, border: `1px solid ${theme.line}`,
              background: 'transparent', color: theme.fgMuted,
              fontFamily: BF_FONTS.sans, fontSize: 11, cursor: 'pointer', marginRight: 10,
            }}
          >{t('settings.preview')}</button>
          <Toggle on={voice} onClick={() => setVoice(!voice)} />
        </Row>
        {voice && (
          <BFVoicePackRow
            theme={theme} accentH={accentH}
            voicePack={voicePack} setVoicePack={setVoicePack}
            ttsVoiceName={ttsVoiceName} setTtsVoiceName={setTtsVoiceName}
          />
        )}
        <Row icon="sparkle" label={t('settings.ambient_sound')}>
          <Toggle on={sound} onClick={() => setSound(!sound)} />
        </Row>
        <Row icon="flame" label={t('settings.haptics')} hint={t('settings.haptics_hint')}>
          {/* Probe on toggle-ON uses the success triplet so the pulse is clearly
              perceptible on a phone. The single `medium()` (25ms) we had before
              was too short — users reported the toggle "did nothing". */}
          <Toggle on={haptics} onClick={() => { setHaptics(!haptics); if (!haptics && typeof bfHaptic !== 'undefined') bfHaptic.success(); }} />
        </Row>
        <BFLanguageRow theme={theme} accentH={accentH} uiLocale={uiLocale} setUiLocale={setUiLocale} />
      </BFCard>

      {sound && (
        <>
          <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('settings.ambient_library')}</div>
          <BFAmbientPicker theme={theme} accentH={accentH}
            current={ambientId}
            onPick={(id) => { setAmbientId(id); bfAmbient.play(id); if (haptics) bfHaptic.light(); }}
            volume={ambientVol}
            onVolume={(v) => { setAmbientVol(v); bfAmbient.setVolume(v); }}
          />
        </>
      )}

      <BFRemindersCard theme={theme} accentH={accentH} signedIn={signedIn} />

      <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('settings.appearance')}</div>
      <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 18 }}>
        <Row icon="moon" label={t('settings.dark_mode')}>
          <Toggle on={darkMode} onClick={() => setDarkMode(!darkMode)} />
        </Row>
        <Row icon="sparkle" label={t('settings.accent')}>
          <div style={{ display: 'flex', gap: 6 }}>
            {Object.keys(BF_ACCENTS).map(k => (
              <button key={k} onClick={() => setAccent(k)} style={{
                width: 22, height: 22, borderRadius: 999, border: 'none', cursor: 'pointer',
                background: bfAccent(k),
                outline: accentH === BF_ACCENTS[k].h ? `2px solid ${theme.fg}` : 'none',
                outlineOffset: 2,
              }} />
            ))}
          </div>
        </Row>
        <Row icon="wind" label={t('settings.typography')}>
          <div style={{ display: 'flex', gap: 4, background: theme.cardSoft, borderRadius: 999, padding: 3 }}>
            {Object.entries(BF_FONT_PAIRINGS).map(([k, p]) => (
              <button key={k} onClick={() => setFontPairing(k)} style={{
                padding: '5px 10px', borderRadius: 999, border: 'none', cursor: 'pointer',
                background: fontPairing === k ? bfAccentSolid(accentH) : 'transparent',
                color: fontPairing === k ? bfAccentSolidFg() : theme.fgMuted,
                fontSize: 11, fontWeight: 500,
              }}>{bfFontPairingLabel(p)}</button>
            ))}
          </div>
        </Row>
      </BFCard>

      <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8 }}>{t('settings.about')}</div>
      <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden' }}>
        <Row icon="wind" label={t('settings.what_is')} onClick={() => setHelpOpen(true)}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
        <Row icon="arrow" label={t('settings.share')} onClick={() => BFShare.app()}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
        <Row icon="sparkle" label={t('settings.show_intro')} onClick={async () => {
          if (typeof BFOnboarding !== 'undefined') await BFOnboarding.reset();
          if (typeof resetOnboarded === 'function') resetOnboarded();
          if (typeof BFToast !== 'undefined') BFToast.info('Intro reset', 'Scroll up to revisit.');
        }}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
        <Row icon="target" label={t('settings.feedback')} onClick={() => {
          if (typeof BFFeedback !== 'undefined') BFFeedback.open();
        }}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
        <Row icon="moon" label={t('settings.privacy')} onClick={() => {
          if (typeof BFPrivacy !== 'undefined') BFPrivacy.open();
        }}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
        <Row icon="sparkle" label={t('settings.about_app')} onClick={() => {
          if (typeof BFAbout !== 'undefined') BFAbout.open();
        }}>
          <BFIcon name="chevron" size={14} color={theme.fgFaint} />
        </Row>
      </BFCard>

      {/* Admin — client-side hint only. The admin page and the /api/admin/*
          endpoints both re-verify against the ADMIN_EMAILS env var, so this
          row being visible never grants real privilege. */}
      {signedIn && typeof BFEntitlement !== 'undefined' && BFEntitlement.isAdmin && BFEntitlement.isAdmin() && onOpenAdmin && (
        <>
          <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8, marginTop: 4 }}>Maintainer</div>
          <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 18 }}>
            <Row icon="target" label="Admin console" onClick={onOpenAdmin}>
              <BFIcon name="chevron" size={14} color={theme.fgFaint} />
            </Row>
          </BFCard>
        </>
      )}

      {/* Danger zone — only visible when signed in. Account deletion is
          destructive and permanent, so we route it through BFDialog.confirm
          and a serverless function that calls Supabase's admin delete-user
          endpoint. The `on delete cascade` FKs clean up sessions/BOLT/etc. */}
      {signedIn && (
        <>
          <div style={{ fontSize: 11, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint, marginBottom: 8, marginTop: 4 }}>{t('settings.danger_zone')}</div>
          <BFCard theme={theme} style={{ padding: 0, overflow: 'hidden', marginBottom: 8 }}>
            <Row icon="target" label={
              <span style={{ color: bfDangerText(theme) }}>{t('settings.delete_account')}</span>
            } onClick={async () => {
              if (typeof BFDialog === 'undefined') return;
              const ok = await BFDialog.confirm({
                title: t('settings.delete_account_confirm_title'),
                body:  t('settings.delete_account_confirm_body'),
                confirmLabel: t('common.delete'),
                cancelLabel:  t('common.keep'),
                danger: true,
              });
              if (!ok) return;

              // Show a 10-second "Deleting…" toast with an Undo button. The
              // admin API call only fires when the countdown elapses, so a
              // misclick is fully recoverable. The toast replaces itself each
              // second to update the label (no live DOM mutation).
              const UNDO_SEC = 10;
              let cancelled = false;
              let toastId = null;
              const clearToast = () => { if (toastId != null) BFToast.dismiss(toastId); toastId = null; };

              const renderToast = (secsLeft) => {
                clearToast();
                toastId = BFToast.show({
                  kind: 'error',
                  title: t('toast.deleting_title'),
                  body:  t('toast.deleting_body', { n: secsLeft }),
                  duration: 0, // we control dismissal
                  action: {
                    label: t('common.undo'),
                    onClick: () => {
                      cancelled = true;
                      clearToast();
                      BFToast.info(t('toast.deletion_cancelled_title'), t('toast.deletion_cancelled_body'));
                    },
                  },
                });
              };
              renderToast(UNDO_SEC);
              for (let i = UNDO_SEC - 1; i >= 1 && !cancelled; i--) {
                await new Promise(r => setTimeout(r, 1000));
                if (cancelled) break;
                renderToast(i);
              }
              if (cancelled) return;
              await new Promise(r => setTimeout(r, 1000));
              clearToast();

              try {
                const sess = BFAuth.getSession();
                const token = sess && sess.access_token;
                if (!token) throw new Error('No session token');
                const r = await fetch('/api/delete-account', {
                  method: 'POST',
                  headers: { 'authorization': 'Bearer ' + token },
                });
                if (!r.ok) {
                  let detail = '';
                  try { const j = await r.json(); detail = j && j.error ? j.error : ''; } catch (e) {}
                  throw new Error('HTTP ' + r.status + (detail ? ' \u2014 ' + detail : ''));
                }
                // Scrub local caches so a future guest session starts clean.
                try {
                  await BFStorage.remove('sessions');
                  await BFStorage.remove('bolt.scores');
                  await BFStorage.remove('milestones.unlocked');
                  await BFStorage.remove('milestones.patternsSeen');
                  await BFStorage.remove('streakTiers.unlocked');
                  localStorage.removeItem('bf.auth.cachedUser');
                } catch (e) {}
                try { await BFAuth.signOut(); } catch (e) {}
                if (typeof BFToast !== 'undefined') BFToast.success(t('toast.account_deleted_title'), t('toast.account_deleted_body'));
              } catch (e) {
                console.warn('[delete-account] failed', e);
                if (typeof BFToast !== 'undefined') BFToast.error(t('toast.delete_error_title'), t('toast.delete_error_body'));
              }
            }}>
              <BFIcon name="chevron" size={14} color={theme.fgFaint} />
            </Row>
          </BFCard>
        </>
      )}

      {/* Version footer — helps confirm a fresh deploy landed past the SW cache. */}
      <div style={{
        textAlign: 'center', marginTop: 24,
        fontFamily: BF_FONTS.mono, fontSize: 11, color: theme.fgFaint, letterSpacing: 0.5,
      }}>
        BreatheFlow {typeof window !== 'undefined' && window.BF_VERSION ? window.BF_VERSION : ''}
      </div>

      {helpOpen && (
        <BFHelp theme={theme} accentH={accentH} onClose={() => setHelpOpen(false)} />
      )}
    </div>
  );
}

// ── Session complete screen ─────────────────────────────────────────
function BFComplete({ theme, accentH, pattern, duration, cycles, voice = true, voicePack = 'en-drew', onDone }) {
  // "Session complete" voice cue. Routes through the pack manifest:
  // tries the pack's mp3, falls back to BFAudio.speak() in the pack's
  // locale so the cue is spoken in the user's language whether or not
  // mp3s have been generated. Fires once on mount.
  React.useEffect(() => {
    if (!voice) return;
    let cancelled = false;
    const packId = (window.BFVoicePacks && window.BFVoicePacks.migrateLegacyPackId)
      ? window.BFVoicePacks.migrateLegacyPackId(voicePack)
      : (voicePack || 'en-drew');
    const pack = (window.BF_VOICE_PACKS || {})[packId];
    const locale = pack ? pack.locale : 'en';
    const ttsLang = pack ? pack.ttsLang : null;
    const ttsText = window.BFVoicePacks ? window.BFVoicePacks.cueText('complete', locale) : 'Well done';
    const speak = () => { if (!cancelled) BFAudio.speak(ttsText, ttsLang ? { lang: ttsLang } : undefined).catch(() => {}); };
    const clipUrl = window.BFVoicePacks ? window.BFVoicePacks.cueUrl(packId, 'complete') : null;
    if (clipUrl) {
      BFAudio.preload('voice.complete', clipUrl, { volume: 0.9 })
        .then(() => { if (!cancelled) BFAudio.play('voice.complete', { restart: true }).catch(speak); })
        .catch(speak);
    } else {
      speak();
    }
    return () => { cancelled = true; };
  }, [voice, voicePack]);
  const t = (k, p) => (window.BFI18n ? window.BFI18n.t(k, p) : k);
  return (
    <div style={{ position: 'absolute', inset: 0, background: `radial-gradient(ellipse at 50% 30%, ${bfIsLightTheme(theme) ? `oklch(0.92 0.06 ${accentH} / 0.7)` : `oklch(0.32 0.05 ${accentH} / 0.7)`}, ${theme.bg})`, color: theme.fg, fontFamily: BF_FONTS.sans, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 30 }}>
      <BreathingVisualizer style="flower" amplitude={0.95} accentH={accentH} size={200} theme={theme} />
      <div style={{ fontFamily: BF_FONTS.serif, fontSize: 36, fontStyle: 'italic', marginTop: 30, letterSpacing: -0.4, textAlign: 'center' }}>{t('complete.well_done')}</div>
      <div style={{ fontSize: 14, color: theme.fgMuted, marginTop: 8, textAlign: 'center', maxWidth: 260 }}>
        {t('complete.summary', { cycles: cycles, pattern: bfPatternName(pattern) })}
      </div>
      <div style={{ display: 'flex', gap: 30, marginTop: 32, fontFamily: BF_FONTS.mono }}>
        <div style={{ textAlign: 'center' }}>
          <div style={{ fontSize: 10, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint }}>{t('complete.duration')}</div>
          <div style={{ fontSize: 20, marginTop: 3 }}>{bfFmt(duration)}</div>
        </div>
        <div style={{ textAlign: 'center' }}>
          <div style={{ fontSize: 10, letterSpacing: 1, textTransform: 'uppercase', color: theme.fgFaint }}>{t('complete.cycles')}</div>
          <div style={{ fontSize: 20, marginTop: 3 }}>{cycles}</div>
        </div>
      </div>
      <div style={{ display: 'flex', gap: 10, marginTop: 40 }}>
        <button
          onClick={() => BFShare.session({ patternName: bfPatternName(pattern), durationSeconds: duration, cycles: cycles })}
          style={{
            padding: '14px 22px', borderRadius: 999, cursor: 'pointer',
            background: theme.cardSoft, color: theme.fg,
            border: `1px solid ${theme.line}`,
            fontFamily: BF_FONTS.sans, fontSize: 14, fontWeight: 500,
            display: 'inline-flex', alignItems: 'center', gap: 8,
          }}
        >
          <BFIcon name="arrow" size={16} /> {t('complete.share')}
        </button>
        <button onClick={onDone} style={{
          padding: '14px 36px', borderRadius: 999, border: 'none', cursor: 'pointer',
          background: bfAccentSolid(accentH), color: bfAccentSolidFg(),
          fontSize: 14, fontWeight: 600,
        }}>{t('complete.close')}</button>
      </div>
    </div>
  );
}

Object.assign(window, { BFHome, BFFlow, BFProgress, BFSettings, BFPresetDetail, BFComplete, BFFamilyPicker, BFFamilyDashboard });
