// QReward Customer App — Core: icons, data, nav, shared components
const { useState, useEffect, useRef, useMemo, useCallback, createContext, useContext } = React;

/* ============ ICONS (Lucide-style, 24px stroke) ============ */
const I = {
  home: 'M3 9.5 12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z',
  points: 'M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
  redeem: 'M20 12v9H4v-9M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z',
  card: 'M2 5h20v14H2zM2 10h20',
  profile: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
  bell: 'M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9M10.3 21a1.94 1.94 0 0 0 3.4 0',
  chevR: 'M9 18l6-6-6-6',
  chevL: 'M15 18l-6-6 6-6',
  chevD: 'M6 9l6 6 6-6',
  chevU: 'M18 15l-6-6-6 6',
  x: 'M18 6 6 18M6 6l12 12',
  check: 'M20 6 9 17l-5-5',
  plus: 'M12 5v14M5 12h14',
  minus: 'M5 12h14',
  search: 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.3-4.3',
  heart: 'M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1L12 21l7.7-7.6 1.1-1a5.5 5.5 0 0 0 0-7.8z',
  cart: 'M1 1h4l2.7 13.4a2 2 0 0 0 2 1.6h9.7a2 2 0 0 0 2-1.6L23 6H6M9 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2zM20 21a1 1 0 1 0 0-2 1 1 0 0 0 0 2z',
  eye: 'M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8zM12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
  eyeOff: 'M17.9 17.9A10.1 10.1 0 0 1 12 20C5 20 1 12 1 12a18.5 18.5 0 0 1 5.1-5.9m3.8-1.8A9.1 9.1 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.2 3.2m-6.7-1a3 3 0 1 1-4.2-4.2M1 1l22 22',
  lock: 'M5 11h14v10H5zM7 11V7a5 5 0 0 1 10 0v4',
  unlock: 'M5 11h14v10H5zM7 11V7a5 5 0 0 1 9.9-1',
  faceId: 'M4 8V6a2 2 0 0 1 2-2h2M16 4h2a2 2 0 0 1 2 2v2M20 16v2a2 2 0 0 1-2 2h-2M8 20H6a2 2 0 0 1-2-2v-2M9 9v1M15 9v1M12 8v4l1 1M9 15a4 4 0 0 0 6 0',
  gift: 'M20 12v9H4v-9M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z',
  link: 'M10 13a5 5 0 0 0 7.5.5l3-3a5 5 0 0 0-7-7l-1.7 1.7M14 11a5 5 0 0 0-7.5-.5l-3 3a5 5 0 0 0 7 7l1.7-1.7',
  arrowR: 'M5 12h14M12 5l7 7-7 7',
  arrowUp: 'M12 19V5M5 12l7-7 7 7',
  arrowDown: 'M12 5v14M19 12l-7 7-7-7',
  clock: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 6v6l4 2',
  info: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 16v-4M12 8h.01',
  alert: 'M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0zM12 9v4M12 17h.01',
  truck: 'M1 3h15v13H1zM16 8h4l3 3v5h-7V8zM5.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM18.5 21a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5z',
  pkg: 'M16.5 9.4 7.5 4.2M21 16V8a2 2 0 0 0-1-1.7l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.7l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16zM3.3 7l8.7 5 8.7-5M12 22V12',
  trash: 'M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2',
  edit: 'M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7M18.5 2.5a2.1 2.1 0 0 1 3 3L12 15l-4 1 1-4z',
  settings: 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 1 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 1 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 1 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 1 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
  globe: 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM2 12h20M12 2a15 15 0 0 1 4 10 15 15 0 0 1-4 10 15 15 0 0 1-4-10 15 15 0 0 1 4-10z',
  key: 'M21 2l-2 2m-7.6 7.6a5.5 5.5 0 1 1-7.8 7.8 5.5 5.5 0 0 1 7.8-7.8zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3',
  shield: 'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
  logout: 'M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9',
  doc: 'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8',
  user: 'M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',
  phone: 'M22 16.9v3a2 2 0 0 1-2.2 2 19.8 19.8 0 0 1-8.6-3 19.5 19.5 0 0 1-6-6 19.8 19.8 0 0 1-3-8.6A2 2 0 0 1 4.1 2h3a2 2 0 0 1 2 1.7c.1 1 .4 1.9.7 2.8a2 2 0 0 1-.5 2.1L8.1 9.9a16 16 0 0 0 6 6l1.3-1.3a2 2 0 0 1 2.1-.4c.9.3 1.8.6 2.8.7a2 2 0 0 1 1.7 2z',
  mail: 'M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2zM22 6l-10 7L2 6',
  mapPin: 'M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0zM12 13a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
  tag: 'M20.6 13.4 13.4 20.6a2 2 0 0 1-2.8 0L2 12V2h10l8.6 8.6a2 2 0 0 1 0 2.8zM7 7h.01',
  star: 'M12 2l3.1 6.3 6.9 1-5 4.9 1.2 6.8L12 17.8 5.8 21l1.2-6.8-5-4.9 6.9-1z',
  spark: 'M12 3v3M5.6 5.6l2.1 2.1M3 12h3M5.6 18.4l2.1-2.1M12 21v-3M18.4 18.4l-2.1-2.1M21 12h-3M18.4 5.6l-2.1 2.1M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6z',
  refresh: 'M23 4v6h-6M1 20v-6h6M3.5 9a9 9 0 0 1 14.9-3.4L23 10M1 14l4.6 4.4A9 9 0 0 0 20.5 15',
  filter: 'M22 3H2l8 9.5V19l4 2v-8.5z',
  copy: 'M9 9h11v11H9zM5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1',
  bookmark: 'M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z',
  qrLogo: '',
  wallet: 'M20 12V8H6a2 2 0 0 1 0-4h12v4M4 6v12a2 2 0 0 0 2 2h14v-4M18 12a2 2 0 0 0 0 4h4v-4z',
  trending: 'M23 6l-9.5 9.5-5-5L1 18M17 6h6v6',
  device: 'M5 2h14a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zM12 18h.01',
  sun: 'M12 17a5 5 0 1 0 0-10 5 5 0 0 0 0 10zM12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
  moon: 'M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z',
};
const Icon = ({ n, s = 22, sw = 1.9, c, style, fill = 'none' }) => (
  <svg width={s} height={s} viewBox="0 0 24 24" fill={fill} stroke={c || 'currentColor'} strokeWidth={sw}
    strokeLinecap="round" strokeLinejoin="round" style={style}>
    {(I[n] || '').split('M').filter(Boolean).map((d, i) => <path key={i} d={'M' + d} />)}
  </svg>
);

