#!/usr/bin/env python3
import os
import re
import json
from datetime import datetime
from pathlib import Path

from PIL import Image  # pip install Pillow

IMAGES_DIR = Path("images")
THUMBS_DIR = IMAGES_DIR / "thumbs"
OUTPUT_HTML = Path("timeline.html")

THUMB_MAX_SIZE = (800, 800)  # max width/height for thumbnails
JPEG_QUALITY = 80            # JPEG thumbnail quality


# --- Helpers -----------------------------------------------------------

def humanize_title(slug: str) -> str:
    """Turn 'my-cool_title' into 'My Cool Title' (used for alt text only)."""
    if not slug:
        return "Untitled"
    text = re.sub(r"[-_]+", " ", slug).strip()
    return re.sub(r"\b\w", lambda m: m.group(0).upper(), text)


def parse_date_from_filename(filename: str):
    """
    Parse date from the filename.

    Tries (in order):
      1) YYYY-MM-DD or YYYY_MM_DD at the start
      2) DD-MM-YYYY-...
      3) MM-YYYY-...
      4) YYYY-...
      5) Any 4-digit year (19xx or 20xx) anywhere in the name

    Returns (date_obj, label, rest_slug)
      - date_obj: datetime or None
      - label: string representation to display
      - rest_slug: remaining part for alt text
    """
    name = os.path.splitext(filename)[0]

    # 1) YYYY-MM-DD or YYYY_MM_DD at the start
    m = re.match(r"^(\d{4})[-_](\d{1,2})[-_](\d{1,2})(?:[-_](.*))?$", name)
    if m:
        yyyy, mm, dd, rest = m.groups()
        try:
            dt = datetime(int(yyyy), int(mm), int(dd))
        except ValueError:
            dt = None
        label = f"{yyyy}-{mm.zfill(2)}-{dd.zfill(2)}"
        return dt, label, (rest or "")

    # 2) DD-MM-YYYY-...
    m = re.match(r"^(\d{1,2})-(\d{1,2})-(\d{4})(?:[-_](.*))?$", name)
    if m:
        dd, mm, yyyy, rest = m.groups()
        try:
            dt = datetime(int(yyyy), int(mm), int(dd))
        except ValueError:
            dt = None
        label = f"{dd.zfill(2)}-{mm.zfill(2)}-{yyyy}"
        return dt, label, (rest or "")

    # 3) MM-YYYY-...
    m = re.match(r"^(\d{1,2})-(\d{4})(?:[-_](.*))?$", name)
    if m:
        mm, yyyy, rest = m.groups()
        try:
            dt = datetime(int(yyyy), int(mm), 1)
        except ValueError:
            dt = None
        label = f"{mm.zfill(2)}-{yyyy}"
        return dt, label, (rest or "")

    # 4) YYYY-...
    m = re.match(r"^(\d{4})(?:[-_](.*))?$", name)
    if m:
        yyyy, rest = m.groups()
        try:
            dt = datetime(int(yyyy), 1, 1)
        except ValueError:
            dt = None
        label = f"{yyyy}"
        return dt, label, (rest or "")

    # 5) Fallback: any 4-digit year (19xx or 20xx) anywhere in the name
    m = re.search(r"(19|20)\d{2}", name)
    if m:
        yyyy = int(m.group(0))
        try:
            dt = datetime(yyyy, 1, 1)
        except ValueError:
            dt = None
        label = str(yyyy)
        # remove that year from the slug for a cleaner title
        rest = name.replace(str(yyyy), "")
        rest = re.sub(r"^[-_]+|[-_]+$", "", rest)
        return dt, label, rest

    # No recognizable date at all
    return None, name, ""


