'use strict';

/* ============================================================
   CONSTANTS
   ============================================================ */
const CAPTURE_W = 360;
const CAPTURE_H = 640;

const SCENE_LABEL = {
  opening:  'スカウト',
  gravure1: 'グラビア撮影会',
};

const ALL_PHOTO_IDS = ['g1_pic1'];

const ALBUMS = {
  trial: {
    id: 'trial',
    title: '体験版アルバム',
    coverPhotoId: 'g1_pic1',
    coverLogoSrc: 'images/cover_logo_normal.png',
    photoIds: ['g1_pic1'],
  },
};

/* ============================================================
   APP STATE
   ============================================================ */
const S = {
  scene:        null,
  audio:        null,
  isDummy:      false,
  firedIdx:     new Set(),
  photos:       {},
  imgCache:     {},
  overlayImg:   null,
  numImgs:      {},
  currentBgImg: null,
  albumShown:   false,
  cameraStream: null,
  hasCamera:    false,
  events:       [],
  seeking:      false,
};

/* ============================================================
   DUMMY AUDIO  (simulates HTMLAudioElement when file is missing)
   ============================================================ */
class DummyAudio {
  #t    = 0;
  #sw   = 0;
  #st   = 0;
  #rate = 1;
  #paused = true;
  #dur  = 9999;
  #iv   = null;

  ontimeupdate = null;
  onended      = null;
  src          = '';

