/* Single Site drilldown — port of prototype site.jsx.
   The real DB has a sites table with name, slug, city, country, timezone
   but no /api/sites endpoint. We reconstruct site info by reading
   /api/zones (each zone carries site_name + city) and grouping.
   Many KPIs (visitors, dwell, weather, staff, campaigns) don't exist
   yet — show clearly-labelled placeholders. */

const ST_THEMES = {
  reef:      { fg: '#5DD3D3', grad: 'linear-gradient(135deg, #1f5961 0%, #0d2a30 100%)' },
  jellyfish: { fg: '#C99BFF', grad: 'linear-gradient(135deg, #3a2454 0%, #15102a 100%)' },
  shark:     { fg: '#9DBCFF', grad: 'linear-gradient(135deg, #1a2748 0%, #0a1024 100%)' },
  pelagic:   { fg: '#7DC0E0', grad: 'linear-gradient(135deg, #14384c 0%, #08182a 100%)' },
  touchpool: { fg: '#F2C879', grad: 'linear-gradient(135deg, #4a3618 0%, #1f1608 100%)' },
  cold:      { fg: '#A6E3FA', grad: 'linear-gradient(135deg, #1a4456 0%, #0a1d28 100%)' },
};

function ST_StatusPill({ lvl }) {
  const map = {
    Live:      { fg: 'var(--aq-success)', bg: 'color-mix(in srgb, var(--aq-success) 14%, transparent)' },
    Scheduled: { fg: '#9DBCFF', bg: 'rgba(157, 188, 255, 0.13)' },
    Draft:     { fg: 'var(--aq-text-dim)', bg: 'rgba(255,255,255,0.05)' },
  };
  const t = map[lvl] || map.Draft;
  return <span className="ss-pill" style={{ background: t.bg, color: t.fg }}>{lvl}</span>;
}

function SiteScreen({ param }) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);
  const [settingsOpen, setSettingsOpen] = useState(false);

  useEffect(() => {
    let cancelled = false;
    Promise.all([
      apiFetchSiteScoped('/api/zones'),
      apiFetchSiteScoped('/api/screens'),
    ]).then(([zonesRes, screensRes]) => {
      if (cancelled) return;
      const zones = zonesRes.zones || [];
      const screens = screensRes.screens || [];

      /* Pick the site by slug param, falling back to the first site in zones. */
      const targetSiteName = (() => {
        if (!param) return zones[0] && zones[0].site_name;
        /* slug match: lowercase, kebab-ish */
        const slug = param.toLowerCase();
        const z = zones.find((z) => (z.site_name || '').toLowerCase().replace(/\s+/g, '-').includes(slug));
        return z ? z.site_name : (zones[0] && zones[0].site_name);
      })();

      if (!targetSiteName) {
        setError('No site found.');
        setLoading(false);
        return;
      }

      const siteZones = zones.filter((z) => z.site_name === targetSiteName);
      const siteScreens = screens.filter((s) => s.site_name === targetSiteName);
      const screensByZone = siteScreens.reduce((acc, s) => {
        (acc[s.zone_id] ||= []).push(s);
        return acc;
      }, {});

      const enrichedZones = siteZones.map((z) => {
        const zScreens = screensByZone[z.id] || [];
        const online = zScreens.filter((s) => {
          /* Liveness audit (2026-06-12): last_seen_at is the feed-fetch
             timestamp (bumped by the snapshotter + CMS previews), not
             device liveness. Trust is_online, fall back to heartbeat. */
          if (s.is_online === true || s.is_online === 1) return true;
          if (s.is_online === false || s.is_online === 0) return false;
          if (!s.last_heartbeat) return false;
          return Date.now() - new Date(s.last_heartbeat).getTime() < 5 * 60 * 1000;
        }).length;
        return { ...z, screens: zScreens.length, online };
      });

      const totalScreens = siteScreens.length;
      const onlineScreens = enrichedZones.reduce((a, z) => a + z.online, 0);
      const totalSpecies = enrichedZones.reduce((a, z) => a + (z.species_count || 0), 0);

      setData({
        site: {
          name: targetSiteName,
          /* city/country are the same per zone — pull from first */
          city: siteZones[0] ? siteZones[0].city : '',
        },
        zones: enrichedZones,
        totalScreens,
        onlineScreens,
        totalSpecies,
      });
      setLoading(false);
    }).catch((err) => {
      if (cancelled) return;
      setError(err.message);
      setLoading(false);
    });
    return () => { cancelled = true; };
  }, [param]);

  if (loading) {
    return (
      <div className="ss-content">
        <div style={{ padding: 60, textAlign: 'center', color: 'var(--aq-text-faint)' }}>
          Loading site…
        </div>
      </div>
    );
  }

  if (error || !data) {
    return (
      <div className="ss-content">
        <div className="x-err-stage" style={{ minHeight: 280 }}>
          <div className="x-err-card">
            <div className="x-err-code">Site not found</div>
            <div className="x-err-glyph is-warn"><Icon name="alert" size={28} /></div>
            <h1 className="x-err-headline">{param || '—'}</h1>
            <p className="x-err-body">{error || 'No site with this slug.'}</p>
          </div>
        </div>
      </div>
    );
  }

  const { site, zones, totalScreens, onlineScreens, totalSpecies } = data;

  return (
    <div className="ss-content">
      <section className="ss-hero">
        <svg className="ss-hero-pattern" viewBox="0 0 1400 240" preserveAspectRatio="none">
          <path d="M0,160 Q200,130 400,150 T800,144 T1200,150 T1600,148" stroke="#5DD3D3" strokeWidth="1" fill="none" opacity="0.25" />
          <path d="M0,180 Q240,150 480,170 T960,165 T1440,170" stroke="#9DBCFF" strokeWidth="0.8" fill="none" opacity="0.18" />
          <path d="M0,200 Q160,180 320,190 T640,188 T960,192 T1280,190" stroke="#C99BFF" strokeWidth="0.6" fill="none" opacity="0.12" />
          {[...Array(40)].map((_, i) => (
            <circle key={i} cx={20 + i * 36} cy={30 + (i % 5) * 16} r={0.5 + (i % 3) * 0.4} fill="#7DC0E0" opacity={0.2 + (i % 3) * 0.08} />
          ))}
        </svg>
        <div className="ss-hero-content">
          <div className="ss-hero-eyebrow">
            <Icon name="grid" size={11} />
            Slate Demo
            <span className="ss-hero-status">
              <span className="ss-hero-status-dot" />
              Active site
            </span>
          </div>
          <h1>{site.name}</h1>
          <p className="ss-hero-meta">
            {site.city ? `${site.city} · ` : ''}{zones.length} zone{zones.length === 1 ? '' : 's'} · {totalScreens} screen{totalScreens === 1 ? '' : 's'}
          </p>
        </div>
        <div className="ss-hero-actions">
          {Auth.canManageOrg() && (
            <button
              className="ss-btn"
              onClick={() => setSettingsOpen(true)}
            ><Icon name="settings" size={13} />Site settings</button>
          )}
          <button
            className="ss-btn primary"
            onClick={() => { window.location.hash = '#campaigns'; window.toast && window.toast.info('Pick a campaign and use Targeting → Site to push it across this whole site.'); }}
          ><Icon name="monitor" size={13} />Push to all screens</button>
        </div>
      </section>

      <section className="ss-kpis">
        <div className="ss-kpi">
          <div className="ss-kpi-label">Visitors today</div>
          <div className="ss-kpi-value">—</div>
          <div className="ss-kpi-bar">
            <div className="ss-kpi-bar-fill" style={{ width: '0%' }} />
          </div>
          <div className="ss-kpi-sub">no analytics yet</div>
        </div>
        <div className="ss-kpi">
          <div className="ss-kpi-label">Screens online</div>
          <div className="ss-kpi-value is-ok">{onlineScreens}<span>/{totalScreens}</span></div>
          <div className="ss-kpi-sub">
            {totalScreens - onlineScreens === 0
              ? totalScreens === 0 ? 'no screens' : 'all healthy'
              : `${totalScreens - onlineScreens} offline`}
          </div>
        </div>
        <div className="ss-kpi">
          <div className="ss-kpi-label">Active campaigns</div>
          <div className="ss-kpi-value">0</div>
          <div className="ss-kpi-sub">campaigns not yet wired</div>
        </div>
        <div className="ss-kpi">
          <div className="ss-kpi-label">Avg dwell time</div>
          <div className="ss-kpi-value">—</div>
          <div className="ss-kpi-sub">no analytics yet</div>
        </div>
        <div className="ss-kpi">
          <div className="ss-kpi-label">Species in rotation</div>
          <div className="ss-kpi-value">{totalSpecies}</div>
          <div className="ss-kpi-sub">across {zones.length} zone{zones.length === 1 ? '' : 's'}</div>
        </div>
        <div className="ss-kpi ss-kpi-weather">
          <div className="ss-kpi-label">Conditions</div>
          <div className="ss-kpi-weather-row">
            <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#F2C879" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
              <circle cx="9" cy="9" r="3" />
              <path d="M9 4v1M9 13v1M4 9h1M13 9h1M5.5 5.5l.7.7M11.8 11.8l.7.7M5.5 12.5l.7-.7M11.8 6.2l.7-.7" />
              <path d="M14 18a4 4 0 1 0-8 0H4a5 5 0 0 1 5-5h6a4 4 0 0 1 0 8H6" />
            </svg>
            <div>
              <div className="ss-kpi-value ss-kpi-value-sm">—</div>
              <div className="ss-kpi-sub">weather not wired</div>
            </div>
          </div>
        </div>
      </section>

      <div className="ss-grid">
        {/* Zones — real */}
        <section className="ss-card ss-zones">
          <header className="ss-card-head">
            <div>
              <h2>Zones</h2>
              <p>{zones.length} zone{zones.length === 1 ? '' : 's'} · {totalScreens} screen{totalScreens === 1 ? '' : 's'}</p>
            </div>
            <button className="ss-link" onClick={() => { window.location.hash = '#zones'; }}>
              Open zones →
            </button>
          </header>
          <div className="ss-zones-grid">
            {zones.map((z) => {
              const t = ST_THEMES[z.theme] || ST_THEMES.reef;
              const offline = z.screens - z.online;
              return (
                <div key={z.id} className="ss-zone">
                  <div className="ss-zone-art" style={{ background: t.grad }}>
                    <svg viewBox="0 0 200 60" preserveAspectRatio="none">
                      <path d="M0,38 Q40,22 80,32 T160,30 T240,36" stroke={t.fg} strokeWidth="0.6" fill="none" opacity="0.45" />
                      <path d="M0,46 Q50,32 100,40 T200,38" stroke={t.fg} strokeWidth="0.5" fill="none" opacity="0.25" />
                      <circle cx="40" cy="14" r="0.8" fill={t.fg} opacity="0.5" />
                      <circle cx="100" cy="10" r="1" fill={t.fg} opacity="0.6" />
                      <circle cx="160" cy="16" r="0.7" fill={t.fg} opacity="0.4" />
                    </svg>
                  </div>
                  <div className="ss-zone-body">
                    <div className="ss-zone-head">
                      <h4>{z.name}</h4>
                      <span className={`ss-zone-status ${offline > 0 ? 'is-warn' : 'is-ok'}`}>
                        <span className="ss-zone-status-dot" />
                        {z.online}/{z.screens}
                      </span>
                    </div>
                    <div className="ss-zone-stats">
                      <span><b>{z.species_count}</b> species</span>
                      <span><b>—</b> dwell</span>
                    </div>
                  </div>
                </div>
              );
            })}
            {zones.length === 0 && (
              <div style={{ color: 'var(--aq-text-faint)', fontSize: 12, padding: 14 }}>
                No zones in this site yet.
              </div>
            )}
          </div>
        </section>

        {/* Alerts — derive from screens that aren't online */}
        <section className="ss-card ss-alerts">
          <header className="ss-card-head">
            <div>
              <h2>Alerts & attention</h2>
              <p>0 warnings · 0 info</p>
            </div>
            <button className="ss-link">View all →</button>
          </header>
          <ul className="ss-alerts-list">
            {totalScreens - onlineScreens > 0 ? (
              <li className="ss-alert ss-alert-warn">
                <span className="ss-alert-mark" />
                <div className="ss-alert-body">
                  <div className="ss-alert-msg">
                    {totalScreens - onlineScreens} screen{totalScreens - onlineScreens === 1 ? '' : 's'} not reporting
                  </div>
                  <div className="ss-alert-meta">Site-wide · last checked just now</div>
                </div>
                <button
                  className="ss-alert-action"
                  onClick={() => { window.location.hash = '#displays'; }}
                >View</button>
              </li>
            ) : (
              <li style={{ padding: '14px 16px', color: 'var(--aq-text-faint)', fontSize: 12 }}>
                Nothing needs attention right now.
              </li>
            )}
          </ul>
        </section>

        {/* Active campaigns — not wired yet */}
        <section className="ss-card ss-campaigns">
          <header className="ss-card-head">
            <div>
              <h2>Active campaigns</h2>
              <p>Campaigns not yet wired to backend</p>
            </div>
            <button className="ss-link">All campaigns →</button>
          </header>
          <table className="ss-camp-table">
            <thead>
              <tr>
                <th>Campaign</th>
                <th>Status</th>
                <th>Zones</th>
                <th>Performance</th>
                <th>Ends</th>
                <th>Owner</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td colSpan={6} style={{
                  textAlign: 'center', padding: '32px 16px',
                  color: 'var(--aq-text-faint)', fontSize: 12,
                }}>
                  No campaigns yet — feature lands in Group G3.
                </td>
              </tr>
            </tbody>
          </table>
        </section>

        {/* Staff on site — not wired yet */}
        <section className="ss-card ss-staff">
          <header className="ss-card-head">
            <div>
              <h2>Staff on site</h2>
              <p>Roster not wired</p>
            </div>
            <button className="ss-link">Roster →</button>
          </header>
          <ul className="ss-staff-list">
            <li className="is-here">
              <span className="ss-avatar lg">AA</span>
              <div className="ss-staff-meta">
                <div className="ss-staff-name">
                  Slate Admin
                  <span className="ss-tag">You</span>
                </div>
                <div className="ss-staff-role">admin · admin@aquaos.local</div>
              </div>
              <span className="ss-staff-dot is-here" />
            </li>
          </ul>
        </section>

        {/* Activity — no audit log endpoint yet */}
        <section className="ss-card ss-activity">
          <header className="ss-card-head">
            <div>
              <h2>Recent activity</h2>
              <p>Audit feed not yet wired</p>
            </div>
            <button className="ss-link">Audit log →</button>
          </header>
          <ul className="ss-activity-list">
            <li>
              <span className="ss-avatar">AA</span>
              <div className="ss-act-body">
                <div><b>Slate Admin</b> logged in <em>via password</em></div>
                <div className="ss-act-time">just now</div>
              </div>
            </li>
            <li>
              <span className="ss-avatar">SD</span>
              <div className="ss-act-body">
                <div><b>Seed data</b> loaded <em>{totalSpecies} species into the demo zone</em></div>
                <div className="ss-act-time">today</div>
              </div>
            </li>
          </ul>
        </section>
      </div>

      <SiteSettingsModal
        open={settingsOpen}
        site={site}
        onClose={() => setSettingsOpen(false)}
        onSaved={(patch) => {
          /* Optimistically merge into local state so the hero updates
             immediately. The site object lives inside `data.site`. */
          setData((d) => d ? { ...d, site: { ...d.site, ...patch } } : d);
          setSettingsOpen(false);
          window.toast && window.toast.success('Site settings saved');
        }}
      />
    </div>
  );
}

