// AdminGate — protects /#admin behind a bearer-token prompt. The token is the
// ADMIN_TOKEN secret set on the backend via `wrangler secret put ADMIN_TOKEN`.
// On submit we validate it by calling GET /api/admin/roster; on success the
// token is stashed in sessionStorage and reused for every admin request.

const AdminGate = ({ go, children }) => {
  const [authed, setAuthed] = React.useState(() => window.api.hasAdminToken());
  const [input, setInput] = React.useState('');
  const [error, setError] = React.useState('');
  const [busy, setBusy] = React.useState(false);

  if (authed) return children;

  const submit = async () => {
    if (busy) return;
    if (!input.trim()) { setError('Token required.'); return; }
    setBusy(true); setError('');
    try {
      const ok = await window.api.validateAdminToken(input.trim());
      if (ok) {
        setAuthed(true);
      } else {
        setError('Token rejected by server.');
      }
    } catch (e) {
      setError(e.message || 'Network error — backend unreachable.');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{ height: '100%', background: T.bg, color: T.bone, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
      <div style={{ width: '100%', maxWidth: 380, border: `1px solid ${T.rule}`, padding: 28, background: T.bg2 }}>
        <Mono size={11} color={T.gold}>// ADMIN · RESTRICTED</Mono>
        <Bebas size={36} style={{ marginTop: 10, lineHeight: 1 }}>Sign in.</Bebas>
        <Body size={13} color={T.mute} style={{ marginTop: 10, display: 'block' }}>
          Paste the admin bearer token to access the panel.
        </Body>
        <div style={{ marginTop: 18, display: 'flex', flexDirection: 'column', gap: 6 }}>
          <Mono size={9} color={error ? T.red : T.mute}>{error ? `TOKEN · ${error.toUpperCase()}` : 'TOKEN'}</Mono>
          <input
            type="password"
            autoFocus
            value={input}
            onChange={e => { setInput(e.target.value); if (error) setError(''); }}
            onKeyDown={e => { if (e.key === 'Enter') submit(); }}
            disabled={busy}
            style={{
              background: 'transparent', color: T.bone,
              border: `1px solid ${error ? T.red : T.rule}`, padding: '10px 12px',
              fontFamily: "'JetBrains Mono',monospace", fontSize: 14, letterSpacing: 2, outline: 'none',
              opacity: busy ? 0.6 : 1,
            }}
          />
        </div>
        <div style={{ marginTop: 18, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <button onClick={() => go('landing')} style={{ background: 'transparent', border: 'none', color: T.mute, fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer' }}>← PUBLIC SITE</button>
          <PrimaryCTA blue onClick={submit} disabled={busy} style={{ fontSize: 13, padding: '10px 18px', opacity: busy ? 0.6 : 1 }}>
            {busy ? 'Checking…' : 'Enter →'}
          </PrimaryCTA>
        </div>
      </div>
    </div>
  );
};

window.AdminGate = AdminGate;

// Admin — primary tabs: EVENTS | WEBPAGE
//   EVENTS  — sub-tabs per city + Add Location, includes Resources panel
//   WEBPAGE — About content editor + Hero video uploader + Slideshow uploader
//
// Top-level state lives in the Admin component; sub-components receive what
// they need via props.

const ABOUT_TAB_INDEX = -1; // unused legacy sentinel — kept for any deep links

// ── Compact form primitives ───────────────────────────────────────────
const AdminTextField = ({ label, value, onChange }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 12 }}>
    <Mono size={9} color={T.mute}>{label}</Mono>
    <input
      value={value || ''}
      onChange={e => onChange?.(e.target.value)}
      style={{
        background: 'transparent', color: T.bone,
        border: `1px solid ${T.rule}`, padding: '8px 10px',
        fontFamily: "'Inter',sans-serif", fontSize: 13, outline: 'none',
      }}
      onFocus={e => (e.target.style.borderColor = T.gold)}
      onBlur={e => (e.target.style.borderColor = T.rule)}
    />
  </div>
);

const AdminTextArea = ({ label, value, onChange, rows = 4 }) => (
  <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 12 }}>
    <Mono size={9} color={T.mute}>{label}</Mono>
    <textarea
      value={value || ''}
      rows={rows}
      onChange={e => onChange?.(e.target.value)}
      style={{
        background: 'transparent', color: T.bone,
        border: `1px solid ${T.rule}`, padding: '8px 10px',
        fontFamily: "'Inter',sans-serif", fontSize: 13, lineHeight: 1.45,
        outline: 'none', resize: 'vertical',
      }}
      onFocus={e => (e.target.style.borderColor = T.gold)}
      onBlur={e => (e.target.style.borderColor = T.rule)}
    />
  </div>
);

// ── EVENTS TAB ────────────────────────────────────────────────────────
// eventTab is either an event code (e.g. 'TAC') or a sentinel ('add' | 'archive').
const EventsTab = ({ events, ev_mut, eventTab, setEventTab, search, setSearch }) => {
  const [waiverViewing, setWaiverViewing] = React.useState(null); // candidate row or null
  const isAdd = eventTab === 'add';
  const isArchive = eventTab === 'archive';
  const ev = (!isAdd && !isArchive) ? events.find(e => e.code === eventTab) : null;
  const activeEvents = events.filter(e => !e.archived);
  const archivedEvents = events.filter(e => e.archived);
  // Highlight Archive sub-tab while viewing an archived event detail page.
  const archiveTabHighlighted = isArchive || (ev && ev.archived);

  // Snap to a valid eventTab if events change and the current selection no longer exists.
  React.useEffect(() => {
    if (events.length === 0) return;
    if (isAdd || isArchive) return;
    const cur = events.find(e => e.code === eventTab);
    if (!cur) {
      const firstActive = events.find(e => !e.archived);
      setEventTab(firstActive?.code || events[0]?.code || 'archive');
    }
  }, [events, eventTab, isAdd, isArchive, setEventTab]);

  // Roster — refetch whenever the active event tab changes. A short suffix of
  // the confirmation number (last 4 chars) is shown in the table to match the
  // prototype's `...{r.num}` style.
  const [roster, setRoster] = React.useState([]);
  const [rosterLoading, setRosterLoading] = React.useState(false);
  const [rosterError, setRosterError] = React.useState('');
  React.useEffect(() => {
    if (isAdd || isArchive || !ev?.code) { setRoster([]); return; }
    let cancelled = false;
    setRosterLoading(true); setRosterError('');
    window.api.apiGet(`/api/admin/roster?event=${encodeURIComponent(ev.code)}`)
      .then(r => {
        if (cancelled) return;
        const list = (r?.roster || []).map(x => ({
          name: `${x.last}, ${x.first}`,
          first: x.first, last: x.last,
          email: x.email, phone: x.phone,
          careers: x.careers,
          age: x.age, minor: x.isMinor,
          waiver: x.waiverSigned,
          num: (x.confirmationNumber || '').split('-').pop() || x.confirmationNumber,
          confirmationNumber: x.confirmationNumber,
          signedAt: x.signupTimestamp,
          sigMode: 'type',
        }));
        setRoster(list);
      })
      .catch(err => { if (!cancelled) setRosterError(err.message || 'Roster fetch failed.'); })
      .finally(() => { if (!cancelled) setRosterLoading(false); });
    return () => { cancelled = true; };
  }, [ev?.code, isAdd, isArchive]);
  const careerLabel = (r) => {
    const list = Array.isArray(r.careers) ? r.careers : (r.career ? [r.career] : []);
    return list.length === 0 ? '—' : list.join(' · ');
  };
  const filtered = roster.filter(r => !search || r.name.toLowerCase().includes(search.toLowerCase()) || r.email.toLowerCase().includes(search.toLowerCase()));

  // Remove a candidate from the roster (and from D1 + R2 server-side). The
  // local optimistic update gives instant UX; on failure we restore.
  const deleteCandidate = async (r) => {
    const fullName = `${r.first || ''} ${r.last || ''}`.trim() || r.name;
    if (!window.confirm(`Remove ${fullName} from the roster?\n\nThis permanently deletes their signup and waiver. Cannot be undone.`)) return;
    const prev = roster;
    setRoster(prev.filter(x => x.confirmationNumber !== r.confirmationNumber));
    try {
      await window.api.apiDelete(`/api/admin/signups/${encodeURIComponent(r.confirmationNumber)}`);
      // Server decremented events.spots; the events cache will pick that up on
      // the next read. For instant feedback we don't refresh the events list
      // here — the next page navigation does it naturally.
    } catch (e) {
      setRoster(prev);
      alert(`Delete failed: ${e.message || e}`);
    }
  };

  return (
    <React.Fragment>
      {/* Sub-tab strip — active cities + Add Location + Archive */}
      <div style={{ display: 'flex', borderBottom: `1px solid ${T.rule}`, background: T.bg2, overflowX: 'auto', flexShrink: 0, gap: 4, padding: '0 24px' }}>
        {activeEvents.map(t => (
          <button key={t.code} onClick={() => setEventTab(t.code)} style={{
            flex: '0 0 auto', padding: '14px 18px', background: 'transparent',
            border: 'none',
            borderBottom: eventTab === t.code ? `2px solid ${T.gold}` : '2px solid transparent',
            cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4, minWidth: 130, color: 'inherit',
            transition: 'border-color .15s',
          }}>
            <Bebas size={17} color={eventTab === t.code ? T.bone : T.mute}>{t.city}, {t.state}</Bebas>
            <div style={{ display: 'flex', gap: 10 }}>
              <Mono size={9} color={T.mute}>{t.dateShort}</Mono>
              <Mono size={9} color={eventTab === t.code ? T.gold : T.mute}>{t.spots}/{t.cap}</Mono>
            </div>
          </button>
        ))}
        <div style={{ flex: '0 0 auto', alignSelf: 'center', width: 1, height: 32, background: T.rule, margin: '0 8px' }} />
        <button onClick={() => setEventTab('add')} style={{
          flex: '0 0 auto', padding: '14px 18px', background: 'transparent',
          border: 'none',
          borderBottom: isAdd ? `2px solid ${T.gold}` : '2px solid transparent',
          cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4, minWidth: 130, color: 'inherit',
          transition: 'border-color .15s',
        }}>
          <Bebas size={17} color={isAdd ? T.gold : T.mute}>+ ADD LOCATION</Bebas>
          <Mono size={9} color={T.mute}>NEW STOP</Mono>
        </button>
        <button onClick={() => setEventTab('archive')} style={{
          flex: '0 0 auto', padding: '14px 18px', background: 'transparent',
          border: 'none',
          borderBottom: archiveTabHighlighted ? `2px solid ${T.gold}` : '2px solid transparent',
          cursor: 'pointer', display: 'flex', flexDirection: 'column', alignItems: 'flex-start', gap: 4, minWidth: 130, color: 'inherit',
          transition: 'border-color .15s',
        }}>
          <Bebas size={17} color={archiveTabHighlighted ? T.gold : T.mute}>↗ ARCHIVE</Bebas>
          <Mono size={9} color={T.mute}>{archivedEvents.length} {archivedEvents.length === 1 ? 'EVENT' : 'EVENTS'}</Mono>
        </button>
      </div>

      {isArchive ? (
        <ArchiveView events={archivedEvents} setEventTab={setEventTab} ev_mut={ev_mut} />
      ) : !isAdd && ev ? (
        <div style={{ padding: 32, flex: 1 }}>
          {ev.archived && (
            <div style={{ marginBottom: 18, padding: '12px 16px', border: `1px solid ${T.red}`, background: 'rgba(200,68,58,0.06)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <Mono size={11} color={T.red}>// ARCHIVED · this event is hidden from the public landing page</Mono>
              <button onClick={() => ev_mut.unarchive(ev.code)} style={{
                background: 'transparent', color: T.gold, border: `1px solid ${T.gold}`,
                padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
              }}>↻ UNARCHIVE</button>
            </div>
          )}
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline' }}>
            <div>
              <Bebas size={48}>{ev.city}, {ev.state}</Bebas>
              <Mono size={11} color={T.gold} style={{ display: 'block', marginTop: 8 }}>// {ev.date} · {ev.venue.toUpperCase()}</Mono>
            </div>
            <div style={{ display: 'flex', gap: 10 }}>
              <GhostCTA disabled title="Backend pending" style={{ fontSize: 13, padding: '10px 16px', opacity: 0.5, cursor: 'not-allowed' }}>+ Manual add</GhostCTA>
              <GhostCTA disabled title="Backend pending" style={{ fontSize: 13, padding: '10px 16px', opacity: 0.5, cursor: 'not-allowed' }}>QR code</GhostCTA>
              <PrimaryCTA blue disabled title="Backend pending" style={{ fontSize: 13, padding: '10px 16px', opacity: 0.5, cursor: 'not-allowed' }}>Export CSV</PrimaryCTA>
            </div>
          </div>

          {/* Edit event details */}
          <div style={{ marginTop: 24, padding: 18, border: `1px solid ${T.rule}` }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
              <Mono size={10} color={T.mute}>// EDIT EVENT DETAILS</Mono>
              <div style={{ display: 'flex', gap: 8 }}>
                {!ev.archived && (
                  <button onClick={() => { if (window.confirm(`Archive ${ev.city}, ${ev.state}? It will be hidden from the public site but kept in admin.`)) ev_mut.archive(ev.code); }} style={{
                    background: 'transparent', color: T.gold, border: `1px solid ${T.gold}`,
                    padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
                  }}>⌕ ARCHIVE EVENT</button>
                )}
                <button onClick={async () => {
                  // Two-step confirm — first asks generally, second asks specifically
                  // about candidate deletion if signups exist. ev.spots is the
                  // server-tracked count.
                  const hasSignups = (ev.spots || 0) > 0;
                  const firstMsg = hasSignups
                    ? `Delete ${ev.city}, ${ev.state}?\n\nThis will also DELETE ${ev.spots} candidate signup${ev.spots === 1 ? '' : 's'} and their waivers.\n\nThis cannot be undone.`
                    : `Delete ${ev.city}, ${ev.state}? This cannot be undone.`;
                  if (!window.confirm(firstMsg)) return;
                  if (hasSignups && !window.confirm(`Confirm: permanently delete ${ev.spots} candidate signup${ev.spots === 1 ? '' : 's'}?`)) return;
                  try {
                    await ev_mut.remove(ev.code, { force: hasSignups });
                    // Cache update fires eventschange; the EventsTab effect at the top
                    // will route us to the first active event (or 'add'/'archive').
                  } catch (e) {
                    alert(`Delete failed: ${e.message}`);
                  }
                }} style={{
                  background: 'transparent', color: T.red, border: `1px solid ${T.red}`,
                  padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
                }}>⌫ DELETE EVENT</button>
              </div>
            </div>
            {/* Location row */}
            <div style={{ marginTop: 10, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
              <AdminTextField label="CITY" value={ev.city} onChange={v => ev_mut.update(ev.code, { city: v })} />
              <AdminTextField label="STATE" value={ev.state} onChange={v => ev_mut.update(ev.code, { state: v })} />
              <div>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6, marginTop: 12 }}>
                  <Mono size={9} color={T.mute}>EVENT CODE · LOCKED</Mono>
                  <input value={ev.code} readOnly style={{
                    background: T.bg2, color: T.mute, border: `1px solid ${T.rule}`,
                    padding: '8px 10px', fontFamily: "'JetBrains Mono',monospace", fontSize: 13, outline: 'none',
                  }} />
                </div>
              </div>
            </div>
            {/* Venue */}
            <AdminTextField label="VENUE" value={ev.venue} onChange={v => ev_mut.update(ev.code, { venue: v })} />
            {/* Date / capacity / signups */}
            <div style={{ marginTop: 6, display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 14 }}>
              <AdminTextField label="DATE (DD MMM YYYY)" value={ev.date} onChange={v => {
                const parts = v.split(' ');
                const dateShort = parts.length >= 2 ? `${parts[0]} ${parts[1]}` : v;
                ev_mut.update(ev.code, { date: v, dateShort });
              }} />
              <AdminTextField label="CAPACITY" value={String(ev.cap)} onChange={v => {
                const n = parseInt(v, 10); if (!Number.isNaN(n) && n > 0) ev_mut.update(ev.code, { cap: n });
              }} />
              <AdminTextField label="CURRENT SIGNUPS" value={String(ev.spots)} onChange={v => {
                const n = parseInt(v, 10); if (!Number.isNaN(n) && n >= 0) ev_mut.update(ev.code, { spots: n });
              }} />
            </div>
            <Mono size={10} color={T.gold} style={{ display: 'block', marginTop: 12 }}>✓ AUTOSAVED</Mono>
          </div>

          <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14, marginTop: 24 }}>
            {[
              ['SIGNUPS', `${ev.spots}/${ev.cap}`, T.bone],
              ['WAIVERS', '14', T.gold],
              ['MINORS', '03', T.bone],
              ['NO-SHOW RISK', '06', T.red],
            ].map(([l,v,col]) => (
              <div key={l} style={{ padding: 18, border: `1px solid ${T.rule}` }}>
                <Mono size={10} color={T.mute}>{l}</Mono>
                <Bebas size={42} color={col} style={{ marginTop: 8 }}>{v}</Bebas>
              </div>
            ))}
          </div>

          <div style={{ marginTop: 32 }}>
            <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12 }}>
              <Mono size={11} color={T.mute}>// ROSTER</Mono>
              <input value={search} onChange={e => setSearch(e.target.value)} placeholder="SEARCH NAME / EMAIL"
                style={{ minWidth: 280, padding: '8px 12px', border: `1px solid ${T.rule}`, background: 'transparent', color: T.bone, fontFamily: "'JetBrains Mono',monospace", fontSize: 11, letterSpacing: 1, outline: 'none' }} />
            </div>
            <div style={{ border: `1px solid ${T.rule}` }}>
              <div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1.2fr 90px 70px 90px 110px 40px', padding: '12px 16px', borderBottom: `1px solid ${T.rule}`, background: T.bg2 }}>
                {['NAME','EMAIL','CAREER','AGE','WAIVER','CONFIRM #',''].map((c, i) => <Mono key={i} size={10} color={T.mute}>{c}</Mono>)}
              </div>
              {filtered.map((r, i) => (
                <div key={r.confirmationNumber || i} style={{ display: 'grid', gridTemplateColumns: '1.4fr 1.2fr 90px 70px 90px 110px 40px', padding: '14px 16px', borderBottom: i < filtered.length - 1 ? `1px solid ${T.rule}` : 'none', alignItems: 'center' }}>
                  <Body size={13} color={T.bone}>{r.name}</Body>
                  <Mono size={11} color={T.mute}>{r.email}</Mono>
                  <Mono size={11} color={T.gold}>{careerLabel(r)}</Mono>
                  <Mono size={11} color={r.minor ? T.gold : T.bone}>{r.age}{r.minor ? ' M' : ''}</Mono>
                  {r.waiver ? (
                    <button onClick={() => setWaiverViewing(r)} style={{
                      background: 'transparent', color: T.gold, border: 'none', padding: 0, textAlign: 'left',
                      fontFamily: "'JetBrains Mono',monospace", fontSize: 11, letterSpacing: 0.5, cursor: 'pointer',
                    }}
                      onMouseEnter={e => (e.currentTarget.style.textDecoration = 'underline')}
                      onMouseLeave={e => (e.currentTarget.style.textDecoration = 'none')}
                    >✓ VIEW</button>
                  ) : (
                    <Mono size={11} color={T.red}>—</Mono>
                  )}
                  <Mono size={11} color={T.mute}>...{r.num}</Mono>
                  <button
                    onClick={() => deleteCandidate(r)}
                    title={`Remove ${r.first || ''} ${r.last || ''}`.trim()}
                    style={{
                      background: 'transparent', color: T.red, border: `1px solid ${T.rule}`,
                      padding: '4px 8px', fontFamily: "'JetBrains Mono',monospace", fontSize: 11,
                      cursor: 'pointer', justifySelf: 'end',
                    }}
                    onMouseEnter={e => { e.currentTarget.style.borderColor = T.red; }}
                    onMouseLeave={e => { e.currentTarget.style.borderColor = T.rule; }}
                  >✕</button>
                </div>
              ))}
              {filtered.length === 0 && (
                <div style={{ padding: 24, textAlign: 'center' }}><Mono size={11} color={T.mute}>NO MATCHES</Mono></div>
              )}
            </div>
          </div>

          <ResourcesPanel />
        </div>
      ) : (
        <AddLocationForm
          events={events}
          createEvent={ev_mut.create}
          resetEvents={ev_mut.reset}
          onDone={(newCode) => {
            // Navigate to the newly created event (or fall back to first active).
            // The cache update has already been dispatched by ev_mut.create when
            // we land here; pick a sensible tab from the in-memory list.
            const fallback = events.find(e => !e.archived)?.code || events[0]?.code || 'archive';
            setEventTab(newCode || fallback);
          }}
        />
      )}

      {waiverViewing && ev && (
        <WaiverModal candidate={waiverViewing} event={ev} onClose={() => setWaiverViewing(null)} />
      )}
    </React.Fragment>
  );
};

// ── WEBPAGE TAB ───────────────────────────────────────────────────────
const WebpageTab = ({ about, setAbout, resetAbout }) => (
  <div style={{ padding: 32, flex: 1, maxWidth: 1080 }}>
    <div>
      <Mono size={11} color={T.gold}>// PUBLIC LANDING PAGE</Mono>
      <Bebas size={48} style={{ marginTop: 10 }}>Edit webpage.</Bebas>
      <Body size={13} color={T.mute} style={{ marginTop: 12, display: 'block', maxWidth: 640 }}>
        Everything that shows up on the public landing page, plus the
        confirmation email candidates receive after they sign up. Changes
        save to Cloudflare D1 and apply to the next page load / next email
        sent.
      </Body>
    </div>

    <HeroVideoEditor />
    <SlideshowEditor />
    <AboutEditor about={about} setAbout={setAbout} resetAbout={resetAbout} />
    <EmailTemplateEditor />
  </div>
);

// ── Email template editor — confirmation copy sent on signup ──────────
const EMAIL_PLACEHOLDERS = [
  ['{{firstName}}',         'First name'],
  ['{{lastName}}',          'Last name'],
  ['{{fullName}}',          'Full name'],
  ['{{confirmationNumber}}', 'Confirmation #'],
  ['{{eventLabel}}',        'City, ST — Date'],
  ['{{eventCity}}',         'Event city'],
  ['{{eventState}}',        'Event state'],
  ['{{eventDate}}',         'Event date'],
  ['{{eventVenue}}',        'Event venue'],
  ['{{eventStartTime}}',    'Show time (e.g. 0600)'],
];

// Sample data used to render the live preview pane.
const EMAIL_PREVIEW_VARS = {
  firstName: 'Alex',
  lastName: 'Reyes',
  fullName: 'Alex Reyes',
  confirmationNumber: 'TAC-A7K3X9',
  eventLabel: 'TACOMA, WA — 11 JUL 2026',
  eventCity: 'TACOMA',
  eventState: 'WA',
  eventDate: '11 JUL 2026',
  eventVenue: 'Joint Base Lewis–McChord · Site TBD',
  eventStartTime: '0600',
};

function _emailSubstitute(template, vars) {
  return String(template || '').replace(/\{\{\s*(\w+)\s*\}\}/g, (_, key) => vars[key] ?? '');
}

const EmailTemplateEditor = () => {
  const [template, setTemplate] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [saveStatus, setSaveStatus] = React.useState(''); // '', 'saving', 'saved', or error string
  const [previewMode, setPreviewMode] = React.useState('html'); // 'html' | 'text'
  const saveTimerRef = React.useRef(null);

  React.useEffect(() => {
    let cancelled = false;
    window.api.apiGet('/api/admin/email-template')
      .then(r => { if (!cancelled) { setTemplate(r.template); setLoading(false); } })
      .catch(err => { if (!cancelled) { setSaveStatus(`Load failed: ${err.message}`); setLoading(false); } });
    return () => { cancelled = true; };
  }, []);

  // Debounced save — fires 800ms after last keystroke.
  const scheduleSave = React.useCallback((next) => {
    setTemplate(next);
    setSaveStatus('saving');
    if (saveTimerRef.current) clearTimeout(saveTimerRef.current);
    saveTimerRef.current = setTimeout(async () => {
      try {
        const r = await window.api.apiPut('/api/admin/email-template', {
          subject: next.subject,
          htmlBody: next.htmlBody,
          textBody: next.textBody,
        });
        if (r?.template) { setTemplate(r.template); setSaveStatus('saved'); }
      } catch (err) {
        setSaveStatus(`Save failed: ${err.message}`);
      }
    }, 800);
  }, []);

  if (loading) {
    return (
      <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
        <Mono size={11} color={T.mute}>// CONFIRMATION EMAIL · loading…</Mono>
      </div>
    );
  }
  if (!template) {
    return (
      <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
        <Mono size={11} color={T.red}>// CONFIRMATION EMAIL · failed to load — {saveStatus}</Mono>
      </div>
    );
  }

  const previewSubject = _emailSubstitute(template.subject, EMAIL_PREVIEW_VARS);
  const previewHtml    = _emailSubstitute(template.htmlBody, EMAIL_PREVIEW_VARS);
  const previewText    = _emailSubstitute(template.textBody, EMAIL_PREVIEW_VARS);

  return (
    <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Mono size={11} color={T.gold}>// CONFIRMATION EMAIL</Mono>
        <Mono size={10} color={saveStatus === 'saving' ? T.gold : (saveStatus.startsWith('Save failed') ? T.red : T.mute)}>
          {saveStatus === 'saving' ? '· SAVING' : saveStatus === 'saved' ? '✓ SAVED' : (saveStatus || '')}
        </Mono>
      </div>
      <Body size={13} color={T.mute} style={{ marginTop: 10, display: 'block', maxWidth: 720 }}>
        Sent automatically when a candidate finishes signup. Use the placeholders below
        anywhere — they'll be filled in with the candidate's real details. Saves automatically.
      </Body>

      {/* Placeholder reference */}
      <div style={{ marginTop: 14, padding: 12, border: `1px solid ${T.rule}`, background: T.bg2 }}>
        <Mono size={10} color={T.mute}>// AVAILABLE PLACEHOLDERS</Mono>
        <div style={{ marginTop: 8, display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 8 }}>
          {EMAIL_PLACEHOLDERS.map(([token, label]) => (
            <div key={token} style={{ display: 'flex', justifyContent: 'space-between', gap: 8 }}>
              <Mono size={11} color={T.gold}>{token}</Mono>
              <Mono size={10} color={T.mute} style={{ textAlign: 'right' }}>{label}</Mono>
            </div>
          ))}
        </div>
      </div>

      {/* Subject */}
      <div style={{ marginTop: 18 }}>
        <Mono size={10} color={T.mute}>SUBJECT</Mono>
        <input
          value={template.subject}
          onChange={e => scheduleSave({ ...template, subject: e.target.value })}
          style={{
            marginTop: 6, width: '100%',
            background: 'transparent', color: T.bone, border: `1px solid ${T.rule}`, padding: '10px 12px',
            fontFamily: "'Inter',sans-serif", fontSize: 14, outline: 'none',
          }}
        />
      </div>

      {/* HTML body */}
      <div style={{ marginTop: 18 }}>
        <Mono size={10} color={T.mute}>HTML BODY</Mono>
        <textarea
          value={template.htmlBody}
          onChange={e => scheduleSave({ ...template, htmlBody: e.target.value })}
          rows={14}
          spellCheck={false}
          style={{
            marginTop: 6, width: '100%',
            background: T.bg2, color: T.bone, border: `1px solid ${T.rule}`, padding: '10px 12px',
            fontFamily: "'JetBrains Mono',monospace", fontSize: 12, lineHeight: 1.5,
            outline: 'none', resize: 'vertical',
          }}
        />
      </div>

      {/* Text body */}
      <div style={{ marginTop: 18 }}>
        <Mono size={10} color={T.mute}>TEXT BODY · plain-text fallback</Mono>
        <textarea
          value={template.textBody}
          onChange={e => scheduleSave({ ...template, textBody: e.target.value })}
          rows={8}
          spellCheck={false}
          style={{
            marginTop: 6, width: '100%',
            background: T.bg2, color: T.bone, border: `1px solid ${T.rule}`, padding: '10px 12px',
            fontFamily: "'JetBrains Mono',monospace", fontSize: 12, lineHeight: 1.5,
            outline: 'none', resize: 'vertical',
          }}
        />
      </div>

      {/* Preview */}
      <div style={{ marginTop: 24, border: `1px solid ${T.rule}` }}>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '12px 14px', background: T.bg2, borderBottom: `1px solid ${T.rule}` }}>
          <Mono size={10} color={T.gold}>// PREVIEW · sample candidate "Alex Reyes"</Mono>
          <div style={{ display: 'flex', gap: 4 }}>
            {['html', 'text'].map(m => (
              <button key={m} onClick={() => setPreviewMode(m)} style={{
                padding: '4px 12px',
                background: previewMode === m ? T.bone : 'transparent',
                color: previewMode === m ? T.bg : T.bone,
                border: `1px solid ${T.bone}`,
                fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
              }}>{m.toUpperCase()}</button>
            ))}
          </div>
        </div>
        <div style={{ padding: '12px 16px', background: T.bg2, borderBottom: `1px solid ${T.rule}` }}>
          <Mono size={10} color={T.mute}>SUBJECT</Mono>
          <Body size={14} color={T.bone} style={{ display: 'block', marginTop: 4 }}>{previewSubject}</Body>
        </div>
        {previewMode === 'html' ? (
          <iframe
            title="Email preview"
            srcDoc={previewHtml}
            style={{ width: '100%', height: 380, border: 'none', background: '#fff' }}
          />
        ) : (
          <pre style={{
            margin: 0, padding: 18, background: '#fff', color: '#111',
            fontFamily: "'JetBrains Mono',monospace", fontSize: 12, lineHeight: 1.5,
            whiteSpace: 'pre-wrap', wordWrap: 'break-word', minHeight: 380,
          }}>{previewText}</pre>
        )}
      </div>
    </div>
  );
};

