// Shared dashboard primitives — used across guest/host/admin sections
const D = window.TLB_DASH_DATA;
const CC = window.TLB_TOKENS.color;

const dashPad = () => {
  const d = (window.TLB_TWEAKS && window.TLB_TWEAKS.density) || 'comfortable';
  return d === 'compact' ? 16 : d === 'spacious' ? 32 : 24;
};
const dashGap = () => {
  const d = (window.TLB_TWEAKS && window.TLB_TWEAKS.density) || 'comfortable';
  return d === 'compact' ? 16 : d === 'spacious' ? 32 : 24;
};

// ── tlbConfirm — dependency-free in-app confirmation ──────────────────
// v141.189 — Destructive dashboard actions (Sign out, Delete villa, …)
// gated on native window.confirm(). Chrome SILENTLY returns false from
// confirm() once the user ticks "prevent this page from creating more
// dialogs", and in embedded/preview/iframe contexts — so those handlers
// returned on their first line and the buttons looked dead ("tidak
// merespon") while non-confirm buttons kept working. This builds its own
// DOM overlay (no native dialog, unaffected by that browser setting or
// iframes) and resolves a Promise<boolean>. Shared by every dashboard
// shell because this module loads before the section scripts.
window.tlbConfirm = function tlbConfirm(message, opts) {
  opts = opts || {};
  const confirmText = opts.confirmText || 'Confirm';
  const cancelText = opts.cancelText || 'Cancel';
  const danger = !!opts.danger;
  const accent = danger ? '#9a1c1c' : '#1f6b3a';
  return new Promise((resolve) => {
    let done = false;
    const finish = (val) => {
      if (done) return; done = true;
      document.removeEventListener('keydown', onKey, true);
      try { ov.remove(); } catch (_) {}
      resolve(val);
    };
    // Fallback if the DOM isn't usable for some reason.
    if (!document || !document.body) {
      let r = true; try { r = window.confirm(message); } catch (_) { r = true; }
      return resolve(r);
    }
    const ov = document.createElement('div');
    ov.setAttribute('role', 'dialog');
    ov.setAttribute('aria-modal', 'true');
    ov.style.cssText = 'position:fixed;inset:0;z-index:2147483600;display:flex;'
      + 'align-items:center;justify-content:center;background:rgba(20,20,20,0.45);'
      + 'backdrop-filter:blur(2px);padding:24px;font-family:ui-sans-serif,system-ui,sans-serif';
    const card = document.createElement('div');
    card.style.cssText = 'background:#fff;color:#1a1a1a;max-width:420px;width:100%;'
      + 'border-radius:14px;padding:24px;box-shadow:0 24px 60px rgba(0,0,0,0.28);'
      + 'border:1px solid rgba(0,0,0,0.08)';
    const msg = document.createElement('div');
    msg.textContent = String(message == null ? 'Are you sure?' : message);
    msg.style.cssText = 'font-size:14px;line-height:1.5;margin-bottom:20px;white-space:pre-wrap';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;gap:10px;justify-content:flex-end';
    const mkBtn = (label, primary) => {
      const b = document.createElement('button');
      b.type = 'button'; b.textContent = label;
      b.style.cssText = 'padding:9px 18px;border-radius:8px;font-size:12.5px;'
        + 'font-weight:600;cursor:pointer;letter-spacing:0.02em;'
        + (primary
          ? 'border:1px solid ' + accent + ';background:' + accent + ';color:#fff'
          : 'border:1px solid rgba(0,0,0,0.18);background:#fff;color:#444');
      return b;
    };
    const cancelB = mkBtn(cancelText, false);
    const okB = mkBtn(confirmText, true);
    cancelB.addEventListener('click', () => finish(false));
    okB.addEventListener('click', () => finish(true));
    ov.addEventListener('click', (e) => { if (e.target === ov) finish(false); });
    const onKey = (e) => {
      if (e.key === 'Escape') { e.preventDefault(); finish(false); }
      else if (e.key === 'Enter') { e.preventDefault(); finish(true); }
    };
    document.addEventListener('keydown', onKey, true);
    row.appendChild(cancelB); row.appendChild(okB);
    card.appendChild(msg); card.appendChild(row);
    ov.appendChild(card); document.body.appendChild(ov);
    try { okB.focus(); } catch (_) {}
  });
};

