// Section 3 — Host Dashboard
function HostDashboard() {
  const h = D.host;
  // v132 — Bug D-07: read initial section from ?section= URL param so
  // deep-links like /host/booking-inbox land on the right tab. Vercel
  // catch-all rewrite (vercel.json) routes /host/:section* → here. The
  // raw URL path is also accepted as a fallback in case the rewrite
  // didn't fire.
  const _initialSection = (function() {
    try {
      const p = new URLSearchParams(window.location.search);
      const s = p.get('section');
      if (s) return s.replace(/^booking-/, '').replace(/-inbox$/, ''); // booking-inbox → inbox
      const m = window.location.pathname.match(/^\/host\/([a-z-]+)/i);
      if (m && m[1]) return m[1].replace(/^booking-/, '').replace(/-inbox$/, '');
    } catch (e) {}
    return 'overview';
  })();
  const [section, setSection] = React.useState(_initialSection);
  const [, bump] = React.useState(0);
  React.useEffect(() => {
    const h = () => bump(t => t + 1);
    window.addEventListener('tlb-live-loaded', h);
    return () => window.removeEventListener('tlb-live-loaded', h);
  }, []);
  // v141.126 — Cross-section drill-ins for HostOverview KPI tile clicks
  // and per-row action buttons (Open → inbox, Manage all → properties).
  // Same pattern as admin's tlb-admin-tab event handler.
  React.useEffect(() => {
    const onJump = (e) => { if (e && e.detail) setSection(e.detail); };
    window.addEventListener('tlb-host-tab', onJump);
    return () => window.removeEventListener('tlb-host-tab', onJump);
  }, []);

  // Sidebar items mirror the operational /host sidebar exactly.
  // Native React views: overview, inbox, calendar, messages, earnings.
  // iframe-embedded operational pages for the rest (analytics, properties,
  // reviews, disputes, tasks, concierge, seo, pricing, site, stories,
  // settings, admin).
  // v141.70 — Classic-dashboard retirement. The host classic at
  // /host-classic had three feature areas with NO native equivalent:
  //   1. Per-villa SEO Center (SERP preview, schema builder, FAQ tools,
  //      keyword rankings) — moved to admin sitewide in v129 BUT hosts
  //      still need the per-villa view; surfaced here via iframe.
  //   2. Settings → Integrations (Aetheria OS connection, auto-reply +
  //      business hours, platform integrations: Airbnb/Booking/GA4/WA,
  //      Trust & Verification panel). Native HostSettings only covers
  //      profile + payout; full Integrations panel is the classic page.
  //   3. Admin Host Management (role-gated): purge demos, publish drafts,
  //      inventory report, hosts CRUD. Lives only in /host-classic today.
  //
  // Also switched native → iframe for `stories` (classic has the full
  // upload/staging/publish flow; native was read-only grid).
  //
  // Native sections preserved because they're functionally complete:
  //   overview, inbox, calendar, messages, earnings, analytics,
  //   properties, availability, bookings, welcome, checkin, reviews,
  //   pricing, disputes, tasks, concierge, site, settings.
  //
  // After this revision, every classic /host-classic?tab=xxx feature is
  // reachable from /dashboards/host — visiting /host-classic directly
  // bounces to /dashboards/host (see public/host-classic.html top-of-body).
  const navItems = [
    // Main
    { key: 'overview',     label: 'Overview',           group: 'Main',        icon: 'home',        mode: 'native' },
    { key: 'inbox',        label: 'Booking inbox',      group: 'Main',        icon: 'inbox',       mode: 'native' },
    { key: 'calendar',     label: 'Calendar',           group: 'Main',        icon: 'calendar',    mode: 'native' },
    { key: 'messages',     label: 'Messages',           group: 'Main',        icon: 'messageSq',   mode: 'native' },
    { key: 'earnings',     label: 'Earnings',           group: 'Main',        icon: 'dollar',      mode: 'native' },
    // Operations
    { key: 'analytics',    label: 'Analytics',          group: 'Operations',  icon: 'barChart',    mode: 'native' },
    { key: 'properties',   label: 'Properties',         group: 'Operations',  icon: 'building',    mode: 'native' },
    { key: 'availability', label: 'Availability',       group: 'Operations',  icon: 'calCheck',    mode: 'native' },
    { key: 'bookings',     label: 'Bookings · all',     group: 'Operations',  icon: 'list',        mode: 'native' },
    { key: 'welcome',      label: 'Welcome packets',    group: 'Operations',  icon: 'mail',        mode: 'native' },
    { key: 'checkin',      label: 'Check-in',           group: 'Service',     icon: 'key',         mode: 'native' },
    { key: 'reviews',      label: 'Reviews',            group: 'Operations',  icon: 'star',        mode: 'native' },
    { key: 'pricing',      label: 'Pricing',            group: 'Operations',  icon: 'tag',         mode: 'native' },
    // Service
    { key: 'disputes',     label: 'Disputes',           group: 'Service',     icon: 'alert',       mode: 'native' },
    { key: 'tasks',        label: 'Tasks',              group: 'Service',     icon: 'checkSquare', mode: 'native' },
    { key: 'concierge',    label: 'Concierge requests', group: 'Service',     icon: 'phone',       mode: 'native' },
    // Marketing — v129: SEO moved to admin (sitewide control); host
    // focuses on per-villa content via Public site editor.
    { key: 'site',         label: 'Public site',        group: 'Marketing',   icon: 'globe',       mode: 'native' },
    { key: 'stories',      label: 'Stories',            group: 'Marketing',   icon: 'bookOpen',    mode: 'iframe' },
    // v141.70 — Per-villa SEO Center accessed via classic iframe.
    // Sitewide SEO controls live in /dashboards/admin → SEO control.
    { key: 'seo',          label: 'SEO · per villa',    group: 'Marketing',   icon: 'search',      mode: 'iframe' },
    // Settings
    { key: 'settings',     label: 'Profile · payout',   group: 'Settings',    icon: 'settings',    mode: 'native' },
    // v141.70 — Integrations + Trust & Verification + Aetheria OS panels
    // (auto-reply, business hours, platform connectors). These live in
    // /host-classic?tab=settings under the "Integrations" / "Trust" /
    // "Aetheria" sub-sections; surfaced here as a separate nav item so
    // the native Settings page can stay focused on profile + payout.
    { key: 'integrations', label: 'Integrations · trust', group: 'Settings',  icon: 'link',        mode: 'iframe' },
    // v141.70 — Admin Host Management (role-gated). Purge demo villas,
    // publish all drafts, inventory report, host CRUD. Sidebar item is
    // hidden for non-admin sessions in the sidebar component below.
    { key: 'admin',        label: 'Host management',    group: 'Settings',    icon: 'shield',      mode: 'iframe', adminOnly: true },
    // v110 — opens the system health dashboard in a new tab. Hosts use
    // it to see at a glance which of their villas need attention
    // (broken iCal, stale sync, missing seasonal rates).
    { key: 'health',       label: 'Villa Health',       group: 'Settings',    icon: 'activity',    mode: 'external', href: '/admin/health' },
  ];

  const liveStatus = window.TLB_LIVE_STATUS && window.TLB_LIVE_STATUS.host;
  const needsAuth = liveStatus && liveStatus.needsAuth && !liveStatus.live;

  // v141.70 — role detection for `adminOnly` nav items (Host management
  // panel). Reads tlb_session JSON; falls back to tlb_admin_token
  // presence so legacy sessions still get the elevated controls.
  const isAdminSession = (function() {
    try {
      const raw = localStorage.getItem('tlb_session') || localStorage.getItem('tlb_user');
      if (raw) {
        const sess = JSON.parse(raw);
        if (sess && (sess.isAdmin || sess.role === 'admin')) return true;
      }
      if (localStorage.getItem('tlb_admin_token')) return true;
    } catch {}
    return false;
  })();
  const visibleNavItems = navItems.filter(n => !n.adminOnly || isAdminSession);

  const activeItem = visibleNavItems.find(n => n.key === section) || visibleNavItems[0];
  const greetingName = (h.name || 'Putu').split(' ')[0];

  function renderSection() {
    // Native sections registered via dashboards-native-sections.jsx —
    // properties/bookings/reviews/pricing/settings all wired here.
    const NATIVE = (window.TLB_NATIVE_SECTIONS && window.TLB_NATIVE_SECTIONS.host) || {};
    if (NATIVE[section]) {
      const Cmp = NATIVE[section];
      return <Cmp />;
    }
    if (activeItem.mode === 'iframe') {
      return <IframeView src={'/host-classic?tab=' + activeItem.key + '&embed=1'} title={activeItem.label} />;
    }
    switch (section) {
      case 'overview':  return <HostOverview />;
      case 'inbox':     return <HostInbox />;
      case 'calendar':  return <HostCalendar />;
      case 'messages':  return <HostMessages />;
      case 'earnings':  return <HostEarnings />;
      default:          return <HostOverview />;
    }
  }

  return (
    <div style={{ display: 'flex', minHeight: '100vh', width: '100%' }}>
      <DashSidebar
        items={visibleNavItems}
        active={section}
        onChange={(key) => {
          // External nav items open in new tab instead of switching section
          const item = visibleNavItems.find(n => n.key === key);
          if (item && item.mode === 'external' && item.href) {
            window.open(item.href, '_blank', 'noopener');
            return;
          }
          setSection(key);
        }}
        brand="TLB"
        sub="Host"
        dark={true}
        footer={
          <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
              <Sans size={11} color="rgba(240,240,240,0.85)" weight={500} style={{ fontFamily: F_TLB.ui }}>{(h.name || 'Host').split(' ')[0]}</Sans>
              <Sans size={10} color="rgba(240,240,240,0.5)" style={{ fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 11 }}>{h.properties || 0} properties.</Sans>
            </div>
            <button
              type="button"
              onClick={() => {
                // v141.189 — Was gated on native confirm(); Chrome
                // silently returns false from confirm() once the user
                // ticks "prevent more dialogs" / in embedded contexts,
                // so the click did nothing ("Sign Out tidak merespon").
                // tlbConfirm renders its own overlay and always works.
                window.tlbConfirm('Sign out of the host dashboard?', { danger: true, confirmText: 'Sign out' }).then((ok) => {
                  if (!ok) return;
                  try {
                    ['tlb_token','tlb_admin_token','tlb_host_token','tlb_session','tlb_user']
                      .forEach(k => localStorage.removeItem(k));
                    document.cookie = 'tlb_session=; path=/; expires=Thu, 01 Jan 1970 00:00:00 GMT';
                  } catch (e) {}
                  // /host-classic?signin=1 shows the classic loginScreen
                  // (its redirect-guard bypasses on ?signin=1 + empty
                  // token). replace() so Back doesn't return to the
                  // signed-out dashboard.
                  location.replace('/host-classic?signin=1');
                });
              }}
              style={{
                width: '100%', padding: '8px 12px', borderRadius: 8,
                border: '1px solid rgba(240,240,240,0.10)', background: 'transparent',
                color: 'rgba(240,240,240,0.65)', fontSize: 11, fontWeight: 500,
                cursor: 'pointer', letterSpacing: '0.04em', fontFamily: F_TLB.ui,
                display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                transition: 'background .15s, color .15s, border-color .15s',
              }}
              onMouseEnter={e => { e.currentTarget.style.background = 'rgba(240,240,240,0.06)'; e.currentTarget.style.color = '#fff'; e.currentTarget.style.borderColor = 'rgba(240,240,240,0.20)'; }}
              onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = 'rgba(240,240,240,0.65)'; e.currentTarget.style.borderColor = 'rgba(240,240,240,0.10)'; }}
              title="Clear session and return to sign-in screen"
            >
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" style={{ width: 13, height: 13 }} aria-hidden="true">
                <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" />
              </svg>
              Sign out
            </button>
          </div>
        }
      />
      <main style={{ flex: 1, padding: dashPad(), background: CC.offWhite, minWidth: 0 }}>
        {/* v141.167 re-skin Stage 5 — global workspace Topbar (delivered
            redesign). Self-contained, sticky, above every section. */}
        <DashTopbar role="Host" />
        {/* v141.137 — Shell page-header collapsed to just the
            group-eyebrow + LivePill. Section-level Display titles were
            duplicating with the section's own internal header. Each
            section since Phase 2 renders its own editorial header
            (eyebrow + Display title + subtitle + action CTAs) to match
            the v141.123 mockup. The overview greeting "Selamat sore, X"
            moves into HostOverview itself if needed. */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap', marginBottom: dashGap() }}>
          <Eyebrow>{activeItem.group || 'Host'}</Eyebrow>
          <LivePill which="host" />
        </div>
        {needsAuth && (
          <Panel pad={18} style={{ marginBottom: dashGap(), borderColor: '#d4a04a', background: '#fbf6ec' }}>
            <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 14 }}>
                <span style={{ width: 8, height: 8, borderRadius: 99, background: '#d4a04a', flexShrink: 0 }} />
                <div>
                  <Sans size={11} weight={600} ls={0.14} upper color={CC.deep}>SIGN IN TO UNLOCK LIVE HOST DATA</Sans>
                  <Sans size={12} color={CC.mid} style={{ marginTop: 4, fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 14 }}>
                    Public villa portfolio (152 properties) is already real. KPIs, action queue, calendar, inbox, earnings, and the embedded operational sections need a host session.
                  </Sans>
                </div>
              </div>
              <div style={{ display: 'flex', gap: 8 }}>
                {/* v141.72 — Was `/host` which rewrites back to this
                    same dashboard. Now points at the classic page's
                    loginScreen (?signin=1 bypasses the auto-redirect). */}
                <Button size="sm" variant="secondary" onClick={() => window.open('/host-classic?signin=1', '_blank')}>Open sign-in →</Button>
                <Button size="sm" onClick={() => location.reload()}>I signed in · reload</Button>
              </div>
            </div>
          </Panel>
        )}
        {/* v141.137 — ErrorBoundary so a crashing section shows a
            fallback instead of a blank screen. Keyed on `section` to
            reset on nav. */}
        <SectionErrorBoundary key={section}>
          {renderSection()}
        </SectionErrorBoundary>
      </main>
    </div>
  );
}