// ── About-section editor (was the EDIT ABOUT tab) ─────────────────────
const AboutEditor = ({ about, setAbout, resetAbout }) => (
  <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
    <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
      <Mono size={11} color={T.gold}>// ABOUT THE EVENT · CARDS</Mono>
      <button onClick={() => { if (window.confirm('Reset all About-section copy to defaults?')) resetAbout(); }} style={{
        background: 'transparent', color: T.red, border: `1px solid ${T.red}`,
        padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
      }}>↺ RESET COPY</button>
    </div>

    {/* Headline + intro */}
    <div style={{ marginTop: 14, padding: 18, border: `1px solid ${T.rule}` }}>
      <Mono size={10} color={T.mute}>// HEADER</Mono>
      <AdminTextField label="HEADLINE" value={about.headline} onChange={v => setAbout({ headline: v })} />
      <AdminTextArea label="INTRO PARAGRAPH 1" value={about.intro[0]} onChange={v => setAbout({ intro: [v, about.intro[1]] })} rows={3} />
      <AdminTextArea label="INTRO PARAGRAPH 2" value={about.intro[1]} onChange={v => setAbout({ intro: [about.intro[0], v] })} rows={3} />
    </div>

    {/* What to Expect cards */}
    <div style={{ marginTop: 14, padding: 18, border: `1px solid ${T.rule}` }}>
      <Mono size={10} color={T.mute}>// WHAT TO EXPECT · 04 CARDS</Mono>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18, marginTop: 10 }}>
        {about.expect.map((b, i) => (
          <div key={i} style={{ padding: 14, border: `1px solid ${T.rule2}`, background: T.bg2 }}>
            <Mono size={10} color={T.gold}>EVENT · {b.num}</Mono>
            <AdminTextField label="TITLE" value={b.title} onChange={v => {
              const next = [...about.expect]; next[i] = { ...next[i], title: v }; setAbout({ expect: next });
            }} />
            <AdminTextArea label="BODY" value={b.body} onChange={v => {
              const next = [...about.expect]; next[i] = { ...next[i], body: v }; setAbout({ expect: next });
            }} rows={5} />
          </div>
        ))}
      </div>
    </div>

    {/* Who Should Come rows */}
    <div style={{ marginTop: 14, padding: 18, border: `1px solid ${T.rule}` }}>
      <Mono size={10} color={T.mute}>// WHO SHOULD COME · 04 ROWS</Mono>
      {about.who.map((w, i) => (
        <div key={i} style={{ display: 'grid', gridTemplateColumns: '180px 1fr', gap: 14, marginTop: 10 }}>
          <AdminTextField label="LABEL" value={w.label} onChange={v => {
            const next = [...about.who]; next[i] = { ...next[i], label: v }; setAbout({ who: next });
          }} />
          <AdminTextArea label="TEXT" value={w.text} onChange={v => {
            const next = [...about.who]; next[i] = { ...next[i], text: v }; setAbout({ who: next });
          }} rows={3} />
        </div>
      ))}
    </div>

    {/* Bottom line */}
    <div style={{ marginTop: 14, padding: 18, border: `1px solid ${T.rule}` }}>
      <Mono size={10} color={T.mute}>// THE BOTTOM LINE</Mono>
      <AdminTextArea label="BODY" value={about.bottomLine} onChange={v => setAbout({ bottomLine: v })} rows={3} />
    </div>

    <Mono size={10} color={T.gold} style={{ display: 'block', marginTop: 14 }}>✓ AUTOSAVED</Mono>
  </div>
);

