/* ============================================================
   MUIY 2.0 — app shell: store, nav, sheets, routing
   Loads LAST. Consumes screens from window.
   ============================================================ */

/* ---------- Tweaks: three expressive feel-axes ----------
   Each control reshapes a whole dimension of mood, not one pixel:
   · Accent  — retints every interactive surface (buttons, plus, chips, links)
   · Softness — co-moves corner radius AND shadow depth (crisp ↔ pillowy)
   · Scene   — swaps the illustrated hero that sets the emotional backdrop
*/
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": ["#FC6B05", "#FF8A2B", "#FFEFE2"],
  "softness": "Soft",
  "scene": "living",
  "monLayout": "Triage",
  "typeWeight": "Refined",
  "accentUse": "Restrained",
  "todayRhythm": "Tight",
  "sparklines": true
}/*EDITMODE-END*/;

const TC_ACCENTS = {
  citrus: { o: '#FC6B05', o2: '#FF8A2B', wash: '#FFEFE2', pop: '252,107,5' },
  grove:  { o: '#57A012', o2: '#74C328', wash: '#E9F5DA', pop: '87,160,18' },
  lagoon: { o: '#1C96A0', o2: '#37B4BD', wash: '#DFF3F4', pop: '28,150,160' },
  berry:  { o: '#DE3F7C', o2: '#F0689A', wash: '#FBE4ED', pop: '222,63,124' },
  crimson:{ o: '#E0322F', o2: '#F25C57', wash: '#FCE5E4', pop: '224,50,47' },
};
const TC_ACCENT_OPTS = [
  ['#FC6B05', '#FF8A2B', '#FFEFE2'],
  ['#57A012', '#74C328', '#E9F5DA'],
  ['#1C96A0', '#37B4BD', '#DFF3F4'],
  ['#DE3F7C', '#F0689A', '#FBE4ED'],
  ['#E0322F', '#F25C57', '#FCE5E4'],
];

/* Status-bar clock — mirrors the real device time, ticking each minute. */
function LiveClock() {
  const fmt = () => {
    const d = (window.TC && TC.now) ? TC.now() : new Date();
    let h = d.getHours();
    const m = String(d.getMinutes()).padStart(2, '0');
    h = h % 12; if (h === 0) h = 12;
    return `${h}:${m}`;
  };
  const [t, setT] = React.useState(fmt);
  React.useEffect(() => {
    const id = setInterval(() => setT(fmt()), 15000);
    return () => clearInterval(id);
  }, []);
  return <>{t}</>;
}
const TC_ACCENT_KEY = {
  '["#FC6B05","#FF8A2B","#FFEFE2"]': 'citrus',
  '["#57A012","#74C328","#E9F5DA"]': 'grove',
  '["#1C96A0","#37B4BD","#DFF3F4"]': 'lagoon',
  '["#DE3F7C","#F0689A","#FBE4ED"]': 'berry',
  '["#E0322F","#F25C57","#FCE5E4"]': 'crimson',
};
const TC_SOFT = {
  Crisp: {
    sm: '9px', r: '12px', lg: '16px', xl: '20px',
    shSm: '0 1px 2px rgba(120,80,20,.06), 0 4px 9px -8px rgba(120,80,20,.34)',
    sh:   '0 1px 2px rgba(120,80,20,.06), 0 11px 20px -16px rgba(120,80,20,.45)',
    shSmD: '0 1px 2px rgba(0,0,0,.5), 0 4px 9px -8px rgba(0,0,0,.62)',
    shD:   '0 1px 2px rgba(0,0,0,.5), 0 11px 20px -16px rgba(0,0,0,.72)',
  },
  Soft: {
    sm: '14px', r: '18px', lg: '24px', xl: '30px',
    shSm: '0 1px 2px rgba(120,80,20,.05), 0 8px 18px -14px rgba(120,80,20,.4)',
    sh:   '0 2px 4px rgba(120,80,20,.05), 0 20px 38px -26px rgba(120,80,20,.5)',
    shSmD: '0 1px 2px rgba(0,0,0,.45), 0 8px 18px -14px rgba(0,0,0,.7)',
    shD:   '0 2px 4px rgba(0,0,0,.45), 0 20px 38px -26px rgba(0,0,0,.8)',
  },
  Pillowy: {
    sm: '18px', r: '26px', lg: '34px', xl: '44px',
    shSm: '0 2px 6px rgba(120,80,20,.08), 0 16px 30px -16px rgba(120,80,20,.5)',
    sh:   '0 5px 12px rgba(120,80,20,.08), 0 36px 64px -28px rgba(120,80,20,.62)',
    shSmD: '0 2px 6px rgba(0,0,0,.5), 0 16px 30px -16px rgba(0,0,0,.8)',
    shD:   '0 5px 12px rgba(0,0,0,.55), 0 36px 64px -28px rgba(0,0,0,.88)',
  },
};
const TC_SCENE_OPTS = [
  { value: 'living', label: 'Living sky' },
  { value: 'sprout', label: 'Sprout' },
  { value: 'sunrise', label: 'Sunrise' },
  { value: 'lagoon', label: 'Lagoon' },
  { value: 'bloom', label: 'Bloom' },
  { value: 'midnight', label: 'Starry night' },
];