// ── Live-data status pill (reads from window.TLB_LIVE_STATUS) ────
function LivePill({ which }) {
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    const h = () => setTick(t => t + 1);
    window.addEventListener('tlb-live-loaded', h);
    // Re-render once after 1.5s in case the event fired before mount
    const to = setTimeout(h, 1500);
    return () => { window.removeEventListener('tlb-live-loaded', h); clearTimeout(to); };
  }, []);
  const s = (window.TLB_LIVE_STATUS && window.TLB_LIVE_STATUS[which]) || { live: false, sources: [], counts: {} };
  const counts = s.counts || {};
  const summary = Object.keys(counts).length
    ? Object.entries(counts).map(([k, v]) => v + ' ' + k).join(' · ')
    : (s.live ? 'real data' : 'loading…');
  return (
    <span style={{
      display: 'inline-flex', alignItems: 'center', gap: 8,
      padding: '6px 14px', borderRadius: 100,
      background: s.live ? '#e7f1ec' : CC.chip,
      border: '1px solid ' + (s.live ? '#9bc8b1' : CC.border),
      fontFamily: F_TLB.ui, fontSize: 11, fontWeight: 600,
      letterSpacing: '0.06em', whiteSpace: 'nowrap',
      color: s.live ? '#2f7a5a' : CC.mid,
    }}>
      <span style={{ width: 6, height: 6, borderRadius: 99, background: s.live ? '#2f7a5a' : CC.muted }} />
      {s.live ? 'LIVE · ' + summary : 'DEMO · mock data'}
    </span>
  );
}

// ── Editorial section header (used in every dashboard view) ─────
function DashHeader({ eyebrow, title, italic, sub, right, liveFor }) {
  return (
    <div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', borderBottom: `1px solid ${CC.border}`, paddingBottom: 20, marginBottom: dashPad() + 4, gap: 16, flexWrap: 'wrap' }}>
      <div style={{ minWidth: 0 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
          <Eyebrow>{eyebrow}</Eyebrow>
          {liveFor && <LivePill which={liveFor} />}
        </div>
        <Display size={40} weight={400} italic={italic} ls={-0.02} style={{ marginTop: 8 }}>{title}</Display>
        {sub && <Sans size={14} color={CC.mid} style={{ marginTop: 8, maxWidth: 560, fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 17 }}>{sub}</Sans>}
      </div>
      {right}
    </div>
  );
}

// ── DashTopbar — v141.167 re-skin Stage 5 ──────────────────────
// Global sticky workspace bar from the delivered redesign Topbar:
// brand lockup left, calm "Concierge online" dotglow + live Bali time
// + profile glyph right. Deliberately self-contained (no data/API
// deps) so mounting it in any role shell is zero-regression-risk.
function DashTopbar({ role = 'Workspace' }) {
  const [t, setT] = React.useState('');
  React.useEffect(() => {
    var fmt = function () {
      try { return new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Makassar' }); }
      catch (e) { return ''; }
    };
    setT(fmt());
    var id = setInterval(function () { setT(fmt()); }, 30000);
    return function () { clearInterval(id); };
  }, []);
  return (
    <div style={{
      position: 'sticky', top: 0, zIndex: 6,
      marginBottom: dashGap(),
      padding: '15px 22px',
      background: 'rgba(255,255,255,0.92)',
      backdropFilter: 'saturate(140%) blur(8px)',
      WebkitBackdropFilter: 'saturate(140%) blur(8px)',
      border: `1px solid ${CC.hairline}`,
      borderRadius: 14,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      gap: 20, flexWrap: 'wrap',
    }}>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 12, minWidth: 0 }}>
        <span style={{ fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 18, color: CC.ink, lineHeight: 1 }}>The Luxury Bali</span>
        <span style={{ fontFamily: F_TLB.ui, fontSize: 10, fontWeight: 700, letterSpacing: '0.22em', textTransform: 'uppercase', color: CC.mid }}>{role}</span>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 16, fontFamily: F_TLB.ui, fontSize: 12, color: CC.mid }}>
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
          <span className="dotglow" style={{ width: 7, height: 7, borderRadius: 99, background: CC.green || '#5fbf7a', display: 'inline-block' }} />
          Concierge online
        </span>
        <span style={{ opacity: 0.45 }}>·</span>
        <span style={{ fontVariantNumeric: 'tabular-nums' }}>Bali {t}</span>
        <span aria-hidden="true" style={{ width: 30, height: 30, borderRadius: 99, background: CC.ink, color: CC.white, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 14 }}>{String(role).charAt(0)}</span>
      </div>
    </div>
  );
}

// ── Card — strict white panel, 18px radius ─────────────────────
// v141.124 (Phase 1 redesign) — added subtle two-layer shadow and an
// optional `accent` prop that places a 1px gold hairline along the top
// edge. Both upgrades are additive: existing callers see a marginally
// softer panel; opting into `accent` lights up the editorial accent.
// All existing prop names + style overrides preserved.
function Panel({ children, style = {}, pad, accent = false }) {
  const p = pad ?? dashPad();
  return (
    <div style={{
      background: CC.white,
      // v141.163 re-skin Stage 2 — flat hairline card on the cream
      // canvas (delivered redesign Card: 1px hairline, radius 18, no
      // heavy shadow). Replaces the old border + 12/32px lift.
      border: `1px solid ${CC.hairline}`,
      borderRadius: 18,
      padding: p,
      boxShadow: '0 1px 2px rgba(20,20,20,0.03)',
      position: 'relative',
      ...style,
    }}>
      {accent && (
        <div style={{
          position: 'absolute',
          left: 18, right: 18, top: 0, height: 1,
          background: '#C9A96E',   // gold-soft accent hairline
        }} />
      )}
      {children}
    </div>
  );
}

