// Timeline dashboard app
const { useState, useMemo, useEffect, useRef, useLayoutEffect } = React;

// Resolve a {year, match} ref to events whose title contains the keyword (case-insensitive).
const resolveRef = (ref, list) => list.filter(e =>
  e.year === ref.year && e.title.toLowerCase().includes(ref.match.toLowerCase())
);

const VERE_CATS = [
{ id: "life", label: "Life events", desc: "Birth, marriage, death" },
{ id: "education", label: "Education", desc: "Cambridge, Oxford, tutors" },
{ id: "travel", label: "Travels", desc: "Italy, France, Germany" },
{ id: "literary", label: "Literary", desc: "Dedications, mentions" },
{ id: "patronage", label: "Patronage", desc: "Theatre & writers" }];


const CANON_CATS = [
{ id: "history", label: "Histories" },
{ id: "comedy", label: "Comedies" },
{ id: "tragedy", label: "Tragedies" },
{ id: "poem", label: "Poems & sonnets" }];


function App() {
  const [yearMin, setYearMin] = useState(1550);
  const [yearMax, setYearMax] = useState(1623);
  const [activeVere, setActiveVere] = useState(new Set(VERE_CATS.map((c) => c.id)));
  const [activeCanon, setActiveCanon] = useState(new Set(CANON_CATS.map((c) => c.id)));
  const [matchesOnly, setMatchesOnly] = useState(false);
  const [search, setSearch] = useState("");
  const [selected, setSelected] = useState(null); // { side, year, title }
  const [activeMatch, setActiveMatch] = useState(null);
  const [view, setView] = useState("parallel"); // parallel | matches
  const [canvasH, setCanvasH] = useState(800);
  const [galleryOpen, setGalleryOpen] = useState(false);
  const [lightbox, setLightbox] = useState(null);
  const [sidebarOpen, setSidebarOpen] = useState(false);
  const [isSmall, setIsSmall] = useState(() =>
    typeof window !== 'undefined' && window.matchMedia('(max-width: 1023px)').matches
  );
  const canvasRef = useRef(null);

  useLayoutEffect(() => {
    const update = () => {
      if (canvasRef.current) setCanvasH(canvasRef.current.offsetHeight);
      const mast = document.querySelector('.masthead');
      const foot = document.querySelector('.footer');
      if (mast) document.documentElement.style.setProperty('--mast-h', mast.offsetHeight + 'px');
      if (foot) document.documentElement.style.setProperty('--foot-h', foot.offsetHeight + 'px');
    };
    update();
    window.addEventListener('resize', update);
    return () => window.removeEventListener('resize', update);
  }, []);

  // Cycle to next/prev painting, wrapping at the ends.
  const navigateLightbox = (dir) => {
    setLightbox((curr) => {
      if (!curr) return curr;
      const i = PAINTINGS.findIndex(p => p.n === curr.n);
      if (i < 0) return curr;
      const len = PAINTINGS.length;
      return PAINTINGS[(i + dir + len) % len];
    });
  };

  // Centralized keys: Esc closes topmost overlay (lightbox > gallery > detail panel);
  // arrows cycle the lightbox.
  useEffect(() => {
    if (!galleryOpen && !lightbox && !selected && !activeMatch) return;
    const onKey = (e) => {
      if (e.key === 'Escape') {
        if (lightbox) setLightbox(null);
        else if (galleryOpen) setGalleryOpen(false);
        else if (selected || activeMatch) {
          setSelected(null);
          setActiveMatch(null);
        }
      } else if (lightbox && e.key === 'ArrowLeft') {
        navigateLightbox(-1);
      } else if (lightbox && e.key === 'ArrowRight') {
        navigateLightbox(1);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [galleryOpen, lightbox, selected, activeMatch]);

  // Watch viewport size — drives whether detail/sidebar render as overlays.
  useLayoutEffect(() => {
    if (typeof window === 'undefined') return;
    const mq = window.matchMedia('(max-width: 1023px)');
    const onChange = () => setIsSmall(mq.matches);
    mq.addEventListener('change', onChange);
    return () => mq.removeEventListener('change', onChange);
  }, []);

  // Single body-scroll-lock keyed on every overlay state.
  useEffect(() => {
    const detailOverlayOpen = isSmall && (selected || activeMatch);
    const lock = galleryOpen || !!lightbox || (isSmall && sidebarOpen) || detailOverlayOpen;
    document.body.style.overflow = lock ? 'hidden' : '';
    return () => { document.body.style.overflow = ''; };
  }, [galleryOpen, lightbox, isSmall, sidebarOpen, selected, activeMatch]);

  const Y0 = 1550,Y1 = 1625;

  // Position helpers — convert year to vertical % within the canvas
  const yearToY = (yr) => (yr - Y0) / (Y1 - Y0) * 100;

  // Reverse-index: each event keyed `${year}|${title}` -> Set<matchId>.
  // Built once from MATCHES — substring-resolves each ref against the title.
  const eventMatchIds = useMemo(() => {
    const vMap = new Map();
    const cMap = new Map();
    const tag = (map, list, year, keyword, id) => {
      list.forEach((e) => {
        if (e.year === year && e.title.toLowerCase().includes(keyword.toLowerCase())) {
          const k = e.year + "|" + e.title;
          if (!map.has(k)) map.set(k, new Set());
          map.get(k).add(id);
        }
      });
    };
    MATCHES.forEach((m) => {
      (m.vere  || []).forEach((r) => tag(vMap, DEVERE_EVENTS, r.year, r.match, m.id));
      (m.canon || []).forEach((r) => tag(cMap, CANON_EVENTS,  r.year, r.match, m.id));
    });
    return { vMap, cMap };
  }, []);

  const getMatchIds = (side, evt) => {
    const map = side === 'left' ? eventMatchIds.vMap : eventMatchIds.cMap;
    return map.get(evt.year + "|" + evt.title) || new Set();
  };

  const filteredVere = useMemo(() => {
    return DEVERE_EVENTS.filter((e) => {
      if (e.year < yearMin || e.year > yearMax) return false;
      if (!activeVere.has(e.category)) return false;
      if (matchesOnly && getMatchIds('left', e).size === 0) return false;
      if (search && !(e.title + ' ' + e.desc).toLowerCase().includes(search.toLowerCase())) return false;
      return true;
    });
  }, [yearMin, yearMax, activeVere, matchesOnly, search, eventMatchIds]);

  const filteredCanon = useMemo(() => {
    return CANON_EVENTS.filter((e) => {
      if (e.year < yearMin || e.year > yearMax) return false;
      if (!activeCanon.has(e.type)) return false;
      if (matchesOnly && getMatchIds('right', e).size === 0) return false;
      if (search && !(e.title + ' ' + e.desc).toLowerCase().includes(search.toLowerCase())) return false;
      return true;
    });
  }, [yearMin, yearMax, activeCanon, matchesOnly, search, eventMatchIds]);

  // Stack-resolved positions to avoid overlap (px-based); attach matchIds for each event.
  const placedVere = useMemo(() =>
    placeEvents(filteredVere, Y0, Y1).map((e) => ({
      ...e, matchIds: Array.from(getMatchIds('left', e))
    })),
  [filteredVere, eventMatchIds]);

  const placedCanon = useMemo(() =>
    placeEvents(filteredCanon, Y0, Y1).map((e) => ({
      ...e, matchIds: Array.from(getMatchIds('right', e))
    })),
  [filteredCanon, eventMatchIds]);

  // Focus state derived from selection: highlights every event in the focused match(es), dims the rest.
  const focusedMatchIds = useMemo(() => {
    if (activeMatch) return new Set([activeMatch]);
    if (selected) {
      const ids = getMatchIds(selected.side, selected);
      if (ids.size > 0) return ids;
    }
    return null; // no focus mode
  }, [activeMatch, selected, eventMatchIds]);

  const isFocusedEvent = (side, evt) => {
    if (!focusedMatchIds) return null; // null = render normally
    const ids = getMatchIds(side, evt);
    for (const id of ids) if (focusedMatchIds.has(id)) return true;
    return false;
  };

  // Canvas height = max of either lane's last card bottom + bottom padding
  const cardPx = 100;
  const lastVere = placedVere.length ? placedVere[placedVere.length - 1].y + cardPx : 0;
  const lastCanon = placedCanon.length ? placedCanon[placedCanon.length - 1].y + cardPx : 0;
  const canvasHeight = Math.max(lastVere, lastCanon, 600) + 40;

  const filtersActive =
    search.trim() !== "" ||
    yearMin !== 1550 || yearMax !== 1623 ||
    activeVere.size !== VERE_CATS.length ||
    activeCanon.size !== CANON_CATS.length ||
    matchesOnly;

  const resetFilters = () => {
    setSearch("");
    setYearMin(1550);
    setYearMax(1623);
    setActiveVere(new Set(VERE_CATS.map(c => c.id)));
    setActiveCanon(new Set(CANON_CATS.map(c => c.id)));
    setMatchesOnly(false);
  };

  const toggleVere = (id) => {
    const n = new Set(activeVere);
    n.has(id) ? n.delete(id) : n.add(id);
    setActiveVere(n);
  };
  const toggleCanon = (id) => {
    const n = new Set(activeCanon);
    n.has(id) ? n.delete(id) : n.add(id);
    setActiveCanon(n);
  };

  // counts
  const vereCounts = useMemo(() => {
    const c = {};
    DEVERE_EVENTS.forEach((e) => {c[e.category] = (c[e.category] || 0) + 1;});
    return c;
  }, []);
  const canonCounts = useMemo(() => {
    const c = {};
    CANON_EVENTS.forEach((e) => {c[e.type] = (c[e.type] || 0) + 1;});
    return c;
  }, []);

  const selectEvent = (side, evt) => {
    // Click the same card again -> deselect.
    if (selected && selected.side === side && selected.title === evt.title) {
      setSelected(null);
    } else {
      setSelected({ side, ...evt });
    }
    setActiveMatch(null);
  };

  const totalMatches = MATCHES.length;
  const totalVere = DEVERE_EVENTS.length;
  const totalCanon = CANON_EVENTS.length;

  // Counts derived from the new schema.
  const matchCount =
    placedVere.filter((e) => e.matchIds && e.matchIds.length > 0).length +
    placedCanon.filter((e) => e.matchIds && e.matchIds.length > 0).length;
  const linkedCount = useMemo(() => {
    const keys = new Set();
    MATCHES.forEach((m) => {
      (m.vere  || []).forEach((r) => keys.add('v|' + r.year + '|' + r.match.toLowerCase()));
      (m.canon || []).forEach((r) => keys.add('c|' + r.year + '|' + r.match.toLowerCase()));
    });
    return keys.size;
  }, []);

  return (
    <>
      <header className="masthead">
        <div className="mast-l">
          <div className="eyebrow">
            <span>An Oxfordian Concordance</span>
            <span className="sep">⁂</span>
            <span>1550 — 1623</span>
            <span className="sep">⁂</span>
            <span>Folio Edition</span>
          </div>
          <div className="title-row">
            <img className="masthead-portrait"
              src="paintings/de-vere-cameo.jpg"
              alt="Edward de Vere, 17th Earl of Oxford, 1575"
              title="Edward de Vere · Welbeck portrait, 1575" />
            <h1 className="title">De Vere & the <b>Bard</b></h1>
          </div>
          <div className="subtitle">
            Plotting the life of Edward de Vere, 17th Earl of Oxford, against the chronology of the Shakespeare canon —
            and the curious places they meet.
          </div>
        </div>
        <div className="mast-r">
          <div className="stat"><div className="stat-num">{totalVere}</div><div className="stat-lbl">DE VERE EVENTS</div></div>
          <div className="stat"><div className="stat-num">{totalCanon}</div><div className="stat-lbl">Canon entries</div></div>
          <div className="stat"><div className="stat-num match">{totalMatches}</div><div className="stat-lbl">Concordances</div></div>
          <button className="gallery-btn" onClick={() => setGalleryOpen(true)}>
            <span className="gb-mark">✦</span>
            <span className="gb-lbl">View the Plates</span>
            <span className="gb-sub">22 paintings · Lynne Suo</span>
          </button>
        </div>
      </header>

      <main className="shell">
        <Sidebar
          search={search} setSearch={setSearch}
          yearMin={yearMin} yearMax={yearMax} setYearMin={setYearMin} setYearMax={setYearMax}
          activeVere={activeVere} toggleVere={toggleVere}
          activeCanon={activeCanon} toggleCanon={toggleCanon}
          matchesOnly={matchesOnly} setMatchesOnly={setMatchesOnly}
          vereCounts={vereCounts} canonCounts={canonCounts}
          filtersActive={filtersActive} resetFilters={resetFilters}
          open={sidebarOpen}
          onClose={() => setSidebarOpen(false)} />

        {isSmall && sidebarOpen &&
          <div className="sidebar-backdrop" onClick={() => setSidebarOpen(false)} aria-hidden="true" />
        }

        <div className="timeline-wrap">
          <div className="timeline-head">
            <button className="sidebar-toggle"
              onClick={() => setSidebarOpen(true)}
              aria-label="Open filters">
              Filters
              {filtersActive && <span className="stb-pip" aria-hidden="true" />}
            </button>
            <div className="tl-title">
              {view === 'parallel'
                ? <>Concordia <b>{filteredVere.length + filteredCanon.length}</b> entries · <b>{matchCount}</b> matches</>
                : <><b>{MATCHES.length}</b> Concordances · <b>{linkedCount}</b> linked entries</>}
            </div>
            <div className="tl-toggle">
              <button className={view === 'parallel' ? 'on' : ''} onClick={() => setView('parallel')}>Parallel</button>
              <button className={view === 'matches' ? 'on' : ''} onClick={() => setView('matches')}>Concordances</button>
            </div>
          </div>

          {view === 'parallel'
            ? <TimelineCanvas
                canvasRef={canvasRef}
                placedVere={placedVere}
                placedCanon={placedCanon}
                yearMin={Y0} yearMax={Y1}
                canvasHeight={canvasHeight}
                selected={selected}
                onSelect={selectEvent}
                focusedMatchIds={focusedMatchIds}
                isFocusedEvent={isFocusedEvent} />
            : <ConcordanceView selected={selected} onSelect={selectEvent} />
          }
        </div>

        <DetailPanel
          selected={selected}
          activeMatch={activeMatch}
          setActiveMatch={(m) => {setActiveMatch(m);setSelected(null);}}
          onClose={() => {setSelected(null);setActiveMatch(null);}} />

      </main>

      <footer className="footer">
        <span>Compiled · MMXXVI</span>
        <span className="seal">✦  ✦  ✦</span>
        <span>Hover · Click · Filter</span>
      </footer>

      {galleryOpen &&
        <GalleryOverlay
          onClose={() => { setGalleryOpen(false); setLightbox(null); }}
          onOpenPlate={p => setLightbox(p)}
        />
      }

      {lightbox &&
        <Lightbox
          painting={lightbox}
          onClose={() => setLightbox(null)}
          onPrev={() => navigateLightbox(-1)}
          onNext={() => navigateLightbox(1)}
        />
      }
    </>);

}

/* ── Stacking algorithm ────────────────────────────────────────── */
// Compute pixel positions: each event placed at its proportional year-position,
// then bumped down so it doesn't overlap the previous one.
function placeEvents(events, yearMin, yearMax, cardPx = 100, paddingTop = 50) {
  const sorted = [...events].sort((a, b) => a.year - b.year);
  const PX_PER_YEAR = 24; // base spacing
  const placed = [];
  let lastBottom = 0;
  for (const e of sorted) {
    let y = paddingTop + (e.year - yearMin) * PX_PER_YEAR;
    if (y < lastBottom + 4) y = lastBottom + 4;
    placed.push({ ...e, y });
    lastBottom = y + cardPx;
  }
  return placed;
}

/* ── Sidebar ───────────────────────────────────────────────────── */
function Sidebar(props) {
  const { search, setSearch, yearMin, yearMax, setYearMin, setYearMax,
    activeVere, toggleVere, activeCanon, toggleCanon,
    matchesOnly, setMatchesOnly, vereCounts, canonCounts,
    filtersActive, resetFilters, open, onClose } = props;

  return (
    <aside className={"sidebar" + (open ? " open" : "")}>
      <div className="filter-status">
        <div className="fs-row">
          <span className={"fs-dot " + (filtersActive ? "on" : "")}></span>
          <span className="fs-lbl">{filtersActive ? "Filters active" : "All entries shown"}</span>
        </div>
        <button className={"reset-btn " + (filtersActive ? "" : "disabled")}
          onClick={resetFilters}
          disabled={!filtersActive}>
          ↺ Reset
        </button>
      </div>

      <div className="filter-group">
        <div className="filter-h">Search the archive</div>
        <div className="search-box">
          <span className="mono" style={{ color: 'var(--ink-3)', fontSize: 11 }}>※</span>
          <input
            type="text" value={search} onChange={(e) => setSearch(e.target.value)}
            placeholder="Hamlet, Venice, Burghley…" />
          
        </div>
      </div>

      <div className="filter-group year-range">
        <div className="filter-h">
          Period
          {(yearMin !== 1550 || yearMax !== 1623) && <span className="active-pip">●</span>}
        </div>
        <div className="year-vals">
          <span>{yearMin}</span>
          <span>—</span>
          <span>{yearMax}</span>
        </div>
        <input type="range" min="1550" max="1623" value={yearMin}
        onChange={(e) => setYearMin(Math.min(+e.target.value, yearMax - 5))} />
        <input type="range" min="1550" max="1623" value={yearMax}
        onChange={(e) => setYearMax(Math.max(+e.target.value, yearMin + 5))} />
      </div>

      <div className="filter-group">
        <div className="filter-h">
          de Vere · Records
          {activeVere.size !== VERE_CATS.length && <span className="active-pip">●</span>}
        </div>
        {VERE_CATS.map((c) =>
        <div key={c.id}
        className={"filter-row " + (activeVere.has(c.id) ? "" : "off")}
        onClick={() => toggleVere(c.id)}>
            <span className={"swatch " + c.id}></span>
            <span className="filter-lbl">{c.label}</span>
            <span className="filter-count">{vereCounts[c.id] || 0}</span>
          </div>
        )}
      </div>

      <div className="filter-group">
        <div className="filter-h">
          Shakespeare Canon
          {activeCanon.size !== CANON_CATS.length && <span className="active-pip">●</span>}
        </div>
        {CANON_CATS.map((c) =>
        <div key={c.id}
        className={"filter-row " + (activeCanon.has(c.id) ? "" : "off")}
        onClick={() => toggleCanon(c.id)}>
            <span className={"swatch " + c.id}></span>
            <span className="filter-lbl">{c.label}</span>
            <span className="filter-count">{canonCounts[c.id] || 0}</span>
          </div>
        )}
      </div>

      <div className="filter-group">
        <div className="filter-h">
          Highlight
          {matchesOnly && <span className="active-pip">●</span>}
        </div>
        <div className={"filter-row " + (matchesOnly ? "" : "off")}
        onClick={() => setMatchesOnly(!matchesOnly)}>
          <span className="swatch match"></span>
          <span className="filter-lbl">Matches only</span>
          <span className="filter-count">✦ {MATCHES.length}</span>
        </div>
      </div>

      <div className="legend-note">
        ✦ marks events where de Vere's life intersects the canon — by date, dedication, setting, or echo.
      </div>
    </aside>);

}

/* ── Timeline canvas with stacked events & connectors ─────────── */
function TimelineCanvas({ canvasRef, placedVere, placedCanon, yearMin, yearMax, canvasHeight, selected, onSelect, focusedMatchIds, isFocusedEvent }) {
  const PX_PER_YEAR = 24;
  const PADDING_TOP = 50;
  const yearToPx = (yr) => PADDING_TOP + (yr - yearMin) * PX_PER_YEAR;
  const ticks = [];
  for (let y = 1550; y <= 1620; y += 5) {
    ticks.push({ year: y, major: y % 10 === 0 });
  }

  // Pairwise connectors: every (vere, canon) cross-product that shares a match id emits a line.
  // Sort dimmed first, plain, focused last so highlighted lines paint on top.
  const connectors = useMemo(() => {
    const lines = [];
    placedVere.forEach((v) => {
      if (!v.matchIds || !v.matchIds.length) return;
      placedCanon.forEach((c) => {
        if (!c.matchIds || !c.matchIds.length) return;
        const shared = v.matchIds.find((id) => c.matchIds.includes(id));
        if (!shared) return;
        const focused = !!(focusedMatchIds && focusedMatchIds.has(shared));
        const dimmed  = !!(focusedMatchIds && !focused);
        lines.push({ from: v.y, to: c.y, matchId: shared, focused, dimmed });
      });
    });
    return [
      ...lines.filter(l => l.dimmed),
      ...lines.filter(l => !l.dimmed && !l.focused),
      ...lines.filter(l => l.focused),
    ];
  }, [placedVere, placedCanon, focusedMatchIds]);

  return (
    <div className="timeline-canvas" ref={canvasRef} style={{ height: canvasHeight + "px" }}>
      {/* LEFT: de Vere */}
      <div className="lane left">
        <div className="lane-head"><span className="lane-title">Edward de <b>Vere</b></span></div>
        <div className="lane-rule"></div>
        {placedVere.map((e, i) =>
        <EventCard key={"v" + e.year + i} side="left" e={e}
        selected={selected && selected.side === 'left' && selected.title === e.title}
        focusState={isFocusedEvent('left', e)}
        onClick={() => onSelect('left', e)} />
        )}
      </div>

      {/* CENTER: axis */}
      <div className="axis">
        {ticks.map((t) =>
        <div key={t.year} className={"axis-tick " + (t.major ? "major" : "")}
        style={{ top: yearToPx(t.year) + "px" }}>
            <span className="yr">{t.year}</span>
          </div>
        )}

        <svg className="match-svg" preserveAspectRatio="none" viewBox={`0 0 100 ${canvasHeight}`}>
          {connectors.map((l, i) =>
          <line key={i} className={l.focused ? "active" : l.dimmed ? "dim" : ""}
          x1="0" y1={l.from + 24} x2="100" y2={l.to + 24} vectorEffect="non-scaling-stroke" />
          )}
        </svg>
      </div>

      {/* RIGHT: canon */}
      <div className="lane right">
        <div className="lane-head"><span className="lane-title">The <b>Shakespeare</b> Canon</span></div>
        <div className="lane-rule"></div>
        {placedCanon.map((e, i) =>
        <EventCard key={"c" + e.year + i} side="right" e={e}
        selected={selected && selected.side === 'right' && selected.title === e.title}
        focusState={isFocusedEvent('right', e)}
        onClick={() => onSelect('right', e)} />
        )}
      </div>
    </div>);

}

function EventCard({ side, e, selected, focusState, onClick }) {
  const cat = side === 'left' ? e.category : e.type;
  const hasMatch = (e.matchIds && e.matchIds.length > 0);
  const cls =
    "evt " +
    (selected ? "selected " : "") +
    (hasMatch ? "match " : "") +
    (focusState === true  ? "focused " : "") +
    (focusState === false ? "dimmed "  : "");
  return (
    <div className={cls}
    data-cat={cat}
    style={{ top: e.y + "px" }}
    onClick={onClick}>
      <div className="evt-yr">{e.year}</div>
      <div className="evt-title">{e.title}</div>
      <div className="evt-cat">{cat}</div>
    </div>);

}

/* ── Detail panel ────────────────────────────────────────────── */
function DetailPanel({ selected, activeMatch, setActiveMatch, onClose }) {
  // If a match is active, find it
  const match = activeMatch ? MATCHES.find((m) => m.id === activeMatch) : null;
  const active = !!(selected || match);

  return (
    <aside className={"detail" + (active ? " detail-active" : "")}>
      <div className="detail-eyebrow">
        <span>{selected ? "Folio · Entry" : match ? "Concordance" : "Index"}</span>
        {(selected || match) &&
        <span style={{ cursor: 'pointer' }} onClick={onClose}>✕ close</span>
        }
      </div>

      {selected &&
      <SelectedView selected={selected} setActiveMatch={setActiveMatch} />
      }

      {match && !selected &&
      <MatchView match={match} />
      }

      {!selected && !match &&
      <DefaultView setActiveMatch={setActiveMatch} />
      }
    </aside>);

}

function SelectedView({ selected, setActiveMatch }) {
  const cat = selected.side === 'left' ? selected.category : selected.type;
  const sideLabel = selected.side === 'left' ? 'de Vere · Life' : 'Shakespeare · Canon';

  // Find related matches by walking the new ref arrays.
  const related = MATCHES.filter((m) => {
    const refs = selected.side === 'left' ? (m.vere || []) : (m.canon || []);
    return refs.some((r) =>
      r.year === selected.year &&
      selected.title.toLowerCase().includes(r.match.toLowerCase())
    );
  });

  return (
    <>
      <div>
        <span className="detail-tag">{sideLabel}</span>
        <span className="detail-tag">{cat}</span>
        {selected.match && <span className="detail-tag match">✦ Match</span>}
      </div>
      <div>
        <div className="detail-yr mono">A.D. {selected.year}</div>
        <h2 className="detail-h">{selected.title}</h2>
      </div>
      <div className="detail-body">{selected.desc}</div>

      {related.length > 0 &&
      <div>
          <div className="filter-h" style={{ marginBottom: 10 }}>Related Concordances</div>
          <div className="matches-list">
            {related.map((m) =>
          <div key={m.id} className="match-pill" onClick={() => setActiveMatch(m.id)}>
                <span className="star">✦</span>
                <span className="mp-title">{m.title}</span>
                <span className="mp-yrs">→</span>
              </div>
          )}
          </div>
        </div>
      }
    </>);

}

function MatchView({ match }) {
  return (
    <>
      <div>
        <span className="detail-tag match">✦ Concordance</span>
      </div>
      <div>
        <div className="detail-yr mono">
          de Vere {(match.vere || []).map(r => r.year).join(', ') || '—'} · Canon {(match.canon || []).map(r => r.year).join(', ') || '—'}
        </div>
        <h2 className="detail-h">{match.title}</h2>
      </div>
      <div className="detail-body">{match.desc}</div>
      <div className="match-card">
        <div className="mc-h">✦ Cross-references</div>
        <div className="mc-refs">
          {(match.vere  || []).map((r) => <span key={'v'+r.year+r.match} className="vere">Vere · {r.year}</span>)}
          {(match.canon || []).map((r) => <span key={'c'+r.year+r.match} className="canon">Canon · {r.year}</span>)}
        </div>
      </div>
    </>);

}

function DefaultView({ setActiveMatch }) {
  return (
    <>
      <div className="empty-state">
        <div className="ornament">✦ ⁂ ✦</div>
        <h3>An archive of coincidences</h3>
        <p>
          Select any entry to read its full annotation, or explore the {MATCHES.length} concordances below —
          places where the Earl's life and the Bard's pages keep curious company.
        </p>
      </div>

      <div>
        <div className="filter-h" style={{ marginBottom: 12 }}>The Concordances</div>
        <div className="matches-list">
          {MATCHES.map((m) =>
          <div key={m.id} className="match-pill" onClick={() => setActiveMatch(m.id)}>
              <span className="star">✦</span>
              <span className="mp-title">{m.title}</span>
              <span className="mp-yrs">{(m.vere || []).map(r => r.year).join(', ')}</span>
            </div>
          )}
        </div>
      </div>

      <div className="match-card">
        <div className="mc-h">⁂ Editor's note</div>
        <div className="mc-title" style={{ fontSize: 15, fontStyle: 'italic' }}>{SONNETS_NOTE.title}</div>
        <div className="mc-body">{SONNETS_NOTE.desc}</div>
      </div>
    </>);

}

/* ── Concordance view (themed rows) ─────────────────────────── */
const ROMANS = ['Ⅰ','Ⅱ','Ⅲ','Ⅳ','Ⅴ','Ⅵ','Ⅶ','Ⅷ','Ⅸ','Ⅹ','Ⅺ'];

function ConcordanceView({ selected, onSelect }) {
  const resolve = (ref, side) => {
    const list = side === 'left' ? DEVERE_EVENTS : CANON_EVENTS;
    return list.find((e) =>
      e.year === ref.year &&
      e.title.toLowerCase().includes(ref.match.toLowerCase())
    );
  };

  return (
    <div className="concord-view">
      <div className="concord-intro">
        <img className="ci-portrait"
          src="paintings/de-vere-cameo.jpg"
          alt="Edward de Vere, 17th Earl of Oxford, 1575"
          title="Edward de Vere · Welbeck portrait, 1575" />
        <span>Themes where the Earl's life and the Bard's pages keep curious company. Each row pairs the man with the work.</span>
      </div>

      {MATCHES.map((m, idx) => {
        const vereEvts  = (m.vere  || []).map((r) => resolve(r, 'left' )).filter(Boolean);
        const canonEvts = (m.canon || []).map((r) => resolve(r, 'right')).filter(Boolean);
        return (
          <section key={m.id} className="concord-row">
            <header className="cr-head">
              <span className="cr-num">{ROMANS[idx] || (idx + 1)}</span>
              <h2 className="cr-title">{m.title}</h2>
              <span className="cr-count mono">
                {vereEvts.length} <span className="cr-arrow">↔</span> {canonEvts.length}
              </span>
            </header>
            <p className="cr-desc">{m.desc}</p>

            <div className="cr-body">
              <div className="cr-col cr-left">
                <div className="cr-col-h">Edward de Vere</div>
                <div className="cr-cards">
                  {vereEvts.length === 0 && <div className="cr-empty">— no de Vere entries —</div>}
                  {vereEvts.map((e, i) =>
                    <ConcordCard key={i} side="left" e={e}
                      selected={selected && selected.side === 'left' && selected.title === e.title}
                      onClick={() => onSelect('left', e)} />
                  )}
                </div>
              </div>

              <div className="cr-link" aria-hidden="true">
                <svg viewBox="0 0 60 100" preserveAspectRatio="none">
                  <path d="M 0 50 C 20 50, 40 50, 60 50"
                    stroke="currentColor" fill="none"
                    strokeWidth="1.5" strokeDasharray="3 3" />
                </svg>
                <span className="cr-link-mark">✦</span>
              </div>

              <div className="cr-col cr-right">
                <div className="cr-col-h">The Canon</div>
                <div className="cr-cards">
                  {canonEvts.length === 0 && <div className="cr-empty">— no canon entries —</div>}
                  {canonEvts.map((e, i) =>
                    <ConcordCard key={i} side="right" e={e}
                      selected={selected && selected.side === 'right' && selected.title === e.title}
                      onClick={() => onSelect('right', e)} />
                  )}
                </div>
              </div>
            </div>
          </section>
        );
      })}
    </div>
  );
}

function ConcordCard({ side, e, selected, onClick }) {
  const cat = side === 'left' ? e.category : e.type;
  return (
    <div className={"cc-card " + (selected ? "selected " : "")}
         data-cat={cat} onClick={onClick}>
      <div className="cc-yr mono">{e.year}</div>
      <div className="cc-title">{e.title}</div>
      <div className="cc-cat">{cat}</div>
    </div>
  );
}

/* ── Gallery overlay ───────────────────────────────────────── */
function GalleryOverlay({ onClose, onOpenPlate }) {
  // Render the subtitle's *italicized* fragment for visual richness
  const subtitleParts = ARTIST.subtitle.split('*');

  return (
    <div className="gallery-overlay" role="dialog" aria-label="Gallery of plates">
      <div className="gallery-inner">
        <header className="gallery-head">
          <div className="gh-top">
            <div className="gh-artist">
              <span className="gh-name">{ARTIST.name}</span>
              <a className="gh-email" href={"mailto:" + ARTIST.email}>{ARTIST.email}</a>
            </div>
            <button className="gallery-close" onClick={onClose} aria-label="Close gallery">
              ✕ <span>close</span>
            </button>
          </div>
          <div className="gh-donations">
            Donations in support of the <a className="gh-society" href="https://deveresociety.co.uk/" target="_blank" rel="noopener noreferrer">DE VERE SOCIETY</a> — {ARTIST.donations.replace(/^Donations in support of DE VERE SOCIETY — /, '')}
            {' '}<a className="gh-society-email" href={"mailto:" + ARTIST.societyEmail}>{ARTIST.societyEmail}</a>
          </div>
          <div className="gh-subtitle">
            {subtitleParts.map((s, i) =>
              i % 2 === 1
                ? <em key={i}>{s}</em>
                : <span key={i}>{s}</span>
            )}
          </div>
        </header>

        <div className="plates-grid">
          {PAINTINGS.map(p =>
            <PaintingPlate key={p.n} p={p} onOpen={() => onOpenPlate(p)} />
          )}
          <div className="bio-panel">
            <div className="bp-h">
              <span className="bp-mark">⁂</span>
              <span>The painter, sub rosa</span>
            </div>
            <div className="bp-body">{ARTIST.bio}</div>
          </div>
        </div>

        <div className="gallery-foot">
          <span>Plates · MMXXVI</span>
          <span className="seal">✦  ✦  ✦</span>
          <span>Click any plate to enlarge</span>
        </div>
      </div>
    </div>
  );
}

function PaintingPlate({ p, onOpen }) {
  const thumbSrc = `paintings/thumbs/${encodeURIComponent(p.file)}`;
  return (
    <figure className="plate" onClick={onOpen}>
      <div className="plate-frame">
        <img className="plate-img" src={thumbSrc} alt={p.play + ' — ' + p.cite} loading="lazy" />
        <span className="plate-num">{p.n}</span>
      </div>
      <figcaption className="plate-caption">
        <div className="pc-title"><span className="pc-n">{p.n}.</span> {p.play}</div>
        <div className="pc-cite">{p.cite}</div>
        <div className="pc-quote">“{p.quote}”</div>
      </figcaption>
    </figure>
  );
}

function Lightbox({ painting, onClose, onPrev, onNext }) {
  const fullSrc = `paintings/${encodeURIComponent(painting.file)}`;
  const stop = (e) => e.stopPropagation();
  const [loaded, setLoaded] = useState(false);

  // Reset loading state whenever the source changes (cycling next/prev).
  useEffect(() => { setLoaded(false); }, [fullSrc]);

  return (
    <div className="lightbox" onClick={onClose} role="dialog" aria-label={painting.play}>
      <button className="lightbox-close" onClick={(e) => { stop(e); onClose(); }} aria-label="Close">✕</button>
      <button className="lightbox-nav lightbox-prev"
        onClick={(e) => { stop(e); onPrev(); }}
        aria-label="Previous painting">‹</button>
      <button className="lightbox-nav lightbox-next"
        onClick={(e) => { stop(e); onNext(); }}
        aria-label="Next painting">›</button>
      <div className="lightbox-inner" onClick={stop}>
        {!loaded && <div className="lightbox-spinner" aria-label="Loading"><div className="lb-ring" /></div>}
        <img
          key={fullSrc}
          className={"lightbox-img " + (loaded ? "is-loaded" : "is-loading")}
          src={fullSrc}
          alt={painting.play + ' — ' + painting.cite}
          onLoad={() => setLoaded(true)}
          onError={() => setLoaded(true)} />
        <div className="lightbox-caption">
          <div className="lc-num">Plate {painting.n} of {PAINTINGS.length}</div>
          <div className="lc-title">{painting.play}</div>
          <div className="lc-cite">{painting.cite}</div>
          <div className="lc-quote">“{painting.quote}”</div>
        </div>
      </div>
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<App />);