// ui_kits/mobile/Components.jsx
// Goji mobile atoms — buttons, cards, nav, FAB, fields, icons.
// Keep stateless; pages compose these.

const { useState } = React;

// ─────────────────────────────────────────────────────────────
// Icon — Lucide-style strokes at 1.75. Inherits currentColor.
// ─────────────────────────────────────────────────────────────
function Icon({ name, size = 22, stroke = 1.75, style }) {
  const common = {
    width: size, height: size, viewBox: '0 0 24 24', fill: 'none',
    stroke: 'currentColor', strokeWidth: stroke,
    strokeLinecap: 'round', strokeLinejoin: 'round', style,
  };
  const paths = {
    home: <><path d="M3 12 12 4l9 8"/><path d="M5 10v10h14V10"/></>,
    wallet: <><rect x="2" y="6" width="20" height="12" rx="2"/><circle cx="12" cy="12" r="2"/><circle cx="6" cy="12" r="0.5" fill="currentColor" stroke="none"/><circle cx="18" cy="12" r="0.5" fill="currentColor" stroke="none"/></>,
    plus: <><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></>,
    book: <><path d="M12 7v14"/><path d="M16 12h2"/><path d="M16 8h2"/><path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/><path d="M6 8h2"/><path d="M6 12h2"/></>,
    sparkle: <><path d="M12 2v3M12 19v3M4.2 4.2l2.1 2.1M17.7 17.7l2.1 2.1M2 12h3M19 12h3M4.2 19.8l2.1-2.1M17.7 6.3l2.1-2.1"/><circle cx="12" cy="12" r="3"/></>,
    send: <><path d="M7 17 17 7"/><path d="M7 7h10v10"/></>,
    bell: <><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M10 21h4"/></>,
    settings: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 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 0 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 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 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 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6c.61-.25 1-.85 1-1.51V3a2 2 0 0 1 4 0v.09A1.65 1.65 0 0 0 15 4.6a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.25.61.85 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.66 0-1.26.39-1.51 1z"/></>,
    bill: <><path d="M6 2h9l5 5v15H6z"/><path d="M14 2v6h6"/><path d="M9 13h6M9 17h4"/></>,
    coffee: <><path d="M17 8h1a4 4 0 0 1 0 8h-1"/><path d="M3 8h14v9a4 4 0 0 1-4 4H7a4 4 0 0 1-4-4z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></>,
    chart: <><path d="M3 17l6-6 4 4 8-9"/><path d="M14 6h6v6"/></>,
    home2: <><path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/></>,
    sun: <><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></>,
    moon: <><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></>,
    arrowUp: <><line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/></>,
    arrowDown: <><line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/></>,
    chevronRight: <polyline points="9 18 15 12 9 6"/>,
    chevronLeft: <polyline points="15 18 9 12 15 6"/>,
    eye: <><path d="M1 12s4-7 11-7 11 7 11 7-4 7-11 7S1 12 1 12z"/><circle cx="12" cy="12" r="3"/></>,
    eyeOff: <><path d="M17.9 17.9A10 10 0 0 1 12 19c-7 0-11-7-11-7a18 18 0 0 1 4.1-5"/><path d="M9.9 5.1A10 10 0 0 1 12 5c7 0 11 7 11 7a18 18 0 0 1-2.1 3"/><path d="M14.1 14.1a3 3 0 1 1-4.2-4.2"/><line x1="2" y1="2" x2="22" y2="22"/></>,
    gear: <><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 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 0 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 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09c.66 0 1.26-.39 1.51-1a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6c.61-.25 1-.85 1-1.51V3a2 2 0 0 1 4 0v.09c0 .66.39 1.26 1 1.51a1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82c.25.61.85 1 1.51 1H21a2 2 0 0 1 0 4h-.09c-.66 0-1.26.39-1.51 1z"/></>,
    chat: <><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></>,
    arrowRight: <><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></>,
    logout: <><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></>,
    info: <><circle cx="12" cy="12" r="9"/><line x1="12" y1="11" x2="12" y2="17"/><circle cx="12" cy="8" r="1" fill="currentColor" stroke="none"/></>,
    mic: <><rect x="9" y="2" width="6" height="12" rx="3"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="8" y1="23" x2="16" y2="23"/></>,
    gemini: <path d="M20.616 10.835a14.147 14.147 0 01-4.45-3.001 14.111 14.111 0 01-3.678-6.452.503.503 0 00-.975 0 14.134 14.134 0 01-3.679 6.452 14.155 14.155 0 01-4.45 3.001c-.65.28-1.318.505-2.002.678a.502.502 0 000 .975c.684.172 1.35.397 2.002.677a14.147 14.147 0 014.45 3.001 14.112 14.112 0 013.679 6.453.502.502 0 00.975 0c.172-.685.397-1.351.677-2.003a14.145 14.145 0 013.001-4.45 14.113 14.113 0 016.453-3.678.503.503 0 000-.975 13.245 13.245 0 01-2.003-.678z" fill="currentColor" stroke="none"/>,
    stopSquare: <rect x="6" y="6" width="12" height="12" rx="2" fill="currentColor" stroke="none"/>,
    calendar: <><rect x="3" y="5" width="18" height="16" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="3" x2="8" y2="7"/><line x1="16" y1="3" x2="16" y2="7"/></>,
    scanFace: <><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M8 14s1.5 2 4 2 4-2 4-2"/><path d="M9 9h.01"/><path d="M15 9h.01"/></>,
    globe: <><circle cx="12" cy="12" r="9"/><path d="M3 12h18"/><path d="M12 3a14 14 0 0 1 4 9 14 14 0 0 1-4 9 14 14 0 0 1-4-9 14 14 0 0 1 4-9z"/></>,
    close: <><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></>,
    user: <><circle cx="12" cy="8" r="4"/><path d="M3 21a9 9 0 0 1 18 0"/></>,
    refresh: <><path d="M21 12a9 9 0 1 1-3.6-7.2"/><polyline points="21 4 21 10 15 10"/></>,
    salary: <><path d="M7 17l5-5 4 4 5-9"/><path d="M3 21h18"/></>,
    target: <><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="5"/><circle cx="12" cy="12" r="1.5" fill="currentColor"/></>,
    family: <><circle cx="9" cy="8" r="3"/><circle cx="17" cy="9" r="2.2"/><path d="M3 21a6 6 0 0 1 12 0"/><path d="M14 21a5 5 0 0 1 8-4"/></>,
  };
  return <svg {...common}>{paths[name] || null}</svg>;
}

// ─────────────────────────────────────────────────────────────
// AmountVisibility — global toggle for masking $ amounts.
// Provider in App; useAmountMask() in any component that renders money.
// ─────────────────────────────────────────────────────────────
const AmountVisibilityContext = React.createContext(false);
function useAmountMask() { return React.useContext(AmountVisibilityContext); }

// ─────────────────────────────────────────────────────────────
// Money — semantic span with tabular numerics. Always use this for $.
// ─────────────────────────────────────────────────────────────
function Money({ amount, size = 'inherit', color = 'inherit', sign = false, currency = '$', children }) {
  const hidden = useAmountMask();
  const display = hidden
    ? (() => {
        const neg = amount != null && amount < 0;
        const prefix = sign ? (neg ? '\u2212' : '+') : (neg ? '\u2212' : '');
        return `${prefix}${currency}\u2022\u2022\u2022\u2022`;
      })()
    : (children != null ? children : (() => {
        const abs = Math.abs(amount);
        const s = abs.toLocaleString('en-US', { minimumFractionDigits: abs % 1 === 0 ? 0 : 2, maximumFractionDigits: 2 });
        const neg = amount < 0;
        const prefix = sign ? (neg ? '\u2212' : '+') : (neg ? '\u2212' : '');
        return `${prefix}${currency}${s}`;
      })());
  return (
    <span style={{
      fontFamily: 'var(--font-sans)',
      fontVariantNumeric: 'tabular-nums lining-nums',
      fontFeatureSettings: '"tnum" 1, "lnum" 1',
      letterSpacing: '-0.02em',
      fontWeight: 600,
      fontSize: size === 'inherit' ? undefined : size,
      color,
    }}>{display}</span>
  );
}