/* ============ FORMAT ============ */
const fmt = (n) => new Intl.NumberFormat('en-US').format(Math.round(n));
// QReward points glyph — the "r" suffix is part of brand identity
const Pts = ({ v, big, cls = '' }) => (
  <span className={cls} style={{ fontFeatureSettings: '"tnum"', whiteSpace: 'nowrap' }}>
    {fmt(v)}<span className={big ? 'r-glyph' : 'r-glyph'} style={big ? { fontSize: '0.62em' } : { fontSize: '0.7em' }}>&thinsp;r</span>
  </span>
);

/* ============ L10N (EN/JP representative subset) ============ */
const STR = {
  home: { en: 'Home', jp: 'ホーム' },
  myPoints: { en: 'My Points', jp: 'ポイント' },
  redeem: { en: 'Redeem', jp: '交換' },
  myCard: { en: 'My Card', jp: 'カード' },
  profile: { en: 'Profile', jp: 'プロフィール' },
  signIn: { en: 'Sign in', jp: 'サインイン' },
  marketplace: { en: 'Marketplace', jp: 'マーケット' },
  totalBalance: { en: 'Total balance', jp: '合計残高' },
  featuredOffers: { en: 'Featured offers', jp: 'おすすめ' },
  recentActivity: { en: 'Recent activity', jp: '最近の履歴' },
  viewAll: { en: 'View all', jp: 'すべて見る' },
  quickActions: { en: 'Quick actions', jp: 'クイック操作' },
  applyForCard: { en: 'Apply for card', jp: 'カード申請' },
  linking: { en: 'Linking', jp: '連携' },
  continue: { en: 'Continue', jp: '次へ' },
  addToCart: { en: 'Add to cart', jp: 'カートに追加' },
  redeemNow: { en: 'Redeem now', jp: '今すぐ交換' },
};
const L = (lang, key) => (STR[key] ? STR[key][lang] : key);