// ── Hero video uploader ───────────────────────────────────────────────
const HeroVideoEditor = () => {
  const [hero, hero_mut] = useHeroVideo();
  const fileRef = React.useRef(null);
  const onPick = (e) => {
    const file = e.target.files?.[0];
    if (file) hero_mut.upload(file);
    e.target.value = '';
  };
  // Custom video is whenever the server has metadata for one. The default
  // (media/hero.mp4) shows when no upload has happened on the backend.
  const usingDefault = !hero.meta;
  return (
    <div style={{ marginTop: 0, paddingBottom: 4 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 28 }}>
        <Mono size={11} color={T.gold}>// HERO VIDEO</Mono>
        {!usingDefault && (
          <button onClick={() => { if (window.confirm('Revert hero video to the default?')) hero_mut.clear(); }} style={{
            background: 'transparent', color: T.mute, border: `1px solid ${T.rule}`,
            padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
          }}>↺ REVERT TO DEFAULT</button>
        )}
      </div>
      <Body size={13} color={T.mute} style={{ marginTop: 10, display: 'block', maxWidth: 640 }}>
        Replace the looping video shown behind the hero on the landing page. MP4 / WebM / MOV.
      </Body>

      <div style={{ marginTop: 14, display: 'grid', gridTemplateColumns: '320px 1fr', gap: 18, alignItems: 'start' }}>
        {/* Preview */}
        <div style={{ aspectRatio: '16 / 9', border: `1px solid ${T.rule}`, background: T.bg2, overflow: 'hidden', position: 'relative' }}>
          <video
            key={hero.src}
            src={hero.src} autoPlay muted loop playsInline
            style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }}
          />
          <div style={{ position: 'absolute', top: 8, right: 8, padding: '3px 6px', background: 'rgba(14,14,16,0.7)' }}>
            <Mono size={9} color={usingDefault ? T.mute : T.gold}>{usingDefault ? 'DEFAULT' : 'CUSTOM'}</Mono>
          </div>
        </div>

        {/* Meta + upload */}
        <div>
          <Mono size={10} color={T.mute}>CURRENT</Mono>
          {usingDefault ? (
            <Body size={13} color={T.bone} style={{ display: 'block', marginTop: 4 }}>media/hero.mp4 (default)</Body>
          ) : (
            <React.Fragment>
              <Body size={13} color={T.bone} style={{ display: 'block', marginTop: 4 }}>{hero.meta.filename}</Body>
              <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 2 }}>{formatFileSize(hero.meta.size)} · {hero.meta.type}</Mono>
            </React.Fragment>
          )}

          <div style={{ marginTop: 16 }}>
            <input ref={fileRef} type="file" accept="video/mp4,video/webm,video/quicktime,video/*" onChange={onPick}
              style={{ position: 'absolute', left: -9999, opacity: 0 }} />
            <button onClick={() => fileRef.current?.click()} style={{
              padding: '12px 22px',
              background: T.gold, color: T.bg, border: 'none',
              fontFamily: "'Bebas Neue',sans-serif", fontSize: 14, letterSpacing: 1.2, cursor: 'pointer',
            }}>+ Upload new video</button>
            <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 8, maxWidth: 480 }}>
              Plays live during this admin session. Production backend will replace media/hero.mp4 permanently on save.
            </Mono>
          </div>
        </div>
      </div>
    </div>
  );
};