// ── KPI tile ────────────────────────────────────────────────────
// v141.124 (Phase 1 redesign) — value bumped 36 → 48px to match the
// new master design ("Make the data the hero" per the v141.123 brief
// §7.2 #2). Added tabular-nums so 7-digit IDR amounts align across the
// KPI grid. All existing props unchanged.
// v141.137 — SectionErrorBoundary. Catches React errors thrown by any
// section component and renders an actionable fallback instead of a
// blank main area. Logged to console.error so devtools still surfaces
// the stack trace for debugging. Reset via section change (parent
// re-keys this boundary using the section key so navigating to another
// section gives the next render a fresh try).
class SectionErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false, error: null };
  }
  static getDerivedStateFromError(error) {
    return { hasError: true, error };
  }
  componentDidCatch(error, info) {
    console.error('[SectionErrorBoundary] render failed:', error, info);
  }
  render() {
    if (this.state.hasError) {
      const errMsg = (this.state.error && this.state.error.message) || 'Unknown error';
      return (
        <div style={{ padding: '40px 32px' }}>
          <div style={{
            background: CC.white, border: `1px solid ${CC.border}`,
            borderRadius: 18, padding: 36, maxWidth: 640,
          }}>
            <Sans size={10} weight={600} ls={0.18} upper color="#9a1c1c">SECTION ERROR</Sans>
            <Display size={28} italic weight={400} ls={-0.018} style={{ marginTop: 12 }}>
              This section hit a snag.
            </Display>
            <Sans size={13} color={CC.deep} lh={1.6} style={{ marginTop: 14 }}>
              The page tried to load and a JavaScript error stopped it before render.
              Other sections still work — switch sections in the sidebar to keep going.
            </Sans>
            <div style={{
              marginTop: 18, padding: '12px 14px',
              background: CC.offWhite, borderRadius: 8,
              fontFamily: 'ui-monospace, monospace', fontSize: 11, color: CC.deep,
            }}>{errMsg.slice(0, 240)}</div>
            <div style={{ marginTop: 18, display: 'flex', gap: 10 }}>
              <button
                onClick={() => { this.setState({ hasError: false, error: null }); }}
                style={{
                  padding: '10px 18px', borderRadius: 100, cursor: 'pointer',
                  background: CC.ink, color: CC.white, border: '1px solid ' + CC.ink,
                  fontFamily: 'inherit', fontSize: 12, fontWeight: 600,
                }}
              >Retry</button>
              <button
                onClick={() => window.location.reload()}
                style={{
                  padding: '10px 18px', borderRadius: 100, cursor: 'pointer',
                  background: CC.white, color: CC.deep, border: '1px solid ' + CC.border,
                  fontFamily: 'inherit', fontSize: 12, fontWeight: 600,
                }}
              >Reload page</button>
            </div>
          </div>
        </div>
      );
    }
    return this.props.children;
  }
}
window.SectionErrorBoundary = SectionErrorBoundary;

function KPI({ label, value, delta, alert }) {
  return (
    <Panel style={{
      position: 'relative',
      // v141.165 re-skin Stage 4 — delivered KpiTile composition: a
      // fixed-min-height tile, label pinned top, the big calm value
      // anchored to the bottom so every KPI in a grid lines up.
      minHeight: 'var(--kpi-h, 138px)',
      display: 'flex', flexDirection: 'column',
    }}>
      <Sans size={10} weight={600} ls={0.20} upper color={CC.mid}>{label}</Sans>
      <Display size={38} weight={400} ls={-0.025} style={{
        marginTop: 'auto',
        color: alert ? '#9a1c1c' : CC.ink,
        fontVariantNumeric: 'tabular-nums',
        fontFeatureSettings: '"tnum"',
        lineHeight: 1,
      }}>{value}</Display>
      {delta && <Sans size={12} color={alert ? '#9a1c1c' : CC.mid} style={{ marginTop: 8 }}>{delta}</Sans>}
    </Panel>
  );
}

// ── Channel badge ──────────────────────────────────────────────
function ChannelBadge({ channel, size = 11 }) {
  const map = {
    whatsapp: { icon: '◐', label: 'WhatsApp', bg: '#f5f5f5' },
    email:    { icon: '◊', label: 'Email',    bg: '#f5f5f5' },
    'in-app': { icon: '◇', label: 'In-app',   bg: '#f5f5f5' },
    web:      { icon: '◯', label: 'Web',      bg: '#f5f5f5' },
    push:     { icon: '◉', label: 'Push',     bg: '#f5f5f5' },
    system:   { icon: '·',  label: 'System',   bg: '#f5f5f5' },
    alert:    { icon: '!',  label: 'Alert',    bg: '#1a1a1a', dark: true },
  };
  const c = map[channel] || map.system;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 100, background: c.dark ? CC.ink : c.bg, color: c.dark ? CC.white : CC.deep, fontFamily: F_TLB.ui, fontSize: size, fontWeight: 600, letterSpacing: '0.06em', textTransform: 'uppercase' }}>
      <span style={{ fontSize: size + 2 }}>{c.icon}</span>{c.label}
    </span>
  );
}