// ─────────────────────────────────────────────────────────────
// Button — primary (gradient), secondary, ghost. 24px radius.
// ─────────────────────────────────────────────────────────────
function Button({ variant = 'primary', size = 'md', leadingIcon, trailingIcon, children, onClick, full, style }) {
  const base = {
    display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
    gap: 8, borderRadius: 'var(--radius-button)',
    fontFamily: 'var(--font-sans)', fontWeight: 600,
    border: '1px solid transparent', cursor: 'pointer',
    transition: 'transform var(--dur-instant) var(--ease-smooth), background var(--dur-fast) var(--ease-smooth)',
    width: full ? '100%' : undefined,
    height: size === 'sm' ? 40 : size === 'lg' ? 56 : 52,
    padding: size === 'sm' ? '0 16px' : '0 22px',
    fontSize: size === 'sm' ? 14 : 15,
    ...style,
  };
  const variants = {
    primary: {
      background: 'var(--goji-gradient)',
      color: 'var(--goji-btn-text)',
      boxShadow: 'var(--shadow-fab)',
    },
    secondary: {
      background: 'var(--color-input-bg)',
      color: 'var(--color-text-primary)',
      borderColor: 'var(--color-border-strong)',
    },
    ghost: {
      background: 'transparent',
      color: 'var(--goji-accent)',
    },
  };
  return (
    <button onClick={onClick} style={{ ...base, ...variants[variant] }}
      onMouseDown={e => e.currentTarget.style.transform = 'scale(0.97)'}
      onMouseUp={e => e.currentTarget.style.transform = ''}
      onMouseLeave={e => e.currentTarget.style.transform = ''}>
      {leadingIcon && <Icon name={leadingIcon} size={18} stroke={2} />}
      {children}
      {trailingIcon && <Icon name={trailingIcon} size={18} stroke={2} />}
    </button>
  );
}