// ── Slideshow uploader ────────────────────────────────────────────────
const SlideshowEditor = () => {
  const [slides, sl_mut] = useSlideshow();
  const fileRef = React.useRef(null);
  const [editingId, setEditingId] = React.useState(null);
  const [editingName, setEditingName] = React.useState('');

  const onPick = async (e) => {
    const files = Array.from(e.target.files || []);
    for (const f of files) await sl_mut.add(f);
    e.target.value = '';
  };
  const startRename = (s) => { setEditingId(s.id); setEditingName(s.name); };
  const commitRename = () => {
    if (editingId && editingName.trim()) sl_mut.rename(editingId, editingName.trim());
    setEditingId(null); setEditingName('');
  };

  return (
    <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Mono size={11} color={T.gold}>// SLIDESHOW</Mono>
        <button onClick={() => { if (window.confirm('Reset slideshow images to defaults?')) sl_mut.reset(); }} style={{
          background: 'transparent', color: T.mute, border: `1px solid ${T.rule}`,
          padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
        }}>↺ RESET LIST</button>
      </div>
      <Body size={13} color={T.mute} style={{ marginTop: 10, display: 'block', maxWidth: 640 }}>
        Images shown in the &quot;What is Training Day?&quot; section. Multiple images crossfade automatically every 5 seconds. PNG / JPG / WebP.
      </Body>

      {/* Upload */}
      <div style={{ marginTop: 14 }}>
        <input ref={fileRef} type="file" multiple accept="image/png,image/jpeg,image/webp,image/*" onChange={onPick}
          style={{ position: 'absolute', left: -9999, opacity: 0 }} />
        <button onClick={() => fileRef.current?.click()} style={{
          padding: '12px 22px',
          background: T.gold, color: T.bg, border: 'none',
          fontFamily: "'Bebas Neue',sans-serif", fontSize: 14, letterSpacing: 1.2, cursor: 'pointer',
        }}>+ Upload image(s)</button>
        <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 8 }}>
          Stored in this browser as data URLs until the backend takes over.
        </Mono>
      </div>

      {/* Grid of slides */}
      <div style={{ marginTop: 18, display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(220px, 1fr))', gap: 12 }}>
        {slides.length === 0 && (
          <div style={{ padding: 22, textAlign: 'center', border: `1px solid ${T.rule}`, gridColumn: '1 / -1' }}>
            <Mono size={11} color={T.mute}>NO IMAGES · upload one to start</Mono>
          </div>
        )}
        {slides.map(s => (
          <div key={s.id} style={{ border: `1px solid ${T.rule}`, background: T.bg2 }}>
            <div style={{ aspectRatio: '4 / 3', background: T.bone, position: 'relative', overflow: 'hidden' }}>
              <img src={s.src} alt={s.name}
                style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'contain' }} />
            </div>
            <div style={{ padding: 10 }}>
              {editingId === s.id ? (
                <input
                  autoFocus
                  value={editingName}
                  onChange={e => setEditingName(e.target.value)}
                  onBlur={commitRename}
                  onKeyDown={e => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') { setEditingId(null); setEditingName(''); } }}
                  style={{
                    width: '100%', background: 'transparent', color: T.bone,
                    border: `1px solid ${T.gold}`, padding: '4px 6px',
                    fontFamily: "'Inter',sans-serif", fontSize: 12, outline: 'none',
                  }}
                />
              ) : (
                <button onClick={() => startRename(s)} title="Click to rename" style={{
                  background: 'transparent', border: 'none', textAlign: 'left', cursor: 'text', padding: 0, color: 'inherit', width: '100%',
                }}>
                  <Body size={12} color={T.bone} style={{ display: 'block', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{s.name}</Body>
                </button>
              )}
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: 6 }}>
                <Mono size={9} color={T.mute}>{s.size > 0 ? formatFileSize(s.size) : '—'}</Mono>
                <button onClick={() => sl_mut.remove(s.id)} title="Remove" style={{
                  background: 'transparent', color: T.red, border: `1px solid ${T.rule}`,
                  padding: '3px 8px', fontFamily: "'JetBrains Mono',monospace", fontSize: 9, letterSpacing: 1, cursor: 'pointer',
                }}>✕</button>
              </div>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

// ── Waiver modal — pulls up an individual signed waiver from the roster ─
const WaiverModal = ({ candidate, event, onClose }) => {
  // Lock body scroll while open + Esc to close.
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    const onKey = (e) => { if (e.key === 'Escape') onClose?.(); };
    window.addEventListener('keydown', onKey);
    return () => {
      document.body.style.overflow = prev;
      window.removeEventListener('keydown', onKey);
    };
  }, [onClose]);

  const fullName = `${candidate.first || ''} ${candidate.last || ''}`.trim() || candidate.name;
  const waiverText = window.WAIVER_TEXT || [];

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, zIndex: 100,
      background: 'rgba(10, 10, 10, 0.85)',
      display: 'flex', alignItems: 'center', justifyContent: 'center',
      padding: 24,
    }}>
      <div onClick={e => e.stopPropagation()} role="dialog" aria-label="Signed waiver" style={{
        background: T.bg2, border: `1px solid ${T.gold}`,
        maxWidth: 760, width: '100%', maxHeight: '90vh', overflowY: 'auto',
        padding: 28,
      }}>
        {/* Header */}
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 18 }}>
          <div>
            <Mono size={11} color={T.gold}>// LIABILITY RELEASE · CONFIRM #...{candidate.num}</Mono>
            <Bebas size={32} style={{ marginTop: 8 }}>{fullName.toUpperCase()}</Bebas>
            <Mono size={11} color={T.mute} style={{ display: 'block', marginTop: 6 }}>
              {event.city}, {event.state} · {event.date}
            </Mono>
            <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 2 }}>
              {event.venue}
            </Mono>
          </div>
          <button onClick={onClose} aria-label="Close" style={{
            background: 'transparent', color: T.bone, border: `1px solid ${T.rule}`,
            padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 11, letterSpacing: 1, cursor: 'pointer',
          }}>✕ CLOSE</button>
        </div>

        {/* Candidate metadata strip */}
        <div style={{ marginTop: 18, padding: 14, border: `1px solid ${T.rule}`, background: T.bg, display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 14 }}>
          {[
            ['EMAIL', candidate.email, T.bone],
            ['CAREER', (Array.isArray(candidate.careers) ? candidate.careers : (candidate.career ? [candidate.career] : [])).join(' · ') || '—', T.gold],
            ['AGE', candidate.age + (candidate.minor ? ' · MINOR' : ''), candidate.minor ? T.gold : T.bone],
            ['SIGNED', candidate.signedAt || '—', T.bone],
          ].map(([l, v, col]) => (
            <div key={l}>
              <Mono size={9} color={T.mute}>{l}</Mono>
              <Mono size={11} color={col} style={{ display: 'block', marginTop: 4 }}>{v}</Mono>
            </div>
          ))}
        </div>

        {/* Waiver body */}
        <div style={{ marginTop: 18, padding: 20, border: `1px solid ${T.rule}`, background: T.bg }}>
          <Mono size={9} color={T.gold} style={{ display: 'block', marginBottom: 4 }}>// LIABILITY RELEASE AND EXPRESS ASSUMPTION OF RISK</Mono>
          <Mono size={9} color={T.mute} style={{ display: 'block', marginBottom: 12 }}>AIR FORCE SPECIAL WARFARE TRAINING ACTIVITIES · {event.city}, {event.state} · {event.date}</Mono>
          <Body size={13} color={T.bone}>
            {window.WAIVER_OPENER_BEFORE || 'I, '}<span style={{ color: T.gold }}>{fullName}</span>{window.WAIVER_OPENER_AFTER || ''}
          </Body>
          {waiverText.map((p, i) => (
            <Body key={i} size={13} color={T.bone} style={{ display: 'block', marginTop: 12 }}>{p}</Body>
          ))}
        </div>

        {/* Candidate signature */}
        <div style={{ marginTop: 22 }}>
          <Mono size={10} color={T.mute}>// CANDIDATE SIGNATURE</Mono>
          <div style={{ marginTop: 8, padding: '18px 18px 14px', border: `1px solid ${T.rule}`, background: T.bg }}>
            <span style={{ fontFamily: "'Caveat',cursive", fontSize: 36, color: T.bone, lineHeight: 1.1 }}>
              {fullName}
            </span>
            <Mono size={9} color={T.mute} style={{ display: 'block', marginTop: 10 }}>
              SIGNED · {(candidate.sigMode || 'TYPE').toUpperCase()} · {candidate.signedAt || '—'}
            </Mono>
          </div>

          {/* Guardian signature for minors */}
          {candidate.minor && candidate.guardianName && (
            <React.Fragment>
              <Mono size={10} color={T.gold} style={{ display: 'block', marginTop: 18 }}>// GUARDIAN CO-SIGNATURE</Mono>
              <div style={{ marginTop: 8, padding: '18px 18px 14px', border: `1px solid ${T.gold}`, background: 'rgba(200,168,78,0.06)' }}>
                <span style={{ fontFamily: "'Caveat',cursive", fontSize: 32, color: T.bone, lineHeight: 1.1 }}>
                  {candidate.guardianName}
                </span>
                <Mono size={9} color={T.mute} style={{ display: 'block', marginTop: 10 }}>
                  GUARDIAN · TYPE · {candidate.signedAt || '—'} · {candidate.guardianEmail || ''}
                </Mono>
              </div>
            </React.Fragment>
          )}
        </div>

        {/* Footer actions */}
        <div style={{ marginTop: 24, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
          <Mono size={10} color={T.mute}>USAF / AFSPECWAR · Liability release</Mono>
          <div style={{ display: 'flex', gap: 10 }}>
            <GhostCTA style={{ fontSize: 13, padding: '10px 16px' }} onClick={() => window.alert('PDF download will be wired up to the backend.')}>↓ Download PDF</GhostCTA>
            <PrimaryCTA blue style={{ fontSize: 13, padding: '10px 16px' }} onClick={onClose}>Close</PrimaryCTA>
          </div>
        </div>
      </div>
    </div>
  );
};