/* Site Settings modal — wired to PUT /api/auth/sites/:id (org_admin
   only). Backend accepts name / location (city) / timezone / slug. */
function SiteSettingsModal({ open, site, onClose, onSaved }) {
  const [name, setName] = useState(site && site.name || '');
  const [city, setCity] = useState(site && site.city || '');
  const [tz, setTz]     = useState(site && site.timezone || (Intl.DateTimeFormat().resolvedOptions().timeZone) || 'UTC');
  const [slug, setSlug] = useState(site && site.slug || '');
  const [busy, setBusy] = useState(false);
  const [err, setErr]   = useState(null);
  /* Resolved site row from /api/auth/sites. The `site` prop is
     reconstructed from zones (no id, no setup_pin) so we have to
     re-fetch on open to get the real backend row. Once we have that
     id, the setup-PIN section becomes interactive. */
  const [resolvedSite, setResolvedSite] = useState(null);
  const [pinBusy, setPinBusy] = useState(false);
  const [pinErr, setPinErr]   = useState(null);

  /* IANA timezone list for the picker. Intl.supportedValuesOf is
     supported by every browser Slate targets; the fallback covers rare
     runtimes. Defined locally rather than shared with sites.jsx so the
     two files don't depend on load order in the babel global scope. */
  const timezones = React.useMemo(() => {
    let list = [];
    try {
      if (typeof Intl.supportedValuesOf === 'function') {
        list = Intl.supportedValuesOf('timeZone');
      }
    } catch (_) { /* fall through */ }
    if (!list.length) {
      list = [
        'UTC', 'Europe/London', 'Europe/Paris', 'Europe/Berlin', 'Europe/Madrid',
        'America/New_York', 'America/Chicago', 'America/Denver', 'America/Los_Angeles',
        'America/Toronto', 'Australia/Sydney', 'Australia/Melbourne', 'Australia/Perth',
        'Pacific/Auckland', 'Asia/Tokyo', 'Asia/Singapore', 'Asia/Dubai',
        'Asia/Kolkata', 'Africa/Johannesburg',
      ];
    }
    return list;
  }, []);

  /* Conservation (charity routing) — see docs/mobile-explorer-plan.md §4.
     Per-site config for the $1-of-every-$3.99 subscription contribution.
     Capability-gated server-side (subscription.configure_charity); we
     render the section unconditionally and let the API 403 if missing. */
  const [charityCfg, setCharityCfg]       = useState(null);
  const [charityPartners, setCharityPartners] = useState([]);
  const [charityBusy, setCharityBusy]     = useState(false);
  const [charityErr, setCharityErr]       = useState(null);
  const [charityAvail, setCharityAvail]   = useState(true); // 403 → hide section

  useEffect(() => {
    if (!open || !site) return;
    setName(site.name || ''); setCity(site.city || '');
    setTz(site.timezone || 'UTC'); setSlug(site.slug || '');
    setErr(null); setBusy(false);
    setPinErr(null); setPinBusy(false);
    setResolvedSite(null);
    // Resolve the real site row so we know the id + setup_pin.
    // Match by name as a fallback for legacy site.jsx (which only has
    // name + city). If the org has duplicate site names this will pick
    // the first — acceptable since duplicates would be a pre-existing
    // data issue.
    apiFetch('/api/auth/sites').then((r) => {
      const list = (r && r.sites) || [];
      const match = list.find((s) =>
        (site.id && s.id === site.id) ||
        (site.slug && s.slug === site.slug) ||
        (s.name === site.name)
      ) || null;
      setResolvedSite(match);
      // Load charity config + partner list once we know the site id.
      const sid = match && match.id;
      if (!sid) return;
      Promise.all([
        apiFetch(`/api/mobile/explorer/admin/charity/sites/${sid}/config`)
          .catch(() => ({ _denied: true })),
        apiFetch('/api/mobile/explorer/admin/charity/partners')
          .catch(() => ({ _denied: true })),
      ]).then(([cfgRes, partnersRes]) => {
        if (cfgRes && cfgRes._denied) {
          setCharityAvail(false);
          return;
        }
        setCharityCfg(cfgRes && cfgRes.config ? cfgRes.config : {
          site_id: sid, mode: 'slate_default', enabled: 1, self_partner_id: null
        });
        setCharityPartners((partnersRes && partnersRes.partners) || []);
      });
    }).catch(() => { /* PIN section just stays disabled */ });
  }, [open, site && site.id, site && site.name]);

  async function saveCharity() {
    if (!resolvedSite || !resolvedSite.id || !charityCfg) return;
    setCharityBusy(true); setCharityErr(null);
    try {
      const r = await apiFetch(
        `/api/mobile/explorer/admin/charity/sites/${resolvedSite.id}/config`,
        {
          method: 'PUT',
          body: JSON.stringify({
            mode: charityCfg.mode,
            enabled: charityCfg.enabled ? true : false,
            self_partner_id: charityCfg.mode === 'venue_self' ? charityCfg.self_partner_id : null,
          }),
        }
      );
      setCharityCfg(r.config || charityCfg);
      if (window.toast) window.toast.success
        ? window.toast.success('Conservation settings saved')
        : window.toast('Conservation settings saved');
    } catch (e) {
      setCharityErr(e.message);
    } finally {
      setCharityBusy(false);
    }
  }

  async function rotatePin() {
    if (!resolvedSite || !resolvedSite.id) return;
    setPinBusy(true); setPinErr(null);
    try {
      const r = await apiFetch(`/api/auth/sites/${resolvedSite.id}/setup-pin`, { method: 'PUT' });
      setResolvedSite((s) => s ? { ...s, setup_pin: r.setup_pin, setup_pin_set_at: r.setup_pin_set_at } : s);
      if (window.toast) window.toast.success
        ? window.toast.success('Setup PIN rotated')
        : window.toast('Setup PIN rotated');
    } catch (e) {
      setPinErr(e.message);
    } finally {
      setPinBusy(false);
    }
  }

  async function save() {
    /* 2026-06-03 (Oli): the `site` prop is reconstructed from zones and
       often has no id, but we already resolve the REAL backend row into
       resolvedSite (same lookup that powers the working Setup-PIN rotate).
       Use that id as the fallback so settings save works for legacy
       zone-reconstructed sites too. Only block when NEITHER id exists
       (a site with no backend row at all). */
    const sid = (site && site.id) || (resolvedSite && resolvedSite.id);
    if (!sid) {
      setErr('Site has no id — settings save is unavailable for legacy site records reconstructed from zones.');
      return;
    }
    /* Reject an invalid IANA zone before it reaches the backend, which
       stores verbatim and would silently fall back to UTC in dayparting.
       The dropdown normally guarantees validity; this guards the case
       where a pre-existing non-standard value was left selected. */
    const tzClean = tz.trim();
    if (tzClean) {
      try { Intl.DateTimeFormat('en', { timeZone: tzClean }); }
      catch (_) {
        setErr(`"${tzClean}" isn't a valid timezone. Pick one from the list.`);
        return;
      }
    }
    setBusy(true); setErr(null);
    try {
      const body = {
        name: name.trim(),
        location: city.trim() || null,
        timezone: tz.trim() || null,
      };
      /* Only send slug when the field has a value. A blank slug must NOT
         be sent as null — sites.slug is NOT NULL, and the old
         `slug.trim() || null` produced the 500 "null value in column slug
         of relation sites". Omitting it leaves the existing slug intact. */
      const slugClean = slug.trim();
      if (slugClean) body.slug = slugClean;
      await apiFetch(`/api/auth/sites/${sid}`, {
        method: 'PUT',
        body: JSON.stringify(body),
      });
      /* Map back to the site object's column names. */
      onSaved && onSaved(Object.assign(
        { name: body.name, city: body.location, timezone: body.timezone },
        slugClean ? { slug: slugClean } : {},
      ));
    } catch (e) {
      setErr(e.message);
    } finally {
      setBusy(false);
    }
  }

  if (!open) return null;
  return (
    <div
      onClick={(e) => { if (e.target === e.currentTarget && !busy) onClose(); }}
      style={{
        position: 'fixed', inset: 0, zIndex: 200,
        background: 'rgba(6, 7, 10, 0.78)', backdropFilter: 'blur(6px)',
        display: 'grid', placeItems: 'center', padding: 24,
      }}
    >
      <div style={{
        width: 'min(520px, 100%)',
        background: 'var(--aq-surface)',
        border: '1px solid var(--aq-line)',
        borderRadius: 14,
        boxShadow: 'var(--aq-shadow-2)',
        overflow: 'hidden',
      }}>
        <header style={{
          padding: '14px 18px', borderBottom: '1px solid var(--aq-line)',
          display: 'flex', alignItems: 'center', gap: 12,
        }}>
          <div style={{
            width: 28, height: 28, borderRadius: 8,
            background: 'var(--aq-accent-soft)', color: 'var(--aq-accent)',
            display: 'grid', placeItems: 'center',
          }}><Icon name="settings" size={14} /></div>
          <div style={{ flex: 1 }}>
            <div style={{ fontFamily: 'var(--aq-ff-display)', fontSize: 14, color: 'var(--aq-text)' }}>
              Site settings
            </div>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-faint)' }}>
              {site ? site.name : ''}
            </div>
          </div>
          <button className="aq-icon-btn" onClick={onClose} disabled={busy}>
            <Icon name="close" size={13} />
          </button>
        </header>

        <div style={{ padding: 18, display: 'flex', flexDirection: 'column', gap: 12 }}>
          {err && (
            <div style={{
              padding: '8px 12px', borderRadius: 6,
              background: 'color-mix(in srgb, var(--aq-danger) 12%, transparent)',
              color: 'var(--aq-danger)', fontSize: 12,
            }}>{err}</div>
          )}
          <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11.5, color: 'var(--aq-text-dim)' }}>
            <span>Site name</span>
            <input className="x-input" value={name} onChange={(e) => setName(e.target.value)} />
          </label>
          <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11.5, color: 'var(--aq-text-dim)' }}>
            <span>City</span>
            <input className="x-input" value={city} onChange={(e) => setCity(e.target.value)} placeholder="e.g. London" />
          </label>
          <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11.5, color: 'var(--aq-text-dim)' }}>
            <span>Timezone</span>
            {/* IANA picker. Native <select> over free text so the value
                can only be a supported zone — the backend stores it
                verbatim and silently falls back to UTC on an invalid
                string, so a typo would quietly break dayparting. If the
                site already holds a non-standard value we surface it as
                the first option so it stays selected until corrected. */}
            <select
              className="x-input"
              value={tz}
              onChange={(e) => setTz(e.target.value)}
            >
              {tz && !timezones.includes(tz) && (
                <option value={tz}>{tz} — current (non-standard)</option>
              )}
              {timezones.map((z) => (
                <option key={z} value={z}>{z}</option>
              ))}
            </select>
          </label>
          <label style={{ display: 'flex', flexDirection: 'column', gap: 4, fontSize: 11.5, color: 'var(--aq-text-dim)' }}>
            <span>Slug</span>
            <input className="x-input" value={slug} onChange={(e) => setSlug(e.target.value)} placeholder="url-safe-handle" />
          </label>

          {/* ── Setup PIN ────────────────────────────────────────────
              Per-site numeric code installers tap into a fresh TV's
              touchscreen to pair it without a phone or laptop. The
              TV sends the PIN to /api/pairings/self-claim/lookup,
              gets back the site's zones, picks a slot, and binds.
              Visual treatment mirrors the read-only Screen code field
              (mono, dim background) so the manager reads it as
              "data to share with the installer", not a settings input. */}
          <div style={{ marginTop: 4, borderTop: '1px solid var(--aq-line)', paddingTop: 14 }}>
            <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>
              Setup PIN
            </div>
            {pinErr && (
              <div style={{
                padding: '8px 12px', borderRadius: 6, marginBottom: 8,
                background: 'color-mix(in srgb, var(--aq-danger) 12%, transparent)',
                color: 'var(--aq-danger)', fontSize: 12,
              }}>{pinErr}</div>
            )}
            <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
              <input
                type="text"
                readOnly
                value={(resolvedSite && resolvedSite.setup_pin) || '— — — —'}
                onFocus={(e) => e.currentTarget.select()}
                style={{
                  flex: 1, padding: '10px 12px',
                  fontSize: 22, fontWeight: 600,
                  background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                  borderRadius: 7, color: 'var(--aq-text)',
                  fontFamily: 'var(--aq-ff-mono)', letterSpacing: '0.35em', textAlign: 'center',
                  outline: 0, cursor: resolvedSite && resolvedSite.setup_pin ? 'text' : 'default',
                }}
              />
              <button
                onClick={rotatePin}
                disabled={pinBusy || !resolvedSite || !resolvedSite.id}
                style={{
                  padding: '10px 14px', fontSize: 12, fontWeight: 600,
                  background: 'var(--aq-surface-2)', border: '1px solid var(--aq-line)',
                  color: 'var(--aq-text)', borderRadius: 7, cursor: 'pointer',
                  whiteSpace: 'nowrap',
                }}
              >
                {pinBusy ? '…' : (resolvedSite && resolvedSite.setup_pin ? 'Rotate' : 'Generate')}
              </button>
            </div>
            <div style={{
              fontSize: 11, color: 'var(--aq-text-faint)',
              marginTop: 6, lineHeight: 1.4,
            }}>
              Hand this PIN to whoever's installing TVs at this site. On a fresh
              screen, they tap "Set up this display," enter this PIN, and pick
              the slot from a list — no phone or laptop needed.
              {resolvedSite && resolvedSite.setup_pin_set_at && (
                <> · Last rotated {new Date(resolvedSite.setup_pin_set_at).toLocaleDateString()}.</>
              )}
            </div>
          </div>

          {/* ── Conservation (Mobile Explorer charity routing) ─────────
              Per docs/mobile-explorer-plan.md §4 — every $3.99 monthly
              subscription routes $1 to marine conservation. This block
              configures where THAT site's visitors' contributions go.
              Three modes:
                slate_default  — Slate's vetted partner rotation
                venue_partners — venue picks 1-3 partners (V1)
                venue_self     — venue's own conservation arm receives it
              Section hidden when the user lacks subscription.configure_charity. */}
          {charityAvail && (
            <div style={{ marginTop: 4, borderTop: '1px solid var(--aq-line)', paddingTop: 14 }}>
              <div style={{ fontSize: 11.5, color: 'var(--aq-text-dim)', marginBottom: 5 }}>
                Conservation · Mobile Explorer charity routing
              </div>
              {charityErr && (
                <div style={{
                  padding: '8px 12px', borderRadius: 6, marginBottom: 8,
                  background: 'color-mix(in srgb, var(--aq-danger) 12%, transparent)',
                  color: 'var(--aq-danger)', fontSize: 12,
                }}>{charityErr}</div>
              )}
              {!charityCfg && (
                <div style={{ fontSize: 12, color: 'var(--aq-text-faint)' }}>Loading…</div>
              )}
              {charityCfg && (
                <>
                  <div style={{
                    fontSize: 11.5, color: 'var(--aq-text-faint)',
                    marginBottom: 10, lineHeight: 1.45,
                  }}>
                    $1 of every $3.99/mo subscription a visitor takes out at this
                    site routes to marine conservation. Pick the destination.
                  </div>

                  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginBottom: 12 }}>
                    {[
                      { v: 'slate_default',  label: 'Slate default rotation', sub: 'Slate-curated partner panel — visitors at this site send their $1 to whichever partner is in rotation that quarter.' },
                      { v: 'venue_partners', label: 'This venue picks partners', sub: '1-3 charities of your choosing. Editor coming in V1; falls back to Slate default until configured.' },
                      { v: 'venue_self',     label: 'This venue is the charity', sub: 'Your own registered conservation arm receives the $1 directly. Requires a charity_partners row with audit_status=verified.' },
                    ].map(opt => (
                      <label key={opt.v} style={{
                        display: 'flex', gap: 10, alignItems: 'flex-start',
                        padding: 10, borderRadius: 8,
                        border: `1px solid ${charityCfg.mode === opt.v ? 'var(--aq-accent)' : 'var(--aq-line)'}`,
                        background: charityCfg.mode === opt.v ? 'var(--aq-accent-soft)' : 'transparent',
                        cursor: 'pointer',
                      }}>
                        <input
                          type="radio"
                          name="charity-mode"
                          checked={charityCfg.mode === opt.v}
                          onChange={() => setCharityCfg(c => ({ ...c, mode: opt.v }))}
                          style={{ marginTop: 2 }}
                        />
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 12.5, color: 'var(--aq-text)', fontWeight: 500 }}>{opt.label}</div>
                          <div style={{ fontSize: 11, color: 'var(--aq-text-faint)', marginTop: 2, lineHeight: 1.4 }}>{opt.sub}</div>
                        </div>
                      </label>
                    ))}
                  </div>

                  {charityCfg.mode === 'venue_self' && (
                    <div style={{ marginBottom: 10 }}>
                      <div style={{ fontSize: 11, color: 'var(--aq-text-dim)', marginBottom: 4 }}>
                        Your conservation arm
                      </div>
                      <select
                        className="x-input"
                        value={charityCfg.self_partner_id || ''}
                        onChange={(e) => setCharityCfg(c => ({ ...c, self_partner_id: e.target.value || null }))}
                      >
                        <option value="">— pick a partner —</option>
                        {charityPartners.map(p => (
                          <option key={p.id} value={p.id}>
                            {p.name}{p.audit_status !== 'verified' ? ` · ${p.audit_status}` : ''}
                          </option>
                        ))}
                      </select>
                      {!charityPartners.length && (
                        <div style={{ fontSize: 11, color: 'var(--aq-text-faint)', marginTop: 4 }}>
                          No partners on file. Ask Slate to add your conservation arm in Platform Admin.
                        </div>
                      )}
                    </div>
                  )}

                  <label style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 12, color: 'var(--aq-text-dim)' }}>
                    <input
                      type="checkbox"
                      checked={!!charityCfg.enabled}
                      onChange={(e) => setCharityCfg(c => ({ ...c, enabled: e.target.checked ? 1 : 0 }))}
                    />
                    Charity routing enabled for this site
                  </label>

                  <div style={{ display: 'flex', justifyContent: 'flex-end', marginTop: 12 }}>
                    <button
                      onClick={saveCharity}
                      disabled={charityBusy}
                      style={{
                        padding: '8px 14px', fontSize: 12,
                        background: 'var(--aq-accent)', color: 'var(--aq-on-accent, #001)',
                        border: 0, borderRadius: 7, cursor: 'pointer', fontWeight: 600,
                      }}
                    >
                      {charityBusy ? 'Saving…' : 'Save conservation settings'}
                    </button>
                  </div>
                </>
              )}
            </div>
          )}
        </div>

        <footer style={{
          padding: '12px 18px', borderTop: '1px solid var(--aq-line)',
          background: 'var(--aq-surface-2)',
          display: 'flex', justifyContent: 'flex-end', gap: 8,
        }}>
          <button
            onClick={onClose}
            disabled={busy}
            style={{
              padding: '7px 12px', fontSize: 12,
              background: 'transparent', border: '1px solid var(--aq-line)',
              color: 'var(--aq-text-dim)', borderRadius: 7, cursor: 'pointer',
            }}
          >Cancel</button>
          <button onClick={save} className="aq-btn-primary" disabled={busy || !name.trim()}>
            {busy ? 'Saving…' : 'Save settings'}
          </button>
        </footer>
      </div>
    </div>
  );
}

window.SiteScreen = SiteScreen;