def ensure_thumbnail(src_path: Path) -> Path:
    """
    Ensure a thumbnail exists for src_path inside THUMBS_DIR.
    Returns the thumbnail Path.
    """
    THUMBS_DIR.mkdir(parents=True, exist_ok=True)
    thumb_path = THUMBS_DIR / src_path.name

    # If thumbnail already exists, reuse it
    if thumb_path.exists():
        return thumb_path

    try:
        with Image.open(src_path) as im:
            im = im.convert("RGB")
            im.thumbnail(THUMB_MAX_SIZE)
            save_kwargs = {"optimize": True}
            if thumb_path.suffix.lower() in {".jpg", ".jpeg"}:
                save_kwargs["quality"] = JPEG_QUALITY
            im.save(thumb_path, **save_kwargs)
    except Exception as e:
        print(f"[WARN] could not create thumbnail for {src_path}: {e}")
        # fallback: copy original
        try:
            import shutil
            shutil.copy2(src_path, thumb_path)
        except Exception as e2:
            print(f"[WARN] and could not copy original either: {e2}")

    return thumb_path


def collect_events():
    """
    Scan IMAGES_DIR for .jpg/.jpeg/.png and build event objects.
    If a matching .txt exists, include its contents in the event.
    If a matching .jxl exists, include it for JPEG XL loading.
    Also generate / use thumbnails in images/thumbs/.
    """
    if not IMAGES_DIR.is_dir():
        raise SystemExit(f"Images directory not found: {IMAGES_DIR}")

    events = []
    for entry in sorted(IMAGES_DIR.iterdir()):
        if not entry.is_file():
            continue
        ext = entry.suffix.lower()
        if ext not in {".jpg", ".jpeg", ".png"}:
            continue

        dt, label, rest_slug = parse_date_from_filename(entry.name)

        # Full-size image path (for fullscreen)
        full_rel_path = f"{IMAGES_DIR.name}/{entry.name}"

        # Thumbnail (for timeline display)
        thumb_path = ensure_thumbnail(entry)
        thumb_rel_path = f"{IMAGES_DIR.name}/{THUMBS_DIR.name}/{thumb_path.name}"

        base_no_ext = os.path.splitext(entry.name)[0]
        title = humanize_title(rest_slug or entry.stem)

        # Robust year detection: any 4-digit year in label or name
        year = None
        if dt:
            year = dt.year
        else:
            m = re.search(r"(19|20)\d{2}", label)
            if m:
                year = int(m.group(0))
            else:
                m2 = re.search(r"(19|20)\d{2}", base_no_ext)
                if m2:
                    year = int(m2.group(0))

        # Matching .txt file next to the image
        txt_path = IMAGES_DIR / (base_no_ext + ".txt")
        text_content = ""
        if txt_path.exists():
            text_content = txt_path.read_text(encoding="utf-8", errors="ignore")

        # Matching .jxl file (for JPEG XL) – used in fullscreen only
        jxl_path = IMAGES_DIR / (base_no_ext + ".jxl")
        jxl_rel_path = None
        if jxl_path.exists():
            jxl_rel_path = f"{IMAGES_DIR.name}/{base_no_ext}.jxl"

        events.append({
            "image": thumb_rel_path,      # thumbnail for timeline
            "full_image": full_rel_path,  # full-res for fullscreen
            "jxl_image": jxl_rel_path,    # optional JXL (fullscreen)
            "date_label": label,          # displayed chip (date)
            "file_label": base_no_ext,    # filename without extension
            "title": title,               # alt text only
            "description": "",            # unused for now
            "text": text_content,         # contents of matching .txt if any
            "year": year,
            "sort_key": dt.isoformat() if dt else None,
        })

    # Sort by date (None at the end, in name order)
    def sort_key(ev):
        return (ev["sort_key"] is None, ev["sort_key"] or "")

    events.sort(key=sort_key)
    return events