// ── Archive view — past events grouped by year, with inline unarchive ─
const ArchiveView = ({ events, setEventTab, ev_mut }) => {
  // Group events by the year extracted from `event.date` (e.g., "11 JUL 2026" → "2026").
  const groups = events.reduce((acc, e) => {
    const m = e.date && e.date.match(/(\d{4})/);
    const year = m ? m[1] : 'Unknown';
    (acc[year] = acc[year] || []).push(e);
    return acc;
  }, {});
  const years = Object.keys(groups).sort().reverse(); // most recent first

  return (
    <div style={{ padding: 32, flex: 1 }}>
      <div>
        <Mono size={11} color={T.gold}>// ARCHIVE</Mono>
        <Bebas size={48} style={{ marginTop: 10 }}>Past events.</Bebas>
        <Body size={13} color={T.mute} style={{ marginTop: 12, display: 'block', maxWidth: 640 }}>
          Events you've archived, grouped by year. Click an event to view its details and roster, or unarchive to bring it back to the active list.
        </Body>
      </div>

      {events.length === 0 && (
        <div style={{ marginTop: 32, padding: '32px 24px', border: `1px solid ${T.rule}`, textAlign: 'center' }}>
          <Mono size={11} color={T.mute}>NO ARCHIVED EVENTS</Mono>
          <Body size={12} color={T.mute} style={{ marginTop: 8, display: 'block' }}>
            When you archive an event, it'll move here and be hidden from the public site.
          </Body>
        </div>
      )}

      {years.map(year => (
        <div key={year} style={{ marginTop: 28 }}>
          <div style={{ display: 'flex', alignItems: 'baseline', gap: 14 }}>
            <Bebas size={32}>{year}</Bebas>
            <Mono size={10} color={T.mute}>{groups[year].length} {groups[year].length === 1 ? 'event' : 'events'}</Mono>
          </div>
          <div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 8 }}>
            {groups[year].map(e => (
              <div key={e.code} style={{
                display: 'grid', gridTemplateColumns: '1fr 200px 130px 140px',
                alignItems: 'center', gap: 16,
                border: `1px solid ${T.rule}`, background: T.bg2,
              }}>
                <button onClick={() => setEventTab(e.code)} style={{
                  background: 'transparent', border: 'none', textAlign: 'left',
                  padding: '14px 18px', cursor: 'pointer', color: 'inherit',
                  display: 'flex', flexDirection: 'column', gap: 4,
                }}>
                  <Bebas size={20}>{e.city}, {e.state}</Bebas>
                  <Mono size={9} color={T.mute}>{e.code} · click to view details</Mono>
                </button>
                <Mono size={11} color={T.mute}>{e.date}</Mono>
                <Mono size={11} color={T.mute}>{e.spots} signups</Mono>
                <div style={{ paddingRight: 14, justifySelf: 'end' }}>
                  <button onClick={() => ev_mut.unarchive(e.code)} style={{
                    background: 'transparent', color: T.gold, border: `1px solid ${T.gold}`,
                    padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
                  }}>↻ UNARCHIVE</button>
                </div>
              </div>
            ))}
          </div>
        </div>
      ))}
    </div>
  );
};