// ─── HOST · ALL SECTIONS — feature-parity grid with operational ────
function HostAllSections() {
  const sections = D.host.sections || [];
  return (
    <div>
      <div style={{ marginBottom: dashGap() }}>
        <Eyebrow>FEATURE PARITY · 17 OPERATIONAL SECTIONS</Eyebrow>
        <Display size={28} italic weight={400} ls={-0.02} style={{ marginTop: 8 }}>Every host workspace section.</Display>
        <Sans size={14} color={CC.mid} style={{ marginTop: 8, fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 17, maxWidth: 640 }}>The five tabs above cover daily flow. The grid below opens each operational section in the editorial-styled live host workspace at <em>/host</em>.</Sans>
      </div>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: dashGap() }}>
        {sections.map(s => (
          <div key={s.key} style={{ padding: 28, border: `1px solid ${CC.border}`, borderRadius: 18, background: CC.white, cursor: 'pointer', transition: 'all .25s ease' }}
               onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-2px)'; e.currentTarget.style.boxShadow = '0 12px 36px rgba(20,20,20,0.06)'; e.currentTarget.style.borderColor = CC.deep; }}
               onMouseLeave={e => { e.currentTarget.style.transform = ''; e.currentTarget.style.boxShadow = ''; e.currentTarget.style.borderColor = CC.border; }}
               onClick={() => window.TLB_NAV && TLB_NAV.go(s.url)}>
            <div style={{ display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', marginBottom: 10 }}>
              <Sans size={9} weight={600} ls={0.18} upper color={CC.mid}>{s.num}</Sans>
              <Sans size={11} color={CC.mid} style={{ textDecoration: 'underline', textUnderlineOffset: 4 }}>Open →</Sans>
            </div>
            <Display size={22} italic weight={400} ls={-0.01}>{s.name}.</Display>
            <Sans size={13} color={CC.deep} style={{ marginTop: 8, lineHeight: 1.6 }}>{s.desc}</Sans>
          </div>
        ))}
      </div>
    </div>
  );
}

// v141.125 — HostOverview, v141.123 mockup port.
//   • 4-KPI unified border-grid (replaces separate KPI panels)
//   • Action queue with Overdue/Today/This-week buckets + Snooze/Open
//   • Activity feed with Dot tone (sage = ok, mid = neutral)
//   • Properties grid — 3 cards with gradient header + bottom info row
//   • All 4 states: loading · error · empty · populated
//   • Buttons wired: Print, Add property, Acknowledge all, Open feed,
//     Manage all, per-row Snooze/Open
function HostOverview() {
  const propsApi = useApi('/api/properties?status=all', { tokenType: 'host' });
  const bookApi  = useApi('/api/bookings?status=all',   { tokenType: 'host' });
  const villas   = (propsApi.data && propsApi.data.properties) || [];
  const bookings = (bookApi.data && bookApi.data.bookings)     || [];

  // ── KPIs (this month) ───────────────────────────────────────
  const now = new Date();
  const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
  const monthBookings = bookings.filter(b => {
    const t = b.check_in || b.created_at;
    return t && new Date(t) >= monthStart;
  });
  const mtdRevenue = monthBookings.reduce((s, b) => s + (parseFloat(b.total_price) || 0), 0);
  const totalNights = monthBookings.reduce((s, b) => s + (parseInt(b.nights, 10) || 0), 0);
  const adr = totalNights > 0 ? Math.round(mtdRevenue / totalNights) : 0;
  const elapsedDays = now.getDate();
  const possibleNights = Math.max(1, villas.length * elapsedDays);
  const occPct = Math.min(100, Math.round((totalNights / possibleNights) * 100));
  const fmtMoney = (n) => n >= 1_000_000 ? 'Rp ' + (n / 1_000_000).toFixed(1) + 'M' : 'Rp ' + Math.round(n).toLocaleString('id-ID');
  // Last-month deltas — needs last month's data; we approximate from the
  // earlier portion of `bookings` (it's an "all-time" feed).
  const lastMonthStart = new Date(now.getFullYear(), now.getMonth() - 1, 1);
  const lastMonthEnd   = new Date(now.getFullYear(), now.getMonth(), 0);
  const lmBookings = bookings.filter(b => {
    const t = b.check_in || b.created_at;
    return t && new Date(t) >= lastMonthStart && new Date(t) <= lastMonthEnd;
  });
  const lmRevenue = lmBookings.reduce((s, b) => s + (parseFloat(b.total_price) || 0), 0);
  const revDelta = lmRevenue > 0 ? Math.round((mtdRevenue - lmRevenue) / lmRevenue * 100) : 0;
  const bookDelta = monthBookings.length - lmBookings.length;
  const kpis = [
    { key: 'rev',  label: 'MTD revenue',   value: fmtMoney(mtdRevenue),         delta: (revDelta >= 0 ? '+' : '') + revDelta + '% MoM' },
    { key: 'bk',   label: 'Bookings',      value: monthBookings.length,         delta: (bookDelta >= 0 ? '+' : '') + bookDelta + ' vs last mo' },
    { key: 'occ',  label: 'Avg occupancy', value: occPct + '%',                 delta: villas.length + ' villas tracked' },
    { key: 'adr',  label: 'ADR',           value: adr > 0 ? fmtMoney(adr) : '—', delta: totalNights + ' night-stays' },
  ];

  // ── Action queue: bucketed by SLA age ───────────────────────
  const queueRaw = bookings.filter(b => /pending|enquir|new/i.test(b.status || ''));
  function bucketOf(b) {
    if (!b.created_at) return 'This week';
    const hoursSince = (Date.now() - new Date(b.created_at).getTime()) / 3600000;
    if (hoursSince > 24) return 'Overdue';
    if (hoursSince > 6)  return 'Today';
    return 'This week';
  }
  const queue = queueRaw.slice(0, 12).map(b => ({
    id: b.id,
    bucket: bucketOf(b),
    kind: (b.status || 'new').toLowerCase(),
    label: (b.guest_name || b.guest_email || 'Guest') + ' · ' + (b.property_name || 'villa'),
    age: b.created_at ? relTime(b.created_at) : '—',
    when: b.check_in ? new Date(b.check_in).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '',
  }));

  // ── Activity feed ───────────────────────────────────────────
  const activity = bookings.slice(0, 6).map(b => {
    const status = (b.status || 'new').toLowerCase();
    return {
      kind: /confirm|complete|paid/i.test(status) ? 'ok' : 'mid',
      who: b.guest_name || b.guest_email || 'Guest',
      what: status + ' · ' + (b.property_name || 'villa') + (b.total_price ? ` · ${fmtMoney(parseFloat(b.total_price))}` : ''),
      when: b.created_at ? relTime(b.created_at) : '—',
      id: b.id,
    };
  });

  // ── Properties grid — top 3 by revenue ──────────────────────
  const propStats = villas.map(v => {
    const myBookings = bookings.filter(b => b.property_id === v.id || b.property_name === v.name);
    const myRev = myBookings
      .filter(b => { const t = b.check_in; return t && new Date(t) >= monthStart; })
      .reduce((s, b) => s + (parseFloat(b.total_price) || 0), 0);
    const myNights = myBookings
      .filter(b => { const t = b.check_in; return t && new Date(t) >= monthStart; })
      .reduce((s, b) => s + (parseInt(b.nights, 10) || 0), 0);
    const occ = elapsedDays > 0 ? Math.min(100, Math.round((myNights / elapsedDays) * 100)) : 0;
    // Cover image — first photo if available, else gradient fallback.
    const cover = v.cover_image || v.image || (Array.isArray(v.photos) && v.photos[0]) || null;
    return {
      id: v.id,
      name: v.name || 'Villa',
      place: (v.location || 'Bali').split(',')[0].trim(),
      slug: v.url_slug || v.id,
      beds: v.bedrooms || v.beds || '—',
      rate: v.nightly_rate_idr ? fmtMoney(v.nightly_rate_idr) : (v.price_per_night ? '$' + v.price_per_night : '—'),
      score: v.health_score != null ? v.health_score : null,
      status: v.status || 'live',
      occ, rev: myRev, cover,
    };
  }).sort((a, b) => b.rev - a.rev).slice(0, 3);

  const loading = propsApi.loading || bookApi.loading;
  const error   = propsApi.error || bookApi.error;
  if (loading) return <Loading msg="Loading your overview…" />;
  if (error)   return <ErrorState msg={error} onRetry={() => { propsApi.refetch(); bookApi.refetch(); }} />;

  const empty = villas.length === 0 && bookings.length === 0;
  const subtitle = empty
    ? 'A new host. Onboarding complete — the first booking will surface here.'
    : `${villas.length} ${villas.length === 1 ? 'villa' : 'villas'}, ${monthBookings.length} ${monthBookings.length === 1 ? 'booking' : 'bookings'} this month, ${queue.length} ${queue.length === 1 ? 'thing' : 'things'} asking for your attention.`;

  return (
    <div>
      {/* ── Header with Print + Add property ─────────────────────── */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 18, flexWrap: 'wrap' }}>
        <div>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 }}>
            <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>Your villas · the month so far</Sans>
            <Pill tone="success">Live data</Pill>
          </div>
          <Sans size={13} color={CC.mid} style={{ marginTop: 4, maxWidth: 600 }}>{subtitle}</Sans>
        </div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
          <HostBtn kind="ghost" onClick={() => window.print()}>Print</HostBtn>
          <HostBtn kind="primary" onClick={() => window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'properties' }))}>Add property</HostBtn>
        </div>
      </div>

      {/* ── KPI grid (unified border) ────────────────────────────── */}
      <div style={{
        display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0,
        border: `1px solid ${CC.border}`, borderRadius: 18, overflow: 'hidden', background: CC.white,
      }}>
        {kpis.map((k, i) => (
          <div key={k.key} style={{
            padding: '28px 28px', borderRight: i < 3 ? `1px solid ${CC.border}` : 'none',
            display: 'flex', flexDirection: 'column', gap: 14, minHeight: 140,
          }}>
            <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>{k.label}</Sans>
            <div style={{
              fontFamily: F_TLB.ui, fontFeatureSettings: '"tnum"', fontSize: 48, fontWeight: 500,
              lineHeight: 0.95, letterSpacing: '-0.028em', color: CC.ink,
              fontVariantNumeric: 'tabular-nums',
            }}>{empty ? '—' : k.value}</div>
            <Sans size={11.5} color={CC.mid}>{empty ? '—' : k.delta}</Sans>
          </div>
        ))}
      </div>

      {/* ── Action queue + Activity feed ─────────────────────────── */}
      <div style={{ marginTop: dashGap(), display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: dashGap() }}>
        <Panel pad={28}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 18 }}>
            <div>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>
                {queue.length} {queue.length === 1 ? 'item' : 'items'} · clustered by SLA
              </Sans>
              <Display size={24} italic weight={400} ls={-0.015} style={{ marginTop: 10 }}>Action queue.</Display>
            </div>
            <HostBtn kind="subtle" onClick={async () => {
              if (!queue.length) return;
              if (!confirm('Acknowledge all ' + queue.length + ' pending items? They will be moved to "viewed" state.')) return;
              // Best-effort batch ack — bulk action endpoint not yet built;
              // for now navigate to inbox where the user can action each.
              window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'inbox' }));
            }} disabled={!queue.length}>Acknowledge all →</HostBtn>
          </div>
          <HostActionQueueBuckets items={queue} empty={empty} fmtMoney={fmtMoney} />
        </Panel>

        <Panel pad={28}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 18 }}>
            <div>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>Last 14 days</Sans>
              <Display size={24} italic weight={400} ls={-0.015} style={{ marginTop: 10 }}>Activity.</Display>
            </div>
            <HostBtn kind="subtle" onClick={() => window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'messages' }))}>Open feed →</HostBtn>
          </div>
          {empty || activity.length === 0 ? (
            <HostEmpty headline="The bridge is quiet." hint="Activity will land here as bookings move through their lifecycle." />
          ) : (
            <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
              {activity.map((a, i) => (
                <div key={a.id || i} style={{
                  display: 'grid', gridTemplateColumns: 'auto 1fr auto', gap: 12, alignItems: 'flex-start',
                  padding: '14px 0', borderTop: i === 0 ? '0' : `1px solid ${CC.border}`,
                }}>
                  <HostDot tone={a.kind === 'ok' ? 'sage' : 'mid'} />
                  <div>
                    <div style={{ fontFamily: F_TLB.ui, fontSize: 12.5 }}>
                      <span style={{ fontWeight: 600 }}>{a.who}</span>{' '}
                      <span style={{ color: CC.mid }}>{a.what}</span>
                    </div>
                  </div>
                  <div style={{ fontFamily: F_TLB.ui, fontSize: 10.5, color: CC.mid, letterSpacing: '0.04em' }}>{a.when}</div>
                </div>
              ))}
            </div>
          )}
        </Panel>
      </div>

      {/* ── Properties grid ──────────────────────────────────────── */}
      <div style={{ marginTop: dashGap() }}>
        <Panel pad={28}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', marginBottom: 18 }}>
            <div>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>
                {villas.length} {villas.length === 1 ? 'listing' : 'listings'} · {villas.filter(v => (v.status || 'live') === 'live').length} live
              </Sans>
              <Display size={24} italic weight={400} ls={-0.015} style={{ marginTop: 10 }}>Properties.</Display>
            </div>
            <HostBtn kind="subtle" onClick={() => window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'properties' }))}>Manage all →</HostBtn>
          </div>
          {propStats.length === 0 ? (
            <HostEmpty headline="No properties yet." hint="Add the first villa to begin taking bookings." />
          ) : (
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 18 }}>
              {propStats.map(p => <HostVillaCard key={p.id} v={p} fmtMoney={fmtMoney} />)}
            </div>
          )}
        </Panel>
      </div>
    </div>
  );
}