/* ============ MOCK DATA ============ */
const DATA = {
  user: { name: 'Giri', nameJp: 'ギリ', initials: 'G', email: 'giri.tanaka@gmail.com', emailMasked: 'g***@gmail.com', joined: '2025/11/12' },
  balance: 12540,
  lifecard: 7180,
  qualia: 5360,
  card: {
    // status: notapplied | submitted | activated | declined
    status: 'submitted',
    submittedDate: '2026/05/01 14:22',
    activatedDate: '2026/05/03 10:31',
    masked: '•••• •••• •••• 4821',
    expiry: '08/30',
    holder: 'GIRI TANAKA',
  },
  sources: [
    { id: 'lifecard', name: 'LifeCard', linked: true, lastSync: '2026/06/07 10:31', lifetime: 18200, color: '#0ea5e9', letter: 'L' },
    { id: 'qualia', name: 'QUALIA', linked: true, lastSync: '2026/06/07 09:14', lifetime: 14600, color: '#7c3aed', letter: 'Q' },
  ],
  offers: [
    { id: 'o1', name: 'Golden Week 15% off', scope: 'Platform-wide', tag: '15% OFF', theme: '' },
    { id: 'o2', name: 'Coffee & Tea Cashback', scope: 'Food & Drink', tag: 'CASHBACK', theme: 'warm' },
    { id: 'o3', name: 'First Buy Bonus', scope: '+3 categories', tag: 'BONUS', theme: 'green' },
    { id: 'o4', name: 'Sakura Gift Promo', scope: 'Gift Cards', tag: 'NEW', theme: 'pink' },
  ],
  activity: [
    { id: 'a1', src: 'lifecard', type: 'earn', desc: 'LifeCard CSV import — June 2026', amt: 840, bal: 12540, date: '2026/06/07 10:31' },
    { id: 'a2', src: 'redeem', type: 'redeem', desc: 'Starbucks e-Gift ¥500 ×1', amt: -500, bal: 11700, date: '2026/06/06 14:22' },
    { id: 'a3', src: 'qualia', type: 'earn', desc: 'QUALIA daily batch', amt: 320, bal: 12200, date: '2026/06/05 09:14' },
    { id: 'a4', src: 'admin', type: 'earn', desc: 'Refund credit — order QR-90122', amt: 200, bal: 11880, date: '2026/06/03 09:42' },
    { id: 'a5', src: 'redeem', type: 'redeem', desc: 'JR East Travel Voucher ×1', amt: -15000, bal: 11680, date: '2026/05/28 16:08' },
    { id: 'a6', src: 'lifecard', type: 'earn', desc: 'LifeCard CSV import — May 2026', amt: 1240, bal: 26680, date: '2026/05/15 10:24' },
  ],
  categories: ['All', 'Food & Drink', 'Electronics', 'Gift Cards', 'Lifestyle', 'Travel', 'Experiences'],
  products: [
    { id: 'p1', nameJp: 'スターバックス e-ギフト ¥500', name: 'Starbucks e-Gift ¥500', brand: 'Starbucks', price: 500, cat: 'Food & Drink', img: '#1a7a5e', offer: '15% OFF', fav: true },
    { id: 'p2', nameJp: 'アマゾンギフト ¥1,000', name: 'Amazon Gift ¥1,000', brand: 'Amazon', price: 1000, cat: 'Gift Cards', img: '#d98324', fav: false },
    { id: 'p3', nameJp: 'ソニー WH-1000XM5', name: 'Sony WH-1000XM5', brand: 'Sony', price: 38000, cat: 'Electronics', img: '#1f2937', fav: false },
    { id: 'p4', nameJp: '無印良品 ホームセット', name: 'MUJI Home Set', brand: 'MUJI', price: 4500, cat: 'Lifestyle', img: '#b1442f', fav: true },
    { id: 'p5', nameJp: 'ネスプレッソ コーヒーセット', name: 'Nespresso Coffee Set', brand: 'Nespresso', price: 6800, cat: 'Food & Drink', img: '#6b3410', fav: false },
    { id: 'p6', nameJp: 'JR東日本 旅行券', name: 'JR East Travel Voucher', brand: 'JR East', price: 15000, cat: 'Travel', img: '#0e7490', fav: false },
    { id: 'p7', nameJp: '銀座資生堂スパ', name: 'Ginza Shiseido Spa', brand: 'Shiseido', price: 22000, cat: 'Experiences', img: '#be185d', fav: false, oos: true },
    { id: 'p8', nameJp: '伊勢丹ディナー券', name: 'Isetan Dining Voucher', brand: 'Isetan', price: 12000, cat: 'Food & Drink', img: '#6d28d9', fav: false },
  ],
  addresses: [
    { id: 'ad1', name: 'Giri Tanaka', zip: '〒107-0061', line: 'Tokyo, Minato-ku, Kita-Aoyama 3-6-7', extra: 'Floor 12, Apt 1208', phone: '090-1234-5678', selected: true },
    { id: 'ad2', name: 'Giri Tanaka (Office)', zip: '〒100-0005', line: 'Tokyo, Chiyoda, Marunouchi 1-9-1', extra: 'Tokyo Bldg 22F', phone: '090-1234-5678', selected: false },
  ],
  notifications: [
    { id: 'n1', type: 'promo', icon: 'tag', title: 'Golden Week 15% off is live', body: 'Check it out before May 6', time: '2m', unread: true, cat: 'Promotions' },
    { id: 'n2', type: 'points', icon: 'arrowUp', title: '+840 r added to your QReward balance', body: 'LifeCard CSV import — June 2026', time: '1h', unread: true, cat: 'Account' },
    { id: 'n3', type: 'wishlist', icon: 'heart', title: 'MUJI Home Set is now on offer', body: 'A wishlisted item just got cheaper', time: '3h', unread: true, cat: 'Promotions' },
    { id: 'n4', type: 'order', icon: 'truck', title: 'Your order is Out for Delivery', body: 'Order QR-90138 · arriving today', time: '5h', unread: false, cat: 'Orders' },
    { id: 'n5', type: 'cart', icon: 'cart', title: 'You left 2 items in your cart', body: 'Finish redeeming before they sell out', time: '1d', unread: false, cat: 'Account' },
    { id: 'n6', type: 'legal', icon: 'doc', title: 'Please review our updated Privacy Policy', body: 'A new version is available', time: '2d', unread: false, cat: 'Account' },
  ],
  orderSteps: ['Processing', 'Dispatched', 'Out for Delivery', 'Delivered'],
};