// ── Resources panel — files attached to event confirmation emails ─────
const ResourcesPanel = () => {
  const [resources, res_mut] = useResources();
  const fileInputRef = React.useRef(null);
  const [editingId, setEditingId] = React.useState(null);
  const [editingName, setEditingName] = React.useState('');

  const onFilePick = (e) => {
    const files = Array.from(e.target.files || []);
    files.forEach(f => res_mut.add(f));
    e.target.value = '';
  };
  const startRename = (r) => { setEditingId(r.id); setEditingName(r.name); };
  const commitRename = () => {
    if (editingId && editingName.trim()) res_mut.rename(editingId, editingName.trim());
    setEditingId(null); setEditingName('');
  };

  return (
    <div style={{ marginTop: 48, paddingTop: 22, borderTop: `1px solid ${T.rule}` }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
        <Mono size={11} color={T.gold}>// RESOURCES</Mono>
        <button onClick={() => { if (window.confirm('Reset resource list to defaults?')) res_mut.reset(); }} style={{
          background: 'transparent', color: T.mute, border: `1px solid ${T.rule}`,
          padding: '6px 12px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
        }}>↺ RESET LIST</button>
      </div>
      <Body size={13} color={T.mute} style={{ marginTop: 10, display: 'block', maxWidth: 640 }}>
        Files attached to every confirmation email and shown on the candidate&apos;s confirmation page.
      </Body>

      <div style={{ marginTop: 16 }}>
        <input
          ref={fileInputRef} type="file" multiple
          onChange={onFilePick}
          style={{ position: 'absolute', left: -9999, opacity: 0 }}
          accept=".pdf,.doc,.docx,.txt,.png,.jpg,.jpeg,.zip"
        />
        <button onClick={() => fileInputRef.current?.click()} style={{
          padding: '12px 22px',
          background: T.gold, color: T.bg, border: 'none',
          fontFamily: "'Bebas Neue',sans-serif", fontSize: 14, letterSpacing: 1.2, cursor: 'pointer',
        }}>+ Upload file(s)</button>
        <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 8 }}>
          PDF / DOCX / TXT / IMG / ZIP. Metadata is captured here; the backend will store actual file bytes.
        </Mono>
      </div>

      <div style={{ marginTop: 18, border: `1px solid ${T.rule}` }}>
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 220px 90px 80px', padding: '10px 14px', borderBottom: `1px solid ${T.rule}`, background: T.bg2 }}>
          <Mono size={10} color={T.mute}>NAME</Mono>
          <Mono size={10} color={T.mute}>FILENAME</Mono>
          <Mono size={10} color={T.mute}>SIZE</Mono>
          <Mono size={10} color={T.mute} style={{ textAlign: 'right' }}>ACTIONS</Mono>
        </div>
        {resources.length === 0 && (
          <div style={{ padding: 22, textAlign: 'center' }}>
            <Mono size={11} color={T.mute}>NO RESOURCES · upload one to start</Mono>
          </div>
        )}
        {resources.map((r, i) => (
          <div key={r.id} style={{
            display: 'grid', gridTemplateColumns: '1fr 220px 90px 80px',
            padding: '12px 14px', borderBottom: i < resources.length - 1 ? `1px solid ${T.rule}` : 'none',
            alignItems: 'center', gap: 10,
          }}>
            {editingId === r.id ? (
              <input
                autoFocus
                value={editingName}
                onChange={e => setEditingName(e.target.value)}
                onBlur={commitRename}
                onKeyDown={e => { if (e.key === 'Enter') commitRename(); if (e.key === 'Escape') { setEditingId(null); setEditingName(''); } }}
                style={{
                  background: 'transparent', color: T.bone,
                  border: `1px solid ${T.gold}`, padding: '6px 8px',
                  fontFamily: "'Inter',sans-serif", fontSize: 13, outline: 'none',
                }}
              />
            ) : (
              <button onClick={() => startRename(r)} title="Click to rename" style={{
                background: 'transparent', border: 'none', textAlign: 'left', cursor: 'text', padding: 0, color: 'inherit',
              }}>
                <Body size={13} color={T.bone}>{r.name}</Body>
              </button>
            )}
            <Mono size={10} color={T.mute} style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.filename}</Mono>
            <Mono size={10} color={T.mute}>{formatFileSize(r.size)}</Mono>
            <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 6 }}>
              <button onClick={() => res_mut.remove(r.id)} title="Remove" style={{
                background: 'transparent', color: T.red, border: `1px solid ${T.rule}`,
                padding: '4px 10px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
              }}>✕</button>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
};

// ── Add Location form (sub-tab inside EVENTS) ─────────────────────────
const AddLocationForm = ({ events, createEvent, resetEvents, onDone }) => {
  const [draft, setDraft] = React.useState({
    city: '', state: '', code: '', cap: '50', date: '', venue: '',
    startTime: '', venueAddress: '',
  });
  const [busy, setBusy] = React.useState(false);
  const [submitError, setSubmitError] = React.useState('');
  const set = (k) => (v) => setDraft(d => ({ ...d, [k]: typeof v === 'string' ? v : v.target.value }));
  const codeUpper = draft.code.toUpperCase();
  const codeTaken = codeUpper && events.some(e => e.code === codeUpper);
  const capNum = parseInt(draft.cap, 10);
  const validCap = !Number.isNaN(capNum) && capNum > 0;
  const valid = draft.city.trim() && draft.state.trim() && codeUpper.length >= 2 && codeUpper.length <= 4 && !codeTaken && validCap && draft.date.trim();

  const submit = async () => {
    if (!valid || busy) return;
    setBusy(true); setSubmitError('');
    const parts = draft.date.trim().split(' ');
    const dateShort = parts.length >= 2 ? `${parts[0]} ${parts[1]}` : draft.date.trim();
    const newEvent = {
      code: codeUpper,
      city: draft.city.trim().toUpperCase(),
      state: draft.state.trim().toUpperCase(),
      date: draft.date.trim(),
      dateShort,
      spots: 0,
      cap: capNum,
      venue: draft.venue.trim() || 'Venue TBD',
      address: draft.venueAddress.trim() || null,
      startTime: draft.startTime.trim() || null,
    };
    try {
      await createEvent(newEvent);
      onDone?.(newEvent.code);
    } catch (e) {
      setSubmitError(e.message || 'Failed to create event.');
    } finally {
      setBusy(false);
    }
  };

  return (
    <div style={{ padding: 40, maxWidth: 820, flex: 1 }}>
      <Mono size={11} color={T.gold}>// NEW EVENT STOP</Mono>
      <Bebas size={48} style={{ marginTop: 10 }}>Add location.</Bebas>
      <Body size={13} color={T.mute} style={{ marginTop: 12 }}>
        Spin up a new tour stop. Confirmation numbers (AFSW-XXX-...) auto-generate from the event code.
      </Body>
      <div style={{ marginTop: 28, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 18 }}>
        <Field label="CITY" ph="PHOENIX" value={draft.city} onChange={set('city')} />
        <Field label="STATE" ph="AZ" value={draft.state} onChange={set('state')} />
        <Field label="VENUE NAME" ph="TBD" value={draft.venue} onChange={set('venue')} style={{ gridColumn: '1 / -1' }} />
        <Field label="VENUE ADDRESS" ph="TBD" value={draft.venueAddress} onChange={set('venueAddress')} style={{ gridColumn: '1 / -1' }} />
        <Field label="DATE" ph="DD MMM 2026" value={draft.date} onChange={set('date')} />
        <Field label="START TIME" ph="0500" value={draft.startTime} onChange={set('startTime')} />
        <Field label="EVENT CODE" ph="PHX (3 letters)" value={draft.code} onChange={set('code')} error={codeTaken ? 'CODE IN USE' : undefined} />
        <Field label="CAPACITY" ph="50" value={draft.cap} onChange={set('cap')} error={!validCap && draft.cap ? 'NUMBER' : undefined} />
      </div>
      <Mono size={10} color={T.mute} style={{ display: 'block', marginTop: 12 }}>
        Start time and venue address are captured for backend handoff but not yet persisted in this prototype.
      </Mono>
      <div style={{ display: 'flex', gap: 12, marginTop: 28, alignItems: 'center' }}>
        <GhostCTA onClick={() => onDone?.(events[0]?.code)}>Cancel</GhostCTA>
        <PrimaryCTA blue disabled={!valid || busy} onClick={submit} style={{ opacity: busy ? 0.6 : 1 }}>
          {busy ? 'Creating…' : 'Create stop →'}
        </PrimaryCTA>
        {submitError && <Mono size={10} color={T.red}>{submitError}</Mono>}
      </div>

      <div style={{ marginTop: 48, paddingTop: 22, borderTop: `1px solid ${T.rule}` }}>
        <Mono size={10} color={T.mute}>// DANGER ZONE</Mono>
        <Body size={12} color={T.mute} style={{ marginTop: 6, display: 'block' }}>
          Reset all events to defaults. Removes any locations you've added and unarchives the originals.
        </Body>
        <button onClick={() => { if (window.confirm('Reset all events to defaults? Custom locations will be deleted.')) resetEvents?.(); }} style={{
          marginTop: 10,
          background: 'transparent', color: T.red, border: `1px solid ${T.red}`,
          padding: '8px 14px', fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer',
        }}>↺ RESET EVENTS TO DEFAULTS</button>
      </div>
    </div>
  );
};