def build_html(events):
    # Strip sort_key before embedding
    for ev in events:
        ev.pop("sort_key", None)

    events_json = json.dumps(events, ensure_ascii=False, indent=2)

    html_template = r"""<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>OSS Jedburghs: Team Augustus</title>
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <style>
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }

    html, body {
      scroll-behavior: smooth;
    }

    body {
      font-family: "Georgia", "Times New Roman", serif;
      background:
        radial-gradient(circle at top left, rgba(255,255,255,0.08), transparent 55%),
        radial-gradient(circle at bottom right, rgba(0,0,0,0.5), #151311 80%);
      color: #f4f0e4;
      padding: 2rem 1rem;
      display: flex;
      justify-content: center;
    }

    .page {
      width: 100%;
      max-width: 960px;
    }

    h1 {
      text-align: center;
      margin-bottom: 0.25rem;
      font-size: 2rem;
      letter-spacing: 0.16em;
      text-transform: uppercase;
      color: #f4f0e4;
    }

    .subtitle {
      text-align: center;
      font-size: 0.85rem;
      letter-spacing: 0.25em;
      text-transform: uppercase;
      color: #b9b2a0;
      margin-bottom: 1.0rem;
    }

    /* Site nav: About / Reports / Roger */
    .site-nav {
      display: flex;
      justify-content: center;
      gap: 2.5rem;
      margin-bottom: 1.5rem;
      font-family: "Courier New", monospace;
      text-transform: uppercase;
      letter-spacing: 0.18em;
      font-size: 2.25rem;
    }

    .site-nav a {
      color: #f4f0e4;
      text-decoration: none;
      padding: 0.2rem 1.2rem;
      border-bottom: 1px solid transparent;
      opacity: 0.85;
      transition: border-color 0.12s ease-out, opacity 0.12s ease-out;
    }

    .site-nav a:hover {
      border-color: #f4f0e4;
      opacity: 1;
    }

    .site-nav a.active {
      border-color: #f4f0e4;
      opacity: 1;
    }

    /* Top clickable timeline bar (years only) */
    .timeline-nav-wrapper {
      display: flex;
      justify-content: center;
      width: 100%;
      margin-bottom: 1.75rem;
      padding: 0.6rem 0.7rem;
      border-radius: 999px;
      background: radial-gradient(circle at top left, rgba(255,255,255,0.09), rgba(0,0,0,0.7));
      border: 1px solid rgba(80, 70, 55, 0.9);
      box-shadow:
        0 10px 20px rgba(0,0,0,0.65),
        inset 0 0 0 1px rgba(255,255,255,0.18);
      overflow: hidden;
      max-width: 100%;
    }

    .timeline-nav {
      display: flex;
      justify-content: center;
      align-items: center;
      flex-wrap: wrap;
      width: 100%;
      text-align: center;
      gap: 1.0rem 2.0rem;
      padding: 0.25rem 0.5rem 0.35rem;
      scrollbar-width: thin;
      scrollbar-color: #8b7b60 transparent;
    }

    .timeline-nav::-webkit-scrollbar {
      height: 6px;
    }

    .timeline-nav::-webkit-scrollbar-track {
      background: transparent;
    }

    .timeline-nav::-webkit-scrollbar-thumb {
      background: #8b7b60;
      border-radius: 999px;
    }

    .nav-pill {
      flex: 0 0 auto;
      font-family: "Courier New", "Lucida Console", monospace;
      font-size: 0.78rem;
      text-transform: uppercase;
      letter-spacing: 0.18em;
      border-radius: 999px;
      border: 1px solid rgba(32, 25, 16, 0.9);
      padding: 0.35rem 1.0rem;
      background: radial-gradient(circle at 15% 10%, #f9f1dc, #d3c4a0);
      color: #3c2d1b;
      cursor: pointer;
      box-shadow:
        0 1px 1px rgba(255,255,255,0.6),
        0 2px 4px rgba(0,0,0,0.6);
      white-space: nowrap;
      transition: transform 0.08s ease-out, box-shadow 0.1s ease-out, background 0.1s ease-out;
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }

    .nav-pill:hover {
      transform: translateY(-1px);
      box-shadow:
        0 2px 6px rgba(0,0,0,0.8),
        0 0 0 1px rgba(255,255,255,0.3);
      background: radial-gradient(circle at 15% 10%, #fff8e3, #d9caac);
    }

    .nav-pill:active {
      transform: translateY(0px);
      box-shadow:
        0 1px 3px rgba(0,0,0,0.9),
        inset 0 0 4px rgba(0,0,0,0.4);
      background: radial-gradient(circle at 25% 20%, #f0e1c2, #c5b18c);
    }

    /* Disabled year buttons (before that year's images are ready) */
    .nav-pill-disabled {
      opacity: 0.35;
      pointer-events: none;
      filter: grayscale(0.6);
    }

    /* Timeline container now holds year groups stacked vertically */
    .timeline {
      margin: 2rem 0;
      padding-left: 0;
      position: relative;
    }

    /* No global vertical line in masonry mode */
    .timeline::before {
      display: none;
    }

    /* Each year section */
    .timeline-year-group {
      margin-bottom: 3rem;
    }

    /* Year heading above each masonry block */
    .timeline-year-heading {
      font-family: "Georgia", serif;
      font-weight: bold;
      font-size: 2.0rem;
      letter-spacing: 0.25em;
      text-transform: uppercase;
      color: #d6cbb2;
      text-align: center;
      margin-bottom: 1rem;
    }

    /* Grid layout for that year's items: sequential horizontally */
    .timeline-year-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
      gap: 2rem;                /* spacing between cards */
    }

    /* Items now just behave like normal grid cells in DOM order */
    .timeline-item {
      margin: 0;
      padding: 0;
      scroll-margin-top: 80px;  /* still helpful for anchor jumps */
    }

    /* Any item with associated text spans full width of the grid */
    .timeline-item-full {
      grid-column: 1 / -1;  /* span all columns */
    }

    /* Let the image breathe more on full-width items */
    .timeline-item-full .timeline-image-wrapper img {
      max-width: 100%;
    }

    .timeline-card {
      background: #f4ecd5;
      background-image:
        linear-gradient(90deg, rgba(255,255,255,0.15) 0, transparent 30%, transparent 70%, rgba(0,0,0,0.04) 100%),
        repeating-linear-gradient(
          to bottom,
          rgba(0,0,0,0.04) 0px,
          rgba(0,0,0,0.04) 1px,
          transparent 1px,
          transparent 22px
        );
      border-radius: 0.5rem;
      padding: 0.9rem 1rem 1.2rem;
      border: 1px solid rgba(70, 60, 40, 0.9);
      box-shadow:
        0 16px 26px rgba(0, 0, 0, 0.7),
        inset 0 0 0 1px rgba(255, 255, 255, 0.2);
      color: #3c2d1b;
      position: relative;
      height: 100%;
    }

    .timeline-card::before {
      content: "";
      position: absolute;
      top: 0.4rem;
      right: 0.75rem;
      width: 70px;
      height: 14px;
      border-radius: 999px;
      border: 1px solid rgba(0,0,0,0.15);
      box-shadow:
        inset 0 0 0 1px rgba(255,255,255,0.6),
        0 1px 2px rgba(0,0,0,0.4);
      opacity: 0.6;
    }

    .back-to-top {
      position: absolute;
      top: 0.4rem;
      right: 0.4rem;

      font-family: "Courier New", monospace;
      font-size: 0.7rem;
      letter-spacing: 0.15em;
      text-transform: uppercase;

      padding: 0.25rem 0.55rem;
      border-radius: 5px;
      border: 1px solid rgba(40, 32, 20, 0.8);

      background: linear-gradient(#faf3dd, #dfcfac);
      color: #3c2d1b;
      cursor: pointer;

      box-shadow:
        0 1px 1px rgba(255,255,255,0.5),
        0 2px 4px rgba(0,0,0,0.4);

      transition: all 0.1s ease-out;
      text-decoration: none;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }

    .back-to-top:hover {
      background: linear-gradient(#fff7e5, #d8c6a3);
      transform: translateY(-1px);
    }

    .back-to-top:active {
      transform: translateY(0);
      box-shadow:
        inset 0 0 4px rgba(0,0,0,0.4);
    }

    .timeline-header {
      display: flex;
      flex-direction: column;
      gap: 0.25rem;
      margin-bottom: 0.5rem;
    }

    .timeline-date {
      font-family: "Courier New", "Lucida Console", monospace;
      font-size: 0.9rem;
      text-transform: uppercase;
      letter-spacing: 0.2em;
      color: #5b4a32;
      padding: 0.15rem 0.55rem;
      border-radius: 999px;
      border: 1px solid rgba(60, 46, 30, 0.7);
      background: linear-gradient(#f9f1dc, #e3d4b2);
      box-shadow:
        0 1px 0 rgba(255,255,255,0.7),
        0 2px 3px rgba(0,0,0,0.25);
      align-self: flex-start;
    }

    .timeline-file {
      font-family: "Courier New", "Lucida Console", monospace;
      font-size: 0.8rem;
      letter-spacing: 0.12em;
      text-transform: uppercase;
      color: #7a6242;
      opacity: 0.9;
      word-break: break-all;
    }

    .timeline-description {
      font-size: 0.9rem;
      color: #4a3825;
      line-height: 1.5;
      margin-top: 0.4rem;
    }

    .timeline-image-wrapper {
      margin-top: 0.75rem;
      border-radius: 0.45rem;
      overflow: hidden;
      border: 1px solid rgba(60, 47, 30, 0.9);
      box-shadow:
        0 3px 8px rgba(0,0,0,0.6),
        inset 0 0 18px rgba(0,0,0,0.25);
      filter: sepia(0.55) contrast(1.02) saturate(0.8);
      display: flex;
      justify-content: center;
      align-items: center;
    }

    .timeline-image-wrapper img {
      max-width: 90%;
      height: auto;
      margin: 0 auto;
      display: block;
      cursor: zoom-in;
      transition: transform 0.12s ease-out;
    }

    .timeline-image-wrapper img:hover {
      transform: scale(1.01);
    }

    .timeline-textbox {
      margin: 1.1rem 0 0 0;
      background: #f7f2e2;
      border: 1px solid rgba(70, 60, 40, 0.9);
      padding: 1rem 1.2rem;
      border-radius: 0.45rem;

      width: 100%;
      box-sizing: border-box;

      text-align: center;
      font-family: "Courier New", monospace;
      font-size: 0.95rem;
      color: #3b2c19;

      white-space: pre-wrap;
      overflow-wrap: break-word;

      max-height: none;
      overflow: visible;

      box-shadow:
        inset 0 0 12px rgba(0,0,0,0.15),
        0 3px 8px rgba(0,0,0,0.4);
    }

    /* Fullscreen overlay for clicked images, with fade in/out */
    .fullscreen-overlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.92);
      display: flex;
      align-items: center;
      justify-content: center;
      z-index: 9999;
      cursor: zoom-out;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.35s ease-out;
    }

    .fullscreen-overlay.is-visible {
      opacity: 1;
      pointer-events: auto;
    }

    .fullscreen-overlay img {
      max-width: 95vw;
      max-height: 95vh;
      box-shadow: 0 0 40px rgba(0,0,0,0.9);
      border-radius: 0.5rem;
    }

    /* Initial message over the first fullscreen image */
    .fullscreen-message {
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);

      background: rgba(0,0,0,0.6);
      padding: 1.2rem 2rem;
      border: 1px solid rgba(255,255,255,0.3);
      border-radius: 8px;

      color: #f7f3e8;
      font-family: "Courier New", monospace;
      font-size: 1.2rem;
      text-align: center;
      letter-spacing: 0.05em;

      max-width: 90%;
      box-shadow: 0 0 18px rgba(0,0,0,0.7);
      opacity: 1;
      transition: opacity 0.35s ease-out;
    }

    .fullscreen-message.hidden {
      opacity: 0;
      pointer-events: none;
    }

    .page-footer {
      margin-top: 3rem;
      text-align: center;
      font-size: 0.75rem;
      color: #9d9482;
      letter-spacing: 0.16em;
      text-transform: uppercase;
    }

    @media (max-width: 640px) {
      body {
        padding: 1.25rem 0.75rem;
      }
      .site-nav {
        gap: 1.2rem;
        font-size: 0.72rem;
      }
    }
  </style>
</head>
<body>
  <!-- Fullscreen overlay for images (message + picture with JXL fallback) -->
  <div id="fullscreen-overlay" class="fullscreen-overlay">
    <div id="initial-message" class="fullscreen-message">
      This project is a work in progress<br><br>
      Click anywhere to continue
    </div>
    <picture id="fullscreen-picture">
      <source id="fullscreen-source-jxl" type="image/jxl">
      <img id="fullscreen-image" alt="Full-size archive image">
    </picture>
  </div>

  <!-- Anchor target for "Back to top" -->
  <main class="page" id="page-top">
    <h1>OSS Jedburghs: Team Augustus</h1>
    <p class="subtitle">Operational Timeline &amp; Archival Records</p>

    <!-- Site-wide navigation -->
    <nav class="site-nav">
      <a href="about.html">About</a>
      <a href="reports.html">Reports</a>
      <a href="roger.html">Roger</a>
      <a href="sources.html">Sources</a>
    </nav>

    <!-- Year timeline navigation -->
    <div class="timeline-nav-wrapper">
      <div id="timeline-nav" class="timeline-nav"></div>
    </div>

    <div id="timeline" class="timeline"></div>

    <p class="page-footer">Office Of Strategic Services &mdash; Declassified Reproduction</p>
  </main>

  <script>
    const events = __EVENTS_JSON__;

    // Track image loading per year: { "1944": { total, loaded, ready } }
    const yearLoadState = {};

    let fullscreenOverlay = null;
    let fullscreenImage = null;

    function openFullscreen(src, alt, jxlSrc = null) {
      if (!fullscreenOverlay || !fullscreenImage) return;

      const source = document.getElementById("fullscreen-source-jxl");
      if (source) {
        if (jxlSrc) {
          source.srcset = jxlSrc;
        } else {
          source.srcset = "";
        }
      }

      fullscreenImage.src = src;
      fullscreenImage.alt = alt || "Full-size archive image";
      fullscreenOverlay.classList.add("is-visible");
    }

    function closeFullscreen() {
      if (!fullscreenOverlay || !fullscreenImage) return;

      const msg = document.getElementById("initial-message");
      if (msg) {
        msg.classList.add("hidden");  // hide permanently after first close
      }

      fullscreenOverlay.classList.remove("is-visible");

      setTimeout(() => {
        if (!fullscreenOverlay.classList.contains("is-visible")) {
          const source = document.getElementById("fullscreen-source-jxl");
          if (source) {
            source.srcset = "";
          }
          fullscreenImage.src = "";
        }
      }, 350);
    }

    function markYearImageLoaded(yearStr) {
      const state = yearLoadState[yearStr];
      if (!state) return;

      state.loaded += 1;

      if (!state.ready && state.loaded >= state.total) {
        state.ready = true;
        const btn = document.querySelector('.nav-pill[data-year="' + yearStr + '"]');
        if (btn) {
          btn.classList.remove("nav-pill-disabled");
        }
      }
    }

    function renderTimeline(events) {
      const container = document.getElementById("timeline");
      container.innerHTML = "";

      // Reset/load state
      Object.keys(yearLoadState).forEach((k) => delete yearLoadState[k]);

      // Build a map: yearStr -> array of { item, index }
      const yearMap = new Map();
      const UNKNOWN_KEY = "Unknown";

      events.forEach((item, index) => {
        let key;
        if (item.year != null) {
          key = String(item.year);
        } else {
          key = UNKNOWN_KEY;
        }

        if (!yearMap.has(key)) {
          yearMap.set(key, []);
        }
        yearMap.get(key).push({ item, index });
      });

      // Sort year keys: numeric years ascending, "Unknown" last (if present)
      const numericYears = Array.from(yearMap.keys())
        .filter((k) => k !== UNKNOWN_KEY)
        .map((k) => parseInt(k, 10))
        .sort((a, b) => a - b)
        .map((n) => String(n));

      const hasUnknown = yearMap.has(UNKNOWN_KEY);

      // Initialize load state per numeric year (thumb per entry)
      numericYears.forEach((yearStr) => {
        const entries = yearMap.get(yearStr) || [];
        yearLoadState[yearStr] = {
          total: entries.length,
          loaded: 0,
          ready: false,
        };
      });

      // Render each numeric year section
      numericYears.forEach((yearStr) => {
        const groupEl = document.createElement("section");
        groupEl.className = "timeline-year-group";
        groupEl.id = yearStr;            // anchor target for the year bar (#1944 etc.)

        const heading = document.createElement("h2");
        heading.className = "timeline-year-heading";
        heading.textContent = yearStr;
        groupEl.appendChild(heading);

        const gridEl = document.createElement("div");
        gridEl.className = "timeline-year-grid";

        const entries = yearMap.get(yearStr) || [];
        entries.forEach(({ item, index }) => {
          const itemEl = document.createElement("article");
          itemEl.className = "timeline-item";
          itemEl.dataset.eventIndex = index.toString();
          itemEl.dataset.year = yearStr;
          itemEl.style.scrollMarginTop = "80px";

          // If there is associated text, make this a full-width item
          if (item.text && item.text.trim().length > 0) {
            itemEl.classList.add("timeline-item-full");
          }

          const cardEl = document.createElement("div");
          cardEl.className = "timeline-card";

          // Back to top link
          const topLink = document.createElement("a");
          topLink.className = "back-to-top";
          topLink.textContent = "Top";
          topLink.href = "#page-top";
          cardEl.appendChild(topLink);

          const headerEl = document.createElement("div");
          headerEl.className = "timeline-header";

          const dateEl = document.createElement("span");
          dateEl.className = "timeline-date";
          dateEl.textContent = item.date_label || "";
          headerEl.appendChild(dateEl);

          if (item.file_label) {
            const fileEl = document.createElement("span");
            fileEl.className = "timeline-file";
            fileEl.textContent = item.file_label;
            headerEl.appendChild(fileEl);
          }

          cardEl.appendChild(headerEl);

          const imgWrapper = document.createElement("div");
          imgWrapper.className = "timeline-image-wrapper";

          const picture = document.createElement("picture");
          const img = document.createElement("img");
          img.src = item.image; // thumbnail
          img.alt = item.title || "Archive photo";
          img.decoding = "async";

          // Track when this year's thumbnails finish loading
          img.addEventListener("load", () => {
            markYearImageLoaded(yearStr);
          });
          img.addEventListener("error", () => {
            markYearImageLoaded(yearStr);
          });

          img.addEventListener("click", () => {
            const fullSrc = item.full_image || item.image;
            const jxlSrc = item.jxl_image || null;
            openFullscreen(
              fullSrc,
              item.title || item.file_label || "Archive photo",
              jxlSrc
            );
          });

          picture.appendChild(img);
          imgWrapper.appendChild(picture);
          cardEl.appendChild(imgWrapper);

          if (item.text && item.text.trim().length > 0) {
            const textBox = document.createElement("div");
            textBox.className = "timeline-textbox";
            textBox.textContent = item.text;
            cardEl.appendChild(textBox);
          }

          itemEl.appendChild(cardEl);
          gridEl.appendChild(itemEl);
        });

        groupEl.appendChild(gridEl);
        container.appendChild(groupEl);
      });

      // Optional: "Unknown Date" group at the end
      if (hasUnknown) {
        const entries = yearMap.get(UNKNOWN_KEY) || [];
        if (entries.length > 0) {
          const groupEl = document.createElement("section");
          groupEl.className = "timeline-year-group";

          const heading = document.createElement("h2");
          heading.className = "timeline-year-heading";
          heading.textContent = "Unknown Date";
          groupEl.appendChild(heading);

          const gridEl = document.createElement("div");
          gridEl.className = "timeline-year-grid";

          entries.forEach(({ item, index }) => {
            const itemEl = document.createElement("article");
            itemEl.className = "timeline-item";
            itemEl.dataset.eventIndex = index.toString();
            itemEl.style.scrollMarginTop = "80px";

            // Full-width if there is associated text
            if (item.text && item.text.trim().length > 0) {
              itemEl.classList.add("timeline-item-full");
            }

            const cardEl = document.createElement("div");
            cardEl.className = "timeline-card";

            const topLink = document.createElement("a");
            topLink.className = "back-to-top";
            topLink.textContent = "Top";
            topLink.href = "#page-top";
            cardEl.appendChild(topLink);

            const headerEl = document.createElement("div");
            headerEl.className = "timeline-header";

            const dateEl = document.createElement("span");
            dateEl.className = "timeline-date";
            dateEl.textContent = item.date_label || "";
            headerEl.appendChild(dateEl);

            if (item.file_label) {
              const fileEl = document.createElement("span");
              fileEl.className = "timeline-file";
              fileEl.textContent = item.file_label;
              headerEl.appendChild(fileEl);
            }

            cardEl.appendChild(headerEl);

            const imgWrapper = document.createElement("div");
            imgWrapper.className = "timeline-image-wrapper";

            const picture = document.createElement("picture");
            const img = document.createElement("img");
            img.src = item.image;
            img.alt = item.title || "Archive photo";
            img.decoding = "async";

            img.addEventListener("click", () => {
              const fullSrc = item.full_image || item.image;
              const jxlSrc = item.jxl_image || null;
              openFullscreen(
                fullSrc,
                item.title || item.file_label || "Archive photo",
                jxlSrc
              );
            });

            picture.appendChild(img);
            imgWrapper.appendChild(picture);
            cardEl.appendChild(imgWrapper);

            if (item.text && item.text.trim().length > 0) {
              const textBox = document.createElement("div");
              textBox.className = "timeline-textbox";
              textBox.textContent = item.text;
              cardEl.appendChild(textBox);
            }

            itemEl.appendChild(cardEl);
            gridEl.appendChild(itemEl);
          });

          groupEl.appendChild(gridEl);
          container.appendChild(groupEl);
        }
      }
    }

    function renderTimelineNav(events) {
      const nav = document.getElementById("timeline-nav");
      nav.innerHTML = "";

      const yearSet = new Set();
      events.forEach((item) => {
        if (item.year != null) {
          yearSet.add(item.year);
        }
      });

      const years = Array.from(yearSet).map(Number).sort((a, b) => a - b);

      years.forEach((year) => {
        const yearStr = String(year);

        const link = document.createElement("a");
        link.className = "nav-pill nav-pill-disabled"; // start disabled/greyed
        link.textContent = yearStr;
        link.href = "#" + yearStr;  // anchor is just #1944, #2020, etc.
        link.dataset.year = yearStr;

        nav.appendChild(link);
      });
    }

    document.addEventListener("DOMContentLoaded", () => {
      fullscreenOverlay = document.getElementById("fullscreen-overlay");
      fullscreenImage = document.getElementById("fullscreen-image");

      if (fullscreenOverlay) {
        fullscreenOverlay.addEventListener("click", () => {
          closeFullscreen();
        });
      }

      document.addEventListener("keydown", (e) => {
        if (e.key === "Escape") {
          closeFullscreen();
        }
      });

      renderTimeline(events);
      renderTimelineNav(events);

      // Initial fullscreen: first event (with popup message)
      if (events.length > 0) {
        const first = events[0];
        const fallbackSrc = first.full_image || first.image;
        const jxlSrc = first.jxl_image || null;

        const msg = document.getElementById("initial-message");
        if (msg) {
          msg.classList.remove("hidden");
        }

        openFullscreen(
          fallbackSrc,
          first.title || first.file_label || "Archive photo",
          jxlSrc
        );
      }
    });
  </script>
</body>
</html>
"""
    return html_template.replace("__EVENTS_JSON__", events_json)


def main():
    events = collect_events()
    html = build_html(events)
    OUTPUT_HTML.write_text(html, encoding="utf-8")
    print(f"Generated {OUTPUT_HTML} with {len(events)} events.")


if __name__ == "__main__":
    main()