// ── Aetheria 4×4 dot marker (used on cross-brand items) ───────
function AetheriaDot({ inline = false }) {
  return (
    <span title="Linked to Aetheria" style={{ display: 'inline-block', width: 4, height: 4, background: CC.ink, marginLeft: inline ? 6 : 0, verticalAlign: 'middle' }} />
  );
}

// ── SLA pill (countdown) ───────────────────────────────────────
function SLAPill({ mins, urgent }) {
  const danger = urgent || mins < 5;
  const warn = !danger && mins < 15;
  const bg = danger ? '#9a1c1c' : warn ? '#1a1a1a' : CC.chip;
  const fg = (danger || warn) ? CC.white : CC.deep;
  return (
    <span style={{ display: 'inline-flex', alignItems: 'center', gap: 6, padding: '4px 10px', borderRadius: 100, background: bg, color: fg, fontFamily: F_TLB.ui, fontSize: 11, fontWeight: 600, letterSpacing: '0.06em' }}>
      {danger ? '● ' : ''}{mins}m left
    </span>
  );
}

// ── Section sub-tab (pill row) ─────────────────────────────────
function SubTabs({ tabs, active, onChange }) {
  return (
    <div style={{ display: 'inline-flex', gap: 2, padding: 3, background: CC.white, border: `1px solid ${CC.border}`, borderRadius: 100 }}>
      {tabs.map(t => {
        const on = active === t.key;
        return (
          <button key={t.key} onClick={() => onChange(t.key)} style={{ padding: '8px 16px', borderRadius: 100, border: 'none', background: on ? CC.ink : 'transparent', color: on ? CC.white : CC.deep, fontFamily: F_TLB.ui, fontSize: 12, fontWeight: 600, letterSpacing: '0.08em', textTransform: 'uppercase', cursor: 'pointer' }}>{t.label}</button>
        );
      })}
    </div>
  );
}

// ── Image placeholder (subtle stripe + monospace caption) ──────
function ImgFrame({ url, ratio = '4/5', tag, num, dark }) {
  return (
    <div style={{ position: 'relative', aspectRatio: ratio, background: url ? `url("${url}") center/cover` : CC.chip, overflow: 'hidden' }}>
      {!url && (
        <div style={{ position: 'absolute', inset: 0, background: 'repeating-linear-gradient(45deg, #f5f5f5 0 8px, #ebebeb 8px 16px)' }} />
      )}
      {url && <div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.35))' }} />}
      {tag && <span style={{ position: 'absolute', top: 12, left: 12, padding: '5px 10px', background: 'rgba(255,255,255,0.94)', fontFamily: F_TLB.ui, fontSize: 9, fontWeight: 600, letterSpacing: '0.14em', textTransform: 'uppercase' }}>{tag}</span>}
      {num && <span style={{ position: 'absolute', top: 10, right: 14, fontFamily: F_TLB.display, fontStyle: 'italic', color: 'rgba(255,255,255,0.92)', fontSize: 32 }}>{num}</span>}
    </div>
  );
}

// ── Per-panel data-source badge ───────────────────────────────
// Drop into any panel to label whether its data is live or mock.
//   <DataBadge dash="host" needs={['villas','bookings']} />
// Renders:
//   ● LIVE · 152 villas · 47 bookings   (any source matched)
//   ○ DEMO · sample data                 (none of the needs loaded)
// `needs` accepts the keys used in TLB_LIVE_STATUS[*].counts:
//   villas, offers, bookings, enquiries, wishlist, disputes,
//   loyalty, vouchers, reviews, payouts, health, admin/pulse
function DataBadge({ dash, needs = [], compact = false }) {
  const [, setTick] = React.useState(0);
  React.useEffect(() => {
    const h = () => setTick(t => t + 1);
    window.addEventListener('tlb-live-loaded', h);
    const to = setTimeout(h, 1500);
    return () => { window.removeEventListener('tlb-live-loaded', h); clearTimeout(to); };
  }, []);
  const s = (window.TLB_LIVE_STATUS && window.TLB_LIVE_STATUS[dash]) || { sources: [], counts: {} };
  const matched = needs.filter(n => s.sources.includes(n) || (s.counts && s.counts[n] != null));
  const live = matched.length > 0;
  const detail = matched.length
    ? matched.map(n => (s.counts[n] != null ? s.counts[n] + ' ' + n : n)).join(' · ')
    : 'sample data';
  return (
    <span title={live ? 'Loaded from ' + matched.map(n => '/api/' + n).join(', ') : 'No live source — preview only'}
      style={{
        display: 'inline-flex', alignItems: 'center', gap: 6,
        padding: compact ? '2px 8px' : '4px 10px', borderRadius: 100,
        background: live ? '#e7f1ec' : CC.chip,
        border: '1px solid ' + (live ? '#9bc8b1' : CC.border),
        fontFamily: F_TLB.ui, fontSize: compact ? 9 : 10, fontWeight: 600,
        letterSpacing: '0.08em', textTransform: 'uppercase',
        color: live ? '#2f7a5a' : CC.mid, whiteSpace: 'nowrap',
      }}>
      <span style={{ width: 5, height: 5, borderRadius: 99, background: live ? '#2f7a5a' : CC.muted }} />
      {live ? 'Live · ' + detail : 'Demo'}
    </span>
  );
}

