// Customer-facing mobile app — auth flow + 5-tab dashboard.
// Renders inside an IOSDevice frame. Tailwind not loaded; using inline styles
// and the design tokens from the host stylesheet.
//
// All visible strings flow through useTranslation() — see i18n.jsx for the
// drop-in equivalent of i18next + react-i18next.

const { useState, useEffect, useRef } = React;

// MEMBER is a `const` exported from data.jsx — reassigning `window.MEMBER` does
// NOT change the binding child components read. syncMember() mutates the
// object IN-PLACE so every reference to MEMBER.points / MEMBER.tier etc. sees
// the fresh values. Always call setMember()/setState as well so React knows
// to re-render the tree.
function syncMember(next) {
  if (!next || typeof next !== 'object') return;
  // Normalize the points field once — D1 returns points_balance.
  const merged = { ...next, points: Number(next.points ?? next.points_balance ?? 0) };
  if (typeof MEMBER === 'object' && MEMBER) {
    Object.assign(MEMBER, merged);
  }
  window.MEMBER = MEMBER;
}

const C = {
  clover: '#1F4435', cloverDeep: '#122A20', cloverMid: '#2E5A47', cloverSoft: '#E6EEE8',
  gold: '#C9A66B', goldDeep: '#A8854A', goldSoft: '#F1E6CE',
  bone: '#F6F3EC', paper: '#FAF8F2',
  ink: '#14140F', ink2: '#2A2A24', mute: '#7A7A72',
  line: '#E6E2D7', line2: '#D5CFBE',
};

// ───────────────────────────────────────────────────────────
// Brand mark — tiny, used in app chrome
// ───────────────────────────────────────────────────────────
function CloverMark({ size = 28, color = C.clover }) {
  return (
    <svg width={size} height={size} viewBox="0 0 32 32" fill="none">
      <g stroke={color} strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
        <path d="M16 16c-2.2-4-5.6-4.5-7.5-2.5-1.6 1.7-1 4.9 3 6.7"/>
        <path d="M16 16c2.2-4 5.6-4.5 7.5-2.5 1.6 1.7 1 4.9-3 6.7"/>
        <path d="M16 16c-4-2.2-4.5-5.6-2.5-7.5C15.2 6.9 18.4 7.5 20.2 11.5"/>
        <path d="M16 16c-1.8 4-1.2 7.4 1 9 1.8-1.8 2.4-5.2-1-9"/>
      </g>
      <circle cx="16" cy="16" r="1.1" fill={color}/>
    </svg>
  );
}

// ───────────────────────────────────────────────────────────
// Shared UI atoms
// ───────────────────────────────────────────────────────────
function Pressable({ children, onClick, style = {}, disabled = false }) {
  const [pressed, setPressed] = useState(false);
  return (
    <div
      onPointerDown={() => !disabled && setPressed(true)}
      onPointerUp={() => setPressed(false)}
      onPointerLeave={() => setPressed(false)}
      onClick={() => !disabled && onClick && onClick()}
      style={{
        transition: 'transform .12s, opacity .12s',
        transform: pressed ? 'scale(0.98)' : 'scale(1)',
        opacity: disabled ? 0.5 : 1,
        cursor: disabled ? 'default' : 'pointer',
        ...style,
      }}
    >
      {children}
    </div>
  );
}

function PrimaryBtn({ children, onClick, style = {}, disabled = false, color = C.clover, textColor = '#fff' }) {
  return (
    <Pressable onClick={onClick} disabled={disabled} style={{
      background: color, color: textColor, borderRadius: 12,
      padding: '15px 20px', textAlign: 'center', fontWeight: 600,
      fontSize: 15, letterSpacing: 0.2,
      boxShadow: '0 1px 0 rgba(255,255,255,0.06) inset, 0 6px 20px rgba(31,68,53,0.18)',
      ...style,
    }}>
      {children}
    </Pressable>
  );
}

function Tag({ children, color = C.clover, bg = C.cloverSoft, style = {} }) {
  return (
    <span style={{
      background: bg, color, borderRadius: 6,
      padding: '3px 8px', fontSize: 10.5, fontWeight: 600,
      letterSpacing: 0.4, textTransform: 'uppercase',
      ...style,
    }}>{children}</span>
  );
}

// ───────────────────────────────────────────────────────────
// AUTH — landing, phone, OTP, magic-link success
// ───────────────────────────────────────────────────────────
const LIFF_ID = '2010101159-xk3hKf84';

function AuthLanding({ onMethod, liffInitialized = false }) {
  const { t } = useTranslation();

  const handleLine = () => {
    if (typeof liff === 'undefined' || !liffInitialized) {
      alert('LINE is loading. Please try again in a moment.');
      return;
    }
    liff.login();
  };

  return (
    <div className="fade-in" style={{
      height: '100%', background: C.paper, padding: '92px 28px 40px',
      display: 'flex', flexDirection: 'column',
    }}>
      {/* Floating language switcher — always reachable, even pre-auth */}
      <div style={{ position: 'absolute', top: 64, right: 22, zIndex: 5 }}>
        <LanguageSwitcher variant="pill" />
      </div>

      <div style={{ textAlign: 'center', marginBottom: 36 }}>
        <CloverMark size={56} />
        <div className="serif" style={{ fontSize: 38, marginTop: 14, color: C.clover, letterSpacing: -0.5 }}>
          The Clover Clinic
        </div>
        <div style={{ fontSize: 12, color: C.mute, letterSpacing: 2.2, marginTop: 6, textTransform: 'uppercase' }}>
          {t('auth.tagline')}
        </div>
      </div>

      <div style={{
        background: '#fff', borderRadius: 16, padding: '24px 22px',
        border: '1px solid ' + C.line,
        boxShadow: '0 1px 0 rgba(255,255,255,0.6) inset',
      }}>
        <div className="serif" style={{ fontSize: 22, color: C.ink, lineHeight: 1.15 }}>
          {t('auth.welcome_back')}
        </div>
        <div style={{ fontSize: 13.5, color: C.mute, marginTop: 6, marginBottom: 22, lineHeight: 1.5 }}>
          {t('auth.subtitle')}
        </div>

        <Pressable onClick={handleLine} style={{
          background: '#06C755', color: '#fff', borderRadius: 10,
          padding: '13px 14px', display: 'flex', alignItems: 'center', gap: 12,
          fontWeight: 600, fontSize: 14.5,
        }}>
          <IconLine size={20} color="#fff" />
          <span>{t('auth.continue_line')}</span>
        </Pressable>

        <Pressable onClick={() => onMethod('phone')} style={{
          marginTop: 10, background: C.bone, color: C.ink, borderRadius: 10,
          padding: '13px 14px', display: 'flex', alignItems: 'center', gap: 12,
          fontWeight: 600, fontSize: 14.5,
        }}>
          <IconPhone size={18} color={C.clover} />
          <span>{t('auth.continue_phone')}</span>
        </Pressable>
      </div>

      <div style={{ flex: 1 }} />
      <div style={{ textAlign: 'center', fontSize: 11, color: C.mute, lineHeight: 1.7 }}>
        {t('auth.terms')}{' '}
        <a href="/privacy.html" target="_blank" style={{ color: C.clover, textDecoration: 'underline' }}>
          {t('auth.privacy_link') || 'Privacy Policy'}
        </a>{' · '}
        <a href="/terms.html" target="_blank" style={{ color: C.clover, textDecoration: 'underline' }}>
          {t('auth.terms_link') || 'Terms of Service'}
        </a>
      </div>
    </div>
  );
}

function AuthPhone({ onBack, onSend }) {
  const { t } = useTranslation();
  const [phone, setPhone] = useState('89 234 4127');
  const ok = phone.replace(/\D/g, '').length >= 9;
  return (
    <div className="fade-in" style={{ height: '100%', background: C.paper, padding: '76px 28px 28px', display:'flex', flexDirection:'column' }}>
      <Pressable onClick={onBack} style={{ width: 36, marginBottom: 20 }}>
        <IconArrowLeft size={20} color={C.ink} />
      </Pressable>
      <div className="serif" style={{ fontSize: 30, color: C.clover, letterSpacing: -0.4 }}>
        {t('auth.your_number')}
      </div>
      <div style={{ fontSize: 13.5, color: C.mute, marginTop: 6, marginBottom: 24, lineHeight: 1.5 }}>
        {t('auth.phone_sub')}
      </div>

      <div style={{
        display:'flex', gap: 10, alignItems:'center',
        background:'#fff', border:'1px solid '+C.line, borderRadius: 12, padding: '4px 4px 4px 14px',
      }}>
        <div style={{ display:'flex', alignItems:'center', gap: 6, color: C.ink, fontWeight: 600, fontSize: 16 }}>
          <span>🇹🇭</span><span>+66</span>
          <span style={{ width: 1, height: 22, background: C.line, marginLeft: 6 }} />
        </div>
        <input
          value={phone} onChange={e => setPhone(e.target.value)}
          style={{
            flex: 1, border: 'none', outline: 'none', background: 'transparent',
            padding: '14px 8px', fontSize: 16, fontFamily: 'inherit', color: C.ink, letterSpacing: 0.5,
          }}
        />
      </div>

      <div style={{ flex: 1 }} />
      <PrimaryBtn onClick={() => ok && onSend(phone)} disabled={!ok}>{t('auth.send_code')}</PrimaryBtn>
    </div>
  );
}

function AuthOTP({ phone, onBack, onVerify }) {
  const { t } = useTranslation();
  const [digits, setDigits] = useState(['','','','','','']);
  const [error, setError] = useState(false);
  const [submitting, setSubmitting] = useState(false);
  const [resendIn, setResendIn] = useState(28);

  useEffect(() => {
    if (resendIn > 0) { const t = setTimeout(() => setResendIn(r => r - 1), 1000); return () => clearTimeout(t); }
  }, [resendIn]);

  const setAt = (i, v) => {
    if (!/^\d?$/.test(v)) return;
    const next = digits.slice(); next[i] = v;
    setDigits(next); setError(false);
    if (v && i < 5) {
      const nxt = document.getElementById('otp-' + (i + 1));
      if (nxt) nxt.focus();
    }
    if (next.every(d => d !== '')) submit(next.join(''));
  };

  const submit = (code) => {
    setSubmitting(true);
    setTimeout(() => {
      if (code === '123456' || code.length === 6) {
        onVerify();
      } else {
        setError(true); setSubmitting(false);
      }
    }, 700);
  };

  return (
    <div className="fade-in" style={{ height: '100%', background: C.paper, padding: '76px 28px 28px', display:'flex', flexDirection:'column' }}>
      <Pressable onClick={onBack} style={{ width: 36, marginBottom: 20 }}>
        <IconArrowLeft size={20} color={C.ink} />
      </Pressable>
      <div className="serif" style={{ fontSize: 30, color: C.clover, letterSpacing: -0.4 }}>
        {t('auth.enter_code')}
      </div>
      <div style={{ fontSize: 13.5, color: C.mute, marginTop: 6, marginBottom: 28, lineHeight: 1.5 }}>
        {t('auth.otp_sub', { phone, code: '' }).replace('{{code}}', '')}
        <span className="mono" style={{ color: C.clover, fontWeight: 600 }}>123456</span>
      </div>

      <div style={{ display: 'flex', gap: 8, justifyContent: 'space-between' }}>
        {digits.map((d, i) => (
          <input key={i}
            id={'otp-' + i}
            inputMode="numeric" maxLength={1}
            value={d}
            onChange={(e) => setAt(i, e.target.value)}
            onKeyDown={(e) => {
              if (e.key === 'Backspace' && !d && i > 0) {
                const prev = document.getElementById('otp-' + (i-1));
                if (prev) prev.focus();
              }
            }}
            style={{
              width: 46, height: 56, textAlign: 'center',
              fontSize: 24, fontWeight: 600, fontFamily: 'inherit',
              color: C.ink, background: '#fff',
              border: '1.5px solid ' + (error ? '#B5453A' : (d ? C.clover : C.line)),
              borderRadius: 12, outline: 'none',
              transition: 'border-color .15s, transform .15s',
            }}
          />
        ))}
      </div>

      <div style={{
        marginTop: 18, fontSize: 12.5,
        color: error ? '#B5453A' : C.mute,
        textAlign: 'center', height: 18,
      }}>
        {submitting ? t('auth.verifying') : (error ? t('auth.code_error') : (
          resendIn > 0 ? t('auth.resend_in', { s: resendIn }) : t('auth.resend_now')
        ))}
      </div>

      <div style={{ flex: 1 }} />
      <PrimaryBtn onClick={() => submit(digits.join(''))} disabled={digits.some(d => d === '')}>
        {submitting ? t('auth.verifying') : t('auth.verify')}
      </PrimaryBtn>
    </div>
  );
}