/* ============ NAV CONTEXT ============ */
// A simple screen stack + tab system inside the phone.
const NavCtx = createContext(null);
const useNav = () => useContext(NavCtx);

/* ============ SHARED UI ============ */
const StatusPill = ({ kind, label, icon }) => {
  const map = {
    active: 'pill-green', linked: 'pill-green', activated: 'pill-green', delivered: 'pill-green',
    locked: 'pill-purple', submitted: 'pill-blue', processing: 'pill-blue', dispatched: 'pill-blue',
    declined: 'pill-red', notdelivered: 'pill-red', unlinked: 'pill-neutral', ended: 'pill-neutral',
    scheduled: 'pill-teal', paused: 'pill-amber', ofd: 'pill-amber',
  };
  return (
    <span className={`pill ${map[kind] || 'pill-neutral'}`}>
      {icon ? <Icon n={icon} s={11} sw={2.4} /> : <span className="pill-dot"></span>}
      {label}
    </span>
  );
};

const TopBar = ({ title, onBack, right, lang, transparent }) => {
  const nav = useNav();
  return (
    <div className="tbar" style={transparent ? { background: 'transparent' } : null}>
      <div style={{ display: 'flex', alignItems: 'center', flex: 1, minWidth: 0 }}>
        {onBack && <button className="tbar-back" onClick={onBack}><Icon n="chevL" s={22} sw={2.2} /></button>}
        {title && <div className="tbar-title" style={{ marginLeft: onBack ? 2 : 0 }}>{title}</div>}
      </div>
      {right}
    </div>
  );
};

const Wordmark = ({ size = 22 }) => (
  <span className="brand-wordmark" style={{ fontSize: size }}>
    QReward
  </span>
);