function App() {
  const [scale, setScale] = React.useState(1);
  React.useEffect(() => {
    const fit = () => setScale(Math.min((window.innerWidth - 24) / 390, (window.innerHeight - 24) / 844, 1.2));
    window.addEventListener('resize', fit); fit();
    return () => window.removeEventListener('resize', fit);
  }, []);

  // first-run / signed-out gate: a brand-new visitor (no saved store) lands on
  // the welcome flow; returning visitors go straight to the app. Initialised
  // BEFORE the data store below, so it reads localStorage before we seed it.
  const [appPhase, setAppPhase] = React.useState(() => {
    const s = TC.loadStore();
    return s && s.settings && s.settings.setupComplete ? 'app' : 'welcome';
  });

  const [data, setData] = React.useState(() => {
    let s = TC.loadStore();
    // Brand-new install (no saved store): boot a genuinely EMPTY profile and let
    // the first-run onboarding flow fill it. Never seed sample data on first run.
    if (!s) return TC.emptyData();
    // ---- returning visitor: migrate / back-fill the existing store ----
    if (!s.settings) s.settings = {};
    if (s.settings.setupComplete == null) s.settings.setupComplete = true;
    if (!s.settings.sceneVariant) s.settings.sceneVariant = 'sprout';
    // Sync/export state back-fill for stores seeded before honest-sync landed.
    // Retire the dead trust-theatre fields outright — no migration of fakes.
    if (s.settings.pendingChanges == null) s.settings.pendingChanges = 0;
    if (s.settings.lastSyncedAt === undefined) s.settings.lastSyncedAt = null;
    if (s.settings.lastExportedAt === undefined) s.settings.lastExportedAt = null;
    delete s.settings.autoBackup;
    delete s.settings.lastBackedUp;
    delete s.settings.isSampleData;
    // monitoring engine migration: ensure profile attrs + richer test logs exist
    if (s.patient && !s.patient.monitoring) s.patient.monitoring = { sex: 'male', thalType: 'TDT', puberty: true, splenectomy: false, age: null };
    if (!Array.isArray(s.testLogs)) s.testLogs = [];
    // One-time ID migration: rewrite any stored testLogs onto canonical
    // registry ids + canonical units (Hb g/L → g/dL), collapsing duplicates.
    // Idempotent — canonical logs pass straight through.
    if (window.MON && Array.isArray(s.testLogs)) s.testLogs = MON.migrateTestLogs(s.testLogs);
    if (s.tests) delete s.tests;   // drop the obsolete legacy test-registry copy
    if (!s.visits) s.visits = [];
    // appointments: future-facing twin of visits (set date + reminder). Back-fill
    // for older stores, then roll any recurring appt past its date forward to the
    // next occurrence so a standing review always shows its NEXT date.
    if (!Array.isArray(s.appointments)) s.appointments = [];
    if (window.TC && TC.appts) TC.appts.rollForward(s);
    TC.syncClock(s);
    return s;
  });
  const [tab, setTab] = React.useState('today');
  const [healthTab, setHealthTab] = React.useState('records');
  // a transient focus request handed to a Health sub-screen (e.g. scroll to the
  // transfusion forecast, or auto-expand the ferritin test in Monitoring)
  const [healthFocus, setHealthFocus] = React.useState(null);
  const [modal, setModal] = React.useState(null);
  const [toast, setToast] = React.useState(null);
  const [poolFilter, setPoolFilter] = React.useState(null);

  // live network state — drives the offline queue + sync status (REAL signal)
  const [online, setOnline] = React.useState(typeof navigator !== 'undefined' ? navigator.onLine !== false : true);

  // resolved colour theme (light / dark / system)
  const [sysDark, setSysDark] = React.useState(false);
  React.useEffect(() => {
    try {
      const mq = window.matchMedia('(prefers-color-scheme: dark)');
      const on = () => setSysDark(mq.matches); on();
      mq.addEventListener ? mq.addEventListener('change', on) : mq.addListener(on);
      return () => { mq.removeEventListener ? mq.removeEventListener('change', on) : mq.removeListener(on); };
    } catch (e) {}
  }, []);
  const darkMode = (data.settings && data.settings.darkMode) || 'light';
  // ---- named theme (Citrus / Lagoon / Midnight) ----
  // The theme bundles a light/dark base (data-theme, drives existing CSS) with
  // an accent + an optional cool palette (data-palette, drives app.css block).
  const themeId = TC.resolveThemeId(data.settings, sysDark);
  const themeDef = TC.themeById(themeId);
  const theme = themeDef.base; // 'light' | 'dark' — what existing CSS keys off

  // ---- Tweaks: accent / softness / scene ----
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // Accent priority: an explicit tweak-panel accent override wins; otherwise
  // the active theme supplies its accent (Citrus→orange, Lagoon→teal, …).
  const tweakAccentJSON = JSON.stringify(tw.accent);
  const accentKey = (tweakAccentJSON !== JSON.stringify(TWEAK_DEFAULTS.accent))
    ? (TC_ACCENT_KEY[tweakAccentJSON] || 'citrus')
    : themeDef.accent;
  const ac = TC_ACCENTS[accentKey] || TC_ACCENTS.citrus;
  // Stamp the active accent so theme-derived art (donor avatars, etc.) can tint
  // itself to the chosen identity. Set during render so children paint in sync.
  if (window.TC) TC.__activeAccent = accentKey;
  const sf = TC_SOFT[tw.softness] || TC_SOFT.Soft;
  const isDark = theme === 'dark';
  // The accent + softness tweaks inject custom properties onto .tc-app, which
  // sits INSIDE .screen[data-theme]. Inline custom props beat the dark theme's
  // variable block for the whole subtree, so in dark mode we must hand these
  // the DARK-correct values ourselves: lift the accent off charcoal, derive a
  // deep tinted wash from the accent hue (never a pale light wash), and use
  // black-based shadows instead of the warm-brown light ones. Derivations use
  // the accent's own hue so any accent — including custom — stays in sync.
  const tweakVars = isDark ? {
    '--orange': `color-mix(in oklab, ${ac.o} 80%, #ffffff)`,
    '--orange-2': `color-mix(in oklab, ${ac.o2} 82%, #ffffff)`,
    '--orange-wash': `color-mix(in oklab, ${ac.o} 22%, #211e17)`,
    '--soon': `color-mix(in oklab, ${ac.o} 80%, #ffffff)`,
    '--soon-wash': `color-mix(in oklab, ${ac.o} 22%, #211e17)`,
    '--sh-pop': `0 14px 30px -10px rgba(${ac.pop},.5)`,
    '--r-sm': sf.sm, '--r': sf.r, '--r-lg': sf.lg, '--r-xl': sf.xl,
    '--sh-sm': sf.shSmD, '--sh': sf.shD,
  } : {
    '--orange': ac.o, '--orange-2': ac.o2, '--orange-wash': ac.wash,
    '--soon': ac.o,
    '--sh-pop': `0 14px 30px -10px rgba(${ac.pop},.45)`,
    '--r-sm': sf.sm, '--r': sf.r, '--r-lg': sf.lg, '--r-xl': sf.xl,
    '--sh-sm': sf.shSm, '--sh': sf.sh,
  };
  // refinement passes — each appends a class the override sheet (refine.css) targets
  const refineCls =
    (tw.typeWeight === 'Refined' ? ' type-refined' : '') +
    (tw.accentUse === 'Restrained' ? ' accent-calm' : '') +
    (tw.todayRhythm === 'Tight' ? ' today-tight' : '');

  React.useEffect(() => { TC.saveStore(data); }, [data]);

  // ---- Honest sync engine ----------------------------------------------
  // The durable store is localStorage on THIS device; every mutation is
  // committed there immediately. "Sync" reflects whether that commit has
  // happened and whether the device is online to reconcile the queue.
  // Nothing here is timer-driven: lastSyncedAt is only ever stamped at the
  // real moment a commit/flush completes.
  const mutate = React.useCallback((fn) => setData((d) => {
    const n = JSON.parse(JSON.stringify(d));
    fn(n);
    if (!n.settings) n.settings = {};
    if (typeof navigator !== 'undefined' && navigator.onLine === false) {
      // queued locally; reconciliation deferred until the device reconnects
      n.settings.pendingChanges = (n.settings.pendingChanges || 0) + 1;
    } else {
      n.settings.pendingChanges = 0;
      n.settings.lastSyncedAt = new Date().toISOString();
    }
    return n;
  }), []);

  // Drain the offline queue on reconnect (and stamp the first real sync on a
  // fresh store). Only writes a timestamp when there is genuinely something to
  // flush, so an untouched session keeps its true last-sync time.
  React.useEffect(() => {
    const flush = () => setData((d) => {
      if (typeof navigator !== 'undefined' && navigator.onLine === false) return d;
      if (!d.settings) return d;
      const needs = (d.settings.pendingChanges || 0) > 0 || !d.settings.lastSyncedAt;
      if (!needs) return d;
      const n = JSON.parse(JSON.stringify(d));
      n.settings.pendingChanges = 0;
      n.settings.lastSyncedAt = new Date().toISOString();
      return n;
    });
    const goOnline = () => { setOnline(true); flush(); };
    const goOffline = () => setOnline(false);
    window.addEventListener('online', goOnline);
    window.addEventListener('offline', goOffline);
    if (typeof navigator === 'undefined' || navigator.onLine !== false) flush();
    return () => { window.removeEventListener('online', goOnline); window.removeEventListener('offline', goOffline); };
  }, []);

  const showToast = React.useCallback((msg, icon) => { setToast({ msg, icon }); setTimeout(() => setToast(null), 2400); }, []);

  // ---- appointment reminders: (re)arm the next 24h whenever appointments or the
  // appointment-reminder preference change. Honest, near-term only (mirrors the
  // chelation reminder layer); gated by the global appointment toggle. ----
  const apptRemPref = data.settings && data.settings.reminders ? data.settings.reminders.appointment : true;
  React.useEffect(() => {
    try { if (window.TC && TC.appts) TC.appts.scheduleReminders(data, apptRemPref !== false); } catch (e) {}
  }, [data.appointments, apptRemPref]);

  // ---- Donor sync pulse ------------------------------------------------
  // A header indicator that animates whenever the donor list materially
  // changes (add / edit / delete / import / defer / log donation / log
  // contact). We watch a signature of the donor data so EVERY code path
  // that mutates a donor triggers it, with no per-call instrumentation.
  const [donorSync, setDonorSync] = React.useState('idle'); // idle|syncing|synced|error
  const _donorSigRef = React.useRef(null);
  const _syncTimers = React.useRef([]);
  React.useEffect(() => {
    const sig = (data.donors || []).map((d) =>
      d.id + '|' + d.name + '|' + d.mobile + '|' + d.bloodGroup + '|' + (d.notes || '') +
      '|' + (d.archived ? 1 : 0) + (d.deferred ? 1 : 0) + (d.deferReturn || '') +
      '|' + (d.donations || []).map((z) => z.id + z.date + z.method + (z.hbValue || '')).join(',') +
      '|' + (d.contactLog || []).length
    ).join('~');
    if (_donorSigRef.current === null) { _donorSigRef.current = sig; return; } // skip first mount
    if (sig === _donorSigRef.current) return;
    _donorSigRef.current = sig;
    _syncTimers.current.forEach(clearTimeout); _syncTimers.current = [];
    const offline = typeof navigator !== 'undefined' && navigator.onLine === false;
    setDonorSync('syncing');
    _syncTimers.current.push(setTimeout(() => {
      if (offline) {
        setDonorSync('error');
        _syncTimers.current.push(setTimeout(() => setDonorSync('idle'), 5200));
      } else {
        setDonorSync('synced');
        _syncTimers.current.push(setTimeout(() => setDonorSync('idle'), 1700));
      }
    }, 820));
  }, [data.donors]);
  React.useEffect(() => () => { _syncTimers.current.forEach(clearTimeout); }, []);
  const openModal = React.useCallback((type, props = {}) => setModal({ type, props }), []);
  const closeModal = React.useCallback(() => setModal(null), []);
  const goPool = React.useCallback((f) => { setPoolFilter(f || null); setTab('pool'); }, []);
  const goHealth = React.useCallback((sub, focus) => { if (sub === 'trends') sub = 'records'; if (sub) setHealthTab(sub); setHealthFocus(focus || null); setTab('health'); }, []);

  // ---- chelation adherence: log / undo a scheduled dose (one writer; offline-queued via mutate) ----
  const logDose = React.useCallback(({ medId, slot, dateStr, status, skipReason, note }) => {
    const key = `${dateStr}|${medId}|${slot}`;
    const iso = new Date().toISOString();
    mutate((d) => {
      if (!d.chelationLog) d.chelationLog = {};
      d.chelationLog[key] = { status, skipReason: skipReason || null, note: note || '', scheduledDatetime: dateStr + 'T' + slot, loggedDatetime: iso, _fresh: true };
    });
    if (status === 'taken') {
      showToast('Dose logged — gently done', '🌱');
      if (window.tcCelebrate) window.tcCelebrate();
    } else {
      showToast('Noted as skipped', '🤍');
    }
  }, [mutate, showToast]);
  const clearDose = React.useCallback((key) => {
    mutate((d) => { if (d.chelationLog) delete d.chelationLog[key]; });
  }, [mutate]);
  const setReminders = React.useCallback((next) => {
    mutate((d) => { if (!d.settings) d.settings = {}; d.settings.reminders = next; });
    try { if (window.TC.reminders) window.TC.reminders.scheduleReminders(next, data.patient.medications); } catch (e) {}
  }, [mutate, data.patient.medications]);

  // Scene: the Tweaks-panel "Scene" override wins only when it's been changed
  // from its default; otherwise the saved Home-illustration setting (Scene
  // picker) drives the Today backdrop.
  const effScene = (JSON.stringify(tw.scene) !== JSON.stringify(TWEAK_DEFAULTS.scene))
    ? tw.scene
    : ((data.settings && data.settings.sceneVariant) || tw.scene);
  const effSettings = Object.assign({}, data.settings, { sceneVariant: effScene });

  // Build the portable backup payload — the SAME complete object that powers a
  // restore and feeds the clinic PDF. Versioned + wrapped so imports can be
  // validated and round-tripped.
  const buildExport = React.useCallback(() => {
    const snapshot = JSON.parse(JSON.stringify(data));
    if (snapshot.settings) { delete snapshot.settings.pendingChanges; }
    return {
      _format: 'mui.backup',
      _schema: 1,
      _appVersion: TC.APP_VERSION,
      _exportedAt: new Date().toISOString(),
      data: snapshot,
    };
  }, [data]);

  const exportData = React.useCallback(() => {
    const payload = buildExport();
    const iso = payload._exportedAt;
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `mui-backup-${iso.slice(0, 10)}.json`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
    // record the REAL export time
    mutate((d) => { d.settings.lastExportedAt = iso; });
    showToast('Backup exported', '📁');
  }, [buildExport, mutate, showToast]);

  // Parse + validate a backup file. Accepts the wrapped format and a bare data
  // object (older exports). Returns { ok, data, error, meta }.
  const parseBackup = React.useCallback((text) => {
    let parsed;
    try { parsed = JSON.parse(text); } catch (e) { return { ok: false, error: 'That file isn’t valid JSON.' }; }
    const payload = parsed && parsed._format === 'mui.backup' && parsed.data ? parsed.data : parsed;
    if (!payload || typeof payload !== 'object' || !payload.patient || !payload.settings || !Array.isArray(payload.donors)) {
      return { ok: false, error: 'This doesn’t look like a MUIY backup.' };
    }
    return {
      ok: true,
      data: payload,
      meta: { exportedAt: parsed && parsed._exportedAt, appVersion: parsed && parsed._appVersion,
        donors: (payload.donors || []).length, transfusions: (payload.transfusions || []).length },
    };
  }, []);

  const restoreData = React.useCallback((payload) => {
    setData(() => {
      const n = JSON.parse(JSON.stringify(payload));
      if (!n.settings) n.settings = {};
      n.settings.pendingChanges = 0;
      n.settings.lastSyncedAt = new Date().toISOString(); // restore just committed
      TC.syncClock(n);
      return n;
    });
    setModal(null); setPoolFilter(null); setTab('today');
    showToast('Backup restored', '🌱');
  }, [showToast]);

  const store = {
    data, mutate, showToast, openModal, closeModal,
    patient: data.patient, settings: effSettings, donors: data.donors,
    sync: { online, pending: (data.settings && data.settings.pendingChanges) || 0, lastSyncedAt: (data.settings && data.settings.lastSyncedAt) || null },
    donorSync,
    exportData, parseBackup, restoreData,
    tab, setTab, goPool, poolFilter, setPoolFilter, healthTab, setHealthTab, goHealth,
    healthFocus, setHealthFocus,
    logDose, clearDose, setReminders, reminders: (data.settings && data.settings.reminders) || null,
    monLayout: tw.monLayout,
    sparklines: tw.sparklines !== false,
    signOut: () => {
      // Cloud mode: end the Supabase session + wipe the local cache (then reload).
      if (window.TCCloud && window.TCsb && window.TCsb.isConfigured()) { window.TCCloud.signOutAndClear(); return; }
      setModal(null); setTab('today'); setAppPhase('welcome');
    },
  };

  const screens = { today: TodayScreen, pool: PoolScreen, health: HealthScreen, me: MeScreen };
  const Screen = screens[tab] || TodayScreen;

  // ---- welcome / onboarding handlers ----
  const handleSignIn = React.useCallback(() => { setAppPhase('app'); setTab('today'); }, []);
  const handleComplete = React.useCallback((form) => {
    setData((d) => {
      const n = JSON.parse(JSON.stringify(d));
      const p = n.patient;
      p.name = (form.name || '').trim();
      p.preferredName = (form.preferredName || '').trim() || p.name.split(/\s+/)[0];
      p.dob = form.dob;
      p.sex = form.sex;
      p.bloodGroup = form.bloodGroup;
      p.thalType = form.thalType;
      const w = parseFloat(form.weight);
      if (w > 0) p.weightKg = Math.round(w * 10) / 10;
      p.treatmentCenter = (form.treatmentCenter || '').trim() || null;
      // usual transfusion interval → label + weeks (seeds the forecast on day one)
      const intMeta = (typeof WF_INTERVALS !== 'undefined' ? WF_INTERVALS : []).find((it) => it.v === form.interval);
      p.usualTransfusionInterval = form.interval || null;
      if (intMeta && intMeta.w) p.transfusionFrequencyWeeks = intMeta.w;
      p.monitoring = Object.assign({}, p.monitoring, {
        sex: form.sex,
        thalType: form.transfusionStatus,
        splenectomy: !!form.splenectomy,
      });
      // record explicit medical-data consent
      p.medicalConsent = { agreed: true, at: new Date().toISOString() };
      // reminders & notification preferences
      const rem = form.reminders || {};
      n.settings.reminders = {
        transfusion: rem.transfusion !== false,
        medication: rem.medication !== false,
        appointment: rem.appointment !== false,
      };
      n.settings.notificationsEnabled = rem.push !== false;
      n.settings.setupComplete = true;
      return n;
    });
    setAppPhase('app'); setTab('today');
  }, []);

  return (
    <StoreCtx.Provider value={store}>
      <div className="desk">
        <div className="device-scale" style={{ transform: `scale(${scale})` }}>
          <div className="phone"><div className="screen" data-theme={theme} data-palette={themeId}>
            <div className="statusbar"><span><LiveClock /></span><span className="sb-r"><svg width="17" height="11" viewBox="0 0 18 11"><rect x="0" y="6" width="3" height="5" rx="1"/><rect x="5" y="4" width="3" height="7" rx="1"/><rect x="10" y="2" width="3" height="9" rx="1"/><rect x="15" y="0" width="3" height="11" rx="1"/></svg><svg width="16" height="11" viewBox="0 0 24 18"><path d="M12 16 1 5a15 15 0 0 1 22 0z" fill="none" stroke="currentColor" strokeWidth="2.4"/></svg><svg width="23" height="11" viewBox="0 0 26 13"><rect x="1" y="1" width="20" height="11" rx="3" fill="none" stroke="currentColor" strokeWidth="1.5"/><rect x="3" y="3" width="15" height="7" rx="1.5" fill="currentColor"/><rect x="22.5" y="4" width="2" height="5" rx="1" fill="currentColor"/></svg></span></div>
            <div className="notch" />
            {appPhase === 'welcome' ? (
              <WelcomeFlow store={store} patient={data.patient} onSignIn={handleSignIn} onComplete={handleComplete} />
            ) : (
            <div className={'tc-app' + refineCls} data-screen-label={tab} style={tweakVars}>
              <div key={tab}>
                <Screen store={store} />
              </div>
              <NavBar store={store} />
              <Modals store={store} modal={modal} />
              {toast && <div className="toast" role="status" aria-live="polite">{toast.icon && <span style={{ fontSize: 16 }}>{toast.icon}</span>}{toast.msg}</div>}
            </div>
            )}
          </div></div>
        </div>
      </div>
      <TweaksPanel title="Tweaks">
        <TweakSection label="Color" />
        <TweakColor label="Accent" value={tw.accent} options={TC_ACCENT_OPTS}
          onChange={(v) => setTweak('accent', v)} />
        <TweakSection label="Form" />
        <TweakRadio label="Softness" value={tw.softness} options={['Crisp', 'Soft', 'Pillowy']}
          onChange={(v) => setTweak('softness', v)} />
        <TweakSection label="Atmosphere" />
        <TweakSelect label="Scene" value={tw.scene} options={TC_SCENE_OPTS}
          onChange={(v) => setTweak('scene', v)} />
        <TweakSection label="Monitoring" />
        <TweakRadio label="Tests layout" value={tw.monLayout} options={['Triage', 'Cards', 'Status']}
          onChange={(v) => setTweak('monLayout', v)} />
        <TweakSection label="Refinement" />
        <TweakRadio label="Type weight" value={tw.typeWeight} options={['Bold', 'Refined']}
          onChange={(v) => setTweak('typeWeight', v)} />
        <TweakRadio label="Accent use" value={tw.accentUse} options={['Vivid', 'Restrained']}
          onChange={(v) => setTweak('accentUse', v)} />
        <TweakRadio label="Today rhythm" value={tw.todayRhythm} options={['Roomy', 'Tight']}
          onChange={(v) => setTweak('todayRhythm', v)} />
        <TweakToggle label="Trend sparklines" value={tw.sparklines}
          onChange={(v) => setTweak('sparklines', v)} />
      </TweaksPanel>
    </StoreCtx.Provider>
  );
}