// ── Dash icons — minimal line glyphs (24×24, stroke 1.5) ──────────
// One source for every sidebar entry. Lightweight inline SVG so
// they inherit currentColor and animate cleanly with the sidebar.
const DashIcons = {
  // Generic / utility
  grid:        'M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM14 14h7v7h-7z',
  activity:    'M22 12h-4l-3 9L9 3l-3 9H2',
  alert:       'M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0zM12 9v4M12 17h.01',
  users:       'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8zM23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75',
  building:    'M3 21h18M5 21V7l8-4v18M19 21V11l-6-4M9 9v.01M9 12v.01M9 15v.01M9 18v.01',
  checkCircle: 'M22 11.08V12a10 10 0 1 1-5.93-9.14M22 4 12 14.01l-3-3',
  list:        'M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01',
  layout:      'M3 3h18v18H3zM3 9h18M9 21V9',
  server:      'M2 4h20v6H2zM2 14h20v6H2zM6 7h.01M6 17h.01',
  dollar:      'M12 1v22M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6',
  link:        'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.72M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.72-1.72',
  fileText:    'M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8',
  sparkles:    'M12 3v3M12 18v3M3 12h3M18 12h3M5.6 5.6l2.1 2.1M16.3 16.3l2.1 2.1M5.6 18.4l2.1-2.1M16.3 7.7l2.1-2.1',
  barChart:    'M12 20V10M18 20V4M6 20v-6',
  target:      'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12zM12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z',
  share:       'M18 8a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM6 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM18 22a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM8.59 13.51l6.83 3.98M15.41 6.51l-6.82 3.98',
  cpu:         'M4 4h16v16H4zM9 9h6v6H9zM9 1v3M15 1v3M9 20v3M15 20v3M20 9h3M20 14h3M1 9h3M1 14h3',
  globe:       'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM2 12h20M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z',
  palette:     'M12 22A10 10 0 0 1 12 2c5.523 0 10 4.477 10 10 0 1.657-1.343 3-3 3h-3a2 2 0 0 0-2 2c0 1.105-.895 2-2 2zM6.5 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM10.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM15.5 7a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3zM17.5 11a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',
  trending:    'M23 6l-9.5 9.5-5-5L1 18M17 6h6v6',
  message:     'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
  wand:        'M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8l1.4 1.4M15 9h.01M17.8 6.2l1.4-1.4M3 21l9-9M12.2 6.2l-1.4-1.4',
  // Host
  home:        'M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2zM9 22V12h6v10',
  inbox:       'M22 12h-6l-2 3h-4l-2-3H2M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z',
  calendar:    'M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zM16 2v4M8 2v4M3 10h18',
  messageSq:   'M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z',
  calCheck:    'M19 4H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2zM16 2v4M8 2v4M3 10h18M9 16l2 2 4-4',
  star:        'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z',
  tag:         'M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82zM7 7h.01',
  checkSquare: 'M9 11l3 3L22 4M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11',
  phone:       'M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z',
  search:      'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM21 21l-4.35-4.35',
  bookOpen:    'M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2zM22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z',
  settings:    'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z',
  // Guest
  map:         'M1 6v16l7-4 8 4 7-4V2l-7 4-8-4zM8 2v16M16 6v16',
  heart:       'M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z',
  award:       'M12 15a7 7 0 1 0 0-14 7 7 0 0 0 0 14zM8.21 13.89 7 23l5-3 5 3-1.21-9.12',
  gift:        'M20 12v10H4V12M2 7h20v5H2zM12 22V7M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7zM12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z',
  shield:      'M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z',
  compass:     'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20zM16.24 7.76l-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z',
};

// Map sidebar item icon names (string) to SVG path.
function DashIcon({ name, size = 18 }) {
  const path = DashIcons[name] || DashIcons.grid;
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" style={{ flexShrink: 0 }}>
      <path d={path} />
    </svg>
  );
}