  get currentTime() {
    if (this.#paused) return this.#t;
    return this.#st + (performance.now() - this.#sw) / 1000 * this.#rate;
  }
  set currentTime(v) {
    this.#t = v;
    if (!this.#paused) { this.#sw = performance.now(); this.#st = v; }
  }

  get paused()        { return this.#paused; }
  get duration()      { return this.#dur; }
  set duration(v)     { this.#dur = v; }
  get playbackRate()  { return this.#rate; }
  set playbackRate(r) {
    if (!this.#paused) { this.#st = this.currentTime; this.#sw = performance.now(); }
    this.#rate = r;
  }

  play() {
    if (!this.#paused) return Promise.resolve();
    this.#paused = false;
    this.#sw = performance.now();
    this.#st = this.#t;
    this.#iv = setInterval(() => this.#tick(), 50);
    return Promise.resolve();
  }

  pause() {
    if (this.#paused) return;
    this.#t = this.currentTime;
    this.#paused = true;
    clearInterval(this.#iv);
    this.#iv = null;
  }

  load() {}

  #tick() {
    if (this.#paused) return;
    if (this.ontimeupdate) this.ontimeupdate();
    if (this.currentTime >= this.#dur) {
      this.pause();
      if (this.onended) this.onended();
    }
  }
}

/* ============================================================
   UTILITY
   ============================================================ */
const $ = id => document.getElementById(id);

function showScreen(name) {
  document.querySelectorAll('.screen').forEach(s => s.classList.add('hidden'));
  $(`screen-${name}`).classList.remove('hidden');
}

/* ============================================================
   PLACEHOLDER GENERATORS
   ============================================================ */
function hashColor(str) {
  let h = 0;
  for (let i = 0; i < str.length; i++) h = (h * 31 + str.charCodeAt(i)) | 0;
  return Math.abs(h);
}

function makeBgPlaceholder(filename, w = CAPTURE_W, h = CAPTURE_H) {
  const c = document.createElement('canvas');
  c.width = w; c.height = h;
  const ctx = c.getContext('2d');
  const hue = hashColor(filename) % 360;
  const grad = ctx.createLinearGradient(0, 0, w, h);
  grad.addColorStop(0, `hsl(${hue},40%,25%)`);
  grad.addColorStop(1, `hsl(${(hue+40)%360},35%,18%)`);
  ctx.fillStyle = grad;
  ctx.fillRect(0, 0, w, h);
  ctx.strokeStyle = `hsla(${hue},40%,60%,0.15)`;
  ctx.lineWidth = 1;
  for (let x = 0; x < w; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,h); ctx.stroke(); }
  for (let y = 0; y < h; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(w,y); ctx.stroke(); }
  ctx.fillStyle = 'rgba(255,255,255,0.7)';
  ctx.font = `bold ${Math.min(w*0.048,16)}px sans-serif`;
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(`[背景: ${filename}]`, w/2, h/2);
  return c.toDataURL('image/png');
}

function makeNumPlaceholder(num) {
  const c = document.createElement('canvas');
  c.width = 200; c.height = 200;
  const ctx = c.getContext('2d');
  ctx.clearRect(0,0,200,200);
  ctx.fillStyle = 'rgba(255,255,255,0.18)';
  ctx.beginPath(); ctx.arc(100,100,92,0,Math.PI*2); ctx.fill();
  ctx.strokeStyle = 'rgba(255,255,255,0.8)';
  ctx.lineWidth = 4;
  ctx.beginPath(); ctx.arc(100,100,92,0,Math.PI*2); ctx.stroke();
  ctx.fillStyle = '#ffffff';
  ctx.font = 'bold 110px sans-serif';
  ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
  ctx.fillText(String(num), 100, 108);
  return c.toDataURL('image/png');
}

function makeOverlayPlaceholder() {
  const c = document.createElement('canvas');
  c.width = CAPTURE_W; c.height = CAPTURE_H;
  const ctx = c.getContext('2d');
  ctx.clearRect(0,0,c.width,c.height);
  const m = 18, s = 28;
  ctx.strokeStyle = 'rgba(255,215,170,0.75)';
  ctx.lineWidth = 2.5;
  [[m,m],[c.width-m,m],[m,c.height-m],[c.width-m,c.height-m]].forEach(([x,y]) => {
    const dx = x < c.width/2 ? 1 : -1;
    const dy = y < c.height/2 ? 1 : -1;
    ctx.beginPath();
    ctx.moveTo(x + dx*s, y); ctx.lineTo(x, y); ctx.lineTo(x, y + dy*s);
    ctx.stroke();
  });
  ctx.fillStyle = 'rgba(255,215,170,0.85)';
  ctx.font = 'bold 14px serif';
  ctx.textAlign = 'center';
  ctx.fillText('GRAVURE PHOTO COLLECTION', c.width/2, 42);
  ctx.font = '11px serif';
  ctx.fillText('写真集', c.width/2, c.height - 30);
  return c.toDataURL('image/png');
}

function makeLogoPlaceholder(isSecret) {
  const c = document.createElement('canvas');
  c.width = CAPTURE_W; c.height = CAPTURE_H;
  const ctx = c.getContext('2d');
  ctx.clearRect(0,0,c.width,c.height);
  // Semi-transparent bottom band
  ctx.fillStyle = isSecret ? 'rgba(80,0,20,0.7)' : 'rgba(0,20,60,0.7)';
  ctx.fillRect(0, c.height * 0.72, c.width, c.height * 0.28);
  const fname = isSecret ? 'cover_logo_secret.png' : 'cover_logo_normal.png';
  ctx.fillStyle = 'rgba(255,255,255,0.8)';
  ctx.font = 'bold 13px sans-serif';
  ctx.textAlign = 'center';
  ctx.fillText(`[${fname}]`, c.width/2, c.height * 0.86);
  return c.toDataURL('image/png');
}

/* ============================================================
   IMAGE LOADING
   ============================================================ */
function loadImageFromSrc(src, fallbackDataURL) {
  return new Promise(resolve => {
    if (S.imgCache[src]) return resolve(S.imgCache[src]);

    const img = new Image();
    let done = false;

    const finish = (el) => {
      if (done) return; done = true;
      S.imgCache[src] = el;
      resolve(el);
    };

    img.onload  = () => finish(img);
    img.onerror = () => {
      const ph = new Image();
      ph.onload = () => finish(ph);
      ph.src = fallbackDataURL;
    };
    img.src = src;
  });
}

async function preloadAssets(onProgress) {
  // Number images
  for (let n = 1; n <= 3; n++) {
    const img = new Image();
    await new Promise(res => {
      img.onload  = () => { S.numImgs[n] = img; res(); };
      img.onerror = () => {
        const ph = new Image();
        ph.onload = () => { S.numImgs[n] = ph; res(); };
        ph.src = makeNumPlaceholder(n);
      };
      img.src = `images/num${n}.png`;
    });
  }

  // Overlay frame
  const ov = new Image();
  await new Promise(res => {
    ov.onload  = () => { S.overlayImg = ov; res(); };
    ov.onerror = () => {
      const ph = new Image();
      ph.onload = () => { S.overlayImg = ph; res(); };
      ph.src = makeOverlayPlaceholder();
    };
    ov.src = 'images/overlay_frame.png';
  });

  // Cover logos
  await loadImageFromSrc('images/cover_logo_normal.png', makeLogoPlaceholder(false));
  await loadImageFromSrc('images/cover_logo_secret.png', makeLogoPlaceholder(true));

  // Collect all unique background filenames
  const bgSet = new Set();
  Object.values(timelineData).forEach(events =>
    events.forEach(e => { if (e.bg) bgSet.add(e.bg); })
  );
  const bgList = [...bgSet];
  let loaded = 0;

  await Promise.all(bgList.map(fname =>
    loadImageFromSrc(`images/${fname}`, makeBgPlaceholder(fname)).then(() => {
      loaded++;
      onProgress(loaded / bgList.length);
    })
  ));
}

/* ============================================================
   AUDIO (WAV)
   ============================================================ */
function createAudio(sceneName) {
  return new Promise(resolve => {
    const src = `audio/${sceneName}.wav`;
    const audio = new Audio();
    let settled = false;

    const useReal = () => {
      if (settled) return; settled = true;
      resolve({ audio, isDummy: false });
    };
    const useDummy = () => {
      if (settled) return; settled = true;
      console.info(`[DummyAudio] ${src}`);
      resolve({ audio: new DummyAudio(), isDummy: true });
    };

    audio.addEventListener('canplaythrough', useReal, { once: true });
    audio.addEventListener('error', useDummy, { once: true });
    setTimeout(useDummy, 5000);

    audio.src = src;
    audio.load();
  });
}

function stopAudio() {
  if (!S.audio) return;
  S.audio.pause();
  S.audio.ontimeupdate = null;
  S.audio.onended      = null;
  S.audio = null;
}

/* ============================================================
   CAMERA
   ============================================================ */
async function initCamera() {
  if (S.cameraStream) return; // already running — reuse existing stream
  if (!navigator.mediaDevices?.getUserMedia) { S.hasCamera = false; return; }
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: {
        facingMode: { ideal: 'environment' },
        width:  { ideal: 1280 },
        height: { ideal: 720 },
      },
    });
    const video = $('camera-video');
    video.srcObject = stream;
    await new Promise(resolve => {
      video.addEventListener('loadedmetadata', resolve, { once: true });
    });
    await video.play();
    S.cameraStream = stream;
    S.hasCamera    = true;
    $('viewfinder').classList.add('cam-active');
  } catch (err) {
    console.warn('[Camera] getUserMedia failed:', err);
    S.hasCamera = false;
  }
}

function stopCamera() {
  if (S.cameraStream) {
    S.cameraStream.getTracks().forEach(t => t.stop());
    S.cameraStream = null;
  }
  const video = $('camera-video');
  if (video) video.srcObject = null;
  S.hasCamera = false;
  $('viewfinder').classList.remove('cam-active');
}

/* ============================================================
   VIDEO CAPTURE HELPER
   ============================================================ */
function drawVideoToCanvas(ctx, video, destW, destH) {
  const vw = video.videoWidth;
  const vh = video.videoHeight;
  if (!vw || !vh) return;

  // object-fit: cover math
  const videoAR = vw / vh;
  const destAR  = destW / destH;

  let sx, sy, sw, sh;
  if (videoAR > destAR) {
    // Video is wider — crop sides
    sh = vh;
    sw = vh * destAR;
    sx = (vw - sw) / 2;
    sy = 0;
  } else {
    // Video is taller — crop top/bottom
    sw = vw;
    sh = vw / destAR;
    sx = 0;
    sy = (vh - sh) / 2;
  }

  ctx.drawImage(video, sx, sy, sw, sh, 0, 0, destW, destH);
}

/* ============================================================
   RENDERING
   ============================================================ */
function applyBg(bgFilename) {
  const src = `images/${bgFilename}`;
  const img = S.imgCache[src];
  const bgLayer = $('bg-layer');

  if (img) {
    bgLayer.style.backgroundImage = `url("${img.src}")`;
    S.currentBgImg = img;
  } else {
    loadImageFromSrc(src, makeBgPlaceholder(bgFilename)).then(i => {
      bgLayer.style.backgroundImage = `url("${i.src}")`;
      S.currentBgImg = i;
    });
  }
}

function applyOverlay() {
  if (S.overlayImg) {
    $('overlay-frame').style.backgroundImage = `url("${S.overlayImg.src}")`;
  }
}

/* ============================================================
   COUNTDOWN & FLASH EFFECTS
   ============================================================ */
const cdOverlay = () => $('countdown-overlay');
const cdImg     = () => $('countdown-img');
let cdTimeout   = null;

function showCountdown(num) {
  const img = cdImg();
  if (S.numImgs[num]) img.src = S.numImgs[num].src;

  img.classList.remove('pop');
  void img.offsetWidth; // force reflow
  img.classList.add('pop');

  cdOverlay().style.opacity = '1';
  clearTimeout(cdTimeout);
  cdTimeout = setTimeout(() => { cdOverlay().style.opacity = '0'; }, 900);
}

function hideCountdown() {
  clearTimeout(cdTimeout);
  cdOverlay().style.opacity = '0';
}

function triggerFlash() {
  const fl = $('flash-overlay');
  fl.classList.add('flashing');
  fl.style.opacity = '1';
  setTimeout(() => {
    fl.classList.remove('flashing');
    fl.style.opacity = '0';
  }, 100);
}

/* ============================================================
   PHOTO CAPTURE (pure camera or pure bg — NO overlay drawn)
   ============================================================ */
function capturePhoto(photoId) {
  const canvas = $('capture-canvas');
  canvas.width  = CAPTURE_W;
  canvas.height = CAPTURE_H;
  const ctx = canvas.getContext('2d');

  const video = $('camera-video');
  if (S.hasCamera && video && video.readyState >= 2) {
    // Capture from live camera
    drawVideoToCanvas(ctx, video, CAPTURE_W, CAPTURE_H);
  } else {
    // Fallback: draw current background (no overlay frame)
    if (S.currentBgImg) {
      ctx.drawImage(S.currentBgImg, 0, 0, CAPTURE_W, CAPTURE_H);
    } else {
      ctx.fillStyle = '#222';
      ctx.fillRect(0, 0, CAPTURE_W, CAPTURE_H);
    }
  }

  const dataURL = canvas.toDataURL('image/jpeg', 0.88);

  S.photos[photoId] = dataURL;
  try { localStorage.setItem(`photo_${photoId}`, dataURL); }
  catch (e) { console.warn('localStorage error:', e); }

  return dataURL;
}

/* ============================================================
   TIMELINE PROCESSING
   ============================================================ */
function processTimeline(currentTime, events) {
  for (let i = 0; i < events.length; i++) {
    const ev = events[i];
    if (currentTime >= ev.time && !S.firedIdx.has(i)) {
      S.firedIdx.add(i);
      handleEvent(ev);
    }
  }
}

function handleEvent(ev) {
  if (ev.bg) applyBg(ev.bg);

  switch (ev.action) {
    case 'countdown_3': showCountdown(3); break;
    case 'countdown_2': showCountdown(2); break;
    case 'countdown_1': showCountdown(1); break;
    case 'shutter':     handleShutter(ev.photoId); break;
    default: break;
  }
}

function handleShutter(photoId) {
  triggerFlash();
  hideCountdown();
  capturePhoto(photoId);
}

/* ============================================================
   SCENE MANAGEMENT
   ============================================================ */
async function startScene(sceneName) {
  stopAudio();
  S.scene        = sceneName;
  S.firedIdx     = new Set();
  S.currentBgImg = null;
  S.albumShown   = false;

  $('track-name').textContent = SCENE_LABEL[sceneName] || sceneName;
  $('btn-playpause').textContent = '⏸';
  $('album-ready-overlay').classList.add('hidden');

  showScreen('play');
  applyOverlay();

  const events = timelineData[sceneName] || [];
  S.events = events;
  if (events.length > 0 && events[0].bg) applyBg(events[0].bg);

  // Start camera
  await initCamera();

  // Load audio (may be dummy)
  const { audio, isDummy } = await createAudio(sceneName);
  S.audio   = audio;
  S.isDummy = isDummy;

  if (isDummy) {
    audio.duration = sceneDurations[sceneName]
      || (events.length > 0 ? events[events.length - 1].time + 15 : 9999);
  }

  audio.ontimeupdate = () => {
    processTimeline(audio.currentTime, events);
    updateSeekBarUI();
  };
  audio.onended      = () => onSceneEnd(sceneName);

  initSeekBar(audio);

  audio.play().catch(err => console.warn('play() rejected:', err));
}

function onSceneEnd(sceneName) {
  if (sceneName === 'opening') {
    startScene('gravure1');
    return;
  }
  if (sceneName === 'gravure1') {
    if (!S.photos['g1_pic1']) capturePhoto('g1_pic1');
    showAlbumReady();
  }
}

/* ============================================================
   ALBUM READY
   ============================================================ */
function showAlbumReady() {
  if (S.albumShown) return;
  S.albumShown = true;
  $('album-ready-overlay').classList.remove('hidden');
}

/* ============================================================
   ALBUM SELECT UI
   ============================================================ */
function renderAlbumSelect() {
  Object.values(ALBUMS).forEach(album => {
    const coverPhoto = S.photos[album.coverPhotoId];
    const count = album.photoIds.filter(id => !!S.photos[id]).length;
    const total = album.photoIds.length;

    const coverPhotoEl = $(`${album.id}-cover-photo`);
    const coverEmptyEl = $(`${album.id}-cover-empty`);
    const coverLogoEl  = $(`${album.id}-cover-logo`);
    const countEl      = $(`${album.id}-count`);

    if (coverPhoto) {
      coverPhotoEl.src = coverPhoto;
      coverPhotoEl.classList.add('visible');
      coverEmptyEl.style.display = 'none';
    } else {
      coverPhotoEl.classList.remove('visible');
      coverEmptyEl.style.display = '';
    }

    // Cover logo from cache
    const logoImg = S.imgCache[album.coverLogoSrc];
    if (logoImg) {
      coverLogoEl.src = logoImg.src;
      coverLogoEl.classList.add('visible');
    } else {
      coverLogoEl.classList.remove('visible');
    }

    countEl.textContent = `${count} / ${total}`;
  });

  // DLC/パッチ拡張フック: 表紙テキストなどの追加機能はこのイベントを購読して描画する
  document.dispatchEvent(new CustomEvent('gca:album-select-rendered', {
    detail: { albums: ALBUMS }
  }));
}

/* ============================================================
   VIEWER
   ============================================================ */
const viewer = {
  albumId:    null,
  pages:      [],
  currentIdx: 0,
};

function openViewer(albumId) {
  const album = ALBUMS[albumId];
  if (!album) return;

  const available = album.photoIds.filter(id => !!S.photos[id]);
  if (available.length === 0) {
    alert('まだ写真が撮影されていません。');
    return;
  }

  viewer.albumId = albumId;
  viewer.pages   = [];

  if (S.photos[album.coverPhotoId]) {
    viewer.pages.push({ type: 'cover', photoId: album.coverPhotoId });
  }
  available.forEach(id => viewer.pages.push({ type: 'photo', photoId: id }));

  viewer.currentIdx = 0;

  showScreen('viewer');
  showViewerPage(0);
}

function showViewerPage(idx) {
  const photo     = $('viewer-photo');
  const coverWrap = $('viewer-cover-wrap');
  photo.classList.add('fade');

  setTimeout(() => {
    viewer.currentIdx = idx;
    const page = viewer.pages[idx];
    photo.src = S.photos[page.photoId] || '';
    $('viewer-indicator').textContent = `${idx + 1} / ${viewer.pages.length}`;

    if (page.type === 'cover') {
      const album   = ALBUMS[viewer.albumId];
      const logoImg = S.imgCache[album.coverLogoSrc];
      $('viewer-cover-logo').src = logoImg ? logoImg.src : '';
      coverWrap.classList.remove('hidden');
      document.dispatchEvent(new CustomEvent('gca:viewer-cover-shown', {
        detail: { albumId: viewer.albumId, container: coverWrap }
      }));
    } else {
      coverWrap.classList.add('hidden');
    }

    photo.classList.remove('fade');
  }, 150);
}

function viewerPrev() {
  if (viewer.currentIdx > 0) {
    showViewerPage(viewer.currentIdx - 1);
  }
}

function viewerNext() {
  if (viewer.currentIdx < viewer.pages.length - 1) {
    showViewerPage(viewer.currentIdx + 1);
  }
}

function closeViewer() {
  showScreen('album');
}

/* ============================================================
   PLAYBACK CONTROLS
   ============================================================ */
function togglePlayPause() {
  if (!S.audio) return;
  if (S.audio.paused) {
    S.audio.play();
    $('btn-playpause').textContent = '⏸';
  } else {
    S.audio.pause();
    $('btn-playpause').textContent = '▶';
  }
}

function skipBy(deltaSec) {
  if (!S.audio) return;
  const dur = isFinite(S.audio.duration) ? S.audio.duration : Infinity;
  const target = Math.min(Math.max(0, S.audio.currentTime + deltaSec), dur);
  seekTo(target);
}

/* Seek to an arbitrary time, updating fired-event state and background */
function seekTo(time) {
  if (!S.audio) return;
  const events = S.events;

  S.audio.currentTime = time;

  const newFired = new Set();
  S.firedIdx.forEach(idx => {
    if (events[idx] && events[idx].time < time) newFired.add(idx);
  });
  S.firedIdx = newFired;

  for (let i = events.length - 1; i >= 0; i--) {
    if (events[i].time <= time && events[i].bg) {
      applyBg(events[i].bg);
      break;
    }
  }

  updateSeekBarUI();
}

/* ============================================================
   SEEK BAR
   ============================================================ */
function formatTime(sec) {
  if (!isFinite(sec) || sec < 0) sec = 0;
  const m = Math.floor(sec / 60);
  const s = Math.floor(sec % 60);
  return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
}

function initSeekBar(audio) {
  const bar = $('seek-bar');

  const setDuration = () => {
    const dur = audio.duration;
    bar.max = isFinite(dur) && dur > 0 ? dur : 0;
    $('seek-time-duration').textContent = formatTime(dur);
  };

  if (audio instanceof HTMLAudioElement) {
    audio.addEventListener('loadedmetadata', setDuration, { once: true });
    if (audio.readyState >= 1) setDuration();
  } else {
    setDuration();
  }

  updateSeekBarUI();
}

function updateSeekBarUI() {
  if (!S.audio || S.seeking) return;
  const bar = $('seek-bar');
  bar.value = S.audio.currentTime;
  $('seek-time-current').textContent = formatTime(S.audio.currentTime);
}

/* ============================================================
   STORAGE
   ============================================================ */
function loadPhotosFromStorage() {
  ALL_PHOTO_IDS.forEach(id => {
    const v = localStorage.getItem(`photo_${id}`);
    if (v) S.photos[id] = v;
  });
}

function clearAllData() {
  ALL_PHOTO_IDS.forEach(id => localStorage.removeItem(`photo_${id}`));
  S.photos = {};
}

/* ============================================================
   INIT
   ============================================================ */
async function init() {
  loadPhotosFromStorage();
  showScreen('loading');

  await preloadAssets(p => {
    $('loading-bar').style.width = Math.round(p * 100) + '%';
  });

  $('loading-bar').style.width = '100%';
  await new Promise(r => setTimeout(r, 400));

  showScreen('title');
}

/* ============================================================
   EVENT LISTENERS
   ============================================================ */
document.addEventListener('DOMContentLoaded', () => {

  // Title
  $('btn-start-all').addEventListener('click', () => startScene('opening'));
  $('btn-clear-data').addEventListener('click', () => {
    if (confirm('撮影データをすべて削除しますか？')) {
      clearAllData();
      alert('データを削除しました。');
    }
  });

  // Play controls
  $('btn-playpause').addEventListener('click', togglePlayPause);
  $('btn-skip-back').addEventListener('click', () => skipBy(-10));
  $('btn-skip-fwd').addEventListener('click', () => skipBy(10));

  // Seek bar
  $('seek-bar').addEventListener('input', () => {
    S.seeking = true;
    $('seek-time-current').textContent = formatTime(Number($('seek-bar').value));
  });
  $('seek-bar').addEventListener('change', () => {
    seekTo(Number($('seek-bar').value));
    S.seeking = false;
  });

  // Album ready overlay
  $('btn-view-album').addEventListener('click', () => {
    stopCamera();
    renderAlbumSelect();
    showScreen('album');
  });

  // Album select
  $('btn-album-back').addEventListener('click', () => {
    stopCamera();
    showScreen('title');
  });
  $('book-card-trial').addEventListener('click', () => openViewer('trial'));

  // Viewer
  $('viewer-close').addEventListener('click', closeViewer);
  $('viewer-tap-prev').addEventListener('click', viewerPrev);
  $('viewer-tap-next').addEventListener('click', viewerNext);

  // Touch swipe on viewer
  let touchStartX = 0;
  $('screen-viewer').addEventListener('touchstart', e => {
    touchStartX = e.changedTouches[0].clientX;
  }, { passive: true });
  $('screen-viewer').addEventListener('touchend', e => {
    const deltaX = e.changedTouches[0].clientX - touchStartX;
    if (Math.abs(deltaX) > 50) {
      if (deltaX < 0) viewerNext();
      else            viewerPrev();
    }
  }, { passive: true });

  // Keyboard navigation (viewer only)
  document.addEventListener('keydown', e => {
    if ($('screen-viewer').classList.contains('hidden')) return;
    if (e.key === 'ArrowLeft')  viewerPrev();
    if (e.key === 'ArrowRight') viewerNext();
    if (e.key === 'Escape')     closeViewer();
  });

  // Boot
  init();
});