/* ---------- bottom nav ---------- */
function NavBar({ store }) {
  const { tab, setTab, openModal } = store;
  const items = [
    { id: 'today', icon: 'home', label: 'Today' },
    { id: 'pool', icon: 'users', label: 'Pool' },
    { id: '__plus' },
    { id: 'health', icon: 'chart', label: 'Health' },
    { id: 'me', icon: 'user', label: 'Me' },
  ];
  return (
    <div className="nav-wrap"><div className="nav">
      {items.map((it) => it.id === '__plus' ? (
        <button key="p" className="plus" onClick={() => openModal('quick', {})} aria-label="Add"><Icon name="plus" size={26} color="#fff" sw={2.6} /></button>
      ) : (
        <button key={it.id} className={tab === it.id ? 'on' : ''} aria-current={tab === it.id ? 'page' : undefined} onClick={() => setTab(it.id)}>
          <Icon name={it.icon} size={22} sw={tab === it.id ? 2.4 : 2} />
          <span className="lbl">{it.label}</span>
        </button>
      ))}
    </div></div>
  );
}

/* ---------- sheet primitives ---------- */
function Sheet({ title, sub, children, onClose }) {
  return (
    <div className="sheet-backdrop" onClick={onClose}>
      <div className="sheet" onClick={(e) => e.stopPropagation()}>
        <div className="sheet-grip" />
        {title && <div className="sheet-title">{title}</div>}
        {sub && <div className="body" style={{ marginBottom: 8 }}>{sub}</div>}
        <div style={{ marginTop: title ? 10 : 0 }}>{children}</div>
      </div>
    </div>
  );
}