// ── Top-level Admin shell ─────────────────────────────────────────────
const Admin = ({ go }) => {
  const [primaryTab, setPrimaryTab] = React.useState('events'); // 'events' | 'webpage'
  const [events, ev_mut] = useEventsStore();
  // Initial sub-tab: first non-archived event code.
  const [eventTab, setEventTab] = React.useState(() => {
    const first = events.find(e => !e.archived) || events[0];
    return first?.code || 'add';
  });
  const [search, setSearch] = React.useState('');
  const [about, setAbout, resetAbout] = useAboutContent();

  return (
    <div style={{ height: '100%', overflowY: 'auto', background: T.bg, color: T.bone, display: 'flex', flexDirection: 'column' }}>
      {/* Top bar — brand on left, primary nav center-left, account info right */}
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '14px 32px', borderBottom: `1px solid ${T.rule}`, background: T.bg2 }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 36 }}>
          <SWLockup size={0.95} />
          <div style={{ display: 'flex', gap: 24, alignItems: 'center' }}>
            {[
              { key: 'events',  label: 'EVENTS' },
              { key: 'webpage', label: 'WEBPAGE' },
            ].map(t => (
              <button
                key={t.key}
                onClick={() => setPrimaryTab(t.key)}
                style={{
                  background: 'transparent', border: 'none', cursor: 'pointer',
                  padding: '4px 0',
                  fontFamily: "'JetBrains Mono',monospace", fontSize: 11, letterSpacing: 1.4,
                  color: primaryTab === t.key ? T.bone : T.mute,
                  borderBottom: primaryTab === t.key ? `1px solid ${T.gold}` : '1px solid transparent',
                  transition: 'color .15s, border-color .15s',
                }}
              >
                {t.label}
              </button>
            ))}
          </div>
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 18 }}>
          <button onClick={() => go('landing')} style={{ background: 'transparent', border: 'none', color: T.mute, fontFamily: "'JetBrains Mono',monospace", fontSize: 10, letterSpacing: 1, cursor: 'pointer' }}>← PUBLIC SITE</button>
        </div>
      </div>

      {primaryTab === 'events' ? (
        <EventsTab
          events={events} ev_mut={ev_mut}
          eventTab={eventTab} setEventTab={setEventTab}
          search={search} setSearch={setSearch}
        />
      ) : (
        <WebpageTab about={about} setAbout={setAbout} resetAbout={resetAbout} />
      )}
    </div>
  );
};

window.Admin = Admin;