// ── DashSidebar — modern collapsible navigation rail ──────────────
//
// Editorial-design sidebar with two states:
//   expanded   (260px) — full labels + group headers + brand block
//   collapsed  (72px)  — icons only, group headers shrink to 1px line,
//                         labels surface as hover tooltips
//
// State persists in localStorage('tlb-sidebar-collapsed') so it
// survives reloads. Smooth 220ms cubic-bezier transition between
// states. Active item: 3px left accent + subtle bg. Hover: very
// gentle bg tint. All chrome respects the editorial palette.
//
// Each item = { key, label, group?, icon?, mode? }
//   icon: name from DashIcons (e.g. 'home', 'users', 'star')
//   mode: 'native' (React tab) or 'iframe' (operational page embed)
function DashSidebar({ items, active, onChange, brand = 'TLB', sub = 'Master', dark = true, footer }) {
  const STORAGE_KEY = 'tlb-sidebar-collapsed';
  const [collapsed, setCollapsed] = React.useState(() => {
    try { return localStorage.getItem(STORAGE_KEY) === '1'; } catch { return false; }
  });
  React.useEffect(() => {
    try { localStorage.setItem(STORAGE_KEY, collapsed ? '1' : '0'); } catch {}
  }, [collapsed]);

  // Group items
  const groups = {};
  items.forEach(it => {
    const g = it.group || 'Main';
    if (!groups[g]) groups[g] = [];
    groups[g].push(it);
  });

  const W_OPEN = 260;
  const W_COLLAPSED = 72;
  const width = collapsed ? W_COLLAPSED : W_OPEN;

  // v141.164 re-skin Stage 3 — light nav rail to the delivered redesign
  // "white" theme: calm subtle-tint selected state + left accent bar
  // (NOT a heavy black pill), hairline border. Dark-mode branch is
  // unchanged. Nav logic/props/structure untouched.
  const fg       = dark ? '#f0f0f0' : '#141414';
  const fgMuted  = dark ? 'rgba(240,240,240,0.55)' : 'rgba(20,20,20,0.55)';
  const fgFaint  = dark ? 'rgba(240,240,240,0.4)'  : 'rgba(20,20,20,0.4)';
  const bg       = dark ? '#0a0a0a' : '#ffffff';
  const bgHover  = dark ? 'rgba(255,255,255,0.05)' : 'rgba(20,20,20,0.04)';
  const bgActive = dark ? 'rgba(255,255,255,0.12)' : 'rgba(20,20,20,0.06)';
  const fgActive = dark ? '#ffffff' : CC.ink;
  const border   = dark ? 'rgba(255,255,255,0.06)' : CC.hairline;

  return (
    <aside style={{
      width: width,
      flexShrink: 0,
      background: bg,
      color: fg,
      borderRight: `1px solid ${border}`,
      display: 'flex', flexDirection: 'column',
      minHeight: '100vh',
      position: 'sticky', top: 0, alignSelf: 'flex-start',
      fontFamily: F_TLB.ui,
      transition: 'width 220ms cubic-bezier(0.25, 0.1, 0.25, 1)',
      overflow: 'hidden',
    }}>
      {/* ── Brand block ─────────────────────────────────────────────
          v141.73 — Replaced the text-only "TLB" square placeholder
          (a 40×40 white tile with the literal letters "TLB") with the
          actual brand mark from /assets/logo-tlb.png. The text version
          was a temporary stand-in; the operator asked for the real
          asset to match the public-site nav. Keeps a subtle dark
          backdrop on dark sidebars so the dark-on-dark PNG stays
          legible — same trick the public nav uses. */}
      <div style={{
        display: 'flex', alignItems: 'center', gap: 12,
        padding: collapsed ? '20px 16px' : '20px 20px',
        borderBottom: `1px solid ${border}`,
        minHeight: 72, transition: 'padding 220ms cubic-bezier(0.25, 0.1, 0.25, 1)',
      }}>
        <div style={{
          width: 40, height: 40, borderRadius: 10, flexShrink: 0,
          background: dark ? '#ffffff' : CC.offWhite,
          display: 'grid', placeItems: 'center',
          overflow: 'hidden',
        }}>
          <img
            src="/assets/logo-tlb.png"
            alt={brand || 'The Luxury Bali'}
            style={{ width: 30, height: 30, objectFit: 'contain', display: 'block' }}
          />
        </div>
        <div style={{
          flex: 1, minWidth: 0,
          opacity: collapsed ? 0 : 1,
          transform: collapsed ? 'translateX(-8px)' : 'translateX(0)',
          transition: 'opacity 160ms ease-out, transform 220ms cubic-bezier(0.25, 0.1, 0.25, 1)',
          pointerEvents: collapsed ? 'none' : 'auto',
          whiteSpace: 'nowrap',
        }}>
          <div style={{ fontFamily: F_TLB.display, fontStyle: 'italic', fontSize: 17, lineHeight: 1.1 }}>The Luxury Bali</div>
          <div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '0.18em', textTransform: 'uppercase', color: fgMuted, marginTop: 4 }}>{sub}</div>
        </div>
      </div>

      {/* ── Nav ──────────────────────────────────────────────────── */}
      {/* v141.138 — Fix: sidebar inner nav couldn't be scrolled when
          a tall section (SEO Control, Properties grid) made the page
          body very long. The sticky parent locked at 100vh, but the
          inner nav needed an explicit max-height + clear scrollbar
          so users can reach lower nav items. Setting maxHeight to the
          viewport (minus brand + footer chrome) so nav is always
          mousewheel-scrollable independent of page scroll. */}
      <nav className="nav-rail" style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden', padding: '14px 0', maxHeight: 'calc(100vh - 180px)', scrollbarWidth: 'thin', scrollbarColor: `${fgFaint} transparent` }}>
        {Object.entries(groups).map(([groupLabel, groupItems]) => (
          <div key={groupLabel} style={{ marginBottom: 4 }}>
            {/* Group label */}
            {collapsed ? (
              <div style={{ height: 1, background: border, margin: '14px 16px 8px' }} />
            ) : (
              <div style={{
                padding: '14px 24px 6px',
                fontSize: 9, fontWeight: 700,
                letterSpacing: '0.2em', textTransform: 'uppercase',
                color: fgFaint,
                whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
              }}>{groupLabel}</div>
            )}
            {/* Items */}
            {groupItems.map(it => {
              const on = active === it.key;
              return (
                <button
                  key={it.key}
                  onClick={() => onChange(it.key)}
                  title={collapsed ? it.label : ''}
                  aria-current={on ? 'page' : undefined}
                  style={{
                    display: 'flex', alignItems: 'center', gap: 12,
                    width: 'calc(100% - 16px)',
                    margin: '1px 8px',
                    padding: collapsed ? '10px 14px' : '10px 16px',
                    border: 'none', borderRadius: 8, cursor: 'pointer', textAlign: 'left',
                    background: on ? bgActive : 'transparent',
                    color: on ? fgActive : (dark ? 'rgba(240,240,240,0.78)' : CC.deep),
                    fontFamily: F_TLB.ui, fontSize: 13, fontWeight: on ? 600 : 500,
                    letterSpacing: '0.005em', position: 'relative',
                    transition: 'background 180ms ease-out, color 180ms ease-out',
                    minHeight: 40,
                  }}
                  onMouseEnter={e => { if (!on) e.currentTarget.style.background = bgHover; }}
                  onMouseLeave={e => { if (!on) e.currentTarget.style.background = 'transparent'; }}
                >
                  {/* Active indicator bar */}
                  {on && <span style={{
                    position: 'absolute', left: 0, top: '50%',
                    transform: 'translateY(-50%)',
                    width: 2, height: 20, background: dark ? '#ffffff' : CC.ink,
                    borderRadius: 2,
                    boxShadow: dark ? '0 0 12px rgba(255,255,255,0.12)' : 'none',
                  }} />}
                  {/* Icon */}
                  <DashIcon name={it.icon || 'grid'} size={18} />
                  {/* Label */}
                  <span style={{
                    flex: 1,
                    whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
                    opacity: collapsed ? 0 : 1,
                    transform: collapsed ? 'translateX(-8px)' : 'translateX(0)',
                    transition: 'opacity 140ms ease-out, transform 220ms cubic-bezier(0.25, 0.1, 0.25, 1)',
                    pointerEvents: collapsed ? 'none' : 'auto',
                  }}>{it.label}</span>
                  {/* OPS indicator */}
                  {it.mode === 'iframe' && !collapsed && (
                    <span style={{
                      fontSize: 8, padding: '2px 7px', borderRadius: 100,
                      background: dark ? 'rgba(255,255,255,0.08)' : '#f0f0f0',
                      color: fgMuted, letterSpacing: '0.14em', fontWeight: 700,
                      flexShrink: 0,
                    }}>OPS</span>
                  )}
                </button>
              );
            })}
          </div>
        ))}
      </nav>

      {/* ── Footer ──────────────────────────────────────────────
          Round-15 fix — was using `display:flex, alignItems:center`
          which collapsed multi-line footer content (identity card +
          Sign out button) into a single row, causing the overlap
          screenshot the user reported. Now: column layout for the
          identity slot, collapse toggle docked at the bottom of its
          own row underneath. `whiteSpace: nowrap` removed (it was
          breaking text wrapping on long host names too). */}
      <div style={{
        borderTop: `1px solid ${border}`,
        padding: collapsed ? '10px 8px' : '14px 16px',
        display: 'flex', flexDirection: 'column', gap: 10,
        flexShrink: 0,  // prevent footer from being squashed by overflow nav
      }}>
        {/* Identity / footer block (host name, sign out, etc.) */}
        <div style={{
          minWidth: 0,
          opacity: collapsed ? 0 : 1,
          maxHeight: collapsed ? 0 : 200,
          overflow: 'hidden',
          transform: collapsed ? 'translateX(-8px)' : 'translateX(0)',
          transition: 'opacity 140ms ease-out, transform 220ms cubic-bezier(0.25, 0.1, 0.25, 1), max-height 220ms cubic-bezier(0.25, 0.1, 0.25, 1)',
          pointerEvents: collapsed ? 'none' : 'auto',
        }}>
          {footer}
        </div>
        {/* Collapse toggle — own row, right-aligned in expanded mode,
            centered when collapsed. Round-15 fix — was sharing a flex
            row with the footer content, causing overlap when footer
            had multi-line content (identity + Sign out button). */}
        <div style={{ display: 'flex', justifyContent: collapsed ? 'center' : 'flex-end' }}>
          <button
            onClick={() => setCollapsed(c => !c)}
            title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
            aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
            style={{
              width: 32, height: 32, borderRadius: 8, flexShrink: 0,
              border: `1px solid ${border}`, background: 'transparent',
              color: fgMuted,
              display: 'grid', placeItems: 'center', cursor: 'pointer',
              transition: 'all 180ms ease-out',
            }}
            onMouseEnter={e => { e.currentTarget.style.background = bgHover; e.currentTarget.style.color = fg; }}
            onMouseLeave={e => { e.currentTarget.style.background = 'transparent'; e.currentTarget.style.color = fgMuted; }}>
            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{ transform: collapsed ? 'rotate(180deg)' : 'none', transition: 'transform 220ms cubic-bezier(0.25, 0.1, 0.25, 1)' }}>
              <path d="M15 18l-6-6 6-6" />
            </svg>
          </button>
        </div>
      </div>
    </aside>
  );
}