function fillTpl(tpl, donor, patient) {
  return (tpl || '').replace(/\[Donor Name\]/g, (donor || '').split(' ')[0] || 'friend').replace(/\[Patient Name\]/g, patient || '');
}

function ContactSheet({ store, close, donor }) {
  const tpl = (store.patient.messageTemplates || {}).donationRequest || 'Hi [Donor Name], would you be able to donate blood for me soon? 🙏❤️\n— [Patient Name]';
  const [msg, setMsg] = React.useState(fillTpl(tpl, donor.name, store.patient.name));
  const wa = () => { window.open(`https://wa.me/${donor.mobile.replace(/\D/g, '')}?text=${encodeURIComponent(msg)}`, '_blank'); close(); store.showToast('Opening WhatsApp', '🟢'); };
  const sms = () => { window.location.href = `sms:${donor.mobile}?&body=${encodeURIComponent(msg)}`; close(); };
  return (
    <Sheet title={`Message ${donor.name.split(' ')[0]}`} onClose={close}>
      <button className="btn btn-ghost" style={{ width: '100%', marginBottom: 12 }} onClick={() => { window.location.href = `tel:${donor.mobile}`; }}><Icon name="phone" size={18} color="var(--green)" /> Call {donor.mobile}</button>
      <label className="field-label">Message</label>
      <textarea className="input textarea" rows={5} value={msg} onChange={(e) => setMsg(e.target.value)} />
      <div className="row" style={{ gap: 10, marginTop: 12 }}>
        <button className="btn" style={{ flex: 1, background: '#25D366', color: '#fff' }} onClick={wa}><Icon name="whatsapp" size={18} color="#fff" /> WhatsApp</button>
        <button className="btn btn-ghost" style={{ flex: 1 }} onClick={sms}><Icon name="sms" size={18} /> SMS</button>
      </div>
    </Sheet>
  );
}