// ─── Helpers for HostOverview ──────────────────────────────────────
function relTime(iso) {
  try {
    const d = (Date.now() - new Date(iso).getTime()) / 1000;
    if (d < 60) return 'just now';
    if (d < 3600) return Math.round(d / 60) + ' min ago';
    if (d < 86400) return Math.round(d / 3600) + 'h ago';
    return Math.round(d / 86400) + 'd ago';
  } catch { return '—'; }
}

function HostBtn({ kind = 'ghost', onClick, children, disabled = false }) {
  const isPrimary = kind === 'primary';
  const isSubtle  = kind === 'subtle';
  const base = {
    fontFamily: F_TLB.ui, fontSize: isSubtle ? 11 : 12, fontWeight: 600,
    letterSpacing: isSubtle ? 0.06 : 0.02, padding: isSubtle ? '6px 12px' : '10px 18px',
    borderRadius: 100, cursor: disabled ? 'not-allowed' : 'pointer',
    transition: 'background .15s ease', opacity: disabled ? 0.5 : 1,
  };
  const style = isPrimary
    ? { ...base, background: CC.ink, color: CC.white, border: '1px solid ' + CC.ink }
    : isSubtle
    ? { ...base, background: 'transparent', color: CC.deep, border: '1px solid ' + CC.border }
    : { ...base, background: CC.white, color: CC.deep, border: '1px solid ' + CC.border };
  return <button style={style} onClick={disabled ? undefined : onClick} disabled={disabled}>{children}</button>;
}

function HostDot({ tone = 'sage' }) {
  const color = tone === 'red' ? '#9a1c1c' : tone === 'amber' ? '#c08a3e' : tone === 'mid' ? CC.mid : '#69d28a';
  return <span style={{ display: 'inline-block', width: 8, height: 8, borderRadius: 99, background: color, marginTop: 6 }} />;
}

function HostEmpty({ headline, hint }) {
  return (
    <div style={{ padding: '36px 8px', textAlign: 'center' }}>
      <Display size={22} italic weight={400} style={{ color: CC.deep }}>{headline}</Display>
      <Sans size={12} color={CC.mid} style={{ marginTop: 8 }}>{hint}</Sans>
    </div>
  );
}

function HostActionQueueBuckets({ items, empty, fmtMoney }) {
  if (empty || !items.length) {
    return <HostEmpty headline="Nothing's asking for you right now." hint="The day is young." />;
  }
  const buckets = ['Overdue', 'Today', 'This week'];
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
      {buckets.map((b, bi) => {
        const list = items.filter(i => i.bucket === b);
        if (!list.length) return null;
        const tone = b === 'Overdue' ? '#9a1c1c' : b === 'Today' ? '#c08a3e' : CC.mid;
        return (
          <div key={b} style={{ marginTop: bi === 0 ? 0 : 18, paddingTop: bi === 0 ? 0 : 14, borderTop: bi === 0 ? 'none' : `1px solid ${CC.border}` }}>
            <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginBottom: 10 }}>
              <Sans size={10} weight={700} ls={0.20} upper color={tone}>{b}</Sans>
              <span style={{ display: 'inline-block', padding: '2px 8px', borderRadius: 100, background: tone + '15', color: tone, fontSize: 10, fontWeight: 700 }}>{list.length}</span>
            </div>
            {list.map((it, i) => (
              <div key={it.id || i} style={{ display: 'grid', gridTemplateColumns: '1fr auto auto', gap: 14, alignItems: 'center', padding: '12px 0', borderTop: i === 0 ? '0' : `1px solid ${CC.border}` }}>
                <div>
                  <div style={{ fontFamily: F_TLB.display, fontSize: 15, letterSpacing: '-0.005em' }}>{it.label}</div>
                  <div style={{ fontFamily: F_TLB.ui, fontSize: 11, color: CC.mid, marginTop: 2, letterSpacing: '0.04em', textTransform: 'uppercase' }}>{it.kind} · {it.age}</div>
                </div>
                <HostBtn kind="subtle" onClick={() => {
                  // Snooze — adds 6 hours to created_at via local override
                  try {
                    const map = JSON.parse(localStorage.getItem('tlb_host_snoozed') || '{}');
                    map[it.id] = Date.now() + 6 * 3600 * 1000;
                    localStorage.setItem('tlb_host_snoozed', JSON.stringify(map));
                  } catch {}
                }}>Snooze</HostBtn>
                <button onClick={() => {
                  // Open — navigate to inbox section pre-selected to this booking
                  try { localStorage.setItem('tlb_host_inbox_target', it.id || ''); } catch {}
                  window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'inbox' }));
                }} style={{
                  fontFamily: F_TLB.ui, fontSize: 11, fontWeight: 600, letterSpacing: 0.06,
                  padding: '6px 12px', borderRadius: 100, cursor: 'pointer',
                  background: CC.ink, color: CC.white, border: '1px solid ' + CC.ink,
                }}>Open</button>
              </div>
            ))}
          </div>
        );
      })}
    </div>
  );
}

