// 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, saveAbout, 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. Edit any
        field, then click Save changes — your edits don't go live until you
        click save.
      </Body>
    </div>

    <HeroVideoEditor />
    <SlideshowEditor />
    <AboutEditor about={about} saveAbout={saveAbout} 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 = () => {
  // Local-draft pattern (same as AboutEditor): typing only updates `draft`,
  // nothing leaves the browser until "Save changes" is clicked. Compare
  // draft against `serverState` to know if there are unsaved edits.
  const [serverState, setServerState] = React.useState(null);
  const [draft, setDraft] = React.useState(null);
  const [loading, setLoading] = React.useState(true);
  const [saving, setSaving] = React.useState(false);
  const [savedAt, setSavedAt] = React.useState(null);
  const [error, setError] = React.useState('');
  const [previewMode, setPreviewMode] = React.useState('html'); // 'html' | 'text'

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

  const dirty = !!serverState && !!draft && (
    draft.subject !== serverState.subject ||
    draft.htmlBody !== serverState.htmlBody ||
    draft.textBody !== serverState.textBody
  );

  const save = async () => {
    if (!dirty || saving) return;
    setSaving(true);
    setError('');
    try {
      const r = await window.api.apiPut('/api/admin/email-template', {
        subject: draft.subject,
        htmlBody: draft.htmlBody,
        textBody: draft.textBody,
      });
      if (r?.template) {
        setServerState(r.template);
        setDraft(r.template);
      }
      setSavedAt(new Date());
    } catch (err) {
      setError(`Save failed: ${err.message}`);
    } finally {
      setSaving(false);
    }
  };

  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 (!draft) {
    return (
      <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
        <Mono size={11} color={T.red}>// CONFIRMATION EMAIL · failed to load — {error}</Mono>
      </div>
    );
  }

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

  return (
    <div style={{ marginTop: 36, paddingTop: 28, borderTop: `1px solid ${T.rule}` }}>
      <Mono size={11} color={T.gold}>// CONFIRMATION EMAIL</Mono>
      <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. Edit any field
        below; click Save changes when you're done.
      </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={draft.subject}
          onChange={e => setDraft({ ...draft, 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={draft.htmlBody}
          onChange={e => setDraft({ ...draft, 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={draft.textBody}
          onChange={e => setDraft({ ...draft, 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>

      {/* Save bar — same pattern as AboutEditor */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, marginTop: 18, padding: '14px 18px', border: `1px solid ${T.rule}`, background: T.bg2 }}>
        <Mono size={10} color={error ? T.red : (dirty ? T.gold : T.mute)}>
          {error ? `· ${error}` : dirty ? '· UNSAVED CHANGES' : (savedAt ? `✓ SAVED @ ${savedAt.toLocaleTimeString()}` : '✓ UP TO DATE')}
        </Mono>
        <button
          onClick={save}
          disabled={!dirty || saving}
          style={{
            padding: '10px 22px', cursor: (!dirty || saving) ? 'not-allowed' : 'pointer',
            background: dirty ? T.gold : 'transparent',
            color: dirty ? T.bg : T.mute,
            border: `1px solid ${dirty ? T.gold : T.rule}`,
            fontFamily: "'Bebas Neue',sans-serif", fontSize: 14, letterSpacing: 1.4,
            opacity: saving ? 0.7 : 1,
          }}
        >{saving ? 'Saving…' : (dirty ? 'Save changes' : '✓ Saved')}</button>
      </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) ─────────────────────
// AboutEditor — local-draft pattern. The user's typing only updates `draft`
// (component state). Nothing leaves the browser until they click "Save
// changes". This eliminates the auto-save race that was making fields look
// like they were rejecting input mid-keystroke.
const AboutEditor = ({ about, saveAbout, resetAbout }) => {
  const [draft, setDraft] = React.useState(about);
  const [saving, setSaving] = React.useState(false);
  const [savedAt, setSavedAt] = React.useState(null);
  const [error, setError] = React.useState('');

  // When the server-side `about` changes (e.g. initial fetch resolves, or a
  // save round-trip echoes a normalized version back), reset the draft to
  // match. Skip if the user has unsaved edits to avoid clobbering them.
  React.useEffect(() => {
    setDraft(prev => {
      // If draft was never touched (matches old about), accept the new about.
      // Otherwise keep the draft so we don't blow away in-progress edits.
      const dirty = JSON.stringify(prev) !== JSON.stringify(about);
      return dirty ? prev : about;
    });
  }, [about]);

  const dirty = JSON.stringify(draft) !== JSON.stringify(about);

  const save = async () => {
    if (!dirty || saving) return;
    setSaving(true);
    setError('');
    try {
      await saveAbout(draft);
      setSavedAt(new Date());
    } catch (e) {
      setError(e.message || String(e));
    } finally {
      setSaving(false);
    }
  };

  const setField = (patch) => setDraft(d => ({ ...d, ...patch }));

  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}>// 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={draft.headline} onChange={v => setField({ headline: v })} />
        <AdminTextArea label="INTRO PARAGRAPH 1" value={draft.intro[0]} onChange={v => setField({ intro: [v, draft.intro[1]] })} rows={3} />
        <AdminTextArea label="INTRO PARAGRAPH 2" value={draft.intro[1]} onChange={v => setField({ intro: [draft.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 }}>
          {draft.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 = [...draft.expect]; next[i] = { ...next[i], title: v }; setField({ expect: next });
              }} />
              <AdminTextArea label="BODY" value={b.body} onChange={v => {
                const next = [...draft.expect]; next[i] = { ...next[i], body: v }; setField({ 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>
        {draft.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 = [...draft.who]; next[i] = { ...next[i], label: v }; setField({ who: next });
            }} />
            <AdminTextArea label="TEXT" value={w.text} onChange={v => {
              const next = [...draft.who]; next[i] = { ...next[i], text: v }; setField({ 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={draft.bottomLine} onChange={v => setField({ bottomLine: v })} rows={3} />
      </div>

      {/* Save bar */}
      <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 14, marginTop: 18, padding: '14px 18px', border: `1px solid ${T.rule}`, background: T.bg2 }}>
        <Mono size={10} color={error ? T.red : (dirty ? T.gold : T.mute)}>
          {error ? `· ${error}` : dirty ? '· UNSAVED CHANGES' : (savedAt ? `✓ SAVED @ ${savedAt.toLocaleTimeString()}` : '✓ UP TO DATE')}
        </Mono>
        <button
          onClick={save}
          disabled={!dirty || saving}
          style={{
            padding: '10px 22px', cursor: (!dirty || saving) ? 'not-allowed' : 'pointer',
            background: dirty ? T.gold : 'transparent',
            color: dirty ? T.bg : T.mute,
            border: `1px solid ${dirty ? T.gold : T.rule}`,
            fontFamily: "'Bebas Neue',sans-serif", fontSize: 14, letterSpacing: 1.4,
            opacity: saving ? 0.7 : 1,
          }}
        >{saving ? 'Saving…' : (dirty ? 'Save changes' : '✓ Saved')}</button>
      </div>
    </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, saveAbout, 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} saveAbout={saveAbout} resetAbout={resetAbout} />
      )}
    </div>
  );
};

window.Admin = Admin;