function ShareDonorSheet({ store, close, donor }) {
  const text = `${donor.name}\n${donor.mobile}` + (donor.bloodGroup ? `\nBlood group: ${donor.bloodGroup}` : '');
  const copy = async () => {
    try {
      if (navigator.clipboard && navigator.clipboard.writeText) await navigator.clipboard.writeText(text);
      else { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); }
      store.showToast('Contact copied', '📋');
    } catch (e) { store.showToast('Couldn’t copy', '⚠️'); }
    close();
  };
  const shareNative = async () => {
    try { if (navigator.share) { await navigator.share({ title: donor.name, text }); close(); } else { await copy(); } }
    catch (e) { close(); }
  };
  const wa = () => { window.open(`https://wa.me/?text=${encodeURIComponent(text)}`, '_blank'); close(); store.showToast('Sharing via WhatsApp', '🟢'); };
  const Item = ({ icon, bg, color, title, desc, onClick }) => (
    <button className="qa-item" onClick={onClick}>
      <div className="qa-ic" style={{ background: bg, color }}><Icon name={icon} size={22} color={color} /></div>
      <div style={{ flex: 1, textAlign: 'left', minWidth: 0 }}><div style={{ fontWeight: 800, fontSize: 15 }}>{title}</div><div className="body" style={{ fontSize: 12.5 }}>{desc}</div></div>
      <Icon name="chevR" size={18} color="var(--ink-3)" />
    </button>
  );
  return (
    <Sheet title="Share contact" onClose={close}>
      <div className="card-flat" style={{ padding: '13px 15px', marginBottom: 14, display: 'flex', gap: 12, alignItems: 'center' }}>
        <Avatar name={donor.name} size={44} photoUrl={donor.photoUrl} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="row" style={{ gap: 7 }}><span style={{ fontWeight: 800, fontSize: 15, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{donor.name}</span><BloodBadge group={donor.bloodGroup} size={10} /></div>
          <div className="body num" style={{ fontSize: 13, marginTop: 1 }}>{donor.mobile}</div>
        </div>
      </div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
        <Item icon="copy" bg="var(--surface-2)" color="var(--ink-2)" title="Copy name & number" desc="Copy to clipboard" onClick={copy} />
        <Item icon="whatsapp" bg="#E4F7EA" color="#25A05A" title="Share via WhatsApp" desc="Send to a chat or group" onClick={wa} />
        {typeof navigator !== 'undefined' && navigator.share && <Item icon="share" bg="var(--sky-wash)" color="var(--sky-deep)" title="Share contact…" desc="Use your device share sheet" onClick={shareNative} />}
      </div>
    </Sheet>
  );
}

function QuickSheet({ store, close }) {
  const item = (icon, bg, color, title, desc, onClick) => (
    <button className="qa-item" onClick={onClick}>
      <div className="qa-ic" style={{ background: bg, color }}><Icon name={icon} size={22} color={color} /></div>
      <div style={{ flex: 1, textAlign: 'left' }}><div style={{ fontWeight: 800, fontSize: 15.5 }}>{title}</div><div className="body" style={{ fontSize: 12.5 }}>{desc}</div></div>
      <Icon name="chevR" size={18} color="var(--ink-3)" />
    </button>
  );
  return (
    <Sheet title="What would you like to do?" onClose={close}>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 11 }}>
        {item('user', 'var(--orange-wash)', 'var(--orange)', 'Add a donor', 'Grow your pool', () => store.openModal('editDonor', {}))}
        {item('drop', 'var(--green-wash)', 'var(--success)', 'Log a donation', 'Record a completed donation', () => store.openModal('pickLogDonor', {}))}
        {item('drop', 'var(--slate-wash)', 'var(--sky-deep)', 'Transfusion log', 'Record a transfusion you received', () => store.openModal('logTransfusion', {}))}
        {item('calendar', 'var(--sky-wash)', 'var(--sky-deep)', 'Schedule an appointment', 'Set a clinic date with a reminder', () => store.openModal('appointment', {}))}
      </div>
    </Sheet>
  );
}