function HostVillaCard({ v, fmtMoney }) {
  const seed = (v.name || v.id || 'x').charCodeAt(0) * 17;
  const hue = seed % 360;
  return (
    <div
      onClick={() => window.location.href = '/villa/' + v.slug}
      style={{ background: CC.white, border: `1px solid ${CC.border}`, borderRadius: 14, overflow: 'hidden', cursor: 'pointer', transition: 'all .25s ease' }}
      onMouseEnter={(e) => { e.currentTarget.style.borderColor = CC.deep; e.currentTarget.style.boxShadow = '0 12px 36px rgba(20,20,20,0.06)'; }}
      onMouseLeave={(e) => { e.currentTarget.style.borderColor = CC.border; e.currentTarget.style.boxShadow = ''; }}
    >
      <div style={{
        height: 120,
        background: v.cover ? `url(${v.cover}) center/cover` : `linear-gradient(135deg, hsl(${hue}, 14%, 40%), hsl(${hue}, 14%, 22%))`,
        position: 'relative',
      }}>
        <div style={{ position: 'absolute', top: 14, left: 14 }}>
          <span style={{ display: 'inline-block', padding: '4px 10px', borderRadius: 100, background: 'rgba(255,255,255,0.92)', color: CC.deep, fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase' }}>{v.place}</span>
        </div>
        <div style={{ position: 'absolute', top: 14, right: 14 }}>
          <span style={{ display: 'inline-block', padding: '4px 10px', borderRadius: 100, background: v.status === 'live' ? 'rgba(105,210,138,0.92)' : 'rgba(255,255,255,0.92)', color: v.status === 'live' ? '#1f3322' : CC.deep, fontSize: 10, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase' }}>{v.status}</span>
        </div>
      </div>
      <div style={{ padding: '18px 20px' }}>
        <h4 style={{ margin: 0, fontFamily: F_TLB.display, fontSize: 19, fontWeight: 500, letterSpacing: '-0.01em' }}>{v.name}</h4>
        <div style={{ fontFamily: F_TLB.ui, fontSize: 11, color: CC.mid, marginTop: 5, letterSpacing: '0.04em' }}>
          {v.beds} {v.beds === 1 ? 'bedroom' : 'bedrooms'} · {v.place}
        </div>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 16, paddingTop: 14, borderTop: `1px solid ${CC.border}` }}>
          <div>
            <Sans size={9.5} weight={600} ls={0.18} upper color={CC.mid}>Rate</Sans>
            <div style={{ fontFamily: F_TLB.display, fontSize: 15, marginTop: 4, letterSpacing: '-0.01em' }}>{v.rate}</div>
          </div>
          <div style={{ textAlign: 'center' }}>
            <Sans size={9.5} weight={600} ls={0.18} upper color={CC.mid}>OCC</Sans>
            <div style={{ fontFamily: F_TLB.display, fontSize: 15, marginTop: 4, letterSpacing: '-0.01em', fontStyle: 'italic' }}>{v.occ}%</div>
          </div>
          <div style={{ textAlign: 'right' }}>
            <Sans size={9.5} weight={600} ls={0.18} upper color={CC.mid}>Health</Sans>
            <div style={{ fontFamily: F_TLB.display, fontSize: 15, marginTop: 4, color: v.score == null ? CC.mid : v.score < 70 ? '#c08a3e' : '#2d6a4f' }}>
              {v.score == null ? '—' : v.score}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// v141.126 — HostInbox, v141.123 mockup port.
//   • 2-pane layout: 380px thread list (left) + 1fr conversation (right)
//   • Thread row: avatar + guest + status pill + villa + dates + late banner
//   • Detail pane: ID + headline ("Guest aboard <i>Villa</i>") + 3-stat
//     row (Total / Per night / Channel) + guest message + Confirm/Quote/Decline
//   • Header: Decline reasons (modal) + Confirm selected (bulk)
//   • All 4 states: loading / error / empty / populated
//   • Buttons wired: Confirm + Decline → POST /api/host-booking-action;
//     Quote a rate → opens classic with prefilled rate form;
//     Decline reasons → modal with quick presets
function HostInbox() {
  const { data, loading, error, refetch } = useApi('/api/bookings?status=all', { tokenType: 'host' });
  const all = (data && data.bookings) || [];
  const inquiries = all.filter(b => /pending|enquir|new|review/i.test(b.status || ''));
  const [selId, setSelId] = React.useState(null);
  const [filter, setFilter] = React.useState('all');
  const [selectedIds, setSelectedIds] = React.useState(new Set());
  const [showDeclineModal, setShowDeclineModal] = React.useState(false);
  const [acting, setActing] = React.useState(null); // booking id currently in flight

  // Re-select first inquiry whenever the visible list changes
  React.useEffect(() => {
    if (!inquiries.length) { setSelId(null); return; }
    if (!inquiries.find(b => b.id === selId)) setSelId(inquiries[0].id);
  }, [inquiries.length]); // eslint-disable-line

  function slaMinutes(b) {
    if (!b.created_at) return 24 * 60;
    return Math.max(0, Math.round((new Date(b.created_at).getTime() + 24 * 3600 * 1000 - Date.now()) / 60000));
  }
  const today = new Date();
  const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate()).getTime();
  const visible = filter === 'urgent' ? inquiries.filter(b => slaMinutes(b) < 60)
                : filter === 'today'  ? inquiries.filter(b => b.created_at && new Date(b.created_at).getTime() >= todayStart)
                : inquiries;
  const sel = visible.find(b => b.id === selId) || visible[0];
  const escalating = inquiries.filter(b => slaMinutes(b) < 5).length;

  async function actOnBooking(bookingId, action, reason) {
    if (!bookingId || !action) return;
    setActing(bookingId);
    try {
      const token = localStorage.getItem('tlb_host_token') || localStorage.getItem('tlb_token') || '';
      const r = await fetch('/api/host-booking-action', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
        body: JSON.stringify({ booking_id: bookingId, action, reason: reason || '' }),
      });
      if (!r.ok) {
        const j = await r.json().catch(() => ({}));
        throw new Error(j.error || j.reason || `Server returned ${r.status}`);
      }
      // Refetch the list so the actioned booking falls off
      refetch();
      // Clear selection set
      setSelectedIds(new Set());
    } catch (e) {
      alert('Could not ' + action + ': ' + (e.message || 'unknown error'));
    } finally {
      setActing(null);
    }
  }

  function bulkConfirm() {
    const ids = [...selectedIds];
    if (!ids.length) return;
    if (!confirm(`Confirm ${ids.length} booking${ids.length === 1 ? '' : 's'}? Each guest gets a confirmation email immediately.`)) return;
    Promise.all(ids.map(id => actOnBooking(id, 'confirm', ''))).then(refetch);
  }

  if (loading) return <Loading msg="Loading inbox…" />;
  if (error)   return <ErrorState msg={error} onRetry={refetch} />;

  const fmtDate = (iso) => iso ? new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }) : '—';
  const fmtMoney = (n) => {
    const num = parseFloat(n) || 0;
    if (num === 0) return '—';
    return num >= 1_000_000 ? 'Rp ' + (num / 1_000_000).toFixed(1) + 'M' : 'Rp ' + Math.round(num).toLocaleString('id-ID');
  };

  return (
    <div>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 18, flexWrap: 'wrap' }}>
        <div>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 8 }}>
            {inquiries.length} pending · {escalating} escalating &lt; 5m
          </Sans>
          <Display size={36} weight={500} ls={-0.02}>Booking inbox</Display>
          <Sans size={13} color={CC.mid} style={{ marginTop: 6, maxWidth: 640 }}>
            Two-pane inquiry queue. Auto-confirm in 24 hours if no response — banner on every late thread.
          </Sans>
        </div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
          <HostBtn kind="ghost" onClick={() => setShowDeclineModal(true)}>Decline reasons</HostBtn>
          <HostBtn kind="primary" onClick={bulkConfirm} disabled={!selectedIds.size}>
            Confirm selected{selectedIds.size ? ` · ${selectedIds.size}` : ''}
          </HostBtn>
        </div>
      </div>

      {/* Filter chips */}
      <div style={{ display: 'flex', gap: 8, marginBottom: 14 }}>
        {[
          { key: 'all',    label: `All · ${inquiries.length}` },
          { key: 'urgent', label: `Urgent · ${inquiries.filter(b => slaMinutes(b) < 60).length}` },
          { key: 'today',  label: `Today · ${inquiries.filter(b => b.created_at && new Date(b.created_at).getTime() >= todayStart).length}` },
        ].map(c => (
          <button key={c.key} onClick={() => setFilter(c.key)} style={{
            padding: '6px 14px', borderRadius: 100, cursor: 'pointer',
            background: filter === c.key ? CC.ink : 'transparent',
            color: filter === c.key ? CC.white : CC.deep,
            border: '1px solid ' + (filter === c.key ? CC.ink : CC.border),
            fontFamily: F_TLB.ui, fontSize: 11, fontWeight: 600, letterSpacing: '0.04em',
          }}>{c.label}</button>
        ))}
      </div>

      {/* 2-pane */}
      <div style={{ display: 'grid', gridTemplateColumns: '380px 1fr', gap: 0, border: `1px solid ${CC.border}`, borderRadius: 18, overflow: 'hidden', background: CC.white, minHeight: 560 }}>
        {/* List pane */}
        <div style={{ borderRight: `1px solid ${CC.border}`, overflowY: 'auto', maxHeight: '70vh' }}>
          {visible.length === 0 ? (
            <HostEmpty headline="No inquiries yet." hint="The first request will arrive in this column." />
          ) : visible.map((b, i) => {
            const active = b.id === selId;
            const sla = slaMinutes(b);
            const late = sla < 60 * 6; // < 6h to auto-confirm
            const isSel = selectedIds.has(b.id);
            const initials = (b.guest_name || b.guest_email || 'G').split(/[\s@]/)[0][0] || 'G';
            return (
              <div
                key={b.id}
                onClick={() => setSelId(b.id)}
                style={{
                  padding: '16px 20px', borderTop: i === 0 ? '0' : `1px solid ${CC.border}`,
                  background: active ? CC.offWhite : 'transparent', cursor: 'pointer', position: 'relative',
                }}
              >
                {active && <div style={{ position: 'absolute', left: 0, top: 0, bottom: 0, width: 2, background: CC.ink }} />}
                <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 5, gap: 8 }}>
                  <div style={{ display: 'flex', gap: 9, alignItems: 'center', flex: 1, minWidth: 0 }}>
                    <input
                      type="checkbox"
                      checked={isSel}
                      onChange={(e) => {
                        e.stopPropagation();
                        setSelectedIds(prev => {
                          const next = new Set(prev);
                          if (next.has(b.id)) next.delete(b.id); else next.add(b.id);
                          return next;
                        });
                      }}
                      onClick={(e) => e.stopPropagation()}
                      style={{ width: 14, height: 14, accentColor: CC.ink, cursor: 'pointer' }}
                    />
                    <div style={{
                      width: 30, height: 30, borderRadius: 99, background: CC.offWhite, color: CC.ink,
                      display: 'inline-flex', alignItems: 'center', justifyContent: 'center',
                      fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 12, flexShrink: 0,
                    }}>{initials.toUpperCase()}</div>
                    <div style={{ fontFamily: F_TLB.ui, fontSize: 12.5, fontWeight: 600, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                      {b.guest_name || b.guest_email || 'Guest'}
                    </div>
                  </div>
                  <Pill tone={late ? 'warn' : 'neutral'}>{(b.status || 'enquiry').toUpperCase()}</Pill>
                </div>
                <div style={{ fontFamily: F_TLB.display, fontSize: 14.5, marginTop: 8, letterSpacing: '-0.005em' }}>{b.property_name || '—'}</div>
                <div style={{ fontFamily: F_TLB.ui, fontSize: 11.5, color: CC.mid, marginTop: 3 }}>
                  {fmtDate(b.check_in)} → {fmtDate(b.check_out)} · {b.nights || '—'} nights · {fmtMoney(b.total_price)}
                </div>
                {late && (
                  <div style={{
                    marginTop: 10, padding: '7px 11px', borderRadius: 10,
                    background: 'rgba(232,168,69,0.12)', color: '#8a6324',
                    fontFamily: F_TLB.ui, fontSize: 10.5, fontWeight: 600, letterSpacing: '0.04em',
                  }}>
                    Auto-confirm in {Math.max(0, Math.round(sla / 60))} hours
                  </div>
                )}
              </div>
            );
          })}
        </div>

        {/* Detail pane */}
        <div>
          {!sel ? (
            <div style={{ padding: '80px 36px', textAlign: 'center' }}>
              <HostEmpty headline="Select a thread." hint="Detail will surface on the right." />
            </div>
          ) : (
            <div style={{ padding: '32px 36px' }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 24, gap: 16, flexWrap: 'wrap' }}>
                <div>
                  <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>
                    BK-{String(sel.id || '').slice(-4).toUpperCase()}
                  </Sans>
                  <h2 style={{
                    margin: '10px 0 0', fontFamily: F_TLB.ui, fontSize: 32, fontWeight: 700,
                    letterSpacing: '-0.02em', lineHeight: 1.05,
                  }}>
                    {sel.guest_name || 'Guest'} aboard <span style={{ fontStyle: 'italic', fontFamily: F_TLB.display, fontWeight: 400 }}>{sel.property_name || 'Villa'}</span>
                  </h2>
                  <div style={{ fontFamily: F_TLB.ui, fontSize: 13, color: CC.mid, marginTop: 8 }}>
                    {fmtDate(sel.check_in)} → {fmtDate(sel.check_out)} · {sel.nights || '—'} nights · {sel.received_via || 'website'}
                  </div>
                </div>
                <Pill tone={slaMinutes(sel) < 60 ? 'warn' : 'success'}>{(sel.status || 'enquiry').toUpperCase()}</Pill>
              </div>

              {/* 3-stat row */}
              <div style={{
                display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0,
                border: `1px solid ${CC.border}`, borderRadius: 14, overflow: 'hidden', marginBottom: 24,
              }}>
                {[
                  ['Total', fmtMoney(sel.total_price)],
                  ['Per night', sel.nights > 0 ? fmtMoney((parseFloat(sel.total_price) || 0) / sel.nights) : '—'],
                  ['Channel', sel.received_via || 'website'],
                ].map(([l, v], i) => (
                  <div key={l} style={{ padding: '18px 20px', borderRight: i < 2 ? `1px solid ${CC.border}` : 'none' }}>
                    <Sans size={9.5} weight={600} ls={0.20} upper color={CC.mid}>{l}</Sans>
                    <div style={{ fontFamily: F_TLB.display, fontSize: 20, marginTop: 8, letterSpacing: '-0.015em' }}>{v}</div>
                  </div>
                ))}
              </div>

              {/* Guest message */}
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>Message from the guest</Sans>
              <p style={{
                margin: '12px 0 0', fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 17,
                color: CC.deep, lineHeight: 1.65, maxWidth: 640,
              }}>
                {sel.guest_message || sel.notes || `Booking ${sel.nights || 'a'} ${sel.nights === 1 ? 'night' : 'nights'} at ${sel.property_name || 'the villa'}. ${sel.guests || 2} guests. Looking forward to it.`}
              </p>

              {/* Actions */}
              <div style={{ marginTop: 32, paddingTop: 24, borderTop: `1px solid ${CC.border}`, display: 'flex', gap: 10, flexWrap: 'wrap' }}>
                <button
                  onClick={() => actOnBooking(sel.id, 'confirm', '')}
                  disabled={acting === sel.id}
                  style={{
                    fontFamily: F_TLB.ui, fontSize: 14, fontWeight: 600,
                    padding: '14px 36px', borderRadius: 100, cursor: acting === sel.id ? 'wait' : 'pointer',
                    background: CC.ink, color: CC.white, border: '1px solid ' + CC.ink,
                    opacity: acting === sel.id ? 0.6 : 1,
                  }}
                >{acting === sel.id ? 'Confirming…' : 'Confirm'}</button>
                <HostBtn kind="ghost" onClick={() => {
                  window.open('/host-classic?tab=bookings&id=' + encodeURIComponent(sel.id || '') + '&action=quote', '_blank');
                }}>Quote a rate</HostBtn>
                <HostBtn kind="subtle" onClick={() => setShowDeclineModal(true)}>Decline</HostBtn>
              </div>
            </div>
          )}
        </div>
      </div>

      {/* Escalation banner */}
      {escalating > 0 && (
        <div style={{ marginTop: 14, padding: '14px 22px', background: CC.ink, color: CC.white, borderRadius: 12, display: 'flex', alignItems: 'center', gap: 12 }}>
          <span style={{ width: 6, height: 6, borderRadius: 99, background: '#e57373' }} />
          <Sans size={11} weight={600} ls={0.12} upper color={CC.white}>
            {escalating} BOOKING{escalating > 1 ? 'S' : ''} WILL ESCALATE TO ADMIN IN &lt; 5 MINUTES
          </Sans>
        </div>
      )}

      {/* Decline reasons modal */}
      {showDeclineModal && sel && (
        <HostDeclineModal
          booking={sel}
          onClose={() => setShowDeclineModal(false)}
          onDecline={async (reason) => {
            await actOnBooking(sel.id, 'decline', reason);
            setShowDeclineModal(false);
          }}
        />
      )}
    </div>
  );
}