// ─────────────────────────────────────────────────────────────
// Field — input. 16px radius, gold focus ring.
// ─────────────────────────────────────────────────────────────
function Field({ label, placeholder, value, onChange, leadingIcon, suffix, type = 'text', focused: focusedProp, error }) {
  const [focusedState, setFocusedState] = useState(false);
  const focused = focusedProp != null ? focusedProp : focusedState;
  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
      {label && <span style={{ fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500, color: 'var(--color-text-primary)' }}>{label}</span>}
      <div style={{
        height: 52, borderRadius: 'var(--radius-input)',
        background: 'var(--color-input-bg)',
        border: `1px solid ${error ? 'rgba(235,87,87,0.55)' : focused ? 'var(--color-input-border-focus)' : 'var(--color-input-border)'}`,
        boxShadow: focused ? (error ? '0 0 0 3px rgba(235,87,87,0.12)' : '0 0 0 3px rgba(193,160,129,0.15)') : 'none',
        padding: '0 16px',
        display: 'flex', alignItems: 'center', gap: 10,
        transition: 'all var(--dur-fast) var(--ease-smooth)',
      }}>
        {leadingIcon && <Icon name={leadingIcon} size={18} style={{ color: 'var(--color-text-muted)' }} />}
        <input
          type={type}
          value={value || ''}
          placeholder={placeholder}
          onChange={e => onChange && onChange(e.target.value)}
          onFocus={() => setFocusedState(true)}
          onBlur={() => setFocusedState(false)}
          style={{
            flex: 1, minWidth: 0, background: 'transparent',
            border: 'none', outline: 'none',
            fontFamily: 'var(--font-sans)', fontSize: 15,
            color: 'var(--color-text-primary)',
          }}
        />
        {suffix && <span style={{ fontFamily: 'var(--font-sans)', fontSize: 13, color: 'var(--color-text-muted)', fontWeight: 500 }}>{suffix}</span>}
      </div>
      {error && <span style={{ fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--goji-error, #EB5757)' }}>{error}</span>}
    </label>
  );
}

// ─────────────────────────────────────────────────────────────
// Chip — pill, both filter and category
// ─────────────────────────────────────────────────────────────
function Chip({ active, color, children, onClick }) {
  const variants = active
    ? { background: 'rgba(193, 160, 129, 0.10)', borderColor: 'rgba(193, 160, 129, 0.32)', color: '#E3B788' }
    : color
      ? { background: `${color}1a`, borderColor: `${color}55`, color: color }
      : { background: 'var(--color-input-bg)', borderColor: 'var(--color-border)', color: 'var(--color-text-body)' };
  return (
    <button onClick={onClick} style={{
      display: 'inline-flex', alignItems: 'center', gap: 6,
      height: 34, padding: '0 14px', borderRadius: 9999,
      fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500,
      border: '1px solid', cursor: 'pointer', whiteSpace: 'nowrap',
      ...variants,
    }}>{children}</button>
  );
}

// ─────────────────────────────────────────────────────────────
// Header — app top bar. Logo · title · theme toggle.
// ─────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────
// SwipePager — transform-based swipeable pager with mouse-drag support.
// `page` is controlled; `onPageChange` fires when drag ends or snap occurs.
// ─────────────────────────────────────────────────────────────
function SwipePager({ children, page, onPageChange }) {
  const containerRef = React.useRef(null);
  const [width, setWidth] = React.useState(0);
  const [drag, setDrag] = React.useState(0);
  const [isDragging, setIsDragging] = React.useState(false);
  const startXRef = React.useRef(0);
  const startYRef = React.useRef(0);
  const axisRef = React.useRef(null); // 'x' | 'y' | null — locked once determined

  const total = React.Children.count(children);

  React.useEffect(() => {
    const el = containerRef.current;
    if (!el) return;
    const measure = () => setWidth(el.offsetWidth);
    measure();
    const ro = new ResizeObserver(measure);
    ro.observe(el);
    return () => ro.disconnect();
  }, []);

  const onPointerDown = (e) => {
    if (e.button !== undefined && e.button !== 0) return;
    startXRef.current = e.clientX;
    startYRef.current = e.clientY;
    axisRef.current = null;
    // Do NOT capture yet — wait until axis is determined so vertical
    // scrolling in inner Page can still happen.
  };

  const onPointerMove = (e) => {
    if (e.buttons === 0 && e.pointerType === 'mouse') return;
    const dx = e.clientX - startXRef.current;
    const dy = e.clientY - startYRef.current;

    if (!isDragging) {
      if (axisRef.current === null) {
        if (Math.abs(dx) > 8 && Math.abs(dx) > Math.abs(dy)) {
          axisRef.current = 'x';
          setIsDragging(true);
          try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {}
        } else if (Math.abs(dy) > 8) {
          axisRef.current = 'y';
          return;
        } else {
          return;
        }
      } else if (axisRef.current === 'y') {
        return;
      }
    }

    let nextDrag = dx;
    // Resist past edges
    if (page === 0 && nextDrag > 0) nextDrag = nextDrag * 0.35;
    if (page === total - 1 && nextDrag < 0) nextDrag = nextDrag * 0.35;
    setDrag(nextDrag);
  };

  const endDrag = (e) => {
    if (!isDragging) {
      axisRef.current = null;
      return;
    }
    try { e.currentTarget.releasePointerCapture(e.pointerId); } catch (_) {}
    const dx = drag;
    let next = page;
    if (Math.abs(dx) > width * 0.2) {
      next = page + (dx > 0 ? -1 : 1);
    }
    next = Math.max(0, Math.min(total - 1, next));
    setDrag(0);
    setIsDragging(false);
    axisRef.current = null;
    if (next !== page) onPageChange(next);
  };

  const translate = -(page * width) + drag;

  return (
    <div ref={containerRef}
      onPointerDown={onPointerDown}
      onPointerMove={onPointerMove}
      onPointerUp={endDrag}
      onPointerCancel={endDrag}
      style={{
        flex: 1, minHeight: 0,
        overflow: 'hidden',
        position: 'relative',
        userSelect: 'none',
        touchAction: 'pan-y',
        cursor: isDragging ? 'grabbing' : undefined,
      }}>
      <div style={{
        display: 'flex',
        width: width ? `${total * width}px` : '100%',
        height: '100%',
        transform: `translateX(${translate}px)`,
        transition: isDragging ? 'none' : 'transform 420ms cubic-bezier(0.22, 1, 0.36, 1)',
        willChange: 'transform',
      }}>
        {React.Children.map(children, (child, i) => (
          <div key={i} style={{
            width: width ? `${width}px` : '100%',
            height: '100%', flexShrink: 0,
            display: 'flex', flexDirection: 'column',
          }}>
            {child}
          </div>
        ))}
      </div>
    </div>
  );
}
// ─────────────────────────────────────────────────────────────
function HeaderActions({ hidden, onToggleHidden, onOpenSettings, showBell }) {
  return (
    <div style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
      {onToggleHidden && (
        <button onClick={onToggleHidden} aria-label={hidden ? 'Show amounts' : 'Hide amounts'} style={{
          width: 32, height: 32, borderRadius: 999,
          background: 'transparent', border: 'none', cursor: 'pointer',
          color: hidden ? 'var(--goji-accent)' : 'var(--color-text-body)',
          display: 'grid', placeItems: 'center',
        }}>
          <Icon name={hidden ? 'eyeOff' : 'eye'} size={18} />
        </button>
      )}
      {showBell && (
        <button aria-label="Notifications" style={{
          width: 32, height: 32, borderRadius: 999,
          background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'var(--color-text-body)', display: 'grid', placeItems: 'center',
        }}>
          <Icon name="bell" size={18} />
        </button>
      )}
      {onOpenSettings && (
        <button onClick={onOpenSettings} aria-label="Settings" style={{
          width: 32, height: 32, borderRadius: 999,
          background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'var(--color-text-body)', display: 'grid', placeItems: 'center',
        }}>
          <Icon name="gear" size={18} />
        </button>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// Header — used in non-Home screens. Shows title + eye only.
// ─────────────────────────────────────────────────────────────
function Header({ title, hidden, onToggleHidden }) {
  return (
    <div style={{
      display: 'flex', alignItems: 'center',
      padding: '10px 12px 10px 20px', height: 48,
      position: 'relative',
    }}>
      <span style={{
        fontFamily: 'var(--font-sans)', fontWeight: 600, fontSize: 14,
        letterSpacing: '-0.01em', color: 'var(--color-text-primary)',
      }}>{title}</span>
      <div style={{ marginLeft: 'auto' }}>
        <HeaderActions hidden={hidden} onToggleHidden={onToggleHidden} />
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// SectionTitle — eyebrow + optional action
// ─────────────────────────────────────────────────────────────
function SectionTitle({ children, action, onAction }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', padding: '24px 20px 12px' }}>
      <span style={{
        fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 500,
        letterSpacing: '0.12em', textTransform: 'uppercase',
        color: 'var(--color-text-muted)',
      }}>{children}</span>
      {action && (
        <button onClick={onAction} style={{
          fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500,
          color: 'var(--goji-accent)', background: 'transparent', border: 'none', cursor: 'pointer',
        }}>{action}</button>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// HeroCard — the Safe to Spend card. The ONLY hero-shadow surface.
// ─────────────────────────────────────────────────────────────
function HeroCard({ amount, safeToSend, dailyAvg, daysToPayday, fxPair = 'USD → NPR', fxRate = 132.99, onShowInfo }) {
  const hidden = useAmountMask();
  const fmtMoney = (v) => hidden ? '$•••' : `$${v.toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`;
  return (
    <div style={{
      margin: '4px 20px 0',
      background: 'var(--color-card-hero-bg)',
      borderRadius: 'var(--radius-card-large)',
      padding: '18px 22px 18px',
      boxShadow: 'var(--shadow-hero)',
      position: 'relative',
      overflow: 'hidden',
    }}>
      <div style={{
        position: 'absolute', inset: '-30% 10% auto 10%', height: 220,
        background: 'radial-gradient(ellipse, var(--color-glow-subtle) 0%, transparent 70%)',
        pointerEvents: 'none',
      }} />

      {/* Eyebrow + info */}
      <div style={{
        position: 'relative',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      }}>
        <div style={{
          fontFamily: 'var(--font-sans)',
          fontSize: 11, fontWeight: 500, letterSpacing: '0.16em',
          textTransform: 'uppercase', color: 'var(--color-text-body)',
        }}>Safe to spend today</div>
        <button onClick={onShowInfo} aria-label="How is this calculated" style={{
          width: 28, height: 28, borderRadius: 999,
          background: 'transparent', border: 'none', cursor: 'pointer',
          color: 'var(--color-text-muted)', display: 'grid', placeItems: 'center',
        }}>
          <Icon name="info" size={16} />
        </button>
      </div>

      {/* Hero number */}
      <div style={{
        position: 'relative',
        textAlign: 'center',
        fontFamily: 'var(--font-serif)', fontWeight: 600,
        fontSize: 76, lineHeight: 1, letterSpacing: '-0.04em',
        background: 'var(--goji-gradient)',
        WebkitBackgroundClip: 'text', backgroundClip: 'text',
        WebkitTextFillColor: 'transparent',
        fontVariantNumeric: 'tabular-nums lining-nums',
        fontFeatureSettings: '"tnum" 1, "lnum" 1',
        margin: '10px 0 8px',
      }}>{hidden ? '$\u2022\u2022\u2022' : `$${amount}`}</div>

      {/* Subline */}
      <div style={{
        position: 'relative',
        textAlign: 'center',
        fontFamily: 'var(--font-sans)', fontSize: 11, color: 'var(--color-text-muted)',
        letterSpacing: '0.04em', textTransform: 'uppercase',
      }}>
        {daysToPayday} days to payday
      </div>

      {/* Divider */}
      <div style={{
        height: 1, background: 'var(--color-border-subtle)',
        margin: '18px -22px 14px',
        position: 'relative',
      }} />

      {/* Metrics strip */}
      <div style={{
        position: 'relative',
        display: 'grid', gridTemplateColumns: '1fr 1px 1fr 1px 1fr',
        alignItems: 'center', gap: 0,
      }}>
        <Metric label="Safe to send" value={fmtMoney(safeToSend)} />
        <div style={{ height: 32, background: 'var(--color-border-subtle)' }} />
        <Metric label="Avg / day" value={fmtMoney(dailyAvg)} />
        <div style={{ height: 32, background: 'var(--color-border-subtle)' }} />
        <Metric label={fxPair} value={fxRate.toFixed(2)} />
      </div>
    </div>
  );
}

function Metric({ label, value }) {
  return (
    <div style={{
      display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 4,
    }}>
      <span style={{
        fontFamily: 'var(--font-sans)', fontSize: 10, fontWeight: 500,
        letterSpacing: '0.08em', textTransform: 'uppercase',
        color: 'var(--color-text-muted)',
      }}>{label}</span>
      <span style={{
        fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600,
        letterSpacing: '-0.02em', color: 'var(--color-text-primary)',
        fontVariantNumeric: 'tabular-nums lining-nums',
        fontFeatureSettings: '"tnum" 1, "lnum" 1',
      }}>{value}</span>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// AccountCard
// ─────────────────────────────────────────────────────────────
function AccountCard({ bank, mark, color, currency, currencySymbol = '$', amount, delta, deltaPositive, lastSynced }) {
  return (
    <div style={{
      background: 'var(--color-surface-2)',
      border: '1px solid var(--color-border)',
      borderRadius: 'var(--radius-card)',
      padding: 18,
      boxShadow: 'var(--shadow-premium)',
    }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <div style={{
            width: 28, height: 28, borderRadius: 8,
            background: `${color}1f`, border: `1px solid ${color}55`,
            color, display: 'grid', placeItems: 'center',
            fontFamily: 'var(--font-sans)', fontSize: 11, fontWeight: 700,
          }}>{mark}</div>
          <div style={{ fontFamily: 'var(--font-sans)', fontSize: 13, fontWeight: 500, color: 'var(--color-text-primary)' }}>{bank}</div>
        </div>
        <div style={{
          background: 'var(--color-input-bg)', border: '1px solid var(--color-border)',
          borderRadius: 6, padding: '3px 7px',
          fontFamily: 'var(--font-sans)', fontSize: 10, fontWeight: 700,
          color: 'var(--color-text-body)', letterSpacing: '0.04em',
        }}>{currency}</div>
      </div>
      <div style={{ marginTop: 16 }}>
        <Money amount={amount} currency={currencySymbol} size="28px" color="var(--color-text-primary)" />
      </div>
      <div style={{ fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--color-text-muted)', marginTop: 6 }}>
        {delta != null ? (
          <>
            <Money amount={delta} sign size="12px" color={deltaPositive ? '#6FCF97' : 'var(--color-text-primary)'} /> this week
          </>
        ) : lastSynced ? `Last synced ${lastSynced}` : null}
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// TransactionRow
// ─────────────────────────────────────────────────────────────
function TransactionRow({ icon, color, name, meta, amount, positive, divider = true }) {
  return (
    <div style={{
      display: 'grid', gridTemplateColumns: '38px 1fr auto',
      alignItems: 'center', gap: 14, padding: '14px 20px',
      borderBottom: divider ? '1px solid var(--color-border-subtle)' : 'none',
    }}>
      <div style={{
        width: 38, height: 38, borderRadius: 12,
        background: `${color}1a`, border: `1px solid ${color}40`,
        color, display: 'grid', placeItems: 'center',
      }}>
        <Icon name={icon} size={18} />
      </div>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--font-sans)', fontSize: 14, fontWeight: 500, color: 'var(--color-text-primary)' }}>{name}</div>
        <div style={{ fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--color-text-muted)', marginTop: 2 }}>{meta}</div>
      </div>
      <Money amount={amount} sign size="15px" color={positive ? '#6FCF97' : 'var(--color-text-primary)'} />
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// InsightCard — gold-tinted observation card
// ─────────────────────────────────────────────────────────────
function InsightCard({ eyebrow, body, tone = 'neutral' }) {
  const tones = {
    neutral: { dot: 'var(--goji-accent)', glow: 'var(--color-glow-subtle)' },
    warn: { dot: '#F2C94C', glow: 'rgba(242, 201, 76, 0.10)' },
    good: { dot: '#6FCF97', glow: 'rgba(111, 207, 151, 0.10)' },
  };
  const t = tones[tone] || tones.neutral;
  return (
    <div style={{
      background: 'var(--color-surface-2)',
      border: '1px solid var(--color-border)',
      borderRadius: 'var(--radius-card)',
      padding: '16px 18px',
      boxShadow: 'var(--shadow-premium)',
      position: 'relative', overflow: 'hidden',
    }}>
      <div style={{
        position: 'absolute', top: -40, right: -40, width: 140, height: 140,
        background: `radial-gradient(circle, ${t.glow} 0%, transparent 70%)`,
        pointerEvents: 'none',
      }} />
      <div style={{
        position: 'relative',
        display: 'flex', alignItems: 'center', gap: 8,
        fontFamily: 'var(--font-sans)', fontSize: 11, fontWeight: 500,
        letterSpacing: '0.12em', textTransform: 'uppercase',
        color: 'var(--color-text-muted)', marginBottom: 8,
      }}>
        <span style={{ width: 6, height: 6, borderRadius: 999, background: t.dot, display: 'inline-block' }} />
        {eyebrow}
      </div>
      <div style={{
        position: 'relative',
        fontFamily: 'var(--font-sans)', fontSize: 15, lineHeight: 1.45,
        color: 'var(--color-text-primary)',
      }}>{body}</div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// BottomNav — pill capsule (4 tabs) + floating FAB on the right.
// Single glass indicator slides between tab slots, Apple-style.
// ─────────────────────────────────────────────────────────────
function BottomNav({ active, onChange, onFab, fabIcon = 'mic' }) {
  const tabs = [
    { key: 'home',     label: 'Home',        icon: 'home' },
    { key: 'money',    label: 'Money',       icon: 'wallet' },
    { key: 'learn',    label: 'Learn',       icon: 'book' },
    { key: 'ai',       label: 'Insights AI', icon: 'sparkle' },
  ];
  const activeIndex = Math.max(0, tabs.findIndex(t => t.key === active));
  const pillRef = React.useRef(null);
  const [indicatorX, setIndicatorX] = React.useState(0);
  const [ready, setReady] = React.useState(false);
  const [chatOpen, setChatOpen] = React.useState(false);
  const [scrollCompact, setScrollCompact] = React.useState(false);

  React.useEffect(() => {
    const onChat = (e) => setChatOpen(!!e.detail);
    const onCompact = (e) => setScrollCompact(!!e.detail);
    window.addEventListener('goji-chat-open', onChat);
    window.addEventListener('goji-nav-compact', onCompact);
    return () => {
      window.removeEventListener('goji-chat-open', onChat);
      window.removeEventListener('goji-nav-compact', onCompact);
    };
  }, []);

  const hidden = chatOpen;
  const compact = scrollCompact && !chatOpen;

  React.useLayoutEffect(() => {
    const pill = pillRef.current;
    if (!pill) return;
    const measure = () => {
      const tabEls = pill.querySelectorAll('[data-tab]');
      const target = tabEls[activeIndex];
      if (!target) return;
      const tabW = target.offsetWidth;
      // Skip degenerate readings while the pill is collapsed (chat-open).
      if (pill.offsetWidth === 0 || tabW === 0) return;
      const indicatorW = 48;
      const x = target.offsetLeft + (tabW - indicatorW) / 2;
      setIndicatorX(x);
    };
    measure();
    if (!ready) {
      requestAnimationFrame(() => setReady(true));
    }
    // Re-measure once the pill finishes its expand transition after chat close.
    const ro = new ResizeObserver(measure);
    ro.observe(pill);
    return () => ro.disconnect();
  }, [activeIndex, ready]);

  return (
    <div style={{
      position: 'absolute', left: 0, right: 0, bottom: 0,
      pointerEvents: 'none',
      zIndex: 30,
    }}>
      {/* Backdrop fade — masks scrolling content behind the nav */}
      <div style={{
        position: 'absolute', left: 0, right: 0, bottom: 0,
        height: 140,
        background: 'linear-gradient(180deg, transparent 0%, var(--color-bg) 55%, var(--color-bg) 100%)',
        pointerEvents: 'none',
      }} />

      <div style={{
        position: 'relative',
        padding: '0 16px 18px',
        display: 'flex', alignItems: 'center', justifyContent: 'flex-end',
        gap: hidden ? 0 : 14,
        transform: compact ? 'scale(0.78)' : 'scale(1)',
        transformOrigin: 'center bottom',
        transition: 'transform 320ms var(--ease-smooth), gap 320ms var(--ease-smooth)',
      }}>
        <div ref={pillRef} style={{
        flex: hidden ? '0 0 auto' : 1,
        position: 'relative',
        display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)',
        alignItems: 'center',
        height: hidden ? 0 : 64,
        maxWidth: hidden ? 0 : '100%',
        padding: hidden ? 0 : 8,
        borderRadius: 9999,
        background: 'var(--color-nav-pill-bg)',
        opacity: hidden ? 0 : 1,
        overflow: 'hidden',
        pointerEvents: hidden ? 'none' : 'auto',
        transition: 'all 320ms var(--ease-smooth)',
      }}>
        {/* Sliding glass indicator */}
        <div style={{
          position: 'absolute', top: 8, left: 0,
          width: 48, height: 48, borderRadius: 9999,
          transform: `translateX(${indicatorX}px)`,
          transition: ready
            ? 'transform 520ms cubic-bezier(0.34, 1.20, 0.64, 1)'
            : 'none',
          background: 'var(--color-nav-tab-active-bg)',
          boxShadow: 'var(--color-nav-tab-active-shadow)',
          backdropFilter: 'blur(14px) saturate(1.4)',
          WebkitBackdropFilter: 'blur(14px) saturate(1.4)',
          pointerEvents: 'none', zIndex: 1,
        }} />

        {tabs.map((t, idx) => (
          <button key={t.key}
            data-tab
            onClick={() => onChange(t.key)}
            aria-label={t.label}
            style={{
              position: 'relative', zIndex: 2,
              justifySelf: 'center',
              width: 48, height: 48, borderRadius: 9999,
              border: 'none', background: 'transparent', cursor: 'pointer',
              display: 'grid', placeItems: 'center',
              color: idx === activeIndex ? 'var(--color-nav-tab-active-fg)' : 'var(--color-text-body)',
              transform: idx === activeIndex ? 'scale(1.06)' : 'scale(1)',
              transition: 'color 320ms var(--ease-smooth), transform 320ms var(--ease-smooth)',
            }}>
            <Icon name={t.icon} size={22} stroke={1.75} />
          </button>
        ))}
      </div>
      <FAB onClick={onFab} shrunk={hidden} icon={fabIcon} />
      </div>
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// FAB — always gold gradient regardless of theme. Floats freely.
// ─────────────────────────────────────────────────────────────
function FAB({ onClick, onPointerDown, onPointerUp, shrunk = false, icon = 'mic' }) {
  const isAi = icon === 'gemini' || icon === 'sparkle';
  const iconBoxSize = shrunk ? 18 : 24;
  return (
    <button onClick={onClick} onPointerDown={onPointerDown} onPointerUp={onPointerUp}
      aria-label={isAi ? 'Open AI coach' : 'Voice command'} style={{
      width: shrunk ? 48 : 64, height: shrunk ? 48 : 64, borderRadius: 999,
      background: 'var(--goji-gradient)',
      border: 'none',
      boxShadow: 'var(--shadow-fab)',
      color: 'var(--goji-btn-text)',
      display: 'grid', placeItems: 'center',
      cursor: 'pointer',
      flexShrink: 0,
      pointerEvents: 'auto',
      transition: 'width 320ms var(--ease-smooth), height 320ms var(--ease-smooth), transform var(--dur-instant) var(--ease-smooth)',
    }}
    onMouseDown={e => e.currentTarget.style.transform = 'scale(0.94)'}
    onMouseUp={e => e.currentTarget.style.transform = ''}
    onMouseLeave={e => e.currentTarget.style.transform = ''}
    >
      <span style={{
        position: 'relative',
        width: iconBoxSize, height: iconBoxSize,
        display: 'grid', placeItems: 'center',
        transition: 'width 320ms var(--ease-smooth), height 320ms var(--ease-smooth)',
      }}>
        {/* Mic — visible on non-AI tabs */}
        <span style={{
          position: 'absolute', inset: 0,
          display: 'grid', placeItems: 'center',
          opacity: icon === 'mic' ? 1 : 0,
          transform: icon === 'mic' ? 'scale(1) rotate(0deg)' : 'scale(0.55) rotate(-60deg)',
          transition: 'opacity 320ms var(--ease-smooth), transform 420ms var(--ease-spring)',
          pointerEvents: 'none',
        }}>
          <Icon name="mic" size={iconBoxSize} stroke={2} />
        </span>
        {/* Gemini — visible on Insights AI tab */}
        <span style={{
          position: 'absolute', inset: 0,
          display: 'grid', placeItems: 'center',
          opacity: icon === 'gemini' ? 1 : 0,
          transform: icon === 'gemini' ? 'scale(1) rotate(0deg)' : 'scale(0.55) rotate(60deg)',
          transition: 'opacity 320ms var(--ease-smooth), transform 420ms var(--ease-spring)',
          pointerEvents: 'none',
        }}>
          <Icon name="gemini" size={iconBoxSize} stroke={2} />
        </span>
      </span>
    </button>
  );
}

// ─────────────────────────────────────────────────────────────
// StackCard — single card body. Type-themed (success / warn / danger /
// info / insight / learn / account). Uniform height for the stack.
// ─────────────────────────────────────────────────────────────
const STACK_CARD_HEIGHT = 116;

const CARD_TONES = {
  danger:       { dot: '#EB5757', glow: 'rgba(235, 87, 87, 0.10)',  border: 'rgba(235, 87, 87, 0.22)',  fg: '#EB5757', label: 'OVERDUE' },
  warn:         { dot: '#F2C94C', glow: 'rgba(242, 201, 76, 0.08)', border: 'rgba(242, 201, 76, 0.18)', fg: '#F2C94C', label: 'DUE SOON' },
  success:      { dot: '#6FCF97', glow: 'rgba(111, 207, 151, 0.08)',border: 'rgba(111, 207, 151, 0.20)',fg: '#6FCF97', label: 'INCOME' },
  insight:      { dot: '#E3B788', glow: 'rgba(227, 183, 136, 0.10)',border: 'rgba(193, 160, 129, 0.22)',fg: '#E3B788', label: 'INSIGHT' },
  info:         { dot: '#7B9ABD', glow: 'rgba(123, 154, 189, 0.08)',border: 'rgba(123, 154, 189, 0.18)',fg: '#7B9ABD', label: 'FX' },
  learn:        { dot: '#9B7BD6', glow: 'rgba(155, 123, 214, 0.08)',border: 'rgba(155, 123, 214, 0.18)',fg: '#9B7BD6', label: 'LEARN' },
  account:      { dot: '#7DBEB7', glow: 'rgba(125, 190, 183, 0.06)',border: 'rgba(125, 190, 183, 0.16)',fg: '#7DBEB7', label: 'ACCOUNT' },
  notification: { dot: 'var(--goji-accent)', glow: 'rgba(193,160,129,0.08)', border: 'rgba(193,160,129,0.18)', fg: 'var(--goji-accent)', label: 'NOTIFICATION' },
};

function StackCard({ card, onDismiss, onAction, depth = 0, position, total, interactive = true }) {
  // Special render path: the "all caught up" sentinel card.
  if (card._isCaughtUp) {
    return (
      <div style={{
        height: STACK_CARD_HEIGHT,
        background: 'var(--color-surface-2)',
        border: '1px solid var(--color-border-accent)',
        borderRadius: 'var(--radius-card)',
        padding: '0 18px 0 16px',
        boxShadow: depth === 0 ? 'var(--shadow-premium)' : 'none',
        display: 'flex', alignItems: 'center', gap: 16,
        position: 'relative', overflow: 'hidden',
      }}>
        {/* Soft halo */}
        <div style={{
          position: 'absolute', top: '50%', left: 36,
          width: 160, height: 160, marginTop: -80, marginLeft: -80,
          background: 'radial-gradient(circle, rgba(227,183,136,0.22) 0%, rgba(193,160,129,0.06) 45%, transparent 75%)',
          filter: 'blur(2px)',
          pointerEvents: 'none',
        }} />

        {/* Mark cluster */}
        <div style={{
          position: 'relative',
          width: 60, height: 60, flexShrink: 0,
          display: 'grid', placeItems: 'center',
        }}>
          {/* Orbiting petals */}
          {[0, 1, 2, 3, 4].map(i => {
            const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
            const r = 26;
            const x = Math.cos(angle) * r;
            const y = Math.sin(angle) * r;
            const size = i % 2 === 0 ? 3 : 2.5;
            return (
              <span key={i} style={{
                position: 'absolute', top: '50%', left: '50%',
                width: size, height: size, borderRadius: 999,
                background: i === 0 ? '#E3B788' : i % 2 ? 'rgba(193,160,129,0.65)' : 'rgba(227,183,136,0.55)',
                transform: `translate(${x - size / 2}px, ${y - size / 2}px)`,
                boxShadow: '0 0 6px rgba(227, 183, 136, 0.5)',
              }} />
            );
          })}
          {/* Triquetra */}
          <svg width="32" height="32" viewBox="0 0 914 914" fill="none"
            style={{
              position: 'relative',
              filter: 'drop-shadow(0 3px 10px rgba(227, 183, 136, 0.35))',
            }}>
            <defs>
              <linearGradient id="goji-caught-a" x1="457" y1="508" x2="454" y2="239" gradientUnits="userSpaceOnUse">
                <stop stopColor="#C39765"/><stop offset="1" stopColor="#F5C989"/>
              </linearGradient>
              <linearGradient id="goji-caught-b" x1="451" y1="756" x2="456" y2="509" gradientUnits="userSpaceOnUse">
                <stop stopColor="#865A3A"/><stop offset="1" stopColor="#A87C52"/>
              </linearGradient>
            </defs>
            <path d="M455.676 239.074C457.234 238.928 458.806 238.998 460.343 239.283C471.546 241.455 491.855 266.556 498.116 275.955C512.569 298.028 520.821 323.302 522.071 349.326C522.337 354.621 522.625 361.797 521.431 366.914C571.63 387.251 608.909 426.125 620.103 478.065C622.258 488.069 621.129 495.539 610.016 499.597C600.918 502.919 591.304 504.564 581.742 506.18C536.786 512.836 494.723 499.971 458.497 474.669C457.569 474.242 456.844 474.451 456.028 475.015C447.517 480.919 439.138 486.095 429.744 490.708C392.269 509.498 348.497 513.141 308.199 500.823C303.761 499.433 297.478 497.085 295.362 492.608C290.701 482.748 299.246 464.131 302.404 453.976C303.614 450.087 307.768 443.041 309.735 439.35C328.551 404.053 356.452 382.667 393.564 366.984C392.618 362.094 392.781 355.475 392.969 350.431C394.176 321.695 403.986 293.898 421.229 270.351C427.738 261.567 444.772 240.857 455.676 239.074ZM325.195 476.31C334.487 479.533 354.294 481.301 363.875 480.359C386.925 479.85 417.889 468.695 435.801 454.885C424.338 443.825 416.138 431.421 408.631 417.69C407.124 414.934 399.289 395.24 398.275 394.805C365.771 410.077 340.093 433.235 328.158 466.722C327.028 469.875 326.039 473.074 325.195 476.31ZM448.557 383.038C442.817 383.404 431.826 384.452 426.45 386.113C431.336 401.432 440.429 417.538 450.884 429.943C452.829 432.254 455.221 435.382 457.556 437.211C459.209 435.427 461.064 433.517 462.567 431.638C473.899 418.397 484.023 403.137 488.678 386.43C480.391 383.246 457.711 382.097 448.557 383.038ZM516.227 394.56C509.636 417.288 495.393 437.303 479.485 454.917C497.703 468.633 529.701 480.635 553.149 480.339C561.973 480.886 581.257 479.385 589.599 476.199C579.977 441.515 558.675 417.547 526.614 399.793C523.209 397.945 519.744 396.2 516.227 394.56Z" fill="url(#goji-caught-a)"/>
            <path d="M243.61 508.156C246.601 508.006 249.854 507.825 252.792 508.352C261.238 509.917 268.295 515.588 271.529 523.402C275.392 532.476 273.669 543.42 274.876 553.269C278.091 579.5 288.969 607.57 303.896 629.38C331.662 670.01 374.788 698.111 423.728 707.459C469.596 716.172 520.481 705.471 558.764 679.4C602.568 649.569 628.15 611.154 638.13 559.848C604.258 559.069 569.107 560.37 535.135 559.73C528.134 559.601 505.742 560.473 500.379 559.58C496.01 558.852 491.951 556.905 488.683 553.966C484.409 550.061 481.198 543.523 481.124 537.718C481.034 531.102 483.682 524.734 488.456 520.06C496.273 512.447 503.595 514.126 513.612 513.976C520.065 513.878 526.56 513.966 533.019 513.981L600.152 513.93C630.039 514.224 661.101 513.986 690.977 513.77C691.462 557.116 684.203 596.208 663.164 634.746C633.308 689.228 582.84 730.023 522.628 748.348C464.38 765.768 401.441 759.431 348.012 730.777C291.838 701.37 250.007 651.113 231.857 591.234C226.937 574.986 218.108 533.788 226.424 519.409C230.338 512.644 236.308 510.061 243.61 508.156Z" fill="url(#goji-caught-b)"/>
          </svg>
        </div>

        {/* Text */}
        <div style={{ position: 'relative', flex: 1, minWidth: 0 }}>
          <div style={{
            fontFamily: 'var(--font-serif)', fontWeight: 600, fontSize: 18,
            letterSpacing: '-0.02em', color: 'var(--color-text-primary)', lineHeight: 1.2,
          }}>All caught up.</div>
          <div style={{
            fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--color-text-muted)',
            marginTop: 4, lineHeight: 1.4,
          }}>Nothing else needs your attention.</div>
        </div>

        {/* Dismiss */}
        {interactive && depth === 0 && (
          <button onClick={(e) => { e.stopPropagation(); onDismiss && onDismiss(card.id); }}
            aria-label="Dismiss"
            style={{
              position: 'absolute', top: 12, right: 12,
              width: 28, height: 28, borderRadius: 999,
              background: 'transparent', border: 'none', cursor: 'pointer',
              color: 'var(--color-text-muted)',
              display: 'grid', placeItems: 'center',
            }}>
            <Icon name="close" size={14} />
          </button>
        )}

        {/* Position counter */}
        {position != null && total != null && (
          <div style={{
            position: 'absolute', bottom: 8, right: 14,
            fontFamily: 'var(--font-sans)', fontSize: 10, fontWeight: 500,
            color: 'var(--color-text-muted)', letterSpacing: '0.06em',
            fontVariantNumeric: 'tabular-nums', fontFeatureSettings: '"tnum" 1',
          }}>{position} / {total}</div>
        )}
      </div>
    );
  }

  const tone = CARD_TONES[card.tone] || CARD_TONES.account;
  return (
    <div style={{
      height: STACK_CARD_HEIGHT,
      background: 'var(--color-surface-2)',
      border: `1px solid ${tone.border}`,
      borderRadius: 'var(--radius-card)',
      padding: '14px 16px 14px 18px',
      boxShadow: depth === 0 ? 'var(--shadow-premium)' : 'none',
      display: 'flex', flexDirection: 'column', gap: 8,
      position: 'relative',
      overflow: 'hidden',
    }}>
      {/* Tone glow accent — top-right radial */}
      <div style={{
        position: 'absolute', top: -40, right: -40, width: 140, height: 140,
        background: `radial-gradient(circle, ${tone.glow} 0%, transparent 70%)`,
        pointerEvents: 'none',
      }} />

      {/* Header: eyebrow + dismiss */}
      <div style={{ position: 'relative', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
        <div style={{
          display: 'flex', alignItems: 'center', gap: 8,
          fontFamily: 'var(--font-sans)', fontSize: 11, fontWeight: 500,
          letterSpacing: '0.12em', textTransform: 'uppercase',
          color: tone.fg,
        }}>
          <span style={{ width: 6, height: 6, borderRadius: 999, background: tone.dot, display: 'inline-block' }} />
          {card.eyebrow || tone.label}
        </div>
        {interactive && depth === 0 && (
          <button onClick={(e) => { e.stopPropagation(); onDismiss && onDismiss(card.id); }}
            aria-label="Dismiss"
            style={{
              width: 28, height: 28, borderRadius: 999,
              background: 'transparent', border: 'none', cursor: 'pointer',
              color: 'var(--color-text-muted)',
              display: 'grid', placeItems: 'center',
            }}>
            <Icon name="close" size={14} />
          </button>
        )}
      </div>

      {/* Body */}
      <div style={{ position: 'relative', flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, minWidth: 0 }}>
        <div style={{ minWidth: 0, flex: 1 }}>
          <div style={{
            fontFamily: 'var(--font-sans)', fontSize: 15, fontWeight: 600,
            color: 'var(--color-text-primary)', lineHeight: 1.3,
            overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
          }}>{card.title}</div>
          {card.body && (
            <div style={{
              fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--color-text-body)',
              marginTop: 4, lineHeight: 1.4,
              overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
            }}>{card.body}</div>
          )}
        </div>

        {/* Right side: amount or CTA */}
        {card.amount != null && (
          <div style={{ textAlign: 'right' }}>
            <Money amount={card.amount} sign={card.amount !== 0} size="18px"
              color={card.amount > 0 ? '#6FCF97' : (card.tone === 'danger' ? '#EB5757' : 'var(--color-text-primary)')} />
            {card.amountMeta && (
              <div style={{ fontFamily: 'var(--font-sans)', fontSize: 11, color: 'var(--color-text-muted)', marginTop: 2 }}>{card.amountMeta}</div>
            )}
          </div>
        )}
        {card.cta && (
          <button onClick={(e) => { e.stopPropagation(); onAction && onAction(card); }}
            style={{
              background: 'var(--color-input-bg)', border: '1px solid var(--color-border-strong)',
              borderRadius: 9999, padding: '8px 14px',
              fontFamily: 'var(--font-sans)', fontSize: 12, fontWeight: 600,
              color: 'var(--color-text-primary)', cursor: 'pointer', whiteSpace: 'nowrap',
            }}>{card.cta}</button>
        )}
        {!card.amount && !card.cta && interactive && (
          <Icon name="chevronRight" size={16} style={{ color: 'var(--color-text-muted)' }} />
        )}
      </div>

      {/* Position counter — bottom right corner, tabular muted */}
      {position != null && total != null && (
        <div style={{
          position: 'absolute', bottom: 6, right: 14,
          fontFamily: 'var(--font-sans)', fontSize: 10, fontWeight: 500,
          color: 'var(--color-text-muted)', letterSpacing: '0.06em',
          fontVariantNumeric: 'tabular-nums', fontFeatureSettings: '"tnum" 1',
          pointerEvents: 'none',
        }}>{position} / {total}</div>
      )}
    </div>
  );
}

// ─────────────────────────────────────────────────────────────
// StackedCards — priority-sorted stack of up to 7 cards.
// Top card is fully visible; subsequent cards peek out from
// the bottom (Apple Wallet style). Dismiss the top card and the
// stack restacks with a smooth transition.
// ─────────────────────────────────────────────────────────────
const STACK_MAX = 7;
const STACK_VISIBLE = 4; // how many show as peeks; rest hidden
const STACK_OFFSET = 8;  // y-offset per layer
const STACK_SCALE = 0.04; // scale-down per layer

const CAUGHT_UP_SENTINEL = {
  id: '__caught_up__',
  priority: -Infinity,
  tone: 'caughtUp',
  _isCaughtUp: true,
};

function StackedCards({ cards: initialCards, onCardAction, title, action, onAction }) {
  const [cards, setCards] = React.useState(() => [...initialCards, CAUGHT_UP_SENTINEL]);
  const [dismissing, setDismissing] = React.useState(() => new Set());
  const [collapsed, setCollapsed] = React.useState(false);

  // Sort by priority. Caught-up sentinel has -Infinity so it always lands last.
  const sorted = React.useMemo(
    () => [...cards].sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0)),
    [cards]
  );

  const dismissCard = (id) => {
    setDismissing(prev => new Set(prev).add(id));
    setTimeout(() => {
      setCards(prev => prev.filter(c => c.id !== id));
      setDismissing(prev => {
        const next = new Set(prev);
        next.delete(id);
        return next;
      });
    }, 480);
  };

  // When everything has been dismissed (including the sentinel), collapse the stack.
  React.useEffect(() => {
    if (cards.length === 0 && !collapsed) {
      setCollapsed(true);
    }
  }, [cards.length, collapsed]);

  // Auto-dismiss the sentinel once it's the only thing left for ~1.6s.
  const activeCards = cards.filter(c => !dismissing.has(c.id));
  const onlyCaughtUp = activeCards.length === 1 && activeCards[0].id === '__caught_up__';
  React.useEffect(() => {
    if (onlyCaughtUp) {
      const t = setTimeout(() => dismissCard('__caught_up__'), 1600);
      return () => clearTimeout(t);
    }
  }, [onlyCaughtUp]);

  const layerFor = (card) => {
    if (dismissing.has(card.id)) return -1;
    let i = 0;
    for (const c of sorted) {
      if (c.id === card.id) return i;
      if (!dismissing.has(c.id)) i++;
    }
    return i;
  };

  const visible = sorted.slice(0, STACK_MAX);
  const fullHeight = STACK_CARD_HEIGHT + (STACK_VISIBLE - 1) * STACK_OFFSET;
  const totalActive = activeCards.length;
  const containerHeight = collapsed
    ? 0
    : totalActive > 0 ? fullHeight : 0;

  return (
    <>
      {title && (
        <div style={{
          maxHeight: collapsed ? 0 : 72,
          opacity: collapsed ? 0 : 1,
          overflow: 'hidden',
          transition: 'max-height 460ms var(--ease-smooth), opacity 240ms var(--ease-smooth)',
        }}>
          <SectionTitle action={action} onAction={onAction}>{title}</SectionTitle>
        </div>
      )}
      <div style={{
        position: 'relative',
        height: containerHeight,
        transition: 'height 600ms cubic-bezier(0.22,1,0.36,1)',
        overflow: 'visible',
      }}>
        {visible.map(card => {
          const layer = layerFor(card);
          const isDismissing = layer === -1;
          const visibleLayer = isDismissing ? 0 : layer;
          const y = isDismissing ? -120 : layer * STACK_OFFSET;
          const scale = isDismissing ? 1.02 : Math.max(0.82, 1 - layer * STACK_SCALE);
          const opacity = isDismissing
            ? 0
            : layer < STACK_VISIBLE ? 1 - layer * 0.05
            : 0;
          const z = 100 - visibleLayer;
          // 1-indexed position among active cards
          const position = isDismissing ? null : (layer + 1);
          return (
            <div key={card.id}
              onClick={() => layer === 0 && onCardAction && onCardAction(card)}
              style={{
                position: 'absolute', top: 0, left: 20, right: 20,
                transform: `translateY(${y}px) scale(${scale})`,
                transformOrigin: 'top center',
                opacity,
                zIndex: z,
                transition: 'transform 480ms cubic-bezier(0.34, 1.10, 0.64, 1), opacity 420ms cubic-bezier(0.22, 1, 0.36, 1)',
                pointerEvents: layer === 0 && !isDismissing ? 'auto' : 'none',
                cursor: layer === 0 ? 'pointer' : 'default',
                willChange: 'transform, opacity',
              }}>
              <StackCard
                card={card}
                depth={Math.max(0, layer)}
                position={position}
                total={totalActive}
                onDismiss={dismissCard}
                onAction={onCardAction}
              />
            </div>
          );
        })}
      </div>
    </>
  );
}

// ─────────────────────────────────────────────────────────────
// CaughtUp — beautiful empty state. Triquetra + halo + orbiting
// petals. Auto-vanishes via parent's emptyPhase prop.
// ─────────────────────────────────────────────────────────────
function CaughtUp({ phase }) {
  // phase: entering | showing | leaving
  const visible = phase === 'showing';
  const goingOut = phase === 'leaving';

  return (
    <div style={{
      position: 'absolute', top: 0, left: 20, right: 20,
      height: STACK_CARD_HEIGHT,
      borderRadius: 'var(--radius-card)',
      background: 'var(--color-surface-2)',
      border: '1px solid var(--color-border)',
      boxShadow: 'var(--shadow-premium)',
      padding: '0 18px 0 16px',
      display: 'flex', alignItems: 'center', gap: 16,
      overflow: 'hidden',
      opacity: visible ? 1 : 0,
      transform: visible ? 'translateY(0) scale(1)' : goingOut ? 'translateY(-6px) scale(0.98)' : 'translateY(8px) scale(0.96)',
      transition: 'opacity 320ms var(--ease-smooth), transform 360ms var(--ease-smooth)',
    }}>
      {/* Mark cluster — fixed-size square on the left */}
      <div style={{
        position: 'relative',
        width: 72, height: 72, flexShrink: 0,
        display: 'grid', placeItems: 'center',
      }}>
        {/* Halo */}
        <div style={{
          position: 'absolute', inset: -12,
          background: 'radial-gradient(circle, rgba(227, 183, 136, 0.20) 0%, rgba(193, 160, 129, 0.06) 45%, transparent 75%)',
          filter: 'blur(2px)',
          pointerEvents: 'none',
        }} />

        {/* Orbiting petals */}
        {[0, 1, 2, 3, 4].map(i => {
          const angle = (i / 5) * Math.PI * 2 - Math.PI / 2;
          const r = 32;
          const x = Math.cos(angle) * r;
          const y = Math.sin(angle) * r;
          const size = i % 2 === 0 ? 3 : 2.5;
          return (
            <span key={i} style={{
              position: 'absolute', top: '50%', left: '50%',
              width: size, height: size, borderRadius: 999,
              background: i === 0 ? '#E3B788' : i % 2 ? 'rgba(193,160,129,0.65)' : 'rgba(227,183,136,0.55)',
              transform: `translate(${x - size / 2}px, ${y - size / 2}px)`,
              boxShadow: '0 0 6px rgba(227, 183, 136, 0.5)',
              opacity: visible ? 1 : 0,
              transition: `opacity 280ms var(--ease-smooth) ${80 + i * 50}ms`,
            }} />
          );
        })}

        {/* Center mark — inline SVG, no asset path */}
        <svg width="36" height="36" viewBox="0 0 914 914" fill="none"
          style={{
            position: 'relative',
            opacity: 0.95,
            transform: visible ? 'scale(1)' : 'scale(0.7)',
            transition: 'transform 520ms cubic-bezier(0.34, 1.20, 0.64, 1)',
            filter: 'drop-shadow(0 3px 10px rgba(227, 183, 136, 0.35))',
          }}>
          <defs>
            <linearGradient id="goji-grad-a" x1="457" y1="508" x2="454" y2="239" gradientUnits="userSpaceOnUse">
              <stop stopColor="#C39765"/><stop offset="1" stopColor="#F5C989"/>
            </linearGradient>
            <linearGradient id="goji-grad-b" x1="451" y1="756" x2="456" y2="509" gradientUnits="userSpaceOnUse">
              <stop stopColor="#865A3A"/><stop offset="1" stopColor="#A87C52"/>
            </linearGradient>
          </defs>
          <path d="M455.676 239.074C457.234 238.928 458.806 238.998 460.343 239.283C471.546 241.455 491.855 266.556 498.116 275.955C512.569 298.028 520.821 323.302 522.071 349.326C522.337 354.621 522.625 361.797 521.431 366.914C571.63 387.251 608.909 426.125 620.103 478.065C622.258 488.069 621.129 495.539 610.016 499.597C600.918 502.919 591.304 504.564 581.742 506.18C536.786 512.836 494.723 499.971 458.497 474.669C457.569 474.242 456.844 474.451 456.028 475.015C447.517 480.919 439.138 486.095 429.744 490.708C392.269 509.498 348.497 513.141 308.199 500.823C303.761 499.433 297.478 497.085 295.362 492.608C290.701 482.748 299.246 464.131 302.404 453.976C303.614 450.087 307.768 443.041 309.735 439.35C328.551 404.053 356.452 382.667 393.564 366.984C392.618 362.094 392.781 355.475 392.969 350.431C394.176 321.695 403.986 293.898 421.229 270.351C427.738 261.567 444.772 240.857 455.676 239.074ZM325.195 476.31C334.487 479.533 354.294 481.301 363.875 480.359C386.925 479.85 417.889 468.695 435.801 454.885C424.338 443.825 416.138 431.421 408.631 417.69C407.124 414.934 399.289 395.24 398.275 394.805C365.771 410.077 340.093 433.235 328.158 466.722C327.028 469.875 326.039 473.074 325.195 476.31ZM448.557 383.038C442.817 383.404 431.826 384.452 426.45 386.113C431.336 401.432 440.429 417.538 450.884 429.943C452.829 432.254 455.221 435.382 457.556 437.211C459.209 435.427 461.064 433.517 462.567 431.638C473.899 418.397 484.023 403.137 488.678 386.43C480.391 383.246 457.711 382.097 448.557 383.038ZM516.227 394.56C509.636 417.288 495.393 437.303 479.485 454.917C497.703 468.633 529.701 480.635 553.149 480.339C561.973 480.886 581.257 479.385 589.599 476.199C579.977 441.515 558.675 417.547 526.614 399.793C523.209 397.945 519.744 396.2 516.227 394.56Z" fill="url(#goji-grad-a)"/>
          <path d="M243.61 508.156C246.601 508.006 249.854 507.825 252.792 508.352C261.238 509.917 268.295 515.588 271.529 523.402C275.392 532.476 273.669 543.42 274.876 553.269C278.091 579.5 288.969 607.57 303.896 629.38C331.662 670.01 374.788 698.111 423.728 707.459C469.596 716.172 520.481 705.471 558.764 679.4C602.568 649.569 628.15 611.154 638.13 559.848C604.258 559.069 569.107 560.37 535.135 559.73C528.134 559.601 505.742 560.473 500.379 559.58C496.01 558.852 491.951 556.905 488.683 553.966C484.409 550.061 481.198 543.523 481.124 537.718C481.034 531.102 483.682 524.734 488.456 520.06C496.273 512.447 503.595 514.126 513.612 513.976C520.065 513.878 526.56 513.966 533.019 513.981L600.152 513.93C630.039 514.224 661.101 513.986 690.977 513.77C691.462 557.116 684.203 596.208 663.164 634.746C633.308 689.228 582.84 730.023 522.628 748.348C464.38 765.768 401.441 759.431 348.012 730.777C291.838 701.37 250.007 651.113 231.857 591.234C226.937 574.986 218.108 533.788 226.424 519.409C230.338 512.644 236.308 510.061 243.61 508.156Z" fill="url(#goji-grad-b)"/>
        </svg>
      </div>

      {/* Text */}
      <div style={{ flex: 1, minWidth: 0 }}>
        <div style={{
          fontFamily: 'var(--font-serif)', fontWeight: 600, fontSize: 20,
          letterSpacing: '-0.02em', color: 'var(--color-text-primary)',
          lineHeight: 1.2,
        }}>All caught up.</div>
        <div style={{
          fontFamily: 'var(--font-sans)', fontSize: 12, color: 'var(--color-text-muted)',
          marginTop: 4, lineHeight: 1.4,
        }}>Nothing else needs your attention.</div>
      </div>
    </div>
  );
}

Object.assign(window, {
  Icon, Money, Button, Field, Chip, Header, HeaderActions, SectionTitle,
  HeroCard, Metric, AccountCard, TransactionRow, InsightCard, BottomNav, FAB,
  StackedCards, StackCard, CaughtUp, SwipePager,
  AmountVisibilityContext, useAmountMask, CARD_TONES,
});