// ── IframeView — embed an operational page inside the new dashboard
// Used for sections we haven't built native React views for yet.
// The operational page detects ?embed=1 (added in admin.html / host.html
// / account.html) and hides its own sidebar/topbar, so it slots in
// cleanly as the right-hand panel.
function IframeView({ src, title }) {
  const [height, setHeight] = React.useState(800);
  const ref = React.useRef(null);

  // v141.70 — Carry deep-link params from the parent URL through to the
  // embedded classic page. Cards in the Site Manager grid link to URLs
  // like `?section=sitemanager&page=studio` — that click hits the
  // admin-classic auto-redirect which converts it to `?section=...`,
  // the new admin shell mounts the iframe, and we want the iframe URL
  // to be `/admin-classic?tab=sitemanager&page=studio&embed=1` so the
  // classic page lands on the right sub-tab. The pass-through params
  // are: `page`, `panel`, `id`, `property`, `action`. Anything not on
  // the whitelist is ignored (no surprise leakage).
  const fullSrc = React.useMemo(() => {
    try {
      var parentParams = new URLSearchParams(window.location.search);
      var passThrough  = ['page', 'panel', 'id', 'property', 'action'];
      var u = new URL(src, window.location.origin);
      passThrough.forEach(function(k) {
        var v = parentParams.get(k);
        if (v != null && !u.searchParams.has(k)) u.searchParams.set(k, v);
      });
      return u.pathname + (u.search || '');
    } catch { return src; }
  }, [src]);

  React.useEffect(() => {
    // Try to auto-size the iframe to its content if same-origin
    const tryResize = () => {
      try {
        const doc = ref.current?.contentWindow?.document;
        if (doc && doc.body) {
          const h = Math.max(800, doc.body.scrollHeight + 32);
          setHeight(h);
        }
      } catch {}
    };
    const t = setTimeout(tryResize, 1500);
    const t2 = setInterval(tryResize, 4000);
    return () => { clearTimeout(t); clearInterval(t2); };
  }, [fullSrc]);

  return (
    <div style={{ background: CC.white, borderRadius: 18, border: `1px solid ${CC.border}`, overflow: 'hidden' }}>
      <div style={{ padding: '12px 20px', borderBottom: `1px solid ${CC.border}`, display: 'flex', alignItems: 'center', justifyContent: 'space-between', background: CC.offWhite }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
          <span style={{ width: 6, height: 6, borderRadius: 99, background: '#2f7a5a' }} />
          <Sans size={10} weight={600} ls={0.14} upper color={CC.deep}>OPERATIONAL VIEW · LIVE DATA</Sans>
        </div>
        {/* v141.70 — "Open in full window" removed. Classic pages now
            auto-redirect direct visits back to /dashboards/admin or
            /dashboards/host, so this button would loop. */}
      </div>
      <iframe ref={ref} src={fullSrc} title={title || 'Operational view'}
        style={{ width: '100%', height: height, border: 'none', display: 'block' }}
        sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-modals allow-downloads allow-top-navigation-by-user-activation"
        loading="lazy" />
    </div>
  );
}

Object.assign(window, { DashHeader, DashTopbar, LivePill, DataBadge, DashSidebar, DashIcon, DashIcons, IframeView, Panel, KPI, ChannelBadge, AetheriaDot, SLAPill, SubTabs, ImgFrame, dashPad, dashGap });