// ─── Decline reasons modal ─────────────────────────────────────────
function HostDeclineModal({ booking, onClose, onDecline }) {
  const [reason, setReason] = React.useState('');
  const [submitting, setSubmitting] = React.useState(false);
  const presets = [
    "Dates aren't available — already booked through another channel",
    'Property under maintenance for this window',
    'Group size exceeds villa capacity',
    'Insufficient notice — needs more lead time for prep',
    'Rate quoted no longer valid for selected dates',
    'Custom (write below)',
  ];

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(20,20,20,0.45)', zIndex: 1000, display: 'grid', placeItems: 'center', padding: 24 }}>
      <div onClick={(e) => e.stopPropagation()} style={{ background: CC.white, borderRadius: 18, padding: 32, maxWidth: 540, width: '100%', maxHeight: '90vh', overflow: 'auto', boxShadow: '0 12px 40px rgba(20,20,20,0.18)' }}>
        <Display size={26} italic weight={400}>Decline reason.</Display>
        <Sans size={12} color={CC.mid} style={{ marginTop: 6, marginBottom: 22 }}>
          The guest sees this message verbatim. Be warm but firm.
        </Sans>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
          {presets.map((p, i) => (
            <button key={i} onClick={() => setReason(p === presets[presets.length - 1] ? '' : p)} style={{
              textAlign: 'left', padding: '12px 14px', borderRadius: 10,
              border: '1px solid ' + (reason === p ? CC.ink : CC.border),
              background: reason === p ? CC.offWhite : CC.white,
              color: CC.deep, fontFamily: F_TLB.ui, fontSize: 13, cursor: 'pointer',
            }}>{p}</button>
          ))}
        </div>
        <textarea
          rows={3}
          value={reason}
          onChange={(e) => setReason(e.target.value)}
          placeholder="Write a custom reason (or use a preset above)…"
          style={{
            width: '100%', marginTop: 14, padding: '12px 14px', borderRadius: 10,
            border: '1px solid ' + CC.border, fontFamily: F_TLB.ui, fontSize: 13,
            color: CC.ink, background: CC.white, resize: 'vertical', outline: 'none',
          }}
        />
        <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
          <HostBtn kind="ghost" onClick={onClose} disabled={submitting}>Cancel</HostBtn>
          <button
            onClick={async () => {
              if (!reason.trim()) { alert('Please write or pick a reason.'); return; }
              setSubmitting(true);
              await onDecline(reason);
              setSubmitting(false);
            }}
            disabled={submitting || !reason.trim()}
            style={{
              padding: '10px 22px', borderRadius: 100, cursor: submitting ? 'wait' : 'pointer',
              background: '#9a1c1c', color: CC.white, border: '1px solid #9a1c1c',
              fontFamily: F_TLB.ui, fontSize: 12, fontWeight: 600,
              opacity: submitting || !reason.trim() ? 0.5 : 1,
            }}
          >{submitting ? 'Declining…' : 'Send decline'}</button>
        </div>
      </div>
    </div>
  );
}

// v141.126 — HostCalendar, v141.123 mockup port.
//   • 28-day rolling Gantt (was calendar-month grid)
//   • Day header with weekday letter + day number + today highlight
//   • Per-row gantt bars: booking (ink solid), block (chip + hatched),
//     iCal sync (dashed outline)
//   • Header CTAs: Month view (toggle 28d ↔ 60d) + Add block (modal)
//   • Legend at bottom
//   • All 4 states: loading / error / empty / populated
function HostCalendar() {
  const propsApi = useApi('/api/properties?status=all', { tokenType: 'host' });
  const bookApi  = useApi('/api/bookings?status=all',   { tokenType: 'host' });
  const villas   = ((propsApi.data && propsApi.data.properties) || []).slice(0, 8);
  const bookings = (bookApi.data && bookApi.data.bookings) || [];

  const [viewDays, setViewDays] = React.useState(28); // 28 or 60
  const [showAddBlock, setShowAddBlock] = React.useState(false);
  const today = new Date();
  today.setHours(0, 0, 0, 0);
  const dayLabels = React.useMemo(() => {
    return Array.from({ length: viewDays }, (_, i) => {
      const d = new Date(today); d.setDate(d.getDate() + i);
      return {
        d: d.getDate(),
        w: ['S','M','T','W','T','F','S'][d.getDay()],
        weekend: d.getDay() === 0 || d.getDay() === 6,
        iso: d.toISOString().slice(0, 10),
        isToday: i === 0,
      };
    });
  }, [viewDays]);

  // Per-villa blocked_dates fetched lazily
  const [blocksMap, setBlocksMap] = useState({});
  useEffect(() => {
    if (!villas.length) return;
    let cancelled = false;
    (async () => {
      const next = {};
      for (const v of villas) {
        try {
          const r = await fetch('/api/blocked-dates?property_id=' + encodeURIComponent(v.id));
          if (r.ok) {
            const j = await r.json();
            next[v.id] = (j.blocks || []).map(b => ({
              start: b.date_start, end: b.date_end,
              source: b.source || 'manual',
              confidence: b.confidence || 'authoritative',
              label: b.source === 'airbnb' || b.source === 'booking.com' ? 'iCal · ' + b.source : 'Block',
            }));
          }
        } catch {}
      }
      if (!cancelled) setBlocksMap(next);
    })();
    return () => { cancelled = true; };
  }, [villas.map(v => v.id).join(',')]);

  // Map a date range onto [startDay, span] within the visible window.
  // startDay is 0-indexed (column offset), span is number of days.
  function bandRange(startStr, endStr) {
    if (!startStr || !endStr) return null;
    const winStart = today.getTime();
    const winEnd   = winStart + viewDays * 86400000;
    const s = new Date(startStr).getTime();
    const e = new Date(endStr).getTime();
    if (e <= winStart || s >= winEnd) return null;
    const startDay = Math.max(0, Math.round((s - winStart) / 86400000));
    const endDay   = Math.min(viewDays, Math.round((e - winStart) / 86400000));
    const span = endDay - startDay;
    return span > 0 ? { s: startDay, span } : null;
  }

  const loading = propsApi.loading || bookApi.loading;
  if (loading) return <Loading msg="Loading calendar…" />;
  if (propsApi.error || bookApi.error) return <ErrorState msg={propsApi.error || bookApi.error} onRetry={() => { propsApi.refetch(); bookApi.refetch(); }} />;

  const empty = villas.length === 0;
  const labelColumnWidth = 160;
  const monthBoundary = (() => {
    const m = today.getMonth();
    return dayLabels.findIndex(dl => new Date(dl.iso).getMonth() !== m);
  })();

  return (
    <div>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 18, flexWrap: 'wrap' }}>
        <div>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 8 }}>
            Next {viewDays} days · {villas.length} {villas.length === 1 ? 'villa' : 'villas'}
          </Sans>
          <Display size={36} weight={500} ls={-0.02}>Calendar</Display>
          <Sans size={13} color={CC.mid} style={{ marginTop: 6, maxWidth: 640 }}>
            Each row is one villa. Click a band to open the booking. Hatched bars are iCal-synced blackouts from Airbnb / Booking.com.
          </Sans>
        </div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
          <HostBtn kind="ghost" onClick={() => setViewDays(viewDays === 28 ? 60 : 28)}>
            {viewDays === 28 ? 'Month view' : '28-day view'}
          </HostBtn>
          <HostBtn kind="primary" onClick={() => setShowAddBlock(true)} disabled={empty}>
            Add block
          </HostBtn>
        </div>
      </div>

      <Panel pad={20}>
        {empty ? (
          <HostEmpty headline="No properties yet." hint="Add a villa to see the calendar grid." />
        ) : (
          <div style={{ overflowX: 'auto' }}>
            <div style={{ minWidth: viewDays * 22 + labelColumnWidth }}>
              {/* Day header */}
              <div style={{
                display: 'grid',
                gridTemplateColumns: `${labelColumnWidth}px repeat(${viewDays}, minmax(0, 1fr))`,
                paddingBottom: 10, borderBottom: `1px solid ${CC.border}`, marginBottom: 6,
              }}>
                <div />
                {dayLabels.map((dl, i) => {
                  const isMonthBreak = i > 0 && monthBoundary === i;
                  return (
                    <div key={i} style={{
                      textAlign: 'center', padding: '0 1px',
                      borderLeft: isMonthBreak ? `1px solid ${CC.border}` : 'none',
                    }}>
                      <div style={{
                        fontFamily: F_TLB.ui, fontSize: 9, fontWeight: 600,
                        color: dl.weekend ? CC.mid : CC.deep, letterSpacing: '0.15em',
                      }}>{dl.w}</div>
                      <div style={{
                        fontFamily: F_TLB.display,
                        fontSize: dl.isToday ? 14 : 12, marginTop: 2,
                        color: dl.isToday ? CC.ink : CC.mid,
                        fontWeight: dl.isToday ? 600 : 400,
                      }}>{dl.d}</div>
                    </div>
                  );
                })}
              </div>

              {/* Villa rows */}
              {villas.map((p, ri) => {
                const villaBookings = bookings.filter(b => b.property_id === p.id || b.property_name === p.name);
                const villaBlocks = blocksMap[p.id] || [];
                const bands = [];
                villaBookings.forEach(b => {
                  const r = bandRange(b.check_in, b.check_out);
                  if (r) bands.push({
                    ...r,
                    label: (b.guest_name || 'BK-' + String(b.id || '').slice(-4)).slice(0, 14),
                    tone: 'ink', id: b.id,
                  });
                });
                villaBlocks.forEach(b => {
                  const r = bandRange(b.start, b.end);
                  if (r) bands.push({
                    ...r,
                    label: b.label,
                    tone: b.source === 'airbnb' || b.source === 'booking.com' ? 'dot' : 'mute',
                  });
                });

                return (
                  <div key={p.id} style={{
                    display: 'grid',
                    gridTemplateColumns: `${labelColumnWidth}px repeat(${viewDays}, minmax(0, 1fr))`,
                    alignItems: 'center', height: 40,
                    borderBottom: ri < villas.length - 1 ? `1px dashed ${CC.border}` : '0',
                    position: 'relative',
                  }}>
                    <div style={{ paddingRight: 12 }}>
                      <div style={{ fontFamily: F_TLB.ui, fontSize: 12, fontWeight: 500 }}>{p.name}</div>
                      <div style={{ fontFamily: F_TLB.ui, fontSize: 10, color: CC.mid, marginTop: 1 }}>
                        {(p.location || 'Bali').split(',')[0].trim()}
                      </div>
                    </div>
                    {dayLabels.map((dl, i) => (
                      <div key={i} style={{
                        height: '100%',
                        borderRight: `1px solid ${CC.border}`,
                        background: dl.weekend ? CC.offWhite : 'transparent',
                        opacity: 0.5,
                      }} />
                    ))}
                    {bands.map((b, bi) => {
                      const left = `calc(${labelColumnWidth}px + ${b.s} * (100% - ${labelColumnWidth}px) / ${viewDays} + 2px)`;
                      const w = `calc(${b.span} * (100% - ${labelColumnWidth}px) / ${viewDays} - 4px)`;
                      const isInk = b.tone === 'ink';
                      const isMute = b.tone === 'mute';
                      const isDot = b.tone === 'dot';
                      return (
                        <div key={bi}
                          onClick={() => {
                            if (b.id) {
                              try { localStorage.setItem('tlb_host_inbox_target', b.id); } catch {}
                              window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'inbox' }));
                            }
                          }}
                          title={b.label + (b.id ? ' · click to open booking' : '')}
                          style={{
                            position: 'absolute', left, width: w, top: 6, bottom: 6,
                            background: isInk ? CC.ink : isMute ? CC.offWhite : 'transparent',
                            color: isInk ? CC.white : CC.deep,
                            border: isDot ? `1px dashed ${CC.mid}` : isMute ? `1px solid ${CC.border}` : '0',
                            borderRadius: 7, padding: '0 12px',
                            display: 'flex', alignItems: 'center',
                            fontFamily: F_TLB.ui, fontSize: 10.5, fontWeight: 600,
                            letterSpacing: '0.02em', whiteSpace: 'nowrap', overflow: 'hidden',
                            backgroundImage: isMute ? 'repeating-linear-gradient(135deg, transparent 0 5px, rgba(20,20,20,0.04) 5px 6px)' : 'none',
                            cursor: b.id ? 'pointer' : 'default',
                          }}
                        >{b.label}</div>
                      );
                    })}
                  </div>
                );
              })}
            </div>
          </div>
        )}

        {/* Legend */}
        {!empty && (
          <div style={{ display: 'flex', gap: 18, marginTop: 18, fontFamily: F_TLB.ui, fontSize: 11.5, color: CC.mid, letterSpacing: '0.01em', flexWrap: 'wrap' }}>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 14, height: 8, background: CC.ink, borderRadius: 3 }} /> Booked
            </span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 14, height: 8, background: CC.offWhite, border: `1px solid ${CC.border}`, borderRadius: 3 }} /> Manual block
            </span>
            <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
              <span style={{ width: 14, height: 8, border: `1px dashed ${CC.mid}`, borderRadius: 3 }} /> iCal sync
            </span>
          </div>
        )}
      </Panel>

      {/* Add block modal */}
      {showAddBlock && (
        <HostAddBlockModal villas={villas} onClose={() => setShowAddBlock(false)} onSuccess={() => {
          setShowAddBlock(false);
          // Re-fetch all blocks
          (async () => {
            const next = {};
            for (const v of villas) {
              try {
                const r = await fetch('/api/blocked-dates?property_id=' + encodeURIComponent(v.id));
                if (r.ok) {
                  const j = await r.json();
                  next[v.id] = (j.blocks || []).map(b => ({ start: b.date_start, end: b.date_end, source: b.source || 'manual', label: 'Block' }));
                }
              } catch {}
            }
            setBlocksMap(next);
          })();
        }} />
      )}
    </div>
  );
}