// The hero 3D points card
const QCard = ({ balance, lifecard, qualia, status = 'active', lang = 'en', loading, zero, animateKey, onTap, tilt = true }) => {
  const [shown, setShown] = useState(loading ? 0 : balance);
  useEffect(() => {
    if (loading) return;
    let raf, start;
    const from = 0, to = balance, dur = 900;
    const tick = (t) => {
      if (!start) start = t;
      const p = Math.min(1, (t - start) / dur);
      const eased = 1 - Math.pow(1 - p, 3);
      setShown(Math.round(from + (to - from) * eased));
      if (p < 1) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [balance, loading, animateKey]);

  if (loading) {
    return (
      <div className="qcard" style={{ transform: 'none' }}>
        <div className="qcard-head"><div className="skel" style={{ width: 90, height: 12, background: 'rgba(255,255,255,0.2)' }}></div></div>
        <div className="qcard-body"><div className="skel" style={{ width: 160, height: 36, background: 'rgba(255,255,255,0.2)', marginTop: 8 }}></div></div>
        <div className="qcard-foot"><div className="skel" style={{ width: 200, height: 12, background: 'rgba(255,255,255,0.2)' }}></div></div>
      </div>
    );
  }
  return (
    <div className={`qcard ${status === 'locked' ? 'locked-state' : ''} ${zero ? 'zero-state' : ''}`}
      style={tilt ? null : { transform: 'none' }} onClick={onTap}>
      <div className="qcard-chip"></div>
      <div className="qcard-head">
        <div className="qcard-cobrand">LifeCard <span className="qcard-cobrand-sep"></span> QUALIA</div>
        {status === 'locked'
          ? <span className="qcard-status locked"><span className="qcard-status-dot"></span>Locked</span>
          : <span className="qcard-status"><span className="qcard-status-dot"></span>Active</span>}
      </div>
      <div className="qcard-body">
        <div className="qcard-label">{lang === 'jp' ? '合計残高' : 'Total balance'}</div>
        <div className="qcard-balance">
          {fmt(shown)}<span className="qcard-balance-r">r</span>
        </div>
      </div>
      <div className="qcard-foot">
        <div className="qcard-foot-item">
          <span className="label">LifeCard</span>
          <span className="val">{fmt(lifecard)}<span className="r-mini">r</span></span>
        </div>
        <div className="qcard-foot-item" style={{ textAlign: 'right' }}>
          <span className="label">QUALIA</span>
          <span className="val">{fmt(qualia)}<span className="r-mini">r</span></span>
        </div>
      </div>
      {status === 'locked' && (
        <div className="qcard-lock-overlay">
          <Icon n="lock" s={28} />
          <div style={{ fontSize: 12, fontWeight: 600 }}>Account locked</div>
        </div>
      )}
    </div>
  );
};

// Bottom tab bar
const TabBar = ({ active, onChange, lang }) => {
  const tabs = [
    { id: 'home', icon: 'home', label: L(lang, 'home') },
    { id: 'points', icon: 'points', label: L(lang, 'myPoints') },
    { id: 'redeem', icon: 'redeem', label: L(lang, 'redeem'), elevated: true },
    { id: 'card', icon: 'card', label: L(lang, 'myCard') },
    { id: 'profile', icon: 'profile', label: L(lang, 'profile') },
  ];
  return (
    <div className="tabbar">
      {tabs.map(t => t.elevated ? (
        <button key={t.id} className={`tab elevated ${active === t.id ? 'active' : ''}`} onClick={() => onChange(t.id)}>
          <span className="tab-glyph"><Icon n={t.icon} s={22} sw={2} /></span>
          <span className="tab-label">{t.label}</span>
        </button>
      ) : (
        <button key={t.id} className={`tab ${active === t.id ? 'active' : ''}`} onClick={() => onChange(t.id)}>
          <Icon n={t.icon} s={22} sw={active === t.id ? 2.2 : 1.8} />
          <span>{t.label}</span>
        </button>
      ))}
    </div>
  );
};

// Bottom sheet
const Sheet = ({ open, onClose, children, title }) => {
  if (!open) return null;
  return (
    <>
      <div className="sheet-backdrop" onClick={onClose}></div>
      <div className="sheet">
        <div className="sheet-handle"></div>
        <div className="sheet-content">
          {title && <div className="sheet-title">{title}</div>}
          {children}
        </div>
      </div>
    </>
  );
};

// Toast
const ToastCtx = createContext(null);
const useToast = () => useContext(ToastCtx);

// Button helper
const Btn = ({ children, variant = 'primary', block, sm, disabled, onClick, style, icon }) => (
  <button className={`btn btn-${variant} ${block ? 'btn-block' : ''} ${sm ? 'btn-sm' : ''} ${disabled ? 'disabled' : ''}`}
    disabled={disabled} onClick={onClick} style={style}>
    {icon && <Icon n={icon} s={sm ? 15 : 18} sw={2} />}
    {children}
  </button>
);

Object.assign(window, {
  Icon, I, fmt, Pts, STR, L, DATA, NavCtx, useNav, StatusPill, TopBar, Wordmark,
  QCard, TabBar, Sheet, ToastCtx, useToast, Btn,
});