function DonorSheet({ store, close, donor }) {
  // always read the freshest donor record (after edits/logs the prop can be stale)
  const live = store.donors.find((z) => z.id === donor.id) || donor;
  const s = TC.donorStatus(live);
  const recent = [...(live.donations || [])].sort((a, b) => TC.parse(b.date) - TC.parse(a.date)).slice(0, 3);
  const contacts = [...(live.contactLog || [])].sort((a, b) => TC.parse(b.date) - TC.parse(a.date)).slice(0, 4);
  const outcomeMap = { confirmed: ['Confirmed', 'var(--green)'], no_answer: ['No answer', 'var(--ink-3)'], answered: ['Answered', 'var(--sky-deep)'], declined: ['Declined', 'var(--waiting)'] };

  const Action = ({ icon, label, onClick, danger }) => (
    <button className="btn btn-ghost" style={{ flex: 1, flexDirection: 'column', gap: 5, padding: '11px 4px', fontSize: 11.5, color: danger ? 'var(--waiting)' : 'var(--ink)' }} onClick={onClick}>
      <Icon name={icon} size={18} color={danger ? 'var(--waiting)' : 'var(--ink)'} />{label}
    </button>
  );
  const SecLabel = ({ children, action }) => (
    <div className="between" style={{ marginBottom: 8 }}>
      <span style={{ fontSize: 11.5, fontWeight: 900, letterSpacing: '.05em', textTransform: 'uppercase', color: 'var(--ink-3)' }}>{children}</span>
      {action}
    </div>
  );

  return (
    <Sheet onClose={close}>
      <div className="row" style={{ gap: 14, marginBottom: 6 }}>
        <Avatar name={live.name} size={58} count={TC.donationCount(live)} photoUrl={live.photoUrl} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div className="row" style={{ gap: 8 }}><span className="h2" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{live.name}</span><BloodBadge group={live.bloodGroup} />{live.awayStatus && <span className="away-chip">AWAY</span>}</div>
          <div className="dmeta" style={{ color: 'var(--ink-2)' }}>{live.mobile}</div>
        </div>
        <button className="icon-btn" style={{ width: 40, height: 40, flex: 'none', background: 'var(--surface-2)' }} onClick={() => store.openModal('shareDonor', { donor: live })} aria-label="Copy or share contact"><Icon name="share" size={18} /></button>
      </div>
      <div style={{ margin: '12px 0' }}><StatusChip donor={live} /></div>

      {/* contact */}
      <div className="row" style={{ gap: 10, marginBottom: 16 }}>
        <button className="btn btn-ghost" style={{ flex: 1 }} onClick={() => { window.location.href = `tel:${live.mobile}`; }}><Icon name="phone" size={18} color="var(--green)" /> Call</button>
        <button className="btn btn-primary" style={{ flex: 1 }} onClick={() => store.openModal('contact', { donor: live })}><Icon name="chat" size={18} color="#fff" /> Message</button>
      </div>

      {/* eligibility */}
      <div className="detail-sec">
        <SecLabel>Eligibility</SecLabel>
        <div className="body" style={{ color: 'var(--ink)' }}>
          {s.status === 'DEFERRED'
            ? <>Deferred — {s.reason}{s.returnDate ? ` · expected back ${TC.fmtDate(s.returnDate)}` : ''}</>
            : <>{(s.method || 'normal') === 'apheresis' ? 'Apheresis' : 'Normal'} · {s.window || 90}-day window{s.eligibleSince ? ` · eligible since ${TC.fmtDate(s.eligibleSince)}` : ''}</>}
        </div>
      </div>

      {/* donation history */}
      <div className="detail-sec">
        <SecLabel action={live.donations.length > 3 && <button className="link" style={{ fontSize: 12 }} onClick={() => store.openModal('donorHistory', { donorId: live.id })}>View all</button>}>Donation history</SecLabel>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {recent.length === 0 && <div className="body">No donations recorded.</div>}
          {recent.map((d) => (
            <div key={d.id} className="between" style={{ gap: 8 }}>
              <div className="body" style={{ color: 'var(--ink)' }}><span style={{ fontWeight: 800 }}>{TC.fmtDate(d.date)}</span> · {d.method === 'apheresis' ? 'Apheresis' : 'Normal'}{d.hbValue ? ` · HB ${d.hbValue}` : ''}</div>
              <div className="row" style={{ gap: 6 }}>
                <button className="icon-btn" style={{ width: 30, height: 30, background: 'var(--surface-2)' }} onClick={() => store.openModal('logDonation', { donor: live, edit: d })} aria-label="Edit"><Icon name="edit" size={14} /></button>
                <button className="icon-btn" style={{ width: 30, height: 30, background: 'var(--surface-2)' }} onClick={() => store.openModal('confirm', { title: 'Delete this donation?', message: `The ${d.method === 'apheresis' ? 'apheresis' : 'normal'} donation on ${TC.fmtDate(d.date)} will be permanently removed. This cannot be undone.`, confirmLabel: 'Delete', onConfirm: () => { store.mutate((s) => { const x = s.donors.find((z) => z.id === live.id); x.donations = x.donations.filter((z) => z.id !== d.id); }); store.showToast('Donation deleted', '🗑️'); } })} aria-label="Delete"><Icon name="trash" size={14} color="var(--waiting)" /></button>
              </div>
            </div>
          ))}
        </div>
      </div>

      {/* contact log */}
      <div className="detail-sec">
        <SecLabel action={<button className="link" style={{ fontSize: 12 }} onClick={() => store.openModal('logContact', { donor: live })}>+ Log</button>}>Contact log</SecLabel>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 9 }}>
          {contacts.length === 0 && <div className="body">No contact attempts logged.</div>}
          {contacts.map((c) => {
            const [t, col] = outcomeMap[c.outcome] || [c.outcome, 'var(--ink-3)'];
            return (
              <div key={c.id} style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
                <div className="between"><span className="body">{TC.fmtDate(c.date)}</span><span className="pill" style={{ fontSize: 11.5, color: col, background: 'var(--surface-2)', padding: '3px 10px' }}>{t}</span></div>
                {c.note && <div className="body" style={{ fontSize: 12.5, fontStyle: 'italic', color: 'var(--ink-2)', lineHeight: 1.4 }}>“{c.note}”</div>}
              </div>
            );
          })}
        </div>
      </div>

      {live.notes && <div className="detail-sec"><SecLabel>Notes</SecLabel><div className="body" style={{ fontStyle: 'italic', color: 'var(--ink)' }}>“{live.notes}”</div></div>}

      {/* actions */}
      <div className="row" style={{ gap: 6, marginTop: 18 }}>
        <Action icon="edit" label="Edit" onClick={() => store.openModal('editDonor', { donor: live })} />
        <Action icon="drop" label="Log" onClick={() => store.openModal('logDonation', { donor: live })} />
        {live.deferred
          ? <Action icon="refresh" label="Un-defer" onClick={() => { store.mutate((d) => { const x = d.donors.find((z) => z.id === live.id); x.deferred = false; x.deferReason = null; x.deferReturn = null; }); store.showToast('Donor reactivated', '🌱'); }} />
          : <Action icon="flag" label="Defer" onClick={() => store.openModal('defer', { donor: live })} />}
        <Action icon="archive" label="Archive" danger onClick={() => store.openModal('archive', { donor: live })} />
      </div>
    </Sheet>
  );
}