// Add block modal — POSTs to /api/blocked-dates with property + range
function HostAddBlockModal({ villas, onClose, onSuccess }) {
  const [form, setForm] = React.useState({
    property_id: villas[0] ? villas[0].id : '',
    date_start: new Date().toISOString().slice(0, 10),
    date_end: new Date(Date.now() + 86400000).toISOString().slice(0, 10),
    reason: '',
  });
  const [submitting, setSubmitting] = React.useState(false);
  const [error, setError] = React.useState(null);

  async function submit() {
    setSubmitting(true);
    setError(null);
    try {
      const token = localStorage.getItem('tlb_host_token') || localStorage.getItem('tlb_token') || '';
      const r = await fetch('/api/blocked-dates', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', ...(token ? { 'Authorization': 'Bearer ' + token } : {}) },
        body: JSON.stringify({
          property_id: form.property_id,
          date_start: form.date_start,
          date_end: form.date_end,
          source: 'manual',
          notes: form.reason || 'Manual block from host dashboard',
        }),
      });
      const j = await r.json().catch(() => ({}));
      if (!r.ok) throw new Error(j.error || j.message || `Server returned ${r.status}`);
      onSuccess();
    } catch (e) {
      setError(e.message || 'Unknown error');
    } finally {
      setSubmitting(false);
    }
  }

  const inputStyle = {
    width: '100%', padding: '10px 14px', border: '1px solid ' + CC.border, borderRadius: 8,
    fontFamily: F_TLB.ui, fontSize: 14, background: CC.white, color: CC.ink, outline: 'none',
  };

  return (
    <div onClick={onClose} style={{ position: 'fixed', inset: 0, background: 'rgba(20,20,20,0.45)', zIndex: 1000, display: 'grid', placeItems: 'center', padding: 24 }}>
      <div onClick={(e) => e.stopPropagation()} style={{ background: CC.white, borderRadius: 18, padding: 32, maxWidth: 480, width: '100%', maxHeight: '90vh', overflow: 'auto', boxShadow: '0 12px 40px rgba(20,20,20,0.18)' }}>
        <Display size={26} italic weight={400}>Add block.</Display>
        <Sans size={12} color={CC.mid} style={{ marginTop: 6, marginBottom: 22 }}>
          Block dates on a villa so it doesn't appear available for booking — maintenance, personal use, owner stays.
        </Sans>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
          <div>
            <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 6 }}>Villa</Sans>
            <select value={form.property_id} onChange={(e) => setForm({ ...form, property_id: e.target.value })} style={inputStyle}>
              {villas.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
            </select>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
            <div>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 6 }}>From</Sans>
              <input type="date" style={inputStyle} value={form.date_start} onChange={(e) => setForm({ ...form, date_start: e.target.value })} />
            </div>
            <div>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 6 }}>To</Sans>
              <input type="date" style={inputStyle} value={form.date_end} onChange={(e) => setForm({ ...form, date_end: e.target.value })} />
            </div>
          </div>
          <div>
            <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 6 }}>Reason (optional)</Sans>
            <input type="text" style={inputStyle} value={form.reason} onChange={(e) => setForm({ ...form, reason: e.target.value })} placeholder="e.g. maintenance, owner stay…" />
          </div>
        </div>
        {error && <Sans size={12} color="#9a1c1c" style={{ marginTop: 14 }}>{error}</Sans>}
        <div style={{ display: 'flex', gap: 10, marginTop: 22, justifyContent: 'flex-end' }}>
          <HostBtn kind="ghost" onClick={onClose} disabled={submitting}>Cancel</HostBtn>
          <HostBtn kind="primary" onClick={submit} disabled={submitting || !form.property_id || !form.date_start || !form.date_end}>
            {submitting ? 'Saving…' : 'Add block'}
          </HostBtn>
        </div>
      </div>
    </div>
  );
}

function HostMessages() {
  // v124 — Full inbox with threaded chat. Two-pane layout: thread list
  // (left, 320px) + active chat (right, fills remaining). Polls for
  // new messages every 4s when a thread is open + tab visible.
  return <ChatInbox role="host" />;
}