function AuthMagicLink({ onContinue }) {
  const { t } = useTranslation();
  const [stage, setStage] = useState('sent');

  useEffect(() => {
    const t1 = setTimeout(() => setStage('opened'),  1800);
    const t2 = setTimeout(() => setStage('success'), 3800);
    const t3 = setTimeout(onContinue, 5000);
    return () => { clearTimeout(t1); clearTimeout(t2); clearTimeout(t3); };
  }, []);
  return (
    <div className="fade-in" style={{
      height:'100%', background:C.paper, display:'flex',
      flexDirection:'column', alignItems:'center', justifyContent:'center', padding: 28,
    }}>
      <div style={{
        width: 84, height: 84, borderRadius: '50%',
        background: stage === 'success' ? C.clover : '#fff',
        border: '1px solid ' + C.line,
        display:'flex', alignItems:'center', justifyContent:'center',
        transition: 'background .3s, transform .3s',
        transform: stage === 'success' ? 'scale(1.05)' : 'scale(1)',
        boxShadow: '0 10px 30px rgba(31,68,53,0.12)',
      }}>
        {stage === 'success'
          ? <IconCheck size={36} color="#fff" />
          : <IconSparkle size={32} color={C.gold} />}
      </div>
      <div className="serif" style={{ fontSize: 26, color: C.clover, marginTop: 24, textAlign:'center' }}>
        {stage === 'sent' && t('auth.link_sent')}
        {stage === 'opened' && t('auth.link_opening')}
        {stage === 'success' && t('auth.signed_in')}
      </div>
      <div style={{ fontSize: 13.5, color: C.mute, marginTop: 8, textAlign:'center', lineHeight: 1.5, maxWidth: 260 }}>
        {stage === 'sent' && t('auth.link_sent_sub')}
        {stage === 'opened' && t('auth.link_verifying_device')}
        {stage === 'success' && t('auth.signed_in_sub')}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// LEGACY MERGE — shown after successful auth when the phone
// number matches an existing legacy customer record.
// ───────────────────────────────────────────────────────────
function AuthLegacyMerge({ onConfirm, onSkip }) {
  const { t } = useTranslation();
  const m = LEGACY_MATCH;
  return (
    <div className="fade-in" style={{ height:'100%', background: C.paper, padding: '76px 22px 28px', display:'flex', flexDirection:'column' }}>
      <div style={{ display:'flex', alignItems:'center', gap: 10, marginBottom: 14 }}>
        <div style={{
          width: 36, height: 36, borderRadius: '50%', background: C.cloverSoft,
          display:'flex', alignItems:'center', justifyContent:'center',
        }}>
          <IconUsers size={18} color={C.clover} />
        </div>
        <div>
          <div style={{ fontSize: 10, letterSpacing: 1.6, textTransform:'uppercase', color: C.gold, fontWeight: 700 }}>
            {t('auth.merge_tag')}
          </div>
          <div className="serif" style={{ fontSize: 24, color: C.clover, lineHeight: 1.1, marginTop: 1, letterSpacing: -0.3 }}>
            {t('auth.merge_title')}
          </div>
        </div>
      </div>
      <div style={{ fontSize: 13, color: C.mute, lineHeight: 1.55, marginBottom: 16 }}>
        {t('auth.merge_sub', { phone: '+66 89 ••• 4127' })}
      </div>

      <div style={{ background:'#fff', border: '1px solid ' + C.line, borderRadius: 14, padding: 16 }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 8 }}>
          <div className="serif" style={{ fontSize: 18, color: C.ink }}>{m.fullName}</div>
          <span style={{
            fontSize: 10, padding: '3px 7px', borderRadius: 4,
            background: C.goldSoft, color: C.goldDeep, fontWeight: 700, letterSpacing: 0.4,
          }}>LEGACY</span>
        </div>
        <div style={{ fontSize: 11.5, color: C.mute, marginBottom: 12 }}>{m.source}</div>

        <div style={{ display:'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
          <MergeStat label={t('auth.merge_points')}    value={m.pointsBalance.toLocaleString() + ' pts'} />
          <MergeStat label={t('auth.merge_visits')}    value={m.visits + ' visits'} />
          <MergeStat label={t('auth.merge_spend')}     value={'฿' + (m.lifetimeSpend/1000).toFixed(0) + 'k'} />
          <MergeStat label={t('auth.merge_last_visit')} value={m.lastVisit} />
        </div>

        <div style={{ marginTop: 14, paddingTop: 14, borderTop: '1px solid ' + C.line }}>
          <div style={{ fontSize: 11, letterSpacing: 1.4, textTransform:'uppercase', color: C.mute, fontWeight: 600, marginBottom: 6 }}>
            {t('auth.merge_courses_label')}
          </div>
          {m.courses.map((c, i) => (
            <div key={i} style={{ display:'flex', justifyContent:'space-between', fontSize: 12.5, padding: '5px 0', color: C.ink }}>
              <span>{c.name}</span>
              <span className="mono" style={{ color: C.clover, fontWeight: 600 }}>{c.remaining}</span>
            </div>
          ))}
        </div>
      </div>

      <div style={{ marginTop: 14, padding: '10px 12px', background: C.cloverSoft, borderRadius: 10, fontSize: 11.5, color: C.ink2, lineHeight: 1.5 }}>
        <IconCheck size={13} color={C.clover} style={{ verticalAlign: -2, marginRight: 5 }} />
        {t('auth.merge_safe_note')}
      </div>

      <div style={{ flex: 1 }} />
      <PrimaryBtn onClick={onConfirm}>{t('auth.merge_confirm')}</PrimaryBtn>
      <Pressable onClick={onSkip} style={{ textAlign:'center', padding: '14px 0', color: C.mute, fontSize: 13, fontWeight: 500 }}>
        {t('auth.merge_not_me')}
      </Pressable>
    </div>
  );
}
function MergeStat({ label, value }) {
  return (
    <div style={{ background: C.bone, padding: '10px 12px', borderRadius: 10 }}>
      <div style={{ fontSize: 10, letterSpacing: 1.2, textTransform:'uppercase', color: C.mute, fontWeight: 600 }}>{label}</div>
      <div className="serif" style={{ fontSize: 18, color: C.clover, marginTop: 2 }}>{value}</div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// PDPA CONSENT — required on first login per Thai data law.
// ───────────────────────────────────────────────────────────
function AuthPDPA({ onAgree }) {
  const { t } = useTranslation();
  const [marketing, setMarketing] = useState(true);
  const [research, setResearch] = useState(false);
  const [agreed, setAgreed] = useState(false);
  return (
    <div className="fade-in" style={{ height:'100%', background: C.paper, padding: '76px 22px 28px', display:'flex', flexDirection:'column' }}>
      <div style={{ display:'flex', alignItems:'center', gap: 10, marginBottom: 14 }}>
        <div style={{
          width: 36, height: 36, borderRadius: '50%', background: C.cloverSoft,
          display:'flex', alignItems:'center', justifyContent:'center',
        }}>
          <IconCheck size={18} color={C.clover} />
        </div>
        <div>
          <div style={{ fontSize: 10, letterSpacing: 1.6, textTransform:'uppercase', color: C.gold, fontWeight: 700 }}>
            {t('auth.pdpa_tag')}
          </div>
          <div className="serif" style={{ fontSize: 24, color: C.clover, lineHeight: 1.1, marginTop: 1, letterSpacing: -0.3 }}>
            {t('auth.pdpa_title')}
          </div>
        </div>
      </div>
      <div style={{ fontSize: 13, color: C.mute, lineHeight: 1.55, marginBottom: 14 }}>
        {t('auth.pdpa_sub')}
      </div>

      {/* Required consents */}
      <div style={{ background:'#fff', border:'1px solid '+C.line, borderRadius: 12, padding: '4px 0' }}>
        <ConsentRow
          required
          checked={true}
          title={t('auth.pdpa_req_records')}
          desc={t('auth.pdpa_req_records_desc')}
        />
        <ConsentRow
          required
          checked={true}
          title={t('auth.pdpa_req_treatment')}
          desc={t('auth.pdpa_req_treatment_desc')}
        />
        <ConsentRow
          checked={marketing}
          onToggle={() => setMarketing(!marketing)}
          title={t('auth.pdpa_opt_marketing')}
          desc={t('auth.pdpa_opt_marketing_desc')}
        />
        <ConsentRow
          checked={research}
          onToggle={() => setResearch(!research)}
          title={t('auth.pdpa_opt_research')}
          desc={t('auth.pdpa_opt_research_desc')}
        />
      </div>

      <div style={{ marginTop: 14, padding: '10px 12px', background: C.bone, borderRadius: 10, fontSize: 11, color: C.mute, lineHeight: 1.55 }}>
        {t('auth.pdpa_rights_note')}
      </div>

      <Pressable onClick={() => setAgreed(!agreed)} style={{ display:'flex', alignItems:'flex-start', gap: 10, marginTop: 14 }}>
        <div style={{
          width: 18, height: 18, borderRadius: 5, flexShrink: 0,
          border: '1.5px solid ' + (agreed ? C.clover : C.line2),
          background: agreed ? C.clover : '#fff',
          display:'flex', alignItems:'center', justifyContent:'center', marginTop: 2,
        }}>
          {agreed && <IconCheck size={11} color="#fff" />}
        </div>
        <div style={{ fontSize: 12, color: C.ink, lineHeight: 1.5 }}>
          {t('auth.pdpa_agreement')}
        </div>
      </Pressable>

      <div style={{ flex: 1 }} />
      <PrimaryBtn onClick={() => agreed && onAgree({ marketing, research })} disabled={!agreed}>
        {t('auth.pdpa_continue')}
      </PrimaryBtn>
    </div>
  );
}
function ConsentRow({ title, desc, checked, onToggle, required }) {
  return (
    <div style={{
      padding: '12px 14px', display:'flex', alignItems:'flex-start', gap: 12,
      borderTop: '1px solid ' + C.line,
    }}>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ display:'flex', alignItems:'center', gap: 6 }}>
          <div style={{ fontSize: 13, fontWeight: 600, color: C.ink }}>{title}</div>
          {required && <Tag bg={C.cloverSoft} color={C.clover} style={{ fontSize: 9 }}>REQUIRED</Tag>}
        </div>
        <div style={{ fontSize: 11.5, color: C.mute, marginTop: 3, lineHeight: 1.45 }}>{desc}</div>
      </div>
      <div onClick={onToggle} style={{
        flexShrink: 0, width: 38, height: 22, borderRadius: 11,
        background: checked ? C.clover : C.line2,
        position:'relative', cursor: required ? 'default' : 'pointer',
        opacity: required ? 0.7 : 1, marginTop: 2,
      }}>
        <div style={{
          position:'absolute', top: 2, left: checked ? 18 : 2,
          width: 18, height: 18, borderRadius: '50%', background:'#fff',
          transition: 'left .15s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
        }} />
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// HOME tab
// ───────────────────────────────────────────────────────────
function HomeTab({ go, lineUser }) {
  const { t, tierName, branchName } = useTranslation();
  const tp = tierProgress(MEMBER);
  // Auto-derive tier from current points (no longer trusts MEMBER.tier)
  const tierLabel = tierName(tp.current.id);
  const [qrOpen, setQrOpen] = useState(false);

  // Prefer real LINE display name over (now-empty) MEMBER constant.
  const displayName = lineUser?.name || MEMBER.nameShort || t('home.welcome') || 'Welcome';

  return (
    <div className="fade-in" style={{ paddingBottom: 100 }}>
      <MemberQRModal open={qrOpen} onClose={() => setQrOpen(false)} context="checkin" />
      {/* Hero — tier card */}
      <div style={{ padding: '0 18px', position: 'relative' }}>
        <div style={{
          background: 'linear-gradient(160deg, ' + C.clover + ' 0%, ' + C.cloverDeep + ' 100%)',
          borderRadius: 22, padding: '22px 22px 24px', color: '#fff',
          position: 'relative', overflow: 'hidden',
          boxShadow: '0 16px 40px rgba(18,42,32,0.22)',
        }}>
          {/* decorative clover */}
          <div style={{ position: 'absolute', right: -24, top: -22, opacity: 0.18 }}>
            <CloverMark size={180} color={C.gold} />
          </div>

          <div style={{
            display:'flex', alignItems:'center', gap: 8, fontSize: 11, letterSpacing: 2,
            color: C.gold, textTransform: 'uppercase', fontWeight: 600,
          }}>
            <IconStar size={11} color={C.gold} />
            {t('home.tier_member', { tier: tierLabel })}
          </div>

          <div className="serif" style={{ fontSize: 26, marginTop: 6, letterSpacing: -0.3 }}>
            {displayName}
          </div>
          {(MEMBER.id || MEMBER.joined) && (
            <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.55)', marginTop: 2, letterSpacing: 0.4 }}>
              {t('common.member')}{MEMBER.id ? ' · ' + MEMBER.id : ''}{MEMBER.joined ? ' · ' + t('common.since') + ' ' + MEMBER.joined : ''}
            </div>
          )}

          <div style={{ marginTop: 22, display:'flex', alignItems:'baseline', gap: 6 }}>
            <div className="serif" style={{ fontSize: 46, color: C.gold, letterSpacing: -1, lineHeight: 1 }}>
              {Number(MEMBER.points || 0).toLocaleString()}
            </div>
            <div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)' }}>{t('home.points_available')}</div>
          </div>

          {/* Tier progress — points-based, auto-derived from MEMBER.points */}
          <div style={{ marginTop: 18 }}>
            <div style={{ display:'flex', justifyContent:'space-between', fontSize: 11, color: 'rgba(255,255,255,0.7)' }}>
              <span>{tierLabel}</span>
              <span>{tp.nextTier ? tierName(tp.nextTier.id) : t('home.top_tier_achieved')}</span>
            </div>
            <div style={{
              marginTop: 6, height: 4, borderRadius: 4, background: 'rgba(255,255,255,0.12)',
              overflow: 'hidden',
            }}>
              <div style={{
                width: (tp.pct * 100) + '%', height: '100%',
                background: 'linear-gradient(90deg, ' + C.gold + ', #E0C896)',
                borderRadius: 4,
              }} />
            </div>
            <div style={{ fontSize: 11, color: 'rgba(255,255,255,0.55)', marginTop: 6 }}>
              {tp.toNext === 0
                ? t('home.top_tier_achieved')
                : `อีก ${tp.toNext.toLocaleString()} คะแนน → ${tierName(tp.nextTier.id)}`}
            </div>
          </div>
        </div>
      </div>

      {/* Quick actions */}
      <div style={{ padding: '18px 18px 0', display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 8 }}>
        {[
          { icon: <IconQR size={20} color={C.clover}/>, label: t('home.my_qr'), onClick: () => setQrOpen(true) },
          { icon: <IconLayers size={20} color={C.clover}/>, label: t('nav.courses'), onClick: () => go('courses') },
          { icon: <IconGift size={20} color={C.clover}/>, label: t('home.rewards'), onClick: () => go('points') },
        ].map((a, i) => (
          <Pressable key={i} onClick={a.onClick} style={{
            background:'#fff', border:'1px solid '+C.line, borderRadius: 14,
            padding:'14px 8px', display:'flex', flexDirection:'column', alignItems:'center', gap: 8,
          }}>
            {a.icon}
            <div style={{ fontSize: 11.5, color: C.ink, fontWeight: 500 }}>{a.label}</div>
          </Pressable>
        ))}
      </div>

      {/* Active courses preview */}
      <div style={{ padding: '20px 18px 0' }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'baseline' }}>
          <SectionLabel>{t('home.active_courses')}</SectionLabel>
          <Pressable onClick={() => go('courses')} style={{ fontSize: 12, color: C.clover, fontWeight: 600 }}>{t('common.view_all')}</Pressable>
        </div>
        <div style={{ marginTop: 8, display:'grid', gap: 10 }}>
          {COURSES.slice(0, 2).map(c => <CourseCard key={c.id} c={c} compact />)}
        </div>
      </div>

      {/* Banner */}
      <div style={{ padding: '20px 18px 0' }}>
        <SectionLabel>{t('home.for_diamond')}</SectionLabel>
        <div style={{
          marginTop: 8, borderRadius: 16, overflow:'hidden',
          border:'1px solid '+C.line, position:'relative',
        }}>
          <div className="stripes" style={{ height: 140, position: 'relative' }}>
            <div style={{ position: 'absolute', inset: 0, padding: 18, color: '#fff',
              background: 'linear-gradient(110deg, rgba(31,68,53,0.92) 38%, rgba(31,68,53,0.4) 75%, transparent)' }}>
              <Tag bg="rgba(201,166,107,0.2)" color={C.gold}>{t('home.banner_tag')}</Tag>
              <div className="serif" style={{ fontSize: 22, marginTop: 8, lineHeight: 1.15, maxWidth: 220 }}>
                {t('home.banner_title')}
              </div>
            </div>
            <div style={{ position:'absolute', right: 14, bottom: 12, fontSize: 9.5, opacity: 0.5, letterSpacing: 1 }}>
              [ banner-image ]
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function SectionLabel({ children }) {
  return (
    <div style={{
      fontSize: 11, letterSpacing: 1.8, textTransform:'uppercase',
      color: C.mute, fontWeight: 600,
    }}>{children}</div>
  );
}

function CourseCard({ c, compact = false, onClick }) {
  const { t, branchName } = useTranslation();
  const pct = c.used / c.total;
  return (
    <Pressable onClick={onClick} style={{
      background:'#fff', border:'1px solid '+C.line, borderRadius: 14,
      padding: 14,
    }}>
      <div style={{ display:'flex', justifyContent:'space-between', alignItems:'flex-start', gap: 10 }}>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{ fontSize: 14.5, fontWeight: 600, color: C.ink }}>{c.name}</div>
          <div style={{ fontSize: 11.5, color: C.mute, marginTop: 2 }}>
            {c.category} · {branchName(c.branch)}
          </div>
        </div>
        <Tag bg={C.cloverSoft} color={C.clover}>{c.category}</Tag>
      </div>

      <div style={{ marginTop: 14, display:'flex', alignItems:'baseline', gap: 6 }}>
        <div className="serif" style={{ fontSize: 28, color: C.clover, lineHeight: 1 }}>
          {c.total - c.used}
        </div>
        <div style={{ fontSize: 12, color: C.mute }}>
          {c.type === 'units'
            ? t('courses.of_x_units', { total: c.total, unit: c.unitLabel })
            : t('courses.of_x_sessions', { total: c.total })}
        </div>
      </div>

      <div style={{
        marginTop: 10, height: 6, borderRadius: 4, background: C.bone, overflow:'hidden',
      }}>
        <div style={{
          width: ((1 - pct) * 100) + '%', height: '100%',
          background: 'linear-gradient(90deg, ' + C.clover + ', ' + C.cloverMid + ')',
          borderRadius: 4,
        }} />
      </div>

    </Pressable>
  );
}

// ───────────────────────────────────────────────────────────
// REWARDS tab — replaces the old Orders/History tab.
// Data flows live from D1 (`/api/rewards/list`) so anything an admin adds
// or edits in the CMS shows up here on the customer's next refresh.
// ───────────────────────────────────────────────────────────
function RewardsTab() {
  const { t } = useTranslation();
  const [rewards, setRewards] = useState(window.REWARDS || []);
  const [filter, setFilter]   = useState('all');     // all | affordable | aspirational
  const [loading, setLoading] = useState(true);

  // Always re-fetch fresh on mount + every 30s so admin edits land fast.
  useEffect(() => {
    const load = () => {
      fetch('/api/rewards/list')
        .then(r => r.json())
        .then(d => {
          if (d?.success && Array.isArray(d.data)) {
            const normalized = d.data.map(r => ({
              ...r,
              cost:        r.points_required ?? r.cost ?? 0,
              category:    r.category || r.description || 'Reward',
              description: r.description || '',
            }));
            window.REWARDS = normalized;
            setRewards(normalized);
          }
          setLoading(false);
        })
        .catch(() => setLoading(false));
    };
    load();
    const t = setInterval(load, 30000);
    return () => clearInterval(t);
  }, []);

  const myPoints = Number(MEMBER.points || 0);

  const filtered = rewards.filter(r => {
    const cost = r.cost ?? r.points_required ?? 0;
    if (filter === 'affordable')   return myPoints >= cost;
    if (filter === 'aspirational') return myPoints < cost;
    return true;
  });

  const filters = [
    { id: 'all',          label: 'ทั้งหมด' },
    { id: 'affordable',   label: 'แลกได้เลย' },
    { id: 'aspirational', label: 'สะสมต่อ' },
  ];

  return (
    <div className="fade-in" style={{ paddingBottom: 100 }}>
      {/* Header */}
      <div style={{ padding: '0 20px 8px' }}>
        <div className="serif" style={{ fontSize: 30, color: C.clover, letterSpacing: -0.3 }}>
          ของรางวัล
        </div>
        <div style={{ fontSize: 13, color: C.mute, marginTop: 6, lineHeight: 1.5 }}>
          แลกรางวัลจากคะแนนสะสมที่คุณมี
        </div>

        {/* Points balance pill */}
        <div style={{
          marginTop: 14, padding: '12px 16px', borderRadius: 12,
          background: 'linear-gradient(135deg, ' + C.clover + ' 0%, ' + C.cloverDeep + ' 100%)',
          color: '#fff', display: 'flex', alignItems: 'center', justifyContent: 'space-between',
          boxShadow: '0 8px 24px rgba(31,68,53,0.18)',
        }}>
          <div>
            <div style={{ fontSize: 10.5, letterSpacing: 1.4, textTransform: 'uppercase', opacity: 0.65 }}>
              คะแนนปัจจุบัน
            </div>
            <div className="serif" style={{ fontSize: 26, color: C.gold, marginTop: 2, lineHeight: 1 }}>
              {myPoints.toLocaleString()}<span style={{ fontSize: 13, color: 'rgba(255,255,255,0.6)', marginLeft: 4 }}>pts</span>
            </div>
          </div>
          <IconGift size={28} color={C.gold} />
        </div>
      </div>

      {/* Filter chips */}
      <div style={{ padding: '14px 20px 4px', display: 'flex', gap: 8 }}>
        {filters.map(f => (
          <Pressable key={f.id} onClick={() => setFilter(f.id)} style={{
            padding: '7px 14px', borderRadius: 999,
            background: filter === f.id ? C.clover : '#fff',
            color: filter === f.id ? '#fff' : C.ink,
            border: '1px solid ' + (filter === f.id ? C.clover : C.line),
            fontSize: 12.5, fontWeight: 500, whiteSpace: 'nowrap',
          }}>{f.label}</Pressable>
        ))}
      </div>

      {/* Reward cards */}
      <div style={{ padding: '12px 20px 0', display: 'grid', gap: 10 }}>
        {loading && rewards.length === 0 && (
          <div style={{ padding: '32px 20px', textAlign: 'center', color: C.mute, fontSize: 13 }}>
            กำลังโหลดของรางวัล…
          </div>
        )}

        {!loading && filtered.length === 0 && (
          <div style={{
            padding: '32px 20px', background: '#fff',
            border: '1px dashed ' + C.line2, borderRadius: 14, textAlign: 'center',
          }}>
            <div style={{ fontSize: 32, marginBottom: 8 }}>🎁</div>
            <div style={{ fontSize: 14, color: C.ink, fontWeight: 600 }}>
              {filter === 'affordable' ? 'ยังไม่มีรางวัลที่แลกได้' :
               filter === 'aspirational' ? 'แลกได้ทั้งหมดแล้ว!' :
               'ยังไม่มีรางวัล'}
            </div>
            <div style={{ fontSize: 12, color: C.mute, marginTop: 6, lineHeight: 1.55 }}>
              {filter === 'affordable' ? 'สะสมคะแนนเพิ่มเพื่อปลดล็อกรางวัล' :
               'แอดมินจะเพิ่มของรางวัลใหม่ ๆ เร็ว ๆ นี้'}
            </div>
          </div>
        )}

        {filtered.map(r => {
          const cost = r.cost ?? r.points_required ?? 0;
          const canAfford = myPoints >= cost;
          const progress = Math.min(1, myPoints / Math.max(1, cost));

          return (
            <div key={r.id} style={{
              background: '#fff', border: '1px solid ' + C.line, borderRadius: 16,
              padding: 14, display: 'flex', gap: 14, alignItems: 'flex-start',
              transition: 'border-color .12s, transform .12s',
              boxShadow: canAfford ? '0 4px 14px rgba(201,166,107,0.14)' : 'none',
            }}>
              {/* Image */}
              {r.image_url ? (
                <img src={r.image_url} alt={r.name} style={{
                  width: 70, height: 70, borderRadius: 12, objectFit: 'cover',
                  border: '1px solid ' + C.line, flexShrink: 0,
                }} />
              ) : (
                <div className="stripes" style={{
                  width: 70, height: 70, borderRadius: 12, fontSize: 9, flexShrink: 0,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                }}>IMG</div>
              )}

              {/* Details */}
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ display:'flex', alignItems:'flex-start', justifyContent:'space-between', gap: 8 }}>
                  <div style={{ minWidth: 0, flex: 1 }}>
                    <div style={{ fontSize: 14, fontWeight: 700, color: C.ink, lineHeight: 1.25 }}>
                      {r.name}
                    </div>
                    {r.category && (
                      <div style={{ fontSize: 10.5, color: C.gold, letterSpacing: 0.6, marginTop: 3, fontWeight: 600, textTransform: 'uppercase' }}>
                        {r.category}
                      </div>
                    )}
                  </div>
                  <div style={{
                    padding: '5px 9px', borderRadius: 8,
                    background: canAfford ? C.cloverSoft : C.bone,
                    color: canAfford ? C.clover : C.mute,
                    fontSize: 11, fontWeight: 700, whiteSpace: 'nowrap', textAlign: 'center', flexShrink: 0,
                  }}>
                    <div>{Number(cost).toLocaleString()}</div>
                    <div style={{ fontSize: 8.5, letterSpacing: 1 }}>PTS</div>
                  </div>
                </div>

                {r.description && r.description !== r.category && (
                  <div style={{ fontSize: 11.5, color: C.mute, marginTop: 5, lineHeight: 1.5 }}>
                    {r.description}
                  </div>
                )}

                {/* Progress / CTA */}
                <div style={{ marginTop: 10 }}>
                  {canAfford ? (
                    <div style={{
                      padding: '8px 12px', borderRadius: 8,
                      background: C.clover, color: '#fff',
                      fontSize: 12, fontWeight: 600, textAlign: 'center',
                    }}>
                      ✓ คะแนนพอแลกแล้ว — ติดต่อสาขาเพื่อแลก
                    </div>
                  ) : (
                    <>
                      <div style={{
                        height: 4, borderRadius: 4, background: C.bone, overflow: 'hidden',
                      }}>
                        <div style={{
                          width: (progress * 100) + '%', height: '100%',
                          background: 'linear-gradient(90deg, ' + C.gold + ', #E0C896)',
                        }}/>
                      </div>
                      <div style={{ fontSize: 11, color: C.mute, marginTop: 4 }}>
                        อีก {(cost - myPoints).toLocaleString()} คะแนน
                      </div>
                    </>
                  )}
                </div>
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// POINTS tab
// ───────────────────────────────────────────────────────────
function PointsTab() {
  const { t, tierName } = useTranslation();
  const [tab, setTab] = useState('redeem');
  const [claimOpen, setClaimOpen] = useState(false);
  const [myClaims, setMyClaims] = useState([]);
  const [rewards, setRewards] = useState(window.REWARDS || []);
  const tp = tierProgress(MEMBER);

  // Load my claims on mount (and after a successful submit).
  const loadClaims = React.useCallback(() => {
    if (!MEMBER.id) return;
    fetch(`/api/claims/list?member_id=${encodeURIComponent(MEMBER.id)}&status=all`)
      .then(r => r.json())
      .then(d => setMyClaims(d?.data || []))
      .catch(() => {});
  }, []);
  useEffect(() => { loadClaims(); }, [loadClaims]);

  // Always pull fresh rewards from D1 when this tab mounts.
  useEffect(() => {
    fetch('/api/rewards/list')
      .then(r => r.json())
      .then(d => {
        if (d?.success && Array.isArray(d.data)) {
          const normalized = d.data.map(r => ({
            ...r,
            cost:     r.points_required ?? r.cost ?? 0,
            category: r.category || r.description || 'Reward',
          }));
          window.REWARDS = normalized;
          setRewards(normalized);
        }
      })
      .catch(() => {});

    // Refetch the member so an admin-approved claim shows up immediately.
    const lineUserId = window.LIFF_USER_ID;
    if (lineUserId) {
      fetch(`/api/members/by-line?lineUserId=${encodeURIComponent(lineUserId)}`)
        .then(r => r.json())
        .then(d => {
          if (d?.success && d.data) {
            syncMember(d.data);
            // Force re-render: the parent CustomerApp will reflect new MEMBER.points
            // because PointsTab reads MEMBER.points each render.
          }
        })
        .catch(() => {});
    }
  }, []);

  // Local trigger so PointsTab re-renders after a refresh writes to window.MEMBER.
  const [tick, setTick] = useState(0);
  useEffect(() => {
    const t = setInterval(() => setTick(x => x + 1), 5000);
    return () => clearInterval(t);
  }, []);

  const pendingCount = myClaims.filter(c => c.status === 'pending').length;

  return (
    <div className="fade-in" style={{ paddingBottom: 100 }}>
      {/* Points hero */}
      <div style={{ padding: '0 20px' }}>
        <div style={{
          background: '#fff', border: '1px solid ' + C.line, borderRadius: 18,
          padding: '20px 20px 22px', position: 'relative', overflow: 'hidden',
        }}>
          <div style={{
            position:'absolute', right: -20, bottom: -20, opacity: 0.06,
          }}>
            <CloverMark size={160} color={C.clover} />
          </div>
          <div style={{ fontSize: 11, letterSpacing: 1.8, textTransform:'uppercase', color: C.mute, fontWeight: 600 }}>
            {t('points.clover_points')}
          </div>
          <div style={{ display:'flex', alignItems:'baseline', gap: 10, marginTop: 8 }}>
            <div className="serif" style={{ fontSize: 54, color: C.clover, lineHeight: 1, letterSpacing: -1.2 }}>
              {Number(MEMBER.points || 0).toLocaleString()}
            </div>
            <div style={{ fontSize: 13, color: C.mute }}>{t('common.points_short')}</div>
          </div>
          <div style={{ fontSize: 12, color: C.mute, marginTop: 6 }}>
            {t('points.in_spend_value', { value: fmtTHB(Number(MEMBER.points || 0) * 25) })}
          </div>

          <div style={{ marginTop: 16, display:'flex', alignItems:'center', gap: 10, padding: '10px 12px', background: C.bone, borderRadius: 10 }}>
            <IconSparkle size={16} color={C.gold} />
            <div style={{ fontSize: 12, color: C.ink2, flex: 1 }}>
              {tp.nextTier
                ? `สะสมอีก ${tp.toNext.toLocaleString()} คะแนน เพื่อเลื่อนเป็น ${tierName(tp.nextTier.id)} (อัตรา ฿25 = 1 pt)`
                : `🏆 คุณอยู่ระดับสูงสุดแล้ว — ${tierName(tp.current.id)}`}
            </div>
          </div>

          {/* ── NEW: Submit-receipt CTA ─────────────────────────── */}
          <PrimaryBtn onClick={() => setClaimOpen(true)} style={{ marginTop: 14, width: '100%' }}>
            📸  ส่งใบเสร็จเพื่อรับคะแนน
          </PrimaryBtn>
          {pendingCount > 0 && (
            <div style={{
              marginTop: 8, padding: '8px 12px', borderRadius: 8,
              background: C.goldSoft, color: C.goldDeep,
              fontSize: 11.5, fontWeight: 600, textAlign: 'center',
            }}>
              ⏳ มีใบเสร็จ {pendingCount} ใบรอแอดมินอนุมัติ
            </div>
          )}
        </div>
      </div>

      {/* My Claims list */}
      {myClaims.length > 0 && (
        <div style={{ padding: '20px 20px 0' }}>
          <SectionLabel>ใบเสร็จที่ส่งล่าสุด</SectionLabel>
          <div style={{ marginTop: 10, background:'#fff', borderRadius: 14, border:'1px solid '+C.line }}>
            {myClaims.slice(0, 6).map((c, i) => (
              <ClaimRow key={c.id} c={c} first={i === 0} />
            ))}
          </div>
        </div>
      )}

      {/* Submit modal */}
      {claimOpen && (
        <SubmitClaimModal
          onClose={() => setClaimOpen(false)}
          onSubmitted={() => { setClaimOpen(false); loadClaims(); }}
        />
      )}

      {/* Tabs */}
      <div style={{ padding: '20px 20px 0', display:'flex', gap: 8 }}>
        {[
          { id: 'redeem', label: t('points.redeem') },
          { id: 'history', label: t('points.activity') },
          { id: 'tiers', label: t('points.tiers') },
        ].map(tt => (
          <Pressable key={tt.id} onClick={() => setTab(tt.id)} style={{
            flex: 1, padding: '10px 0', textAlign:'center',
            borderRadius: 10, fontSize: 13, fontWeight: 600,
            background: tab === tt.id ? C.clover : 'transparent',
            color: tab === tt.id ? '#fff' : C.mute,
          }}>{tt.label}</Pressable>
        ))}
      </div>

      {tab === 'redeem' && (
        <div style={{ padding: '16px 20px 0', display:'grid', gap: 10 }}>
          {rewards.length === 0 ? (
            <div style={{
              padding: '32px 20px', background: '#fff',
              border: '1px dashed ' + C.line2, borderRadius: 14, textAlign:'center',
            }}>
              <div style={{ fontSize: 32, marginBottom: 8 }}>🎁</div>
              <div style={{ fontSize: 14, color: C.ink, fontWeight: 600 }}>ยังไม่มีรางวัล</div>
              <div style={{ fontSize: 12, color: C.mute, marginTop: 6, lineHeight: 1.55 }}>
                เมื่อแอดมินเพิ่มของรางวัลใหม่ จะแสดงที่นี่
              </div>
            </div>
          ) : rewards.map(r => {
            const cost = r.cost ?? r.points_required ?? 0;
            const canAfford = (MEMBER.points || 0) >= cost;
            return (
              <div key={r.id} style={{
                background:'#fff', border:'1px solid '+C.line, borderRadius: 14,
                padding: 14, display:'flex', alignItems:'center', gap: 14,
              }}>
                {r.image_url ? (
                  <img src={r.image_url} alt={r.name}
                    style={{ width: 54, height: 54, borderRadius: 10, objectFit: 'cover', border: '1px solid ' + C.line }} />
                ) : (
                  <div className="stripes" style={{ width: 54, height: 54, borderRadius: 10, fontSize: 8 }}>
                    IMG
                  </div>
                )}
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600, color: C.ink, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.name}</div>
                  <div style={{ fontSize: 11, color: C.mute, marginTop: 2, letterSpacing: 0.3, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {r.category || r.description || 'Reward'}
                  </div>
                </div>
                <Pressable disabled={!canAfford} style={{
                  padding: '9px 12px', borderRadius: 10, fontWeight: 600, fontSize: 12,
                  background: canAfford ? C.cloverSoft : C.bone,
                  color: canAfford ? C.clover : C.mute,
                  display:'flex', flexDirection:'column', alignItems:'center', gap: 1, flexShrink: 0,
                }}>
                  <span>{Number(cost).toLocaleString()}</span>
                  <span style={{ fontSize: 9, letterSpacing: 1 }}>PTS</span>
                </Pressable>
              </div>
            );
          })}
        </div>
      )}

      {tab === 'history' && (
        <PointsHistoryList />
      )}

      {tab === 'tiers' && (
        <div style={{ padding: '16px 20px 0' }}>
          {/* Ascending order (Silver → Diamond). Current tier auto-derived
              from MEMBER.points and highlighted with a gold "ปัจจุบัน" pill. */}
          {TIERS.map((tier, i) => {
            const active = tier.id === tp.current.id;
            const nextTier = TIERS[i + 1] || null;
            // Display the points range in pts
            const rangeText = nextTier
              ? `${tier.min.toLocaleString()} – ${(nextTier.min - 1).toLocaleString()} pts`
              : `${tier.min.toLocaleString()}+ pts`;
            return (
              <div key={tier.id} style={{
                marginBottom: 10, padding: 16, borderRadius: 14,
                background: active ? C.clover : '#fff',
                color: active ? '#fff' : C.ink,
                border: '1px solid ' + (active ? C.clover : C.line),
                position: 'relative', overflow: 'hidden',
              }}>
                {active && (
                  <div style={{ position:'absolute', right: -18, top: -18, opacity: 0.2 }}>
                    <CloverMark size={100} color={C.gold} />
                  </div>
                )}
                <div style={{ display:'flex', alignItems:'baseline', justifyContent:'space-between' }}>
                  <div className="serif" style={{ fontSize: 22, color: active ? C.gold : C.clover }}>{tierName(tier.id)}</div>
                  {active && <Tag color={C.clover} bg={C.gold}>{t('points.current') || 'ปัจจุบัน'}</Tag>}
                </div>
                <div style={{
                  fontSize: 11.5, marginTop: 4,
                  opacity: active ? 0.85 : 1,
                  color: active ? 'rgba(255,255,255,0.85)' : C.mute,
                  fontFamily: 'JetBrains Mono, monospace',
                }}>
                  {rangeText}
                </div>
                <div style={{ marginTop: 10, fontSize: 12, opacity: active ? 0.85 : 1, color: active ? 'rgba(255,255,255,0.85)' : C.ink2 }}>
                  {tier.id === 'diamond' && t('points.diamond_perks')}
                  {tier.id === 'platinum' && t('points.platinum_perks')}
                  {tier.id === 'gold' && t('points.gold_perks')}
                  {tier.id === 'silver' && t('points.silver_perks')}
                </div>
              </div>
            );
          })}
        </div>
      )}
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// ───────────────────────────────────────────────────────────
// NotificationsPanel — slides in from the top of the iPhone frame.
// Shows recent approved/rejected claims as cards.
// ───────────────────────────────────────────────────────────
function NotificationsPanel({ items, onClose }) {
  // Hide the bottom-nav while open
  useEffect(() => {
    document.body.classList.add('modal-open');
    return () => document.body.classList.remove('modal-open');
  }, []);

  const fmtTime = (ts) => {
    if (!ts) return '';
    try {
      const d = new Date(String(ts).replace(' ', 'T') + (String(ts).endsWith('Z') ? '' : 'Z'));
      const diff = (Date.now() - d.getTime()) / 1000;
      if (diff < 60)    return Math.floor(diff) + 'วิ ที่แล้ว';
      if (diff < 3600)  return Math.floor(diff / 60) + ' นาทีที่แล้ว';
      if (diff < 86400) return Math.floor(diff / 3600) + ' ชม. ที่แล้ว';
      return d.toLocaleDateString('th-TH');
    } catch { return ts; }
  };

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 9999,
      background: 'rgba(20,20,15,0.55)', backdropFilter: 'blur(4px)',
      display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className="slide-up" style={{
        width: '100%', maxWidth: 480, marginTop: 70,
        background: '#fff', borderRadius: 16,
        boxShadow: '0 20px 60px rgba(0,0,0,0.25)',
        maxHeight: '78%', display: 'flex', flexDirection: 'column',
        overflow: 'hidden', margin: '70px 14px 0',
      }}>
        <div style={{ padding: '16px 20px', borderBottom: '1px solid '+C.line,
                      display:'flex', justifyContent:'space-between', alignItems:'center' }}>
          <div className="serif" style={{ fontSize: 20, color: C.clover, letterSpacing: -0.3 }}>
            การแจ้งเตือน
          </div>
          <Pressable onClick={onClose} style={{
            width: 30, height: 30, borderRadius: 15, background: C.bone,
            display:'flex', alignItems:'center', justifyContent:'center',
            color: C.mute, fontSize: 16,
          }}>×</Pressable>
        </div>

        <div style={{ overflowY: 'auto', flex: 1 }}>
          {items.length === 0 ? (
            <div style={{ padding: '50px 20px', textAlign: 'center', color: C.mute, fontSize: 13 }}>
              <div style={{ fontSize: 32, marginBottom: 8 }}>🔔</div>
              ยังไม่มีการแจ้งเตือน
            </div>
          ) : items.map((n, i) => (
            <div key={n.id} style={{
              padding: '14px 18px',
              borderTop: i === 0 ? 'none' : '1px solid ' + C.line,
              background: n.unread ? 'rgba(201,166,107,0.06)' : '#fff',
              display: 'flex', alignItems: 'flex-start', gap: 12,
            }}>
              {n.unread && (
                <div style={{
                  width: 8, height: 8, borderRadius: 4, background: C.gold,
                  marginTop: 7, flexShrink: 0,
                }}/>
              )}
              <div style={{ flex: 1, minWidth: 0, marginLeft: n.unread ? 0 : 20 }}>
                <div style={{ fontSize: 14, color: C.ink, fontWeight: 600 }}>{n.title}</div>
                <div style={{ fontSize: 12, color: C.ink2, marginTop: 3, lineHeight: 1.5 }}>{n.body}</div>
                <div style={{ fontSize: 11, color: C.mute, marginTop: 6 }}>{fmtTime(n.ts)}</div>
              </div>
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Points History — REAL transactions from D1.
// Falls back to an empty-state card for new customers (no fake data).
// ───────────────────────────────────────────────────────────
function PointsHistoryList() {
  const [items, setItems] = useState(null);   // null = loading; [] = empty

  useEffect(() => {
    if (!MEMBER.id) { setItems([]); return; }
    // Approved claims = positive entries. Future: pull transactions table too.
    fetch(`/api/claims/list?member_id=${encodeURIComponent(MEMBER.id)}&status=approved`)
      .then(r => r.json())
      .then(d => setItems(d?.data || []))
      .catch(() => setItems([]));
  }, []);

  if (items === null) {
    return (
      <div style={{ padding: '40px 20px', textAlign: 'center', color: C.mute, fontSize: 13 }}>
        กำลังโหลด…
      </div>
    );
  }

  if (items.length === 0) {
    return (
      <div style={{
        margin: '8px 20px 0', padding: '28px 20px',
        background: '#fff', border: '1px dashed ' + C.line2, borderRadius: 12,
        textAlign: 'center',
      }}>
        <div style={{ fontSize: 32, marginBottom: 8 }}>📭</div>
        <div style={{ fontSize: 14, color: C.ink, fontWeight: 600 }}>ยังไม่มีประวัติคะแนน</div>
        <div style={{ fontSize: 12, color: C.mute, marginTop: 6, lineHeight: 1.55 }}>
          เมื่อมีการรับ/ใช้คะแนน รายการจะแสดงที่นี่
        </div>
      </div>
    );
  }

  return (
    <div style={{ padding: '8px 20px 0' }}>
      {items.map((row, i) => (
        <div key={row.id} style={{
          padding: '14px 0', display:'flex', alignItems:'center', gap: 12,
          borderTop: i === 0 ? 'none' : '1px solid ' + C.line,
        }}>
          <div style={{
            width: 34, height: 34, borderRadius: 8,
            background: C.cloverSoft, display:'flex',
            alignItems:'center', justifyContent:'center',
            color: C.clover, fontWeight: 700,
          }}>+</div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 13, color: C.ink, fontWeight: 500 }}>
              ใบเสร็จ ฿{Number(row.amount_thb).toLocaleString()}
            </div>
            <div style={{ fontSize: 11, color: C.mute, marginTop: 2 }}>
              {new Date(row.created_at).toLocaleDateString('th-TH')}
            </div>
          </div>
          <div className="mono" style={{ fontSize: 13, fontWeight: 600, color: C.clover }}>
            +{(row.points_awarded || 0).toLocaleString()} pts
          </div>
        </div>
      ))}
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// Claim Points — submit a receipt photo + bill amount.
// The claim sits in `point_claims` (status='pending') until an admin
// approves or rejects it from the Admin Console.
// ───────────────────────────────────────────────────────────
function SubmitClaimModal({ onClose, onSubmitted }) {
  const [amount,    setAmount]    = useState('');
  const [note,      setNote]      = useState('');
  const [imageData, setImageData] = useState(null);    // data URL
  const [imageSize, setImageSize] = useState(0);
  const [submitting, setSubmitting] = useState(false);
  const [error,     setError]     = useState(null);
  const [memberReady, setMemberReady] = useState(!!MEMBER.id);
  const fileRef = React.useRef(null);

  // While the modal is open, mark <body> so the BottomNav can hide itself.
  useEffect(() => {
    document.body.classList.add('modal-open');
    return () => document.body.classList.remove('modal-open');
  }, []);

  // Make sure MEMBER.id is populated. If LINE login completed but the
  // /api/members/by-line fetch hasn't finished yet (or failed), retry now.
  useEffect(() => {
    if (MEMBER.id) { setMemberReady(true); return; }

    // Try to get the LINE userId from LIFF
    const getProfile = async () => {
      if (typeof liff === 'undefined' || !liff.isLoggedIn?.()) return null;
      try { return await liff.getProfile(); } catch { return null; }
    };

    getProfile().then(profile => {
      if (!profile?.userId) {
        setError('ไม่พบข้อมูลการ login — กรุณา login ใหม่');
        return;
      }
      console.log('[ClaimModal] Fetching member for LINE userId:', profile.userId);
      // POST so we can include the profile in case line_users hasn't been
      // upserted yet (e.g. session restored from cookie, network hiccup).
      fetch('/api/members/by-line', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          lineUserId:  profile.userId,
          displayName: profile.displayName || null,
          pictureUrl:  profile.pictureUrl  || null,
        }),
      })
        .then(r => r.json())
        .then(d => {
          if (d?.success && d.data?.id) {
            // Normalize: points_balance from D1 → points for the UI
            const mem = { ...d.data, points: Number(d.data.points ?? d.data.points_balance ?? 0) };
            window.MEMBER  = mem;
            window.ORDERS  = d.data.orders  || [];
            window.COURSES = d.data.courses || [];
            setMemberReady(true);
            console.log('[ClaimModal] Member loaded:', d.data.id, 'pts=', mem.points);
          } else {
            setError('สร้างโปรไฟล์ลูกค้าไม่สำเร็จ — ' + (d?.error || 'กรุณา login ใหม่'));
          }
        })
        .catch(err => setError('โหลดข้อมูลลูกค้าล้มเหลว: ' + err.message));
    });
  }, []);

  // Resize + JPEG-compress the photo client-side. Targets the 10 MB server
  // limit but tries to keep the smallest acceptable file so the upload stays
  // fast on mobile data. Strategy: try high quality → step down if too big.
  const SERVER_LIMIT = 14_000_000;            // matches /api/claims/create
  const MAX_PRACTICAL = 10 * 1024 * 1024;     // 10 MB

  const handleFile = (file) => {
    if (!file) return;
    setError(null);

    // Sanity-check the raw file size up front
    if (file.size > MAX_PRACTICAL * 2) {
      setError(`รูปใหญ่เกินไป (${(file.size / 1024 / 1024).toFixed(1)} MB) — กรุณาเลือกรูปที่เล็กกว่า`);
      return;
    }

    const reader = new FileReader();
    reader.onload = (e) => {
      const img = new Image();
      img.onload = () => {
        // Step-down compression: start at 2560 / 90% — drop until under limit.
        const attempts = [
          { width: 2560, quality: 0.90 },
          { width: 2048, quality: 0.85 },
          { width: 1600, quality: 0.82 },
          { width: 1280, quality: 0.78 },
          { width: 1024, quality: 0.72 },
          { width:  800, quality: 0.68 },
        ];
        let chosen = null;
        for (const a of attempts) {
          const scale = Math.min(1, a.width / img.width);
          const w = Math.round(img.width  * scale);
          const h = Math.round(img.height * scale);
          const canvas = document.createElement('canvas');
          canvas.width = w; canvas.height = h;
          canvas.getContext('2d').drawImage(img, 0, 0, w, h);
          const dataUrl = canvas.toDataURL('image/jpeg', a.quality);
          // Pick the first attempt that fits under the server limit
          if (dataUrl.length <= SERVER_LIMIT) {
            chosen = { dataUrl, w, h, q: a.quality };
            break;
          }
        }
        if (!chosen) {
          setError('รูปนี้ขนาดใหญ่เกินไป แม้บีบอัดแล้ว — กรุณาลองรูปอื่น');
          return;
        }
        setImageData(chosen.dataUrl);
        setImageSize(chosen.dataUrl.length);
        console.log(`[ClaimModal] Image: ${chosen.w}×${chosen.h} @ q${chosen.q} → ${(chosen.dataUrl.length / 1024).toFixed(0)} KB`);
      };
      img.onerror = () => setError('ไม่สามารถอ่านรูปได้ ลองใช้รูปอื่น');
      img.src = e.target.result;
    };
    reader.onerror = () => setError('โหลดรูปไม่สำเร็จ — ลองใหม่อีกครั้ง');
    reader.readAsDataURL(file);
  };

  const submit = async () => {
    setError(null);
    const amt = parseFloat(amount);
    if (!amt || amt <= 0) { setError('กรอกยอดบิลด้วยครับ'); return; }
    if (!imageData)        { setError('ถ่ายรูปใบเสร็จก่อน'); return; }
    if (!MEMBER.id) {
      setError('โปรไฟล์ลูกค้ายังโหลดไม่เสร็จ — กรุณารอสักครู่แล้วลองใหม่');
      return;
    }

    setSubmitting(true);
    try {
      const res = await fetch('/api/claims/create', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          member_id: MEMBER.id,
          amount_thb: amt,
          receipt_image: imageData,
          customer_note: note || null,
          branch_id: MEMBER.branch_id || MEMBER.homeBranch || null,
        }),
      });
      const data = await res.json();
      if (!res.ok || !data.success) throw new Error(data.error || 'Failed');
      onSubmitted();
    } catch (e) {
      setError(e.message);
    } finally {
      setSubmitting(false);
    }
  };

  const previewPts = amount ? Math.floor(parseFloat(amount) / 25) : 0;

  return (
    <div style={{
      position: 'fixed', inset: 0, zIndex: 9999,
      background: 'rgba(20,20,15,0.55)', backdropFilter: 'blur(4px)',
      display: 'flex', alignItems: 'flex-end', justifyContent: 'center',
    }} onClick={onClose}>
      <div onClick={e => e.stopPropagation()} className="slide-up" style={{
        width: '100%', maxWidth: 480,
        background: C.paper, borderRadius: '20px 20px 0 0',
        // Bottom nav is hidden via `body.modal-open`, so normal padding is enough.
        padding: '20px 22px 40px', maxHeight: '92%', overflowY: 'auto',
        position: 'relative', zIndex: 9999,
      }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 14 }}>
          <div className="serif" style={{ fontSize: 22, color: C.clover, letterSpacing: -0.3 }}>
            ส่งใบเสร็จ
          </div>
          <Pressable onClick={onClose} style={{
            width: 32, height: 32, borderRadius: 16, background: '#fff', border: '1px solid '+C.line,
            display:'flex', alignItems:'center', justifyContent:'center', fontSize: 16, color: C.mute,
          }}>×</Pressable>
        </div>

        {/* Photo picker */}
        <div style={{ marginBottom: 16 }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 0.8, textTransform:'uppercase',
                        color: C.mute, marginBottom: 8 }}>รูปใบเสร็จ</div>
          {imageData ? (
            <div style={{ position:'relative', borderRadius: 12, overflow:'hidden', border:'1px solid '+C.line }}>
              <img src={imageData} alt="receipt" style={{ width: '100%', display:'block' }} />
              <Pressable onClick={() => { setImageData(null); setImageSize(0); fileRef.current && (fileRef.current.value = ''); }}
                style={{
                  position:'absolute', top: 8, right: 8,
                  background: 'rgba(0,0,0,0.6)', color: '#fff',
                  padding: '4px 10px', borderRadius: 16, fontSize: 11, fontWeight: 600,
                }}>เปลี่ยนรูป</Pressable>
              <div style={{ position:'absolute', bottom: 6, left: 8,
                            background: 'rgba(0,0,0,0.5)', color: '#fff',
                            padding: '2px 8px', borderRadius: 10, fontSize: 10 }}>
                {imageSize > 1024 * 1024
                  ? (imageSize / 1024 / 1024).toFixed(1) + ' MB'
                  : (imageSize / 1024).toFixed(0) + ' KB'}
              </div>
            </div>
          ) : (
            <Pressable onClick={() => fileRef.current?.click()} style={{
              padding: '32px 20px', borderRadius: 12,
              border: '2px dashed ' + C.line2, background: '#fff',
              textAlign: 'center', color: C.mute,
            }}>
              <div style={{ fontSize: 32 }}>📷</div>
              <div style={{ fontSize: 13, marginTop: 6, fontWeight: 600 }}>แตะเพื่อถ่ายรูป</div>
              <div style={{ fontSize: 11, marginTop: 2 }}>หรือเลือกรูปจากแกลเลอรี่</div>
            </Pressable>
          )}
          <input
            ref={fileRef} type="file" accept="image/*" capture="environment"
            onChange={(e) => handleFile(e.target.files?.[0])}
            style={{ display: 'none' }}
          />
        </div>

        {/* Amount */}
        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 0.8, textTransform:'uppercase',
                        color: C.mute, marginBottom: 8 }}>ยอดบิล (บาท)</div>
          <input
            type="number" inputMode="decimal" min="0" step="0.01"
            placeholder="เช่น 2500"
            value={amount} onChange={(e) => setAmount(e.target.value)}
            style={{
              width: '100%', padding: '14px 16px', borderRadius: 12,
              border: '1.5px solid ' + C.line2, background: '#fff',
              fontSize: 18, color: C.ink, fontFamily: 'inherit', outline: 'none',
            }}
          />
          {previewPts > 0 && (
            <div style={{ marginTop: 8, fontSize: 12.5, color: C.clover, fontWeight: 600 }}>
              ≈ {previewPts.toLocaleString()} คะแนน (อัตรา ฿25 / 1 pt)
            </div>
          )}
        </div>

        {/* Note */}
        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 700, letterSpacing: 0.8, textTransform:'uppercase',
                        color: C.mute, marginBottom: 8 }}>หมายเหตุ (ถ้ามี)</div>
          <input
            type="text" placeholder="เช่น สาขาทองหล่อ / ใบเสร็จเลขที่..."
            value={note} onChange={(e) => setNote(e.target.value)}
            style={{
              width: '100%', padding: '12px 14px', borderRadius: 12,
              border: '1px solid ' + C.line2, background: '#fff',
              fontSize: 14, color: C.ink, fontFamily: 'inherit', outline: 'none',
            }}
          />
        </div>

        {error && (
          <div style={{
            padding: '10px 14px', borderRadius: 10, marginBottom: 12,
            background: '#FEF0EF', border: '1px solid #F5C5C2',
            fontSize: 12.5, color: '#B5453A',
          }}>{error}</div>
        )}

        <PrimaryBtn onClick={submit} disabled={submitting || !memberReady} style={{ width: '100%' }}>
          {submitting ? 'กำลังส่ง…' : !memberReady ? 'กำลังโหลดโปรไฟล์…' : 'ส่งให้แอดมินอนุมัติ'}
        </PrimaryBtn>
        <div style={{ textAlign:'center', fontSize: 11, color: C.mute, marginTop: 10, lineHeight: 1.5 }}>
          คะแนนจะเข้าหลังจากแอดมินอนุมัติใบเสร็จเรียบร้อยแล้ว
        </div>
      </div>
    </div>
  );
}

function ClaimRow({ c, first }) {
  const isApproved = c.status === 'approved';
  const isRejected = c.status === 'rejected';
  const isPending  = c.status === 'pending';

  const statusLabel = isApproved ? 'อนุมัติ' : isRejected ? 'ไม่อนุมัติ' : 'รออนุมัติ';
  const statusColor = isApproved ? C.clover : isRejected ? '#B5453A' : C.goldDeep;
  const statusBg    = isApproved ? C.cloverSoft : isRejected ? '#FEF0EF' : C.goldSoft;

  return (
    <div style={{
      padding: '14px 16px', display:'flex', alignItems:'center', gap: 12,
      borderTop: first ? 'none' : '1px solid ' + C.line,
    }}>
      <div style={{
        width: 36, height: 36, borderRadius: 8,
        background: statusBg, color: statusColor,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        fontSize: 16,
      }}>
        {isApproved ? '✓' : isRejected ? '✕' : '⏳'}
      </div>
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{ fontSize: 13, color: C.ink, fontWeight: 600 }}>
          ฿{Number(c.amount_thb).toLocaleString()} ·{' '}
          {isApproved
            ? `+${c.points_awarded?.toLocaleString() || 0} คะแนน`
            : `${c.points_requested?.toLocaleString() || 0} คะแนน`}
        </div>
        <div style={{ fontSize: 11, color: C.mute, marginTop: 2 }}>
          {new Date(c.created_at).toLocaleDateString('th-TH')}
          {c.admin_note ? ' · ' + c.admin_note : ''}
        </div>
      </div>
      <Tag color={statusColor} bg={statusBg}>{statusLabel}</Tag>
    </div>
  );
}

// COURSES tab (Remaining Courses) — the "Vital" one per brief
// ───────────────────────────────────────────────────────────
function CoursesTab() {
  const { t } = useTranslation();
  const [selected, setSelected] = useState(null);
  if (selected) return <CourseDetail c={selected} onBack={() => setSelected(null)} />;

  const totalSessions = COURSES.filter(c => c.type === 'sessions').reduce((s,c) => s + (c.total - c.used), 0);
  const totalUnits = COURSES.filter(c => c.type === 'units').reduce((s,c) => s + (c.total - c.used), 0);

  return (
    <div className="fade-in" style={{ paddingBottom: 100 }}>
      <div style={{ padding: '0 20px 14px' }}>
        <div className="serif" style={{ fontSize: 30, color: C.clover, letterSpacing: -0.3 }}>{t('courses.your_courses')}</div>
        <div style={{ fontSize: 13, color: C.mute, marginTop: 6 }}>
          {t('courses.your_courses_sub')}
        </div>

        <div style={{ display:'grid', gridTemplateColumns: '1fr 1fr', gap: 10, marginTop: 18 }}>
          <div style={{ padding: 14, borderRadius: 12, background: C.clover, color: '#fff' }}>
            <div style={{ fontSize: 10, letterSpacing: 1.5, textTransform:'uppercase', opacity: 0.7 }}>{t('courses.sessions_left')}</div>
            <div className="serif" style={{ fontSize: 30, marginTop: 4, color: C.gold }}>{totalSessions}</div>
            <div style={{ fontSize: 11, opacity: 0.7, marginTop: 2 }}>{t('courses.across_n', { n: COURSES.filter(c => c.type === 'sessions').length })}</div>
          </div>
          <div style={{ padding: 14, borderRadius: 12, background: C.goldSoft }}>
            <div style={{ fontSize: 10, letterSpacing: 1.5, textTransform:'uppercase', color: C.goldDeep, fontWeight: 600 }}>{t('courses.units_reserved')}</div>
            <div className="serif" style={{ fontSize: 30, marginTop: 4, color: C.clover }}>{totalUnits}</div>
            <div style={{ fontSize: 11, color: C.ink2, marginTop: 2 }}>{t('courses.tox_balance')}</div>
          </div>
        </div>
      </div>

      <div style={{ padding: '0 20px', display:'grid', gap: 10 }}>
        {COURSES.length === 0 ? (
          <div style={{
            padding: '32px 20px', background: '#fff',
            border: '1px dashed ' + C.line2, borderRadius: 14,
            textAlign: 'center',
          }}>
            <div style={{ fontSize: 32, marginBottom: 8 }}>📚</div>
            <div style={{ fontSize: 14, color: C.ink, fontWeight: 600 }}>ยังไม่มีคอร์ส</div>
            <div style={{ fontSize: 12, color: C.mute, marginTop: 6, lineHeight: 1.55 }}>
              เมื่อคุณซื้อคอร์ส รายละเอียดจะแสดงที่นี่
            </div>
          </div>
        ) : COURSES.map(c => <CourseCard key={c.id} c={c} onClick={() => setSelected(c)} />)}
      </div>
    </div>
  );
}

function CourseDetail({ c, onBack }) {
  const { t, branchName } = useTranslation();
  const pct = c.used / c.total;
  const [qrOpen, setQrOpen] = useState(false);
  // Real visit log comes from the course record's `visits` array (populated
  // by the server when the course has any usage). New customers see an empty
  // state instead of mock entries.
  const visits = Array.isArray(c.visits) ? c.visits : [];

  return (
    <div className="fade-in" style={{ paddingBottom: 100 }}>
      <div style={{ padding: '0 20px 4px' }}>
        <Pressable onClick={onBack} style={{ display:'flex', alignItems:'center', gap: 6, color: C.clover, fontSize: 13, fontWeight: 600, marginBottom: 14 }}>
          <IconArrowLeft size={16} color={C.clover} /> {t('courses.all_courses')}
        </Pressable>

        <div style={{ background: C.clover, color: '#fff', borderRadius: 18, padding: 22, position:'relative', overflow:'hidden' }}>
          <div style={{ position:'absolute', right: -22, top: -22, opacity: 0.15 }}>
            <CloverMark size={140} color={C.gold} />
          </div>
          <Tag bg="rgba(201,166,107,0.18)" color={C.gold}>{c.category}</Tag>
          <div className="serif" style={{ fontSize: 28, marginTop: 8, letterSpacing: -0.3 }}>{c.name}</div>
          <div style={{ fontSize: 12, color: 'rgba(255,255,255,0.6)', marginTop: 4 }}>
            {t('courses.purchased_at', { date: c.purchased })} · {branchName(c.branch)}
          </div>

          <div style={{ marginTop: 18, display:'flex', alignItems:'baseline', gap: 10 }}>
            <div className="serif" style={{ fontSize: 60, color: C.gold, lineHeight: 1, letterSpacing: -1.5 }}>
              {c.total - c.used}
            </div>
            <div style={{ fontSize: 13, color: 'rgba(255,255,255,0.7)' }}>
              {c.type === 'sessions'
                ? t('courses.of_x_sessions', { total: c.total })
                : t('courses.of_x_units', { total: c.total, unit: c.unitLabel })}
            </div>
          </div>

          <div style={{ marginTop: 12, height: 6, borderRadius: 4, background: 'rgba(255,255,255,0.1)', overflow:'hidden' }}>
            <div style={{
              width: ((1 - pct) * 100) + '%', height: '100%',
              background: 'linear-gradient(90deg, ' + C.gold + ', #E0C896)', borderRadius: 4,
            }} />
          </div>
          <div style={{ marginTop: 8, fontSize: 11, color: 'rgba(255,255,255,0.6)' }}>{c.notes}</div>
        </div>

        <div style={{ marginTop: 14 }}>
          <PrimaryBtn onClick={() => setQrOpen(true)} style={{ width: '100%', display:'flex', alignItems:'center', justifyContent:'center', gap: 8 }}>
            <IconQR size={16} color="#fff" /> {t('qr.show_qr')}
          </PrimaryBtn>
        </div>
      </div>

      <MemberQRModal open={qrOpen} onClose={() => setQrOpen(false)} context="course" />

      <div style={{ padding: '24px 20px 0' }}>
        <SectionLabel>{c.type === 'sessions' ? t('courses.session_log') : t('courses.usage_log')}</SectionLabel>
        <div style={{ marginTop: 10, background:'#fff', border:'1px solid '+C.line, borderRadius: 14, overflow:'hidden' }}>
          {visits.length === 0 ? (
            <div style={{ padding: '28px 20px', textAlign:'center', color: C.mute, fontSize: 12.5 }}>
              ยังไม่มีบันทึกการใช้
            </div>
          ) : visits.map((v, i) => (
            <div key={i} style={{
              padding: '14px 16px', display:'flex', alignItems:'center', gap: 12,
              borderTop: i === 0 ? 'none' : '1px solid ' + C.line,
            }}>
              <div style={{
                width: 32, height: 32, borderRadius: 8, background: C.cloverSoft,
                display:'flex', alignItems:'center', justifyContent:'center',
                color: C.clover, fontWeight: 700, fontSize: 12,
              }}>{c.type === 'sessions' ? '#' + v.n : '·'}</div>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 13, color: C.ink, fontWeight: 500 }}>
                  {c.type === 'sessions' ? v.date : v.date}
                </div>
                <div style={{ fontSize: 11, color: C.mute, marginTop: 2 }}>
                  {c.type === 'sessions' ? `${branchName(v.branch)} · ${v.by}` : v.by}
                </div>
              </div>
              {c.type === 'sessions' && <IconCheck size={16} color={C.clover} />}
            </div>
          ))}
        </div>
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// PROFILE tab
// ───────────────────────────────────────────────────────────
function ProfileTab({ onSignOut, lineUser }) {
  const { t, tierName, branchName } = useTranslation();
  const displayName = lineUser ? lineUser.name : MEMBER.name;
  const initials = (displayName || 'M').split(' ').map(w => w[0]).join('').slice(0, 2).toUpperCase();
  const [editing, setEditing] = useState(false);
  const [saving, setSaving] = useState(false);
  const [toast, setToast] = useState(null);     // {msg, kind}
  const [formData, setFormData] = useState({
    phone:      MEMBER.phone || '',
    email:      MEMBER.email || '',
    birthday:   MEMBER.date_of_birth || MEMBER.birthday || '',
    homeBranch: MEMBER.branch_id || MEMBER.homeBranch || '',
  });

  // Refresh form when MEMBER changes (e.g. after by-line fetch finishes)
  useEffect(() => {
    setFormData({
      phone:      MEMBER.phone || '',
      email:      MEMBER.email || '',
      birthday:   MEMBER.date_of_birth || MEMBER.birthday || '',
      homeBranch: MEMBER.branch_id || MEMBER.homeBranch || '',
    });
  }, [MEMBER.id]);

  const showToast = (msg, kind = 'ok') => {
    setToast({ msg, kind });
    setTimeout(() => setToast(null), 3000);
  };

  const handleSave = async () => {
    if (!MEMBER.id) {
      showToast('โปรไฟล์ยังโหลดไม่เสร็จ ลองใหม่อีกครั้ง', 'err');
      return;
    }
    setSaving(true);
    try {
      const response = await fetch(`/api/members/${MEMBER.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name:          MEMBER.name || displayName,
          phone:         formData.phone || null,
          email:         formData.email || null,
          date_of_birth: formData.birthday || null,
          branch_id:     formData.homeBranch || null,
          gender:        MEMBER.gender || '',
          tier_id:       tierForPoints(MEMBER.points).id,   // auto from current points
          notes:         MEMBER.notes || null,
        }),
      });
      const result = await response.json();
      if (!response.ok || !result.success) {
        throw new Error(result.error || `HTTP ${response.status}`);
      }
      // Mirror the saved values into MEMBER (in place) so the read-only view shows them.
      syncMember({
        ...MEMBER,
        phone:         formData.phone,
        email:         formData.email,
        date_of_birth: formData.birthday,
        birthday:      formData.birthday,
        branch_id:     formData.homeBranch,
        homeBranch:    formData.homeBranch,
      });
      setEditing(false);
      showToast('✓ บันทึกแล้ว');
      console.log('[ProfileTab] Member updated:', result.data);
    } catch (err) {
      console.warn('[ProfileTab] Error saving profile:', err);
      showToast('บันทึกไม่สำเร็จ: ' + err.message, 'err');
    } finally {
      setSaving(false);
    }
  };

  return (
    <div className="fade-in" style={{ paddingBottom: 100, position: 'relative' }}>
      {toast && (
        <div className="slide-up" style={{
          position: 'fixed', top: 80, left: 20, right: 20, zIndex: 1000,
          padding: '12px 16px', borderRadius: 10,
          background: toast.kind === 'err' ? '#B5453A' : C.clover,
          color: '#fff', fontSize: 13, fontWeight: 600, textAlign: 'center',
          boxShadow: '0 10px 30px rgba(0,0,0,0.25)',
        }}>{toast.msg}</div>
      )}
      <div style={{ padding: '0 20px', textAlign:'center' }}>
        <div style={{
          width: 80, height: 80, borderRadius: '50%',
          background: C.cloverSoft, margin: '0 auto',
          display:'flex', alignItems:'center', justifyContent:'center',
          border: '2px solid ' + C.gold,
          overflow: 'hidden', position: 'relative',
        }}>
          {lineUser && lineUser.picture
            ? <img src={lineUser.picture} alt={displayName}
                style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
            : <span className="serif" style={{ fontSize: 30, color: C.clover }}>{initials}</span>
          }
        </div>
        {lineUser && (
          <div style={{ display:'flex', alignItems:'center', justifyContent:'center', gap: 5, marginTop: 8 }}>
            <IconLine size={13} color="#06C755" />
            <span style={{ fontSize: 11, color: '#06C755', fontWeight: 600, letterSpacing: 0.4 }}>LINE Connected</span>
          </div>
        )}
        <div className="serif" style={{ fontSize: 24, color: C.clover, marginTop: lineUser ? 4 : 12 }}>{displayName}</div>
        <div style={{ fontSize: 12, color: C.mute, marginTop: 4, letterSpacing: 0.4 }}>
          {MEMBER.id} · {t('home.tier_member', { tier: tierName(tierForPoints(MEMBER.points).id) })} · {Number(MEMBER.points || 0).toLocaleString()} pts
        </div>
      </div>

      <div style={{ padding: '24px 20px 0' }}>
        <div style={{ display:'flex', justifyContent:'space-between', alignItems:'center', marginBottom: 10 }}>
          <SectionLabel>{t('profile.account')}</SectionLabel>
          {!editing && (
            <Pressable onClick={() => setEditing(true)} style={{
              fontSize: 12, color: C.clover, fontWeight: 600, padding: '4px 8px'
            }}>
              ✎ {t('common.edit') || 'Edit'}
            </Pressable>
          )}
        </div>
        <div style={{ marginTop: 10, background:'#fff', borderRadius: 14, border:'1px solid '+C.line }}>
          {editing ? (
            <>
              <EditableRow
                label={t('profile.phone')}
                value={formData.phone}
                onChange={(v) => setFormData({...formData, phone: v})}
                placeholder="+66 89 234 4127"
              />
              <EditableRow
                label={t('profile.email')}
                value={formData.email}
                onChange={(v) => setFormData({...formData, email: v})}
                placeholder="you@example.com"
              />
              <EditableRow
                label={t('profile.birthday')}
                value={formData.birthday}
                onChange={(v) => setFormData({...formData, birthday: v})}
                placeholder="YYYY-MM-DD"
                type="date"
              />
              <EditableSelect
                label={t('profile.home_branch')}
                value={formData.homeBranch}
                onChange={(v) => setFormData({...formData, homeBranch: v})}
                options={BRANCHES.map(b => ({ id: b.id, name: b.name }))}
              />
              <div style={{ padding: '14px 16px', display:'flex', gap: 8, borderTop: '1px solid ' + C.line }}>
                <Pressable onClick={handleSave} disabled={saving} style={{
                  flex: 1, padding: '10px 12px', borderRadius: 10, background: C.clover, color: '#fff',
                  fontSize: 13, fontWeight: 600, textAlign: 'center', opacity: saving ? 0.6 : 1
                }}>
                  {saving ? 'Saving...' : 'Save'}
                </Pressable>
                <Pressable onClick={() => setEditing(false)} style={{
                  flex: 1, padding: '10px 12px', borderRadius: 10, background: C.bone, color: C.ink,
                  fontSize: 13, fontWeight: 600, textAlign: 'center'
                }}>
                  Cancel
                </Pressable>
              </div>
            </>
          ) : (
            <>
              <Row label={t('profile.phone')} value={MEMBER.phone || '—'} />
              <Row label={t('profile.email')} value={MEMBER.email || '—'} />
              <Row label={t('profile.birthday')} value={MEMBER.birthday || '—'} />
              <Row label={t('profile.home_branch')} value={MEMBER.homeBranch ? branchName(MEMBER.homeBranch) : '—'} />
            </>
          )}
        </div>
      </div>

      <div style={{ padding: '20px 20px 0' }}>
        <SectionLabel>{t('profile.preferences')}</SectionLabel>
        <div style={{ marginTop: 10, background:'#fff', borderRadius: 14, border:'1px solid '+C.line }}>
          <ToggleRow label={t('profile.treatment_reminders')} defaultOn />
          <ToggleRow label={t('profile.promo_offers')} defaultOn={false} />
          <ToggleRow label={t('profile.line_receipts')} defaultOn />
          {/* Language switcher integrated as a settings row */}
          <LanguageSwitcher variant="full" />
        </div>
      </div>

      <div style={{ padding: '20px 20px 0' }}>
        <Pressable onClick={onSignOut} style={{
          padding: '15px 16px', borderRadius: 12, background: 'transparent',
          border: '1px solid ' + C.line, textAlign:'center',
          color: '#B5453A', fontWeight: 600, fontSize: 14,
        }}>
          {t('profile.sign_out')}
        </Pressable>
        <div style={{ textAlign:'center', fontSize: 10.5, color: C.mute, marginTop: 14, letterSpacing: 0.4 }}>
          v 1.4.2 · build 26050.alpha
        </div>
      </div>
    </div>
  );
}

function Row({ label, value }) {
  return (
    <div style={{ padding: '14px 16px', display:'flex', justifyContent:'space-between',
      borderTop: '1px solid ' + C.line, fontSize: 13.5 }}>
      <span style={{ color: C.mute }}>{label}</span>
      <span style={{ color: C.ink, fontWeight: 500 }}>{value}</span>
    </div>
  );
}

function EditableRow({ label, value, onChange, placeholder, type = 'text' }) {
  // Coerce date-input value: HTML date inputs only accept YYYY-MM-DD format.
  const inputValue = type === 'date'
    ? (value && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : '')
    : (value || '');

  return (
    <div style={{ padding: '14px 16px', display:'flex', flexDirection:'column', gap: 6,
      borderTop: '1px solid ' + C.line }}>
      <span style={{ color: C.mute, fontSize: 12, fontWeight: 500 }}>{label}</span>
      <input
        type={type}
        value={inputValue}
        onChange={(e) => onChange(e.target.value)}
        placeholder={placeholder}
        style={{
          padding: '10px 12px', borderRadius: 8, border: '1px solid ' + C.line,
          fontSize: 13.5, color: C.ink, fontFamily: 'inherit', outline: 'none',
          transition: 'border-color .15s',
          // Make sure date input doesn't have system-default ugly look
          background: '#fff',
          minHeight: 42,
        }}
        onFocus={(e) => (e.target.style.borderColor = C.clover)}
        onBlur={(e) => (e.target.style.borderColor = C.line)}
      />
    </div>
  );
}

function EditableSelect({ label, value, onChange, options }) {
  return (
    <div style={{ padding: '14px 16px', display:'flex', flexDirection:'column', gap: 6,
      borderTop: '1px solid ' + C.line }}>
      <span style={{ color: C.mute, fontSize: 12, fontWeight: 500 }}>{label}</span>
      <select
        value={value}
        onChange={(e) => onChange(e.target.value)}
        style={{
          padding: '10px 12px', borderRadius: 8, border: '1px solid ' + C.line,
          fontSize: 13.5, color: C.ink, fontFamily: 'inherit', outline: 'none',
          transition: 'border-color .15s',
          background: '#fff',
        }}
        onFocus={(e) => (e.target.style.borderColor = C.clover)}
        onBlur={(e) => (e.target.style.borderColor = C.line)}
      >
        <option value="">Select branch...</option>
        {options.map(opt => (
          <option key={opt.id} value={opt.id}>{opt.name}</option>
        ))}
      </select>
    </div>
  );
}
function ToggleRow({ label, defaultOn }) {
  const [on, setOn] = useState(defaultOn);
  return (
    <Pressable onClick={() => setOn(!on)} style={{
      padding: '14px 16px', display:'flex', justifyContent:'space-between', alignItems:'center',
      borderTop: '1px solid ' + C.line, fontSize: 13.5,
    }}>
      <span style={{ color: C.ink }}>{label}</span>
      <div style={{
        width: 38, height: 22, borderRadius: 11,
        background: on ? C.clover : C.line2,
        position:'relative', transition: 'background .15s',
      }}>
        <div style={{
          position:'absolute', top: 2, left: on ? 18 : 2,
          width: 18, height: 18, borderRadius: '50%', background:'#fff',
          transition: 'left .15s', boxShadow: '0 1px 3px rgba(0,0,0,0.2)',
        }} />
      </div>
    </Pressable>
  );
}

// ───────────────────────────────────────────────────────────
// Bottom nav
// ───────────────────────────────────────────────────────────
function BottomNav({ tab, setTab }) {
  const { t } = useTranslation();
  const tabs = [
    { id: 'home',    label: t('nav.home'),    I: IconHome },
    { id: 'orders',  label: t('nav.orders'),  I: IconGift },   // "ของรางวัล" / Rewards
    { id: 'points',  label: t('nav.points'),  I: IconSparkle },
    { id: 'courses', label: t('nav.courses'), I: IconLayers },
    { id: 'profile', label: t('nav.profile'), I: IconUser },
  ];

  // Hide the nav whenever a modal opens (e.g. SubmitClaimModal).
  const [hidden, setHidden] = useState(false);
  useEffect(() => {
    const sync = () => setHidden(document.body.classList.contains('modal-open'));
    sync();
    const mo = new MutationObserver(sync);
    mo.observe(document.body, { attributes: true, attributeFilter: ['class'] });
    return () => mo.disconnect();
  }, []);
  if (hidden) return null;

  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, bottom: 0,
      zIndex: 55, paddingBottom: 22, pointerEvents: 'auto',
    }}>
      <div style={{
        margin: '0 14px', borderRadius: 24,
        background: 'rgba(255,255,255,0.85)',
        backdropFilter: 'blur(20px) saturate(180%)',
        WebkitBackdropFilter: 'blur(20px) saturate(180%)',
        border: '1px solid rgba(0,0,0,0.06)',
        boxShadow: '0 8px 30px rgba(0,0,0,0.08)',
        padding: '8px 4px',
        display:'flex', justifyContent:'space-around',
      }}>
        {tabs.map(t => {
          const active = tab === t.id;
          return (
            <Pressable key={t.id} onClick={() => setTab(t.id)} style={{
              padding: '6px 2px',
              display:'flex', flexDirection:'column', alignItems:'center', gap: 3,
              flex: 1, minWidth: 0,   // allow shrinking, prevent overflow
            }}>
              <t.I size={20} color={active ? C.clover : C.mute} strokeWidth={active ? 1.9 : 1.6} />
              <div style={{
                fontSize: 10, fontWeight: active ? 700 : 500,
                color: active ? C.clover : C.mute,
                letterSpacing: 0.1,
                whiteSpace: 'nowrap',  // one line only
                overflow: 'hidden',
                textOverflow: 'ellipsis',
                maxWidth: '100%',
                textAlign: 'center',
              }}>{t.label}</div>
            </Pressable>
          );
        })}
      </div>
    </div>
  );
}

// ───────────────────────────────────────────────────────────
// App shell
// ───────────────────────────────────────────────────────────
function CustomerApp({ initialScreen = 'auth-landing' }) {
  const [screen, setScreen] = useState(initialScreen);
  const [phone, setPhone] = useState('');
  const [tab, setTab] = useState('home');
  const [lineUser, setLineUser] = useState(null);        // { name, picture } from LINE
  const [liffInitialized, setLiffInitialized] = useState(false);

  // Notifications — driven by status changes on the customer's claims.
  const [notifOpen, setNotifOpen] = useState(false);
  const [notifs,    setNotifs]    = useState([]);    // [{ id, title, body, ts, unread }]
  const [unreadCount, setUnreadCount] = useState(0);

  // Build notifications from approved/rejected claims, comparing against
  // what we've already shown the user (stored in localStorage).
  const refreshNotifications = React.useCallback(async () => {
    // Read live MEMBER.id from the window global so this callback always
    // sees the latest value (avoids closure capturing an empty MEMBER.id).
    const memberId = window.MEMBER?.id;
    if (!memberId) return;
    try {
      const res = await fetch(`/api/claims/list?member_id=${encodeURIComponent(memberId)}&status=all`);
      const data = await res.json();
      const claims = (data?.data || []).filter(c => c.status === 'approved' || c.status === 'rejected');
      const seen = new Set(JSON.parse(localStorage.getItem('clover-seen-claims') || '[]'));
      const items = claims.slice(0, 25).map(c => ({
        id: c.id,
        kind: c.status,           // 'approved' | 'rejected'
        title: c.status === 'approved'
          ? `🎉 ได้รับ ${c.points_awarded || 0} คะแนน`
          : '❌ ใบเสร็จไม่ผ่านการอนุมัติ',
        body: c.status === 'approved'
          ? `จากบิล ฿${Number(c.amount_thb || 0).toLocaleString()}${c.admin_note ? ' · ' + c.admin_note : ''}`
          : `บิล ฿${Number(c.amount_thb || 0).toLocaleString()}${c.admin_note ? ' · ' + c.admin_note : ''}`,
        ts: c.reviewed_at || c.created_at,
        unread: !seen.has(c.id),
      }));
      setNotifs(items);
      setUnreadCount(items.filter(n => n.unread).length);
    } catch {}
  }, []);

  // Refresh on mount + every 30s + immediately when member becomes available
  useEffect(() => {
    refreshNotifications();
    const t = setInterval(refreshNotifications, 30000);
    return () => clearInterval(t);
  }, [refreshNotifications, member?.id]);

  // Periodic MEMBER refetch — so admin's point/profile updates appear in the
  // customer app within ~20s without requiring a manual refresh.
  useEffect(() => {
    const tick = async () => {
      const lineUserId = window.LIFF_USER_ID;
      if (!lineUserId) return;
      try {
        const r = await fetch(`/api/members/by-line?lineUserId=${encodeURIComponent(lineUserId)}`);
        const d = await r.json();
        if (d?.success && d.data) {
          syncMember(d.data);
          setMember({ ...MEMBER });
        }
      } catch {}
    };
    const interval = setInterval(tick, 20000);
    return () => clearInterval(interval);
  }, []);

  const openNotifications = () => {
    setNotifOpen(true);
    // Mark currently-known notifications as seen
    const seen = new Set(JSON.parse(localStorage.getItem('clover-seen-claims') || '[]'));
    notifs.forEach(n => seen.add(n.id));
    localStorage.setItem('clover-seen-claims', JSON.stringify([...seen]));
    setUnreadCount(0);
    setNotifs(ns => ns.map(n => ({ ...n, unread: false })));
    // Also refresh the member balance so the points number is current
    if (MEMBER.id) {
      fetch(`/api/members/by-line?lineUserId=${encodeURIComponent(window.LIFF_USER_ID || '')}`)
        .then(r => r.json())
        .then(d => {
          if (d?.success && d.data) {
            syncMember(d.data);
            setMember({ ...MEMBER });   // new object identity → React re-renders
          }
        })
        .catch(() => {});
    }
  };

  // Data state — fetched from D1 APIs
  const [branches, setBranches] = useState([]);
  const [rewards, setRewards] = useState([]);
  const [member, setMember] = useState(null);
  const [memberOrders, setMemberOrders] = useState([]);
  const [memberCourses, setMemberCourses] = useState([]);

  // On mount: fetch global data (branches, rewards, brand) from D1 + init LIFF.
  //   D1 is the single source of truth — admin saves go there, customer reads from there.
  useEffect(() => {
    // ── Branches ──────────────────────────────────────────────
    fetch('/api/branches/list')
      .then(r => r.json())
      .then(data => {
        if (data.success && data.data) {
          window.BRANCHES = data.data;
          setBranches(data.data);
          console.log('[CustomerApp] Branches loaded:', data.data.length);
        }
      })
      .catch(err => console.warn('[CustomerApp] Failed to fetch branches:', err));

    // ── Rewards ───────────────────────────────────────────────
    fetch('/api/rewards/list')
      .then(r => r.json())
      .then(data => {
        if (data.success && data.data) {
          window.REWARDS = data.data;
          setRewards(data.data);
          console.log('[CustomerApp] Rewards loaded:', data.data.length);
        }
      })
      .catch(err => console.warn('[CustomerApp] Failed to fetch rewards:', err));

    // ── Brand settings (colors + logo) ────────────────────────
    // Whatever the admin saves at /api/settings/brand is what the customer sees.
    fetch('/api/settings/brand')
      .then(r => r.json())
      .then(d => {
        if (d?.success && d.data) {
          window.BRAND = d.data;
          // Push brand colors onto CSS variables so the whole app re-themes.
          const root = document.documentElement;
          const setVar = (k, v) => v && root.style.setProperty(k, v);
          setVar('--clover',      d.data.clover);
          setVar('--clover-deep', d.data.cloverDeep);
          setVar('--clover-mid',  d.data.cloverMid);
          setVar('--clover-soft', d.data.cloverSoft);
          setVar('--gold',        d.data.gold);
          setVar('--gold-deep',   d.data.goldDeep);
          setVar('--gold-soft',   d.data.goldSoft);
          setVar('--bone',        d.data.bone);
          setVar('--paper',       d.data.paper);
          setVar('--ink',         d.data.ink);
          console.log('[CustomerApp] Brand loaded:', d.updated_at || 'defaults');
        }
      })
      .catch(err => console.warn('[CustomerApp] Failed to fetch brand:', err));

    // Initialize LIFF SDK
    if (typeof liff !== 'undefined') {
      liff.init({ liffId: LIFF_ID })
        .then(() => {
          setLiffInitialized(true);
          if (liff.isLoggedIn()) {
            return liff.getProfile();
          }
        })
        .then(profile => {
          if (!profile) return;
          setLineUser({ name: profile.displayName, picture: profile.pictureUrl || '' });
          window.LIFF_USER_ID = profile.userId;   // remembered for notification refreshes
          const pdpaAccepted = localStorage.getItem('clover_pdpa_accepted');
          setScreen(pdpaAccepted ? 'app' : 'auth-pdpa');

          // Record this login in D1 so the admin console can see who logged in.
          fetch('/api/line/upsert', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              lineUserId:    profile.userId,
              displayName:   profile.displayName,
              pictureUrl:    profile.pictureUrl    || '',
              statusMessage: profile.statusMessage || '',
            }),
          })
          .then(() => {
            // After recording the LINE login, fetch the member profile
            return fetch('/api/members/by-line', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({
                lineUserId:  profile.userId,
                displayName: profile.displayName,
                pictureUrl:  profile.pictureUrl || '',
              }),
            });
          })
          .then(r => r.json())
          .then(data => {
            if (data.success && data.data) {
              syncMember(data.data);
              window.ORDERS = data.data.orders || [];
              window.COURSES = data.data.courses || [];
              setMember({ ...MEMBER });
              setMemberOrders(data.data.orders || []);
              setMemberCourses(data.data.courses || []);
              console.log('[CustomerApp] Member profile loaded:', data.data.id, 'pts=', MEMBER.points);
            }
          })
          .catch(err => console.warn('[CustomerApp] Failed to fetch member profile:', err));
        })
        .catch(err => console.warn('LIFF init:', err));
    }

    // Fallback: check existing session cookie (covers old-flow users)
    const cookie = document.cookie.split('; ')
      .find(row => row.startsWith('clover_line_session='));
    if (cookie) {
      try {
        const payload = JSON.parse(atob(cookie.split('=').slice(1).join('=')));
        if (payload.lineUserId && payload.displayName) {
          setLineUser({ name: payload.displayName, picture: payload.pictureUrl || '' });
          const pdpaAccepted = localStorage.getItem('clover_pdpa_accepted');
          setScreen(pdpaAccepted ? 'app' : 'auth-pdpa');

          // Fetch the member profile for this LINE user
          fetch(`/api/members/by-line?lineUserId=${encodeURIComponent(payload.lineUserId)}`)
            .then(r => r.json())
            .then(data => {
              if (data.success && data.data) {
                syncMember(data.data);
                window.ORDERS = data.data.orders || [];
                window.COURSES = data.data.courses || [];
                setMember({ ...MEMBER });
                setMemberOrders(data.data.orders || []);
                setMemberCourses(data.data.courses || []);
                console.log('[CustomerApp] Member profile loaded (from cookie):', data.data.id);
              }
            })
            .catch(err => console.warn('[CustomerApp] Failed to fetch member profile from cookie:', err));
        }
      } catch (_) {}
    }
  }, []);

  // header chrome (shown only when authed)
  const inApp = !screen.startsWith('auth');

  return (
    <div style={{ position: 'relative', height: '100%', background: C.paper }}>
      {/* Top status pad — keep clear of dynamic island */}
      {inApp && (
        <div style={{
          paddingTop: 56, padding: '56px 20px 12px',
          display:'flex', alignItems:'center', justifyContent:'space-between',
          background: C.paper, position: 'sticky', top: 0, zIndex: 30,
        }}>
          <div style={{ display:'flex', alignItems:'center', gap: 8 }}>
            <CloverMark size={22} color={C.clover} />
            <div className="serif" style={{ fontSize: 18, color: C.clover, letterSpacing: -0.2 }}>Clover</div>
          </div>
          <div style={{ display:'flex', gap: 10, alignItems:'center' }}>
            <LanguageSwitcher variant="pill" />
            <Pressable onClick={openNotifications} style={{
              width: 36, height: 36, borderRadius: 18, background:'#fff',
              border:'1px solid '+C.line, display:'flex', alignItems:'center',
              justifyContent:'center', position: 'relative',
            }}>
              <IconBell size={16} color={C.ink} />
              {unreadCount > 0 && (
                <div style={{
                  position: 'absolute', top: -2, right: -2,
                  minWidth: 18, height: 18, borderRadius: 9,
                  background: '#B5453A', color: '#fff',
                  fontSize: 10, fontWeight: 700,
                  display: 'flex', alignItems: 'center', justifyContent: 'center',
                  padding: '0 5px', border: '2px solid ' + C.paper,
                }}>{unreadCount > 9 ? '9+' : unreadCount}</div>
              )}
            </Pressable>
          </div>
        </div>
      )}

      <div style={{ overflow: 'auto', height: inApp ? 'calc(100% - 92px)' : '100%' }} className="noscroll">
        {screen === 'auth-landing' && (
          <AuthLanding
            liffInitialized={liffInitialized}
            onMethod={(m) => setScreen(m === 'phone' ? 'auth-phone' : 'auth-landing')}
          />
        )}
        {screen === 'auth-phone' && <AuthPhone onBack={() => setScreen('auth-landing')} onSend={(p) => { setPhone(p); setScreen('auth-otp'); }} />}
        {screen === 'auth-otp' && <AuthOTP phone={phone} onBack={() => setScreen('auth-phone')} onVerify={() => setScreen('auth-merge')} />}
        {screen === 'auth-merge' && <AuthLegacyMerge onConfirm={() => setScreen('auth-pdpa')} onSkip={() => setScreen('auth-pdpa')} />}
        {screen === 'auth-pdpa' && (
          <AuthPDPA onAgree={() => {
            localStorage.setItem('clover_pdpa_accepted', '1');
            // Wait a moment for member profile to be fetched, then show app
            setTimeout(() => setScreen('app'), 100);
          }} />
        )}

        {screen === 'app' && tab === 'home' && <HomeTab go={setTab} lineUser={lineUser} />}
        {screen === 'app' && tab === 'orders' && <RewardsTab />}
        {screen === 'app' && tab === 'points' && <PointsTab />}
        {screen === 'app' && tab === 'courses' && <CoursesTab />}
        {screen === 'app' && tab === 'profile' && (
          <ProfileTab
            lineUser={lineUser}
            onSignOut={() => {
              // Clear LINE session cookie
              document.cookie = 'clover_line_session=; Max-Age=0; Path=/';
              localStorage.removeItem('clover_pdpa_accepted');
              if (typeof liff !== 'undefined' && liff.isLoggedIn()) liff.logout();
              setLineUser(null);
              setLiffInitialized(false);
              setScreen('auth-landing');
              setTab('home');
            }}
          />
        )}
      </div>

      {inApp && <BottomNav tab={tab} setTab={setTab} />}

      {notifOpen && (
        <NotificationsPanel
          items={notifs}
          onClose={() => setNotifOpen(false)}
        />
      )}
    </div>
  );
}

Object.assign(window, { CustomerApp });