function EditDonorSheet({ store, close, donor }) {
  const editing = !!donor;
  const last = donor ? TC.lastDonation(donor) : null;
  const [f, setF] = React.useState({
    name: donor?.name || '', mobile: (donor?.mobile || '+960').replace(/^\+960/, ''),
    bloodGroup: donor?.bloodGroup || '', lastDate: last?.date || TC.daysAgoDate(0),
    method: last?.method || 'normal', hb: last?.hbValue || '', notes: donor?.notes || '',
  });
  const set = (k, v) => setF((s) => ({ ...s, [k]: v }));
  const [warn, setWarn] = React.useState('');
  const [dupDonor, setDupDonor] = React.useState(null);

  const doSave = () => {
    const mobile = '+960' + f.mobile.replace(/\D/g, '');
    store.mutate((d) => {
      if (editing) {
        const x = d.donors.find((z) => z.id === donor.id);
        x.name = f.name; x.mobile = mobile; x.bloodGroup = f.bloodGroup; x.notes = f.notes;
        if (last) { const dn = x.donations.find((z) => z.id === last.id); dn.date = f.lastDate; dn.method = f.method; dn.hbValue = f.hb ? +f.hb : null; }
        else x.donations.push({ id: TC.uid(), date: f.lastDate, method: f.method, hbValue: f.hb ? +f.hb : null });
      } else {
        d.donors.unshift({ id: TC.uid(), name: f.name, mobile, countryCode: '+960', bloodGroup: f.bloodGroup, status: 'active', awayStatus: false, addedOn: TC.daysAgoDate(0), notes: f.notes, donations: [{ id: TC.uid(), date: f.lastDate, method: f.method, hbValue: f.hb ? +f.hb : null }], contactLog: [], archived: false, deferred: false });
      }
    });
    store.showToast(editing ? 'Donor updated' : 'Donor added', '🌱'); close();
  };

  const onSave = () => {
    if (!f.name.trim() || !f.mobile.trim() || !f.bloodGroup) { setWarn('Please fill name, mobile and blood group.'); return; }
    const mobile = '+960' + f.mobile.replace(/\D/g, '');
    // exact phone match (active or archived) — this donor already exists
    const phoneMatch = TC.findDonorByMobile(store.donors, mobile, donor?.id);
    if (phoneMatch) { setDupDonor(phoneMatch); setWarn('phone'); return; }
    // softer name-only similarity → allow override
    const nameMatch = store.donors.find((z) => !z.archived && z.id !== donor?.id && z.name.toLowerCase() === f.name.trim().toLowerCase());
    if (nameMatch && warn !== 'name') { setWarn('name'); setF((s) => ({ ...s, _dupName: nameMatch.name })); return; }
    doSave();
  };

  const phoneDupFirst = dupDonor ? dupDonor.name.split(' ')[0] : '';

  return (
    <Sheet title={editing ? 'Edit donor' : 'Add a donor'} onClose={close}>
      <div className="field"><label className="field-label">Full name *</label><input className="input" value={f.name} onChange={(e) => set('name', e.target.value)} placeholder="e.g. Ahmed Ali" /></div>
      <div className="field"><label className="field-label">Mobile number *</label><div className="row" style={{ gap: 8 }}><span className="input" style={{ width: 60, flex: 'none', textAlign: 'center' }}>+960</span><input className="input" type="tel" value={f.mobile} onChange={(e) => { set('mobile', e.target.value); if (warn === 'phone') { setWarn(''); setDupDonor(null); } }} placeholder="7XXXXXX" /></div></div>
      <div className="field"><label className="field-label">Blood group *</label><BloodGrid value={f.bloodGroup} onChange={(v) => set('bloodGroup', v)} /></div>
      <div className="field"><label className="field-label">Last donation date *</label><input className="input" type="date" value={f.lastDate} onChange={(e) => set('lastDate', e.target.value)} /></div>
      <div className="field"><label className="field-label">Donation method *</label><Segmented options={[{ value: 'normal', label: 'Normal' }, { value: 'apheresis', label: 'Apheresis' }]} value={f.method} onChange={(v) => set('method', v)} /></div>
      <div className="field"><label className="field-label">HB value (optional)</label><input className="input" type="number" step="0.1" min="12" max="18" value={f.hb} onChange={(e) => set('hb', e.target.value)} placeholder="e.g. 14.2" />{f.hb && (+f.hb < 12 || +f.hb > 18) ? <div className="body" style={{ fontSize: 12, marginTop: 5, color: 'var(--alert)' }}>Outside the 12.0–18.0 g/dL donation range — double-check.</div> : null}</div>
      <div className="field"><label className="field-label">Notes (optional)</label><textarea className="input textarea" rows={2} value={f.notes} onChange={(e) => set('notes', e.target.value)} placeholder="e.g. Prefers calls after 6pm" /></div>
      {warn === 'phone' && dupDonor
        ? <MWarn tone="soon">A donor with this number already exists — <b>{dupDonor.name}</b> ({dupDonor.mobile}){dupDonor.archived ? ' · archived' : ''}. Open their card to log a donation or update their details.</MWarn>
        : warn && <MWarn>{warn === 'name' ? `Looks similar to existing donor “${f._dupName}”. Save anyway?` : warn}</MWarn>}
      <div className="row" style={{ gap: 10 }}>
        <button className="btn btn-ghost" style={{ flex: 1 }} onClick={close}>Cancel</button>
        {warn === 'phone' && dupDonor
          ? <button className="btn btn-primary" style={{ flex: 1, whiteSpace: 'nowrap' }} onClick={() => store.openModal('donor', { donor: dupDonor })}>Open card</button>
          : <button className="btn btn-primary" style={{ flex: 1.4 }} onClick={onSave}>{warn === 'name' ? 'Save anyway' : 'Save'}</button>}
      </div>
    </Sheet>
  );
}