// ChatInbox — used by both HostMessages and AdminInbox
function ChatInbox({ role }) {
  const tokenType = role === 'admin' ? 'admin' : 'host';
  const threadsApi = useApi('/api/chat?action=threads&status=open', { tokenType });
  const [activeThreadId, setActiveThreadId] = useState(null);
  const [messages, setMessages] = useState([]);
  const [input, setInput] = useState('');
  const [internal, setInternal] = useState(false);
  const [sending, setSending] = useState(false);
  const [lastSeenAt, setLastSeenAt] = useState(null);
  const [pollTick, setPollTick] = useState(0);
  const messagesEndRef = React.useRef(null);

  const threads = (threadsApi.data && threadsApi.data.threads) || [];
  const activeThread = threads.find(t => t.id === activeThreadId);

  // Token getter
  function getToken() {
    return localStorage.getItem(role === 'admin' ? 'tlb_admin_token' : 'tlb_host_token')
      || localStorage.getItem('tlb_token') || '';
  }

  // Initial load when thread switches
  useEffect(() => {
    if (!activeThreadId) { setMessages([]); return; }
    let cancelled = false;
    (async () => {
      const r = await fetch('/api/chat?thread_id=' + activeThreadId, {
        headers: { Authorization: 'Bearer ' + getToken() },
      });
      if (cancelled) return;
      if (r.ok) {
        const j = await r.json();
        setMessages(j.messages || []);
        if (j.messages && j.messages.length) {
          setLastSeenAt(j.messages[j.messages.length - 1].created_at);
        } else {
          setLastSeenAt(new Date().toISOString());
        }
      }
      // Mark thread as read on open
      fetch('/api/chat?action=mark_read', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() },
        body: JSON.stringify({ thread_id: activeThreadId }),
      }).then(() => threadsApi.refetch());
    })();
    return () => { cancelled = true; };
  }, [activeThreadId]);

  // Polling loop — every 4 seconds when a thread is open + tab visible
  useEffect(() => {
    if (!activeThreadId || !lastSeenAt) return;
    let interval = setInterval(() => {
      if (document.hidden) return;
      setPollTick(t => t + 1);
    }, 4000);
    return () => clearInterval(interval);
  }, [activeThreadId, lastSeenAt]);

  useEffect(() => {
    if (!activeThreadId || !lastSeenAt || pollTick === 0) return;
    let cancelled = false;
    (async () => {
      const r = await fetch('/api/chat?action=poll&thread_id=' + activeThreadId + '&since=' + encodeURIComponent(lastSeenAt), {
        headers: { Authorization: 'Bearer ' + getToken() },
      });
      if (cancelled || !r.ok) return;
      const j = await r.json();
      if (Array.isArray(j.messages) && j.messages.length) {
        setMessages(prev => {
          const seenIds = new Set(prev.map(m => m.id));
          const fresh = j.messages.filter(m => !seenIds.has(m.id));
          if (!fresh.length) return prev;
          return [...prev, ...fresh];
        });
        setLastSeenAt(j.messages[j.messages.length - 1].created_at);
      }
    })();
    return () => { cancelled = true; };
  }, [pollTick]);

  // Auto-scroll on new message
  useEffect(() => {
    if (messagesEndRef.current) messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
  }, [messages.length]);

  async function send() {
    const text = input.trim();
    if (!text || !activeThreadId) return;
    setSending(true);
    try {
      const r = await fetch('/api/chat', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + getToken() },
        body: JSON.stringify({ thread_id: activeThreadId, body: text, is_internal_note: internal }),
      });
      if (r.ok) {
        const j = await r.json();
        setMessages(prev => [...prev, j.message]);
        setLastSeenAt(j.message.created_at);
        setInput('');
        threadsApi.refetch();
      } else {
        const j = await r.json().catch(() => ({}));
        window.alert('Send failed: ' + (j.error || r.statusText));
      }
    } finally {
      setSending(false);
    }
  }

  // v141.138 — Loading timeout. Was: stayed "Loading inbox…" forever
  // if the fetch hung. Now after 12 seconds, switch to an actionable
  // "Taking longer than expected" message with a Retry button so the
  // user isn't stuck.
  const [loadingTimedOut, setLoadingTimedOut] = React.useState(false);
  React.useEffect(() => {
    if (!threadsApi.loading) { setLoadingTimedOut(false); return; }
    const t = setTimeout(() => setLoadingTimedOut(true), 12_000);
    return () => clearTimeout(t);
  }, [threadsApi.loading]);

  if (threadsApi.loading && !loadingTimedOut) return <Loading msg="Loading inbox…" />;
  if (threadsApi.loading && loadingTimedOut) {
    return (
      <ErrorState
        msg="Taking longer than expected. Sign in as admin or retry below."
        onRetry={() => { setLoadingTimedOut(false); threadsApi.refetch(); }}
      />
    );
  }
  if (threadsApi.error) return <ErrorState msg={threadsApi.error} onRetry={threadsApi.refetch} />;

  const totalUnread = threads.reduce((s, t) => s + (t['unread_for_' + role] || 0), 0);

  return (
    <div style={{ display: 'grid', gridTemplateColumns: '340px 1fr', gap: 0, height: '78vh', minHeight: 540, border: '1px solid ' + CC.border, borderRadius: 12, overflow: 'hidden', background: CC.white }}>
      {/* Thread list */}
      <div style={{ borderRight: '1px solid ' + CC.border, display: 'flex', flexDirection: 'column', background: CC.offWhite }}>
        <div style={{ padding: '16px 18px', borderBottom: '1px solid ' + CC.border }}>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            <Display size={18} italic weight={400}>Inbox.</Display>
            {totalUnread > 0 && <Pill tone="danger">{totalUnread} unread</Pill>}
          </div>
          <Sans size={11} color={CC.mid} style={{ marginTop: 4 }}>{threads.length} thread{threads.length !== 1 ? 's' : ''} · auto-refresh 4s</Sans>
        </div>
        <div style={{ overflowY: 'auto', flex: 1 }}>
          {threads.length === 0 ? (
            <div style={{ padding: 24, textAlign: 'center' }}>
              <Sans size={12} color={CC.mid}>No threads yet.</Sans>
            </div>
          ) : threads.map(t => {
            const isActive = t.id === activeThreadId;
            const unread = t['unread_for_' + role] || 0;
            return (
              <button key={t.id} onClick={() => setActiveThreadId(t.id)}
                style={{
                  display: 'block', width: '100%', textAlign: 'left',
                  padding: '14px 18px', border: 'none',
                  borderBottom: '1px solid ' + CC.border,
                  background: isActive ? CC.white : 'transparent',
                  borderLeft: '3px solid ' + (isActive ? CC.ink : (t.is_escalated ? '#9a1c1c' : 'transparent')),
                  cursor: 'pointer', fontFamily: 'inherit',
                }}>
                <div style={{ display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 4 }}>
                  <Sans size={13} weight={unread > 0 ? 600 : 500} color={CC.ink} style={{ flex: 1, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                    {t.guest_email || t.host_email || 'Thread'}
                  </Sans>
                  {unread > 0 && <span style={{ background: CC.ink, color: CC.white, borderRadius: 100, padding: '2px 7px', fontSize: 10, fontWeight: 700 }}>{unread}</span>}
                </div>
                <Sans size={11} color={CC.mid} style={{ marginBottom: 4, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{t.subject || (t.property_id ? 'Property thread' : 'Direct')}</Sans>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, fontSize: 10, color: CC.mid }}>
                  <span>{t.last_message_at ? new Date(t.last_message_at).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : '—'}</span>
                  {t.is_escalated && <Pill tone="danger">Escalated</Pill>}
                </div>
              </button>
            );
          })}
        </div>
      </div>

      {/* Active chat panel */}
      <div style={{ display: 'flex', flexDirection: 'column', background: CC.white }}>
        {!activeThread ? (
          <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
            <div style={{ textAlign: 'center', padding: 40 }}>
              <Display size={20} italic weight={400} style={{ marginBottom: 8 }}>Select a thread.</Display>
              <Sans size={12} color={CC.mid}>Choose a conversation from the left to read or reply.</Sans>
            </div>
          </div>
        ) : (
          <>
            {/* Chat header */}
            <div style={{ padding: '14px 22px', borderBottom: '1px solid ' + CC.border, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14 }}>
              <div>
                <Sans size={14} weight={600} color={CC.ink}>{activeThread.guest_email || activeThread.host_email || 'Thread'}</Sans>
                <Sans size={11} color={CC.mid} style={{ marginTop: 2 }}>{activeThread.subject || (activeThread.property_id ? 'Property thread' : 'Direct')}</Sans>
              </div>
              <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
                {activeThread.is_escalated && <Pill tone="danger">Escalated</Pill>}
                <Pill tone="success">Live · 4s polling</Pill>
              </div>
            </div>

            {/* Messages scrollable */}
            <div style={{ flex: 1, overflowY: 'auto', padding: '18px 22px', display: 'flex', flexDirection: 'column', gap: 12 }}>
              {messages.length === 0 ? (
                <Sans size={12} color={CC.mid} style={{ textAlign: 'center', padding: 24 }}>No messages in this thread yet. Start the conversation.</Sans>
              ) : messages.map((m, i) => {
                const isMe = (role === 'admin' && m.sender_type === 'admin') || (role === 'host' && m.sender_type === 'host');
                const isSystem = m.sender_type === 'system';
                const isInternal = m.is_internal_note;
                if (isSystem) {
                  return <div key={m.id || i} style={{ textAlign: 'center', padding: '8px 0' }}>
                    <Sans size={11} color={CC.mid} style={{ fontStyle: 'italic' }}>· {m.body} ·</Sans>
                  </div>;
                }
                return (
                  <div key={m.id || i} style={{ display: 'flex', justifyContent: isMe ? 'flex-end' : 'flex-start' }}>
                    <div style={{
                      maxWidth: '72%', padding: '10px 14px', borderRadius: 14,
                      background: isInternal ? '#fef3c7' : (isMe ? CC.ink : CC.offWhite),
                      color: isInternal ? '#92400e' : (isMe ? CC.white : CC.ink),
                      border: isInternal ? '1px dashed #f59e0b' : 'none',
                    }}>
                      {isInternal && <Sans size={9} weight={700} ls={0.16} upper color="#92400e" style={{ marginBottom: 4 }}>Internal note · only admin sees this</Sans>}
                      <Sans size={10} weight={600} ls={0.12} upper color={isMe ? 'rgba(255,255,255,0.7)' : CC.mid} style={{ marginBottom: 4 }}>
                        {m.sender_type} · {m.sender_email || ''}
                      </Sans>
                      <div style={{ fontSize: 13.5, lineHeight: 1.55, whiteSpace: 'pre-wrap', fontFamily: 'inherit' }}>{m.body}</div>
                      <Sans size={9} color={isMe ? 'rgba(255,255,255,0.5)' : CC.mid} style={{ marginTop: 4 }}>
                        {m.created_at ? new Date(m.created_at).toLocaleString('en-GB', { day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit' }) : '—'}
                      </Sans>
                    </div>
                  </div>
                );
              })}
              <div ref={messagesEndRef} />
            </div>

            {/* Composer */}
            <div style={{ padding: '14px 22px', borderTop: '1px solid ' + CC.border, background: CC.white }}>
              {role === 'admin' && (
                <label style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 11, color: CC.mid, marginBottom: 8, cursor: 'pointer' }}>
                  <input type="checkbox" checked={internal} onChange={e => setInternal(e.target.checked)} />
                  Internal note (admin-only — guest + host won't see)
                </label>
              )}
              <div style={{ display: 'flex', gap: 8 }}>
                <textarea value={input} onChange={e => setInput(e.target.value)} rows={2}
                  onKeyDown={e => { if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) { e.preventDefault(); send(); } }}
                  placeholder={internal ? 'Internal note (Ctrl+Enter to save)…' : 'Type a reply (Ctrl+Enter to send)…'}
                  style={{ flex: 1, padding: '10px 14px', border: '1px solid ' + CC.border, borderRadius: 8, fontSize: 13, fontFamily: 'inherit', resize: 'vertical', minHeight: 56 }} />
                <button onClick={send} disabled={sending || !input.trim()}
                  style={{ padding: '0 22px', background: CC.ink, color: CC.white, border: 'none', borderRadius: 8, fontSize: 12, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', cursor: 'pointer', opacity: (sending || !input.trim()) ? 0.5 : 1 }}>
                  {sending ? '…' : 'Send'}
                </button>
              </div>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// Expose ChatInbox so AdminInbox section in dashboards-native-sections.jsx can use it
window.TLB_CHAT_INBOX = ChatInbox;

// v141.126 — HostEarnings, v141.123 mockup port.
//   • 1.6fr/1fr split: YTD hero (giant Money + YoY pill + AreaChart) + FX preview
//   • Annual goal + on-pace stats below chart
//   • Per-villa YTD net with horizontal progress bar + Statement button
//   • 4 payout tiles preserved at top (Pending/Available/Paid Out/Total)
//   • Payout history table preserved at bottom
//   • Header CTAs: Statement (CSV) + Bank settings (nav to integrations)
//   • All 4 states preserved
function HostEarnings() {
  const { data, loading, error, refetch } = useApi('/api/bookings?status=all', { tokenType: 'host' });
  const propsApi = useApi('/api/properties?status=all', { tokenType: 'host' });
  const balanceQ = useApi('/api/payouts?action=balance', { tokenType: 'host' });
  const historyQ = useApi('/api/payouts', { tokenType: 'host' });
  const bookings = (data && data.bookings) || [];
  const villas   = (propsApi.data && propsApi.data.properties) || [];
  const balance  = (balanceQ.data && balanceQ.data.balance) || {};
  const payoutHistory = (historyQ.data && historyQ.data.payouts) || [];

  function nextMonday() {
    const d = new Date();
    const day = d.getDay();
    const offset = day === 1 ? 7 : (8 - day) % 7 || 7;
    const m = new Date(d.getTime() + offset * 86400000);
    return m.toLocaleDateString('en-GB', { day: '2-digit', month: 'short' });
  }

  // Group bookings by month for current year
  const now = new Date();
  const year = now.getFullYear();
  const byMonth = {};
  let totalYtd = 0;
  let payoutCount = 0;
  bookings.forEach(b => {
    const t = b.check_in || b.created_at;
    if (!t) return;
    const d = new Date(t);
    if (d.getFullYear() !== year) return;
    if (!/confirmed|paid|complete/i.test(b.status || '')) return;
    const mIdx = d.getMonth();
    const amount = parseFloat(b.total_price) || 0;
    byMonth[mIdx] = (byMonth[mIdx] || 0) + amount;
    totalYtd += amount;
    payoutCount++;
  });
  let lyTotal = 0;
  bookings.forEach(b => {
    const t = b.check_in || b.created_at;
    if (!t) return;
    const d = new Date(t);
    if (d.getFullYear() !== year - 1) return;
    if (d > new Date(year - 1, now.getMonth(), now.getDate())) return;
    if (!/confirmed|paid|complete/i.test(b.status || '')) return;
    lyTotal += parseFloat(b.total_price) || 0;
  });
  const lyDelta = lyTotal > 0 ? ((totalYtd - lyTotal) / lyTotal * 100) : null;

  // Per-villa YTD totals (sorted descending)
  const villaTotals = villas.map(v => {
    const total = bookings.filter(b => {
      if (b.property_id !== v.id && b.property_name !== v.name) return false;
      const t = b.check_in || b.created_at;
      if (!t || new Date(t).getFullYear() !== year) return false;
      return /confirmed|paid|complete/i.test(b.status || '');
    }).reduce((s, b) => s + (parseFloat(b.total_price) || 0), 0);
    return { id: v.id, name: v.name, area: (v.location || 'Bali').split(',')[0].trim(), beds: v.bedrooms || v.beds, earned: total };
  }).sort((a, b) => b.earned - a.earned);
  const maxVillaEarned = Math.max(1, ...villaTotals.map(v => v.earned));

  // Annual goal — use 1.5x last year if available, else 4.2B IDR fallback (matches mockup)
  const annualGoal = lyTotal > 0 ? Math.round(lyTotal * 1.5) : 4_200_000_000;
  const daysInYear = 365;
  const daysElapsed = Math.floor((now - new Date(year, 0, 1)) / 86400000);
  const expectedByNow = annualGoal * (daysElapsed / daysInYear);
  const pacePercent = expectedByNow > 0 ? Math.round(totalYtd / expectedByNow * 100) : 0;
  const onPace = pacePercent >= 100;

  const monthShort = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const chartData = monthShort.map((label, i) => ({ label, v: byMonth[i] || 0 }));

  const fmtIDRm = (n) => {
    const num = parseFloat(n) || 0;
    if (num >= 1_000_000_000) return 'Rp ' + (num / 1_000_000_000).toFixed(2) + 'B';
    if (num >= 1_000_000) return 'Rp ' + (num / 1_000_000).toFixed(1) + 'M';
    return 'Rp ' + Math.round(num).toLocaleString('id-ID');
  };

  function downloadStatement() {
    // Annual statement: per-booking CSV for this year
    const headers = ['Date', 'Booking', 'Guest', 'Villa', 'Status', 'Total IDR'];
    const rows = bookings
      .filter(b => {
        const t = b.check_in || b.created_at;
        return t && new Date(t).getFullYear() === year && /confirmed|paid|complete/i.test(b.status || '');
      })
      .map(b => [
        (b.check_in || b.created_at || '').slice(0, 10),
        (b.confirmation_code || b.id || '').slice(0, 16),
        b.guest_name || '',
        b.property_name || '',
        b.status || '',
        Math.round(parseFloat(b.total_price) || 0),
      ]);
    const csv = [headers, ...rows].map(r => r.map(c => /[",\n]/.test(String(c)) ? '"' + String(c).replace(/"/g, '""') + '"' : c).join(',')).join('\n');
    const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url; a.download = `tlb-earnings-${year}.csv`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  if (loading) return <Loading msg="Loading earnings…" />;
  if (error)   return <ErrorState msg={error} onRetry={refetch} />;

  return (
    <div>
      {/* Header */}
      <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 18, flexWrap: 'wrap' }}>
        <div>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid} style={{ marginBottom: 8 }}>
            YTD · {year} · your {villas.length} {villas.length === 1 ? 'villa' : 'villas'}
          </Sans>
          <Display size={36} weight={500} ls={-0.02}>Earnings</Display>
          <Sans size={13} color={CC.mid} style={{ marginTop: 6, maxWidth: 640 }}>
            Year-to-date revenue, per-payout breakdown, foreign-currency conversion preview, and your annual goal.
          </Sans>
        </div>
        <div style={{ display: 'flex', gap: 10, alignItems: 'center' }}>
          <HostBtn kind="ghost" onClick={downloadStatement}>Statement</HostBtn>
          <HostBtn kind="primary" onClick={() => window.dispatchEvent(new CustomEvent('tlb-host-tab', { detail: 'settings' }))}>
            Bank settings
          </HostBtn>
        </div>
      </div>

      {/* Payout state tiles — preserved (operator-critical for cashflow) */}
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0, border: `1px solid ${CC.border}`, borderRadius: 18, overflow: 'hidden', background: CC.white, marginBottom: dashGap() }}>
        {[
          { label: 'Pending', value: balance.pending || 0, badge: '7-day hold', tone: 'warn', sub: 'Bookings within dispute window' },
          { label: 'Available', value: balance.available || 0, badge: 'Pays ' + nextMonday(), tone: 'success', sub: 'Auto-wires next Monday 13:00 WITA' },
          { label: 'Paid out', value: balance.paid_out || 0, badge: null, tone: null, sub: 'Cumulative wire transfers' },
          { label: 'Total earned', value: balance.total_earned || 0, badge: null, tone: null, sub: 'Net of TLB commission' },
        ].map((t, i) => (
          <div key={t.label} style={{ padding: '24px 26px', borderRight: i < 3 ? `1px solid ${CC.border}` : 'none', display: 'flex', flexDirection: 'column', gap: 12, minHeight: 130 }}>
            <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
              <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>{t.label}</Sans>
              {t.badge && <Pill tone={t.tone}>{t.badge}</Pill>}
            </div>
            <div style={{ fontFamily: F_TLB.ui, fontSize: 28, fontWeight: 600, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums', color: CC.ink }}>
              {fmtIDRm(t.value)}
            </div>
            <Sans size={11} color={CC.mid} style={{ marginTop: 'auto' }}>{t.sub}</Sans>
          </div>
        ))}
      </div>

      {/* YTD hero + FX preview */}
      <div style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: dashGap(), marginBottom: dashGap() }}>
        <Panel pad={32} accent>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>YTD net · {year}</Sans>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 20, marginTop: 18, flexWrap: 'wrap' }}>
            <div style={{
              fontFamily: F_TLB.ui, fontFeatureSettings: '"tnum"', fontSize: 58, fontWeight: 500,
              letterSpacing: '-0.03em', lineHeight: 0.95,
              fontVariantNumeric: 'tabular-nums',
            }}>{fmtIDRm(totalYtd)}</div>
            {lyDelta !== null && (
              <Pill tone={lyDelta >= 0 ? 'success' : 'danger'}>
                {lyDelta >= 0 ? '+' : ''}{lyDelta.toFixed(1)}% YoY
              </Pill>
            )}
          </div>
          <div style={{ marginTop: 28 }}>
            <AreaChart
              height={220}
              keys={['v']}
              data={chartData}
              formatY={(v) => v >= 1_000_000 ? 'Rp ' + (v / 1_000_000).toFixed(0) + 'M' : 'Rp ' + Math.round(v / 1000) + 'k'}
              highlightIdx={now.getMonth()}
            />
          </div>
          <div style={{ marginTop: 18, paddingTop: 18, borderTop: `1px solid ${CC.border}`, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 12 }}>
            <div>
              <Sans size={10} weight={600} ls={0.18} upper color={CC.mid}>Annual goal</Sans>
              <div style={{ fontFamily: F_TLB.display, fontSize: 18, marginTop: 6 }}>{fmtIDRm(annualGoal)}</div>
            </div>
            <div style={{ textAlign: 'right' }}>
              <Sans size={10} weight={600} ls={0.18} upper color={CC.mid}>On pace</Sans>
              <div style={{ fontFamily: F_TLB.display, fontSize: 18, marginTop: 6, color: onPace ? '#2d6a4f' : '#c08a3e' }}>
                {pacePercent}% by {now.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' })}
              </div>
            </div>
          </div>
        </Panel>

        <Panel pad={28}>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>FX preview</Sans>
          <Display size={22} italic weight={400} ls={-0.015} style={{ marginTop: 10 }}>
            In your <span style={{ fontFamily: F_TLB.display, fontStyle: 'italic' }}>currency</span>
          </Display>
          <HostFxBlock totalYtd={totalYtd} />
        </Panel>
      </div>

      {/* Per-villa YTD net */}
      <Panel pad={0} style={{ marginBottom: dashGap() }}>
        <div style={{ padding: '18px 28px', borderBottom: `1px solid ${CC.border}` }}>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>Per villa · YTD net</Sans>
        </div>
        {villaTotals.length === 0 ? (
          <div style={{ padding: 36 }}>
            <HostEmpty headline="No villa earnings yet." hint="Per-villa totals appear here as bookings clear." />
          </div>
        ) : villaTotals.map((v, i) => (
          <div key={v.id} style={{
            display: 'grid', gridTemplateColumns: '1.4fr 1fr 1fr auto', alignItems: 'center', gap: 18,
            padding: '18px 28px', borderTop: i === 0 ? '0' : `1px solid ${CC.border}`,
            background: i % 2 === 1 ? '#fafaf7' : CC.white,
          }}>
            <div>
              <div style={{ fontFamily: F_TLB.display, fontSize: 17, letterSpacing: '-0.01em' }}>{v.name}</div>
              <div style={{ fontFamily: F_TLB.ui, fontSize: 11, color: CC.mid, marginTop: 4 }}>{v.area} · {v.beds} {v.beds === 1 ? 'bedroom' : 'bedrooms'}</div>
            </div>
            <div style={{ height: 8, borderRadius: 99, background: CC.offWhite, overflow: 'hidden' }}>
              <div style={{ width: `${(v.earned / maxVillaEarned) * 100}%`, height: '100%', background: CC.ink, borderRadius: 99 }} />
            </div>
            <div style={{ fontFamily: F_TLB.display, fontSize: 19, letterSpacing: '-0.015em', textAlign: 'right' }}>
              {fmtIDRm(v.earned)}
            </div>
            <HostBtn kind="subtle" onClick={() => {
              // Per-villa statement — re-uses the same CSV generator filtered by villa
              try { localStorage.setItem('tlb_host_statement_villa', v.id); } catch {}
              downloadStatement();
            }}>Statement</HostBtn>
          </div>
        ))}
      </Panel>

      {/* Payout history */}
      <Panel pad={28}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 14 }}>
          <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>Recent payouts</Sans>
          <Pill tone="neutral">Weekly · Mondays</Pill>
        </div>
        {payoutHistory.length === 0 ? (
          <Sans size={13} color={CC.mid}>
            No payouts yet — your first weekly transfer will appear here after a guest checks out and clears the 7-day dispute window.
          </Sans>
        ) : (
          <table style={{ width: '100%', borderCollapse: 'collapse', fontFamily: F_TLB.ui, fontSize: 12 }}>
            <thead>
              <tr style={{ borderBottom: '1px solid ' + CC.border }}>
                {['Date', 'Booking', 'Amount', 'Status', 'Reference'].map((h, i) => (
                  <th key={h} style={{ padding: '10px 12px', textAlign: i === 2 ? 'right' : 'left', fontSize: 10, fontWeight: 700, letterSpacing: '0.16em', textTransform: 'uppercase', color: CC.mid }}>{h}</th>
                ))}
              </tr>
            </thead>
            <tbody>
              {payoutHistory.slice(0, 12).map((p, i) => (
                <tr key={p.id || i} style={{ borderBottom: '1px solid ' + CC.border }}>
                  <td style={{ padding: '12px' }}>{p.processed_at ? new Date(p.processed_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : (p.created_at ? new Date(p.created_at).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) : '—')}</td>
                  <td style={{ padding: '12px', fontFamily: 'monospace', fontSize: 11 }}>{(p.booking_id || '').slice(0, 8)}</td>
                  <td style={{ padding: '12px', textAlign: 'right', fontVariantNumeric: 'tabular-nums', fontWeight: 600 }}>
                    {p.currency === 'USD' ? '$' + Number(p.amount).toLocaleString('en-US') : 'Rp ' + Number(p.amount).toLocaleString('id-ID')}
                  </td>
                  <td style={{ padding: '12px' }}>
                    <Pill tone={p.status === 'processed' ? 'success' : (p.status === 'failed' ? 'danger' : 'warn')}>{p.status || 'pending'}</Pill>
                  </td>
                  <td style={{ padding: '12px', fontFamily: 'monospace', fontSize: 11, color: CC.mid }}>{p.stripe_transfer_id || '—'}</td>
                </tr>
              ))}
            </tbody>
          </table>
        )}
        <Sans size={11} color={CC.mid} style={{ marginTop: 14, lineHeight: 1.55 }}>
          Payouts run every Monday at 13:00 WITA after the 7-day hold. Each transfer covers all guest checkouts from {balance.hold_days || 7}+ days ago.
          Indonesian taxes (PHR 10% etc.) are bundled into the displayed nightly_rate; the equivalent share is included in your payout — please remit to your kabupaten Bapenda by the 15th of the following month.
        </Sans>
      </Panel>
    </div>
  );
}

function HostFxBlock({ totalYtd }) {
  const [fx, setFx] = useState(null);
  useEffect(() => {
    fetch('/api/currency').then(r => r.json()).then(j => setFx(j)).catch(() => {});
  }, []);
  const idrPerUsd = (fx && (fx.IDR || fx.rates?.IDR)) || 17394; // fallback
  const usdEquivalent = idrPerUsd > 0 ? totalYtd / idrPerUsd : 0;
  return (
    <>
      <div style={{ marginTop: 10, fontFamily: F_TLB.ui, fontSize: 26, fontWeight: 600, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>1 USD = {Math.round(idrPerUsd).toLocaleString()} IDR</div>
      <Sans size={12} color={CC.mid} style={{ marginTop: 4 }}>{fx && fx.timestamp ? 'Live · refreshed ' + new Date(fx.timestamp).toLocaleString() : fx ? 'Live mid-market' : 'Mid-market · cached'}</Sans>
      <div style={{ marginTop: 24, padding: 18, border: `1px solid ${CC.border}`, borderRadius: 12, background: CC.offWhite }}>
        <Sans size={10} weight={600} ls={0.14} upper color={CC.mid}>YTD revenue equivalent</Sans>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', marginTop: 10 }}>
          <div><Money value={Math.round(usdEquivalent)} size={26} weight={600} /><Sans size={10} color={CC.mid} style={{ marginTop: 4 }}>USD</Sans></div>
          <div style={{ textAlign: 'right' }}><Money value={(totalYtd/1_000_000).toFixed(0) + 'M'} currency="IDR" size={26} weight={600} /><Sans size={10} color={CC.mid} style={{ marginTop: 4 }}>IDR</Sans></div>
        </div>
      </div>
      <div style={{ marginTop: 18 }}>
        <Sans size={11} color={CC.deep} lh={1.65}>If IDR weakens 5%, USD-priced bookings appreciate <Money value={Math.round(usdEquivalent * 0.05)} size={13} weight={600} /> on the next clearing.</Sans>
      </div>
    </>
  );
}

window.HostDashboard = HostDashboard;
