/*! trentpower.fr · authored source */
/* app-enhance source. edit here, not in app-enhance.js. the build
   header is prepended at build time by generate_site.py.

   loaded after first paint by app.js via requestidlecallback +
   document.createElement('script') — the script url passes through
   the tp-i18n trusted types policy. carries the project-overlay
   lifecycle (focus trap, click-outside, escape) and the
   print-time document.title swap. none of this is needed for
   first paint, language detection or accessibility, so it does
   not enter the lcp critical path. */

(function () {
  'use strict';

  // shared globals come from /app.js: window.I18N, window.LANG_CYCLE.
  // /app.js exports those before scheduling this bundle.
  var I18N = window.I18N || null;

  // ═══════════════════════════════════════════════════
  // overlay , accessible, focus-trapped, reusable
  // the same lifecycle (blur backdrop, scroll lock, inert siblings,
  // focus trap, escape close, click-outside close) is used by both
  // the homepage's "view project" modal and the cite-this-page
  // overlay rendered by cite.js. exposed via window.TP_OVERLAY so
  // cite.js can register its own (overlay, trigger) pair without
  // duplicating the lifecycle.
  // ═══════════════════════════════════════════════════

  var siteContent = document.getElementById('main');
  var supportsInert = 'inert' in document.documentElement;

  function setInert(el, state) {
    if (!el) return;
    if (state) {
      if (supportsInert) el.setAttribute('inert', '');
      el.setAttribute('aria-hidden', 'true');
    } else {
      if (supportsInert) el.removeAttribute('inert');
      el.removeAttribute('aria-hidden');
    }
  }

  function focusableIn(el) {
    return el.querySelectorAll('a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"]), input:not([disabled]), select:not([disabled]), textarea:not([disabled])');
  }

  var activeOverlay = null;
  var activeTrigger = null;
  // history integration · whether this opener pushed a state. only set
  // when opts.hash is supplied. cleared whenever an overlay closes.
  var pushedHistoryState = false;

  function openOverlay(overlay, trigger, opts) {
    if (!overlay) return;
    activeOverlay = overlay;
    activeTrigger = trigger || null;
    overlay.setAttribute('aria-hidden', 'false');
    overlay.classList.add('active');
    document.body.style.overflow = 'hidden';
    setInert(siteContent, true);
    if (trigger) trigger.setAttribute('aria-expanded', 'true');
    var focusable = focusableIn(overlay);
    if (focusable.length > 0) focusable[0].focus();
    // optional history integration. caller passes opts.hash (e.g. '#cite')
    // and the overlay becomes back-button-closable + deep-linkable. the
    // opts.fromPopstate flag lets the popstate listener re-open without
    // pushing an extra entry.
    pushedHistoryState = false;
    if (opts && opts.hash && !opts.fromPopstate &&
        typeof history !== 'undefined' && typeof history.pushState === 'function') {
      try {
        history.pushState({ tp_overlay: opts.hash.replace(/^#/, '') }, '', opts.hash);
        pushedHistoryState = true;
      } catch (_) { /* sandboxed iframe etc. — silently no-op */ }
    }
  }

  function closeOverlay(opts) {
    if (!activeOverlay) return;
    var overlay = activeOverlay;
    var trigger = activeTrigger;
    overlay.classList.remove('active');
    overlay.setAttribute('aria-hidden', 'true');
    document.body.style.overflow = '';
    setInert(siteContent, false);
    if (trigger) {
      trigger.setAttribute('aria-expanded', 'false');
      trigger.focus();
    }
    activeOverlay = null;
    activeTrigger = null;
    // if we pushed a history entry on open and this close is user-
    // initiated (× / esc / click-outside, not a popstate-driven close),
    // pop our entry so the url reverts cleanly. wrapped in try because
    // history.back() can fail when the page is the only entry.
    if (pushedHistoryState && !(opts && opts.fromPopstate) &&
        typeof history !== 'undefined' && typeof history.back === 'function') {
      try { history.back(); } catch (_) { /* no prior entry — stay put */ }
    }
    pushedHistoryState = false;
  }

  // popstate · browser back/forward. if an overlay is open and the new
  // location no longer carries our state, close from popstate so we
  // don't double-pop the history. Open-on-forward isn't supported in
  // this version — initial-hash auto-open is handled by the overlay's
  // own init code (cite.template.js).
  window.addEventListener('popstate', function () {
    if (activeOverlay) {
      var st = history.state;
      var stillOpen = st && st.tp_overlay;
      if (!stillOpen) closeOverlay({ fromPopstate: true });
    }
  });

  document.addEventListener('keydown', function (e) {
    if (!activeOverlay) return;
    if (e.key === 'Escape') { closeOverlay(); return; }
    if (e.key === 'Tab') {
      var focusable = focusableIn(activeOverlay);
      if (focusable.length === 0) return;
      var first = focusable[0];
      var last = focusable[focusable.length - 1];
      if (e.shiftKey) {
        if (document.activeElement === first) { e.preventDefault(); last.focus(); }
      } else {
        if (document.activeElement === last) { e.preventDefault(); first.focus(); }
      }
    }
  });

  // expose for cite.js (and any future overlay).
  window.TP_OVERLAY = { open: openOverlay, close: closeOverlay };

  // project "view project" modal , wires its trigger / close button into
  // the shared lifecycle. pages without #modal silently skip.
  var modal = document.getElementById('modal');
  var btn = document.getElementById('access-btn');
  var closeBtn = document.getElementById('modal-close');

  if (modal && btn && closeBtn) {
    btn.addEventListener('click', function () { openOverlay(modal, btn); });
    closeBtn.addEventListener('click', closeOverlay);
    modal.addEventListener('click', function (e) {
      if (e.target === modal) closeOverlay();
    });
  }

  // ═══════════════════════════════════════════════════
  // print , temporarily swap document.title so the browser's
  // "save as pdf" dialogue suggests a clean filename, then restore
  // on afterprint so the on-screen tab title is unaffected.
  // Reveal-classed elements are forced visible defensively (the
  // live screen layout is hidden by the print.css direct-child rule,
  // but the visibility flag is harmless on screen and protects
  // against future regressions).
  // ═══════════════════════════════════════════════════

  var savedDocTitle = null;

  function onBeforePrint() {
    var lang = document.documentElement.lang || 'en';
    var t = (I18N && I18N[lang]) || (I18N && I18N.en) || null;
    var page = (document.body && document.body.getAttribute('data-page')) || 'home';
    var printTitle =
      (page === 'privacy'      && t && t.privacy      && t.privacy.print      && t.privacy.print.doc_title) ||
      (page === 'integrity'    && t && t.integrity    && t.integrity.print    && t.integrity.print.doc_title) ||
      (page === 'security'     && t && t.security     && t.security.print     && t.security.print.doc_title) ||
      (page === 'source'       && t && t.source       && t.source.print       && t.source.print.doc_title) ||
      (page === 'releases'     && t && t.releases     && t.releases.print     && t.releases.print.doc_title) ||
      (page === 'forbidden'    && t && t.error        && t.error['403']       && t.error['403'].print && t.error['403'].print.doc_title) ||
      (page === 'not-found'    && t && t.error        && t.error['404']       && t.error['404'].print && t.error['404'].print.doc_title) ||
      (page === 'server-error' && t && t.error        && t.error['500']       && t.error['500'].print && t.error['500'].print.doc_title) ||
      (page === 'verify'       && t && t.verify       && t.verify.doc_title) ||
      (t && t.print && t.print.doc_title);
    if (printTitle) {
      savedDocTitle = document.title;
      document.title = printTitle;
    }
    var revealSelector = '.principle, .trajectory-item, .project-card, '
      + '.hero-name, .hero-statement, .hero-body, .trust-mark';
    document.querySelectorAll(revealSelector).forEach(function (el) {
      el.classList.add('visible');
    });
    document.documentElement.classList.add('is-printing');
  }

  function onAfterPrint() {
    if (savedDocTitle !== null) {
      document.title = savedDocTitle;
      savedDocTitle = null;
    }
    document.documentElement.classList.remove('is-printing');
  }

  if (window.matchMedia) {
    try {
      window.matchMedia('print').addEventListener('change', function (e) {
        if (e.matches) onBeforePrint(); else onAfterPrint();
      });
    } catch (_) { /* older browsers fall back to onbeforeprint */ }
  }
  window.addEventListener('beforeprint', onBeforePrint);
  window.addEventListener('afterprint', onAfterPrint);

  // ═══════════════════════════════════════════════════
  // POST-LCP FULL-FONT upgrade
  // /fonts-full.css carries the full editorial @font-face declarations
  // and a `.fonts-loaded` :root override that flips --serif / --sans /
  // --mono to put the full families first. loaded as a same-origin
  // stylesheet (csp `style-src 'self'` allows it; no trusted types
  // gate applies — `require-trusted-types-for 'script'` only governs
  // script-URL sinks). subset and full font share identical glyph
  // metrics so the swap is glyph-by-glyph with cls = 0.
  // ═══════════════════════════════════════════════════

  (function _loadFullFonts() {
    if (document.querySelector('link[data-tp-fonts-full]')) return;
    var link = document.createElement('link');
    link.rel = 'stylesheet';
    link.href = '/fonts-full.css';
    link.setAttribute('data-tp-fonts-full', '');
    // wait until every pending font load has resolved (loaded or hit
    // its font-display timeout), then run the class flip inside the
    // next paint frame so the recalc rides the browser's normal paint
    // pipeline rather than appearing as a free-standing reflow.
    function paintFlip() {
      if (typeof requestAnimationFrame === 'function') {
        requestAnimationFrame(function () {
          document.documentElement.classList.add('fonts-loaded');
        });
      } else {
        document.documentElement.classList.add('fonts-loaded');
      }
    }
    link.addEventListener('load', function () {
      if (document.fonts && document.fonts.ready && typeof document.fonts.ready.then === 'function') {
        document.fonts.ready.then(paintFlip);
      } else {
        setTimeout(paintFlip, 1500);
      }
    });
    document.head.appendChild(link);
  })();

  // quiet hello to anyone reading the console.
  // one line, once per page load. no tracking, no network, no ui.
  var canLog = window.console && typeof console.info === "function";
  if (canLog) {
    console.info("trentpower.fr · static, signed, inspectable · ctrl+u still works");
  }

})();