function Modals({ store, modal }) {
  if (!modal) return null;
  const close = store.closeModal;
  const p = modal.props || {};
  const map = {
    contact: <ContactSheet store={store} close={close} donor={p.donor} />,
    shareDonor: <ShareDonorSheet store={store} close={close} donor={p.donor} />,
    quick: <QuickSheet store={store} close={close} />,
    donor: <DonorSheet store={store} close={close} donor={p.donor} />,
    editDonor: <EditDonorSheet store={store} close={close} donor={p.donor} />,
    editProfile: <EditProfileSheet store={store} close={close} />,
    monProfile: <MonProfileSheet store={store} close={close} />,
    medicalProfile: <MedicalProfileSheet store={store} close={close} field={p.field} />,
    template: <TemplateSheet store={store} close={close} which={p.which} />,
    scenePicker: <ScenePickerSheet store={store} close={close} />,
    themePicker: <ThemePickerSheet store={store} close={close} />,
    weight: <WeightSheet store={store} close={close} />,
    account: <AccountSheet store={store} close={close} />,
    dataBackup: <DataBackupSheet store={store} close={close} />,
    contactUs: <ContactUsSheet store={store} close={close} />,
    help: <HelpSheet store={store} close={close} />,
    about: <AboutSheet store={store} close={close} />,
    rate: <RateSheet store={store} close={close} />,
    tipDetail: <TipDetailSheet store={store} close={close} tip={p.tip} />,
    privacyData: <PrivacyDataSheet store={store} close={close} />,
    tribute: (typeof TributeSheet !== 'undefined') ? <TributeSheet store={store} close={close} /> : null,
    terms: (typeof TermsSheet !== 'undefined') ? <TermsSheet store={store} close={close} /> : null,
    // install/reminders & caregiver sharing (cloud.jsx)
    getApp: <GetAppSheet store={store} close={close} />,
    caregivers: <CaregiversSheet store={store} close={close} />,
    inviteCaregiver: <InviteCaregiverSheet store={store} close={close} resend={p.resend} />,
    // donor flows (sheets.jsx)
    pickLogDonor: <PickLogDonor store={store} close={close} />,
    logDonation: <LogDonationSheet store={store} close={close} donor={p.donor} edit={p.edit} />,
    thankYou: <ThankYouDialog store={store} close={close} donor={p.donor} />,
    defer: <DeferSheet store={store} close={close} donor={p.donor} />,
    archive: <ArchiveSheet store={store} close={close} donor={p.donor} />,
    logContact: <LogContactSheet store={store} close={close} donor={p.donor} />,
    donorHistory: <DonorHistorySheet store={store} close={close} donorId={p.donorId} />,
    // health + data flows (sheets.jsx)
    logTransfusion: <LogTransfusionSheet store={store} close={close} edit={p.edit} />,
    appointment: <AddAppointmentSheet store={store} close={close} edit={p.edit} />,
    transfusionHistory: <TransfusionHistorySheet store={store} close={close} />,
    logHb: <LogHbSheet store={store} close={close} edit={p.edit} />,
    logFerritin: <LogFerritinSheet store={store} close={close} edit={p.edit} />,
    levelHistory: <LevelHistorySheet store={store} close={close} kind={p.kind} />,
    uploadLab: <UploadLabSheet store={store} close={close} />,
    addMed: <ChelationSetupWizard store={store} close={close} med={p.med} />,
    skipDose: <SkipDoseSheet store={store} close={close} slot={p.slot} />,
    reminders: <RemindersSheet store={store} close={close} />,
    shareAdherence: <ShareAdherenceSheet store={store} close={close} />,
    import: <ImportSheet store={store} close={close} />,
    exportDonors: <ExportDonorsSheet store={store} close={close} />,
    confirm: <ConfirmDialog store={store} close={close} title={p.title} message={p.message} confirmLabel={p.confirmLabel} danger={p.danger !== false} onConfirm={p.onConfirm} />,
  };
  return map[modal.type] || null;
}

window.App = App;
window.mountMUI = function () { ReactDOM.createRoot(document.getElementById('root')).render(<App />); };
