<?php
// admin/view.php

session_start();

$config = require __DIR__ . '/../lib/config.php';
require __DIR__ . '/../lib/db.php';
require __DIR__ . '/../lib/helpers.php';

require_admin_auth($config); // Require signed-in admin/staff

// PDO error mode (dev-friendly)
if ($pdo instanceof PDO) {
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
$moreMsg = $_GET['more_msg'] ?? null;

// Current user / tenancy context
$me        = $_SESSION['user'] ?? [];
$orgId     = isset($me['organisation_id']) ? (int)$me['organisation_id'] : 0;
$companyId = isset($me['company_id'])      ? (int)$me['company_id']      : 0;
$role      = $me['role'] ?? '';

$isAdmin    = ($role === 'admin');
$isOrgAdmin = ($role === 'org_admin') || !empty($me['is_org_admin']); // keep legacy flag

if ($orgId <= 0) {
    http_response_code(403);
    exit('Forbidden: no organisation scope.');
}

// Request id
$id = (int)($_GET['id'] ?? 0);
if ($id <= 0) {
    http_response_code(404);
    exit('Not found');
}

/**
 * Tenancy rules:
 * - Org admins (or admins flagged as org-admin) can view any request in their organisation.
 * - Other users can only view requests in their organisation AND their own company.
 */
if ($isOrgAdmin || $isAdmin) {
    $stmt = $pdo->prepare("
        SELECT *
        FROM requests
        WHERE id = ?
          AND organisation_id = ?
        LIMIT 1
    ");
    $stmt->execute([$id, $orgId]);
} else {
    if ($companyId <= 0) {
        http_response_code(403);
        exit('Forbidden: no company scope.');
    }
    $stmt = $pdo->prepare("
        SELECT *
        FROM requests
        WHERE id = ?
          AND organisation_id = ?
          AND company_id = ?
        LIMIT 1
    ");
    $stmt->execute([$id, $orgId, $companyId]);
}

$req = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$req) {
    // Deliberate 404 so we don't leak cross-tenant existence
    http_response_code(404);
    exit('Not found');
}

// Ensure share tables exist (safe on fresh DBs)
$pdo->exec("CREATE TABLE IF NOT EXISTS request_shares (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  request_id INT UNSIGNED NOT NULL,
  alias VARCHAR(120) NOT NULL,
  code_hash VARCHAR(255) NOT NULL,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  expires_at DATETIME DEFAULT NULL,
  UNIQUE KEY uq_req_alias (request_id, alias),
  KEY idx_req (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

$pdo->exec("CREATE TABLE IF NOT EXISTS request_share_files (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  share_id INT UNSIGNED NOT NULL,
  file_id INT UNSIGNED NOT NULL,
  UNIQUE KEY uq_share_file (share_id, file_id),
  KEY idx_share (share_id),
  KEY idx_file (file_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4");

// Base + flags
$base   = rtrim($config['base_url'] ?? '', '/');
$isDemo = !empty($config['demo_mode']);

// Page data
$filesStmt = $pdo->prepare("SELECT * FROM request_files WHERE request_id=? ORDER BY id DESC");
$filesStmt->execute([$id]);
$files = $filesStmt->fetchAll(PDO::FETCH_ASSOC);

$notesStmt = $pdo->prepare("SELECT * FROM request_notes WHERE request_id=? ORDER BY id DESC");
$notesStmt->execute([$id]);
$notes = $notesStmt->fetchAll(PDO::FETCH_ASSOC);

$token        = $req['token'];
$baseShareUrl = $base . '/public/share.php';
$uploadLink   = $base . '/public/upload.php?token=' . $token;

$uploadDir = realpath(__DIR__ . '/../uploads/' . $token);

$errors  = [];
$notices = [];
$flash   = null;

// Accept simple flash msg from redirects (e.g. request_more.php)
if (!empty($_GET['msg'])) {
    $notices[] = $_GET['msg'];
}

// Helpers
function alias_is_valid($s) { return (bool)preg_match('/^[A-Za-z0-9_-]+$/', $s); }
function alias_normalize($s) { return strtolower($s); }

// -------- DOWNLOAD HANDLERS (single + all as ZIP) --------
if (isset($_GET['download'])) {
    // Single file
    if ($_GET['download'] === 'file' && isset($_GET['fid'])) {
        $fid   = (int)$_GET['fid'];
        $fstmt = $pdo->prepare("SELECT * FROM request_files WHERE id=? AND request_id=?");
        $fstmt->execute([$fid, $id]);
        $file = $fstmt->fetch();

        if (!$file || !$uploadDir) { http_response_code(404); exit('File not found'); }

        $filename = basename($file['filename']);
        $path     = $uploadDir . '/' . $filename;
        if (!is_file($path)) { http_response_code(404); exit('File missing'); }

        $downloadName = $file['original'] ?: $filename;
        $dl           = basename($downloadName);
        $ascii        = preg_replace('/[^A-Za-z0-9._-]+/', '_', $dl);
        $utf8         = rawurlencode($dl);

        header('Content-Description: File Transfer');
        header('Content-Type: application/octet-stream');
        header('Content-Disposition: attachment; filename="' . $ascii . '"; filename*=UTF-8\'\'' . $utf8);
        header('X-Content-Type-Options: nosniff');
        header('Content-Length: ' . filesize($path));
        readfile($path);
        exit;
    }

    // All files in ZIP
    if ($_GET['download'] === 'all') {
        if (empty($files) || !$uploadDir) { http_response_code(404); exit('No files to download'); }
        if (!class_exists('ZipArchive')) { http_response_code(500); exit('ZipArchive not available'); }

        $zipName = 'Request_' . $id . '_files.zip';
        $zipPath = sys_get_temp_dir() . '/' . $zipName;

        $zip = new ZipArchive();
        if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            http_response_code(500); exit('Could not create ZIP');
        }

        foreach ($files as $f) {
            $filename = basename($f['filename']);
            $path     = $uploadDir . '/' . $filename;
            if (is_file($path)) {
                $zip->addFile($path, $f['original'] ?: $filename);
            }
        }
        $zip->close();

        $ascii = preg_replace('/[^A-Za-z0-9._-]+/', '_', $zipName);
        $utf8  = rawurlencode($zipName);

        header('Content-Type: application/zip');
        header('Content-Disposition: attachment; filename="' . $ascii . '"; filename*=UTF-8\'\'' . $utf8);
        header('X-Content-Type-Options: nosniff');
        header('Content-Length: ' . filesize($zipPath));
        readfile($zipPath);
        @unlink($zipPath);
        exit;
    }
}

// -------- SHARE ACTIONS --------
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['share_action'])) {
    $action = $_POST['share_action'];

    // Create share
    if ($action === 'create') {
        $aliasRaw  = trim($_POST['alias'] ?? '');
        $expiryRaw = trim($_POST['expiry'] ?? '');
        $chosen    = $_POST['files'] ?? [];

        if ($aliasRaw === '') {
            $errors[] = 'Alias is required.';
        }
        if (!alias_is_valid($aliasRaw)) {
            $errors[] = 'Alias can only contain letters, numbers, hyphens, and underscores.';
        }
        $alias = alias_normalize($aliasRaw);

        if (empty($chosen) || !is_array($chosen)) {
            $errors[] = 'Please choose at least one image.';
        }

        if (!$errors) {
            $chk = $pdo->prepare("SELECT 1 FROM request_shares WHERE request_id=? AND alias=?");
            $chk->execute([$id, $alias]);
            if ($chk->fetchColumn()) {
                $errors[] = 'That alias already exists for this request.';
            }
        }

        $expires_at = null;
        if ($expiryRaw !== '') {
            $ts = strtotime($expiryRaw);
            if ($ts === false) {
                $errors[] = 'Invalid expiry date/time.';
            } else {
                $expires_at = date('Y-m-d H:i:s', $ts);
            }
        }

        if (!$errors) {
            $placeholders = implode(',', array_fill(0, count($chosen), '?'));
            $sql          = "SELECT COUNT(*) FROM request_files WHERE request_id=? AND id IN ($placeholders)";
            $params       = array_merge([$id], array_map('intval', $chosen));
            $owns         = $pdo->prepare($sql);
            $owns->execute($params);
            if ((int)$owns->fetchColumn() !== count($chosen)) {
                $errors[] = 'One or more selected files are not valid.';
            }
        }

        if (!$errors) {
            try {
                $pdo->beginTransaction();

                $code = (string)random_int(1000, 9999);

                $ins = $pdo->prepare("
                    INSERT INTO request_shares (request_id, alias, code_hash, created_at, expires_at)
                    VALUES (?, ?, ?, NOW(), ?)
                ");
                $ins->execute([
                    $id,
                    $alias,
                    password_hash($code, PASSWORD_DEFAULT),
                    $expires_at
                ]);
                $shareId = (int)$pdo->lastInsertId();

                $insF = $pdo->prepare("INSERT INTO request_share_files (share_id, file_id) VALUES (?, ?)");
                foreach ($chosen as $fid) {
                    $insF->execute([$shareId, (int)$fid]);
                }

                $pdo->commit();

                $flash = [
                    'link'  => $baseShareUrl . '?token=' . urlencode($token) . '&a=' . urlencode($alias),
                    'code'  => $code,
                    'reg'   => $req['reg_mark'],
                    'alias' => $alias,
                ];
                $notices[] = 'Share created.';
            } catch (Throwable $e) {
                $pdo->rollBack();
                $errors[] = 'Error creating share: ' . $e->getMessage();
            }
        }
    }

    // Delete share
    if ($action === 'delete') {
        $alias = alias_normalize($_POST['alias'] ?? '');
        try {
            $del = $pdo->prepare("DELETE FROM request_shares WHERE request_id=? AND alias=?");
            $del->execute([$id, $alias]);
            $notices[] = 'Share deleted (if it existed).';
        } catch (Throwable $e) {
            $errors[] = 'Error deleting share: ' . $e->getMessage();
        }
    }

    // Update share files
    if ($action === 'update_files') {
        $alias  = alias_normalize($_POST['alias'] ?? '');
        $chosen = $_POST['files'] ?? [];

        if (!alias_is_valid($alias)) {
            $errors[] = 'Invalid alias.';
        }

        $s = $pdo->prepare("SELECT id FROM request_shares WHERE request_id=? AND alias=?");
        $s->execute([$id, $alias]);
        $row = $s->fetch();
        if (!$row) {
            $errors[] = 'Share not found.';
        }
        $shareId = $row ? (int)$row['id'] : 0;

        if (empty($chosen) || !is_array($chosen)) {
            $errors[] = 'Please choose at least one image.';
        } else {
            $placeholders = implode(',', array_fill(0, count($chosen), '?'));
            $sql          = "SELECT COUNT(*) FROM request_files WHERE request_id=? AND id IN ($placeholders)";
            $params       = array_merge([$id], array_map('intval', $chosen));
            $owns         = $pdo->prepare($sql);
            $owns->execute($params);
            if ((int)$owns->fetchColumn() !== count($chosen)) {
                $errors[] = 'One or more selected files are not valid.';
            }
        }

        if (!$errors) {
            try {
                $pdo->beginTransaction();

                $cur = $pdo->prepare("SELECT file_id FROM request_share_files WHERE share_id=?");
                $cur->execute([$shareId]);
                $currentIds = array_map('intval', $cur->fetchAll(PDO::FETCH_COLUMN));

                $newIds = array_map('intval', $chosen);
                $toAdd  = array_values(array_diff($newIds, $currentIds));
                $toDel  = array_values(array_diff($currentIds, $newIds));

                if ($toAdd) {
                    $ins = $pdo->prepare("INSERT IGNORE INTO request_share_files (share_id, file_id) VALUES (?, ?)");
                    foreach ($toAdd as $fid) {
                        $ins->execute([$shareId, $fid]);
                    }
                }

                if ($toDel) {
                    $placeholders = implode(',', array_fill(0, count($toDel), '?'));
                    $params       = array_merge([$shareId], $toDel);
                    $del          = $pdo->prepare("
                        DELETE FROM request_share_files
                        WHERE share_id=? AND file_id IN ($placeholders)
                    ");
                    $del->execute($params);
                }

                $pdo->commit();
                $notices[]       = 'Share updated.';
                $_GET['tab']     = 'share';
                $_GET['edit_alias'] = $alias;
            } catch (Throwable $e) {
                $pdo->rollBack();
                $errors[] = 'Error updating share: ' . $e->getMessage();
            }
        } else {
            $_GET['tab']        = 'share';
            $_GET['edit_alias'] = $alias;
        }
    }
}

// Load shares
$listSql = "
  SELECT s.id, s.alias, s.created_at, s.expires_at, COUNT(rsf.id) AS file_count
  FROM request_shares s
  LEFT JOIN request_share_files rsf ON rsf.share_id = s.id
  WHERE s.request_id = ?
  GROUP BY s.id, s.alias, s.created_at, s.expires_at
  ORDER BY s.created_at DESC
";
$list = $pdo->prepare($listSql);
$list->execute([$id]);
$shares = $list->fetchAll(PDO::FETCH_ASSOC);

// If editing an alias, load its file selections
$editAlias         = isset($_GET['edit_alias']) ? alias_normalize($_GET['edit_alias']) : '';
$editingShare      = null;
$editingSelectedIds= [];

if ($editAlias !== '') {
    $q = $pdo->prepare("SELECT id, alias FROM request_shares WHERE request_id=? AND alias=?");
    $q->execute([$id, $editAlias]);
    $editingShare = $q->fetch();
    if ($editingShare) {
        $qf = $pdo->prepare("SELECT file_id FROM request_share_files WHERE share_id=?");
        $qf->execute([(int)$editingShare['id']]);
        $editingSelectedIds = array_map('intval', $qf->fetchAll(PDO::FETCH_COLUMN));
    }
}

// Initial tab
$initialTab = ($_GET['tab'] ?? '') ?: ($editingShare ? 'share' : 'files');
?>
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Request <?= esc($req['reg_mark']) ?><?= $isDemo ? ' (Demo Mode)' : '' ?></title>
  <meta name="viewport" content="width=device-width, initial-scale=1">

  <style>
  /* ===== Custom Scrollbar ===== */
  ::-webkit-scrollbar { width:10px; height:10px; }
  ::-webkit-scrollbar-track { background:#f1f4f8; border-left:1px solid var(--line); }
  ::-webkit-scrollbar-thumb {
    background:linear-gradient(var(--brand),var(--brand-dark));
    border-radius:5px;
    border:1px solid rgba(255,255,255,0.4);
  }
  ::-webkit-scrollbar-thumb:hover { background:linear-gradient(var(--brand-dark),#1e4c83); }
  * { scrollbar-width:thin; scrollbar-color:var(--brand) #f1f4f8; }

  /* ===== Classic Commercial Theme ===== */
  :root{
    --bg:#eceff3; --win:#ffffff; --ink:#1f2933; --muted:#5d6b78; --line:#cfd6de;
    --brand:#2f6db2; --brand-dark:#255a95; --ok:#2f7d3d; --warn:#b28a2f; --danger:#b22f2f;
    --radius:4px; --shadow:0 1px 0 rgba(0,0,0,.04),0 2px 4px rgba(0,0,0,.06);
    --hdr-grad:linear-gradient(#f8fafc,#e8edf3); --tab-grad:linear-gradient(#f5f7fa,#e9eef4);
    --btn-grad:linear-gradient(#f8fafc,#e6ecf2); --btn-grad-active:linear-gradient(#e6ecf2,#d9e1e9);
    --chip:#f1f3f6;
  }
  *{box-sizing:border-box}
  html,body{height:100%}
  body{
    margin:0;
    font:13px/1.45 Segoe UI,Tahoma,Arial,Helvetica,sans-serif;
    color:var(--ink);
    background:var(--bg);
    -webkit-user-select:none;
    -ms-user-select:none;
    user-select:none;
  }
  .app{min-height:100%;display:flex;flex-direction:column}

  .ribbon{background:var(--hdr-grad);border-bottom:1px solid var(--line);box-shadow:var(--shadow)}
  .ribbon-inner{
    max-width:1100px;margin:0 auto;padding:10px 14px;
    display:flex;align-items:center;justify-content:space-between;gap:10px
  }
  .title{display:flex;align-items:center;gap:10px}
  .mark{width:26px;height:26px;border-radius:4px;background:#2f6db2;box-shadow:inset 0 0 0 1px rgba(255,255,255,.5)}
  h1{font-size:16px;margin:0}
  .sub{font-size:12px;color:var(--muted)}

  .btn{
    appearance:none;border:1px solid #bfc8d2;background:var(--btn-grad);
    color:#123;font-weight:600;padding:6px 10px;border-radius:var(--radius);
    cursor:pointer;box-shadow:inset 0 1px 0 rgba(255,255,255,.85),0 1px 0 rgba(255,255,255,.5);
    position:relative;transition:background .15s ease;
  }
  .btn:hover{background:var(--btn-grad-active)}
  .btn.primary{border-color:#2a5f9c;color:#fff;background:linear-gradient(#3b79c1,#2b67ab)}
  .btn.danger{border-color:#9c2a2a;color:#fff;background:linear-gradient(#c93b3b,#ab2b2b)}
  .btn.small{padding:4px 8px;font-size:12px}
  .btn.loading{pointer-events:none;color:transparent}
  .btn.loading::after{
    content:"";position:absolute;top:50%;left:50%;
    width:16px;height:16px;margin:-8px 0 0 -8px;
    border:2px solid rgba(0,0,0,.25);border-top-color:rgba(0,0,0,.6);
    border-radius:50%;animation:spin 1s linear infinite;
  }
  .btn.primary.loading::after{
    border-color:rgba(255,255,255,.35);
    border-top-color:#fff;
  }
  @keyframes spin{to{transform:rotate(360deg)}}

  .window{max-width:1100px;margin:16px auto 24px;padding:0 14px;width:100%}
  .strip{
    background:#f6f8fb;border:1px solid var(--line);border-radius:var(--radius);
    padding:10px 12px;box-shadow:var(--shadow);
    display:flex;justify-content:space-between;gap:12px;align-items:center;
  }
  .meta{color:var(--muted);font-size:12px}
  .pill{
    display:inline-block;padding:3px 8px;border:1px solid var(--line);
    background:var(--chip);border-radius:999px;font-size:12px;font-weight:700;
  }

  .tabs{margin-top:14px;border-bottom:1px solid var(--line)}
  .tabs button{
    display:inline-block;background:var(--tab-grad);border:1px solid var(--line);
    border-bottom:none;padding:8px 12px;margin-right:6px;
    border-radius:4px 4px 0 0;color:#223;cursor:pointer;
    font-size:12px;
  }
  .tabs button.active{background:#fff;font-weight:700}
  .tabpane{
    background:#fff;border:1px solid var(--line);border-top:none;
    padding:12px;border-radius:0 0 4px 4px;box-shadow:var(--shadow);
  }
  .hidden{display:none}

  .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(220px,1fr));gap:10px}
  .card{border:1px solid var(--line);border-radius:4px;padding:8px;background:#fff}
  img{max-width:100%;height:auto;border-radius:4px;cursor:pointer;-webkit-user-drag:none;user-select:none}
  .card-foot{margin-top:6px;display:flex;align-items:center;justify-content:space-between}
  .size{font-size:12px;color:#5d6b78}
  .muted{opacity:.75}

  table{width:100%;border-collapse:separate;border-spacing:0;background:#fff}
  th,td{padding:8px 10px;border-bottom:1px solid #e7ecf1;text-align:left}
  thead th{
    background:linear-gradient(#eef3f8,#e2e9f1);
    color:#2a3b4b;border-bottom:1px solid var(--line);font-size:12px
  }
  tbody tr:nth-child(even){background:#fafbfd}
  tbody tr:hover{background:#fffde7}

  #pane-notes tbody tr{cursor:copy;}
  #pane-notes tbody tr.copied{background:#e8f5e9 !important;}

  input[type="text"],input[type="datetime-local"],textarea{
    padding:8px;border:1px solid #cfd6de;border-radius:4px;outline:none;min-width:220px;
    box-shadow:inset 0 1px 0 rgba(255,255,255,.8);
    -webkit-user-select:none;-ms-user-select:none;user-select:none;
  }
  input[type="text"]:focus,input[type="datetime-local"]:focus{border-color:#99b4d6}
  .copybox{
    width:100%;min-height:70px;border:1px solid var(--line);
    border-radius:4px;padding:8px;background:var(--win)
  }

  .alert{padding:10px;border-radius:4px;margin:10px 0}
  .alert.error{background:#fae7e7;border:1px solid #e3b1b1;color:#7f2020}
  .alert.note{background:#eaf2fd;border:1px solid #b9d0f5;color:#163a6c}

  .lightbox{
    display:none;position:fixed;inset:0;background:rgba(0,0,0,.9);
    justify-content:center;align-items:center;z-index:9999;
  }
  .lightbox img{
    max-width:90%;max-height:90%;border-radius:4px;
    transform:translate(0,0) scale(1);
    transition:transform .08s ease-out;
    cursor:default;touch-action:none;
  }
  .lightbox .x,.lightbox .prev,.lightbox .next{
    position:absolute;color:#fff;font-size:34px;
    font-weight:bold;cursor:pointer;user-select:none;
    padding:10px;line-height:1;
  }
  .lightbox .x{top:10px;right:20px}
  .lightbox .prev{top:50%;left:20px;transform:translateY(-50%)}
  .lightbox .next{top:50%;right:20px;transform:translateY(-50%)}

  .tools{
    position:fixed;left:50%;bottom:20px;transform:translateX(-50%);
    display:flex;gap:8px;background:rgba(0,0,0,.35);
    padding:6px 8px;border-radius:999px;
    box-shadow:0 2px 12px rgba(0,0,0,.25);
    backdrop-filter:blur(4px);z-index:10000;
  }
  .tools button{
    appearance:none;border:0;border-radius:6px;
    padding:6px 10px;font-weight:700;font-size:13px;
    color:#fff;background:rgba(255,255,255,.18);cursor:pointer;
  }
  .tools button:hover{background:rgba(255,255,255,.28);}
  .tools .sep{
    width:1px;height:20px;background:rgba(255,255,255,.35);margin:0 2px;
  }

  .demo-flag{
    border:1px solid #f5c2c7;background:#f8d7da;
    color:#842029;padding:10px 12px;border-radius:4px;
    margin:10px 0;font-weight:600;text-align:center;
  }
  .demo-badge{
    position:fixed;right:16px;bottom:16px;
    background:#f8d7da;color:#842029;border:1px solid #f5c2c7;
    border-radius:999px;padding:6px 10px;font-size:12px;
    box-shadow:0 2px 8px rgba(0,0,0,.08);
  }

  #loadingOverlay{
    position:fixed;inset:0;background:#f8fafc;
    display:flex;align-items:center;justify-content:center;
    flex-direction:column;z-index:99999;
    transition:opacity .5s ease,visibility .5s ease;
    font-family:Segoe UI,sans-serif;
  }
  #loadingOverlay.hidden{opacity:0;visibility:hidden}
  .loader{
    border:4px solid #dbe2ea;border-top:4px solid var(--brand);
    border-radius:50%;width:46px;height:46px;
    animation:spin 1s linear infinite;margin-bottom:12px;
  }
  #loadingOverlay p{color:#374151;font-size:15px;margin:0}

  .aslink{
    background:none;border:0;padding:0;margin:0;
    font:inherit;color:#2f6db2;text-decoration:underline;cursor:pointer;
  }
  .aslink:hover{text-decoration:none}

  .dl-modal-backdrop{
    position:fixed;inset:0;background:rgba(0,0,0,.5);
    display:none;align-items:center;justify-content:center;z-index:10000;
  }
  .dl-modal{
    width:min(520px,92vw);background:#fff;border:1px solid var(--line);
    border-radius:var(--radius);box-shadow:var(--shadow);
  }
  .dl-modal .hdr{
    display:flex;align-items:center;justify-content:space-between;
    padding:10px 12px;background:var(--hdr-grad);border-bottom:1px solid var(--line);
  }
  .dl-modal .hdr h3{margin:0;font-size:14px}
  .dl-modal .body{padding:12px}
  .dl-modal .row{
    display:flex;gap:10px;align-items:center;margin:8px 0;flex-wrap:wrap;
  }
  .dl-modal .foot{
    display:flex;justify-content:flex-end;gap:8px;
    padding:10px 12px;border-top:1px solid var(--line);background:#f7f9fc;
  }
  .dl-close{
    appearance:none;background:none;border:0;font-size:18px;cursor:pointer;
  }

  .toast{
    position:fixed;right:16px;bottom:16px;
    background:#16a34a;color:#fff;border-radius:6px;
    padding:10px 12px;box-shadow:0 2px 10px rgba(0,0,0,.15);
    display:none;z-index:11000;
  }
  .toast.ok{background:#16a34a}
  .toast.warn{background:#f59e0b}
  .toast.err{background:#dc2626}

  #dl_iframe{display:none}
  </style>
</head>
<body>

<div id="loadingOverlay">
  <div class="loader"></div>
  <p>Loading, please wait…</p>
</div>

<div class="app">
  <!-- Ribbon -->
  <div class="ribbon">
    <div class="ribbon-inner">
      <div class="title">
        <!-- <div class="mark" aria-hidden="true"></div> -->
        <div>
          <h1>Request - <?= esc($req['reg_mark']) ?></h1>
          <div class="sub">Files · Notes · Share links</div>
        </div>
      </div>

      <div class="actions" style="display:flex; gap:8px">
        <?php if (!empty($files)): ?>
          <button type="button"
                  class="btn primary js-download"
                  data-href="?id=<?= (int)$id ?>&download=all">
            ⬇ Download all (ZIP)
          </button>
        <?php endif; ?>

        <button type="button"
                class="btn"
                data-open="<?= esc($uploadLink) ?>">
          Open upload page
        </button>

<?php
  $status = strtolower($req['status'] ?? '');
  if ($status === 'submitted' && !empty($req['customer_email'])):
?>
  <form method="post"
        action="<?= esc($base) ?>/admin/request_more.php"
        style="margin:0 0 0 8px; display:inline-block">
    <input type="hidden" name="id" value="<?= (int)$id ?>">
    <button type="submit"
            class="btn small"
            onclick="return confirm('Send another secure upload link to request more photos from this customer?');">
      Request more photos
    </button>
  </form>
<?php endif; ?>


      </div>
    </div>
  </div>

  <!-- Window -->
  <div class="window">
    <?php if ($isDemo): ?>
      <div class="demo-flag">DEMO MODE – visual flag only. Features remain functional.</div>
    <?php endif; ?>

    <!-- Summary strip -->
    <div class="strip" style="margin-bottom:12px">
      <div style="display:flex; gap:14px; flex-wrap:wrap">
        <div><strong>Status:</strong> <span class="pill"><?= esc($req['status']) ?></span></div>
        <div class="meta">
          <strong>Name:</strong>
          <?= esc($req['customer_name'] ?: '-') ?>
          (<?= esc($req['customer_email'] ?: '-') ?>)
        </div>
        <div class="meta"><strong>Expires:</strong> <?= esc($req['expires_at'] ?: '-') ?></div>
      </div>
      <div style="display:flex; gap:8px">
        <button class="btn small"
                id="copyUploadLinkBtn"
                data-link="<?= esc($uploadLink) ?>">
          Copy upload link
        </button>
      </div>
    </div>

<?php if ($moreMsg): ?>
  <div class="alert note"><?= esc($moreMsg) ?></div>
<?php endif; ?>

    <?php foreach ($errors as $e): ?>
      <div class="alert error"><?= esc($e) ?></div>
    <?php endforeach; ?>

    <?php foreach ($notices as $n): ?>
      <div class="alert note"><?= esc($n) ?></div>
    <?php endforeach; ?>

    <?php if (!empty($flash)): ?>
      <div class="alert note">
        <strong>Copy &amp; send:</strong>
        <textarea class="copybox" id="copyMsg" readonly><?=
          esc("We would like to share some images related to the registration {$flash['reg']}, you can view these here: {$flash['link']} using this code {$flash['code']}")
        ?></textarea>
        <div style="margin-top:6px; display:flex; align-items:center; gap:10px">
          <button class="btn small" id="copyBtn">Copy message</button>
          <span class="meta muted">
            The access code is shown once and not stored in plaintext.
          </span>
        </div>
      </div>
    <?php endif; ?>

    <!-- Tabs -->
    <nav class="tabs" id="tabbar">
      <button type="button" data-tab="files" class="active">Files</button>
      <button type="button" data-tab="notes">Notes</button>
      <button type="button" data-tab="share">Share</button>
    </nav>

    <!-- Files tab -->
    <section id="pane-files" class="tabpane">
      <?php if (!$files): ?>
        <p class="meta">No files uploaded.</p>
      <?php else: ?>
        <div class="grid">
          <?php foreach ($files as $i => $f):
            $src = '../uploads/' . esc($token) . '/' . esc($f['filename']);
          ?>
            <div class="card">
              <div class="meta">
                <?= esc($f['kind']) ?> · <?= esc($f['mime']) ?> · <?= number_format($f['size_bytes']) ?> B
              </div>
              <img src="<?= $src ?>" alt="" data-index="<?= (int)$i ?>">
              <div class="meta" title="<?= esc($f['original']) ?>">
                <?= esc($f['original']) ?>
              </div>
              <div class="card-foot">
                <span class="size"><?= number_format($f['size_bytes'] / 1024, 1) ?> KB</span>
                <button type="button"
                        class="btn small js-download"
                        data-href="?id=<?= (int)$id ?>&download=file&fid=<?= (int)$f['id'] ?>">
                  Download
                </button>
              </div>
            </div>
          <?php endforeach; ?>
        </div>
      <?php endif; ?>
    </section>

    <!-- Notes tab -->
    <section id="pane-notes" class="tabpane hidden">
      <?php if (!$notes): ?>
        <p class="meta">No notes yet.</p>
      <?php else: ?>
        <p class="meta">Tip: click a note row to copy its text.</p>
        <table id="notesTable">
          <thead>
            <tr><th style="width:180px">Created</th><th>Notes</th></tr>
          </thead>
          <tbody>
            <?php foreach ($notes as $n): ?>
              <tr class="note-row"
                  data-copy="<?= esc($n['notes']) ?>"
                  title="Click to copy">
                <td class="meta"><?= esc($n['created_at']) ?></td>
                <td><?= nl2br(esc($n['notes'])) ?></td>
              </tr>
            <?php endforeach; ?>
          </tbody>
        </table>
      <?php endif; ?>
    </section>

    <!-- Share tab -->
    <section id="pane-share" class="tabpane hidden">
      <h3 style="margin:6px 0 8px">Existing shares</h3>
      <?php if (empty($shares)): ?>
        <p class="meta">No shares yet.</p>
      <?php else: ?>
        <table>
          <thead>
            <tr>
              <th>Alias</th>
              <th>Created</th>
              <th>Expiry</th>
              <th>Files</th>
              <th>Link</th>
              <th style="width:210px"></th>
            </tr>
          </thead>
          <tbody>
            <?php foreach ($shares as $s):
              $shareLink = $baseShareUrl . '?token=' . urlencode($token) . '&a=' . urlencode($s['alias']);
              $isEditing = ($editingShare && $editingShare['alias'] === $s['alias']);
            ?>
              <tr>
                <td><strong><?= esc($s['alias']) ?></strong></td>
                <td class="meta"><?= esc($s['created_at']) ?></td>
                <td class="meta"><?= esc($s['expires_at'] ?: '-') ?></td>
                <td><?= (int)$s['file_count'] ?></td>
                <td class="meta">
                  <button type="button" class="aslink" data-open="<?= esc($shareLink) ?>">Open</button>
                  &nbsp;·&nbsp;
                  <button type="button" class="aslink copy-share" data-link="<?= esc($shareLink) ?>">Copy</button>
                </td>
                <td style="display:flex; gap:6px; flex-wrap:wrap">
                  <button type="button"
                          class="btn small"
                          data-nav="?id=<?= (int)$id ?>&tab=share&edit_alias=<?= urlencode($s['alias']) ?>">
                    Edit files
                  </button>
                  <form method="post" style="margin:0"
                        onsubmit="return confirm('Delete this share?');">
                    <input type="hidden" name="share_action" value="delete">
                    <input type="hidden" name="alias" value="<?= esc($s['alias']) ?>">
                    <button class="btn danger small" type="submit">Delete</button>
                  </form>
                </td>
              </tr>

              <?php if ($isEditing): ?>
                <tr>
                  <td colspan="6">
                    <div class="card" style="margin-top:8px">
                      <div style="margin-bottom:8px; font-weight:700">
                        Edit share: <?= esc($s['alias']) ?>
                      </div>
                      <form method="post">
                        <input type="hidden" name="share_action" value="update_files">
                        <input type="hidden" name="alias" value="<?= esc($s['alias']) ?>">

                        <div style="border:1px solid var(--line);border-radius:4px;max-height:300px;overflow:auto;padding:8px;background:#fff">
                          <?php if (empty($files)): ?>
                            <p class="meta">No images found.</p>
                          <?php else: foreach ($files as $f):
                            $fid     = (int)$f['id'];
                            $checked = in_array($fid, $editingSelectedIds, true) ? 'checked' : '';
                          ?>
                            <label style="display:flex;align-items:center;gap:8px;margin:4px 0">
                              <input type="checkbox" name="files[]" value="<?= $fid ?>" <?= $checked ?>>
                              <span class="meta"><?= esc($f['filename']) ?></span>
                              <span class="meta muted">- <?= esc($f['original'] ?: '') ?></span>
                            </label>
                          <?php endforeach; endif; ?>
                        </div>

                        <div style="margin-top:10px;display:flex;gap:8px">
                          <button class="btn primary" type="submit">Save changes</button>
                          <button type="button"
                                  class="btn"
                                  data-nav="?id=<?= (int)$id ?>&tab=share">
                            Close
                          </button>
                        </div>
                      </form>
                    </div>
                  </td>
                </tr>
              <?php endif; ?>

            <?php endforeach; ?>
          </tbody>
        </table>
      <?php endif; ?>

      <h3 style="margin:18px 0 8px">Create a new share</h3>
      <form method="post">
        <input type="hidden" name="share_action" value="create">

        <div style="display:flex;gap:12px;flex-wrap:wrap;margin-bottom:8px">
          <label>Alias<br>
            <input name="alias" required placeholder="e.g. insurer-jan18">
          </label>
          <label>Expiry (optional)<br>
            <input type="datetime-local" name="expiry">
          </label>
        </div>

        <div style="margin:8px 0 6px">
          <strong>Choose images</strong>
          <label class="meta" style="margin-left:10px">
            <input type="checkbox" id="selAll"> Select all
          </label>
        </div>

        <div style="border:1px solid var(--line);border-radius:4px;max-height:300px;overflow:auto;padding:8px;background:#fff">
          <?php if (empty($files)): ?>
            <p class="meta">No images found.</p>
          <?php else: foreach ($files as $f): ?>
            <label style="display:flex;align-items:center;gap:8px;margin:4px 0">
              <input type="checkbox" name="files[]" value="<?= (int)$f['id'] ?>">
              <span class="meta"><?= esc($f['filename']) ?></span>
              <span class="meta muted">- <?= esc($f['original'] ?: '') ?></span>
            </label>
          <?php endforeach; endif; ?>
        </div>

        <div style="margin-top:10px">
          <button class="btn primary" type="submit">Create share</button>
        </div>
        <p class="meta" style="margin-top:8px">
          A 4-digit access code will be generated and shown once after creation.
        </p>
      </form>
    </section>

    <p style="margin-top:16px"></p>
  </div>

  <?php if ($isDemo): ?>
    <div class="demo-badge" aria-hidden="true">DEMO</div>
  <?php endif; ?>
</div>

<!-- Lightbox -->
<div id="lightbox" class="lightbox" aria-hidden="true">
  <span class="x" title="Close">&times;</span>
  <span class="prev" title="Previous">&#10094;</span>
  <img id="lightbox-img" src="" alt="">
  <span class="next" title="Next">&#10095;</span>
</div>

<!-- Zoom tools -->
<div class="tools" id="zoomTools" aria-label="Zoom controls">
  <button type="button" id="zoomOutBtn" title="Zoom out (−)">−</button>
  <button type="button" id="zoomInBtn" title="Zoom in (+)">+</button>
  <span class="sep" aria-hidden="true"></span>
  <button type="button" id="zoomFitBtn" title="Fit to screen">Fit</button>
  <button type="button" id="zoom100Btn" title="Actual size (100%)">100%</button>
</div>

<!-- Download iframe + modal + toast -->
<iframe id="dl_iframe" name="dl_iframe"></iframe>

<div class="dl-modal-backdrop" id="dlBackdrop" aria-hidden="true">
  <div class="dl-modal" role="dialog" aria-modal="true" aria-labelledby="dlTitle">
    <div class="hdr">
      <h3 id="dlTitle">Download file</h3>
      <button class="dl-close" id="dlClose" aria-label="Close">&times;</button>
    </div>
    <div class="body">
      <div class="row">
        <div class="muted">You’re about to download:</div>
        <strong id="dlFilename">file</strong>
      </div>
      <div class="row">
        <label><input type="checkbox" id="dlDontShow"> Don’t show this again on this device</label>
      </div>
      <div class="row muted" style="font-size:12px">
        The download will start in the background and appear in your browser’s downloads.
      </div>
    </div>
    <div class="foot">
      <button class="btn" id="dlCancel">Cancel</button>
      <button class="btn primary" id="dlConfirm">Download</button>
    </div>
  </div>
</div>

<div class="toast" id="toast">Download started…</div>

<script>
  // Disable right-click & image drag
  window.addEventListener('contextmenu', e => e.preventDefault());
  document.addEventListener('dragstart', e => {
    if (e.target && e.target.tagName === 'IMG') e.preventDefault();
  });
  document.addEventListener('selectstart', e => e.preventDefault());
  document.addEventListener('mousedown', e => { if (e.shiftKey) e.preventDefault(); });

  // Tabs
  const tabs = { files:'pane-files', notes:'pane-notes', share:'pane-share' };
  function setTab(t){
    for (const k in tabs){
      document.getElementById(tabs[k]).classList.toggle('hidden', k !== t);
    }
    document.querySelectorAll('#tabbar [data-tab]').forEach(btn => {
      btn.classList.toggle('active', btn.getAttribute('data-tab') === t);
    });
    history.replaceState(null, '', '#' + t);
  }
  document.getElementById('tabbar').addEventListener('click', e => {
    const btn = e.target.closest('[data-tab]');
    if (!btn) return;
    const t = btn.getAttribute('data-tab');
    if (tabs[t]) setTab(t);
  });
  setTab('<?= esc($initialTab) ?>');

  // Copy upload link
  document.getElementById('copyUploadLinkBtn')?.addEventListener('click', async e => {
    const link = e.currentTarget.getAttribute('data-link');
    try {
      await navigator.clipboard.writeText(link);
      e.currentTarget.textContent = 'Copied!';
      setTimeout(() => e.currentTarget.textContent = 'Copy upload link', 1200);
    } catch {}
  });

  // Copy share creation message
  document.getElementById('copyBtn')?.addEventListener('click', async e => {
    const ta = document.getElementById('copyMsg');
    try {
      await navigator.clipboard.writeText(ta.value);
      e.target.textContent = 'Copied!';
      setTimeout(() => e.target.textContent = 'Copy message', 1200);
    } catch {}
  });

  // Select all images for share form
  document.getElementById('selAll')?.addEventListener('change', e => {
    document.querySelectorAll('input[name="files[]"]').forEach(cb => {
      cb.checked = e.target.checked;
    });
  });

  // Copy share links
  document.querySelectorAll('.copy-share').forEach(el => {
    el.addEventListener('click', async () => {
      const link = el.getAttribute('data-link');
      try {
        await navigator.clipboard.writeText(link);
        el.textContent = 'Copied';
        setTimeout(() => el.textContent = 'Copy', 1200);
      } catch {}
    });
  });

  // Lightbox
  const imgs = Array.from(document.querySelectorAll('#pane-files .grid img'));
  const lb   = document.getElementById('lightbox');
  const lbImg= document.getElementById('lightbox-img');
  const x    = lb.querySelector('.x');
  const prev = lb.querySelector('.prev');
  const next = lb.querySelector('.next');
  let idx    = 0;

  function openLB(i){
    idx = i;
    lbImg.src = imgs[idx].src;
    lb.style.display = 'flex';
    resetZoom();
  }
  function closeLB(){
    lb.style.display = 'none';
    resetZoom();
  }
  function n(){
    if (!imgs.length) return;
    idx = (idx + 1) % imgs.length;
    lbImg.src = imgs[idx].src;
    resetZoom();
  }
  function p(){
    if (!imgs.length) return;
    idx = (idx - 1 + imgs.length) % imgs.length;
    lbImg.src = imgs[idx].src;
    resetZoom();
  }

  imgs.forEach((im, i) => im.addEventListener('click', () => openLB(i)));
  x.addEventListener('click', closeLB);
  next.addEventListener('click', n);
  prev.addEventListener('click', p);

  document.addEventListener('keydown', e => {
    if (lb.style.display === 'flex') {
      if (e.key === 'Escape') closeLB();
      if (e.key === 'ArrowRight') n();
      if (e.key === 'ArrowLeft') p();
    }
  });

  // Loading overlay fade out
  window.addEventListener('load', () => {
    const overlay = document.getElementById('loadingOverlay');
    if (overlay) {
      overlay.classList.add('hidden');
      setTimeout(() => overlay.remove(), 700);
    }
  });

  // Download modal + toast
  const dlBackdrop = document.getElementById('dlBackdrop');
  const dlClose    = document.getElementById('dlClose');
  const dlCancel   = document.getElementById('dlCancel');
  const dlConfirm  = document.getElementById('dlConfirm');
  const dlFilename = document.getElementById('dlFilename');
  const dlDontShow = document.getElementById('dlDontShow');
  const dlIframe   = document.getElementById('dl_iframe');
  const toastEl    = document.getElementById('toast');

  let pendingHref = null;
  let pendingBtn  = null;

  function showToast(msg, cls){
    toastEl.textContent = msg;
    toastEl.className   = 'toast ' + (cls || 'ok');
    toastEl.style.display = 'block';
    setTimeout(() => { toastEl.style.display = 'none'; }, 2200);
  }

  function openDlModal(href, displayName, btn){
    pendingHref = href;
    pendingBtn  = btn || null;
    dlFilename.textContent = displayName || 'file';
    dlDontShow.checked = (localStorage.getItem('dl_skip_modal') === '1');
    if (dlDontShow.checked) {
      startDownload();
      return;
    }
    dlBackdrop.style.display = 'flex';
  }

  function closeDlModal(){
    dlBackdrop.style.display = 'none';
    if (pendingBtn) {
      pendingBtn.classList.remove('loading');
      pendingBtn.removeAttribute('aria-busy');
    }
  }

  function startDownload(){
    if (!pendingHref) return;
    dlIframe.src = pendingHref;
    dlBackdrop.style.display = 'none';
    showToast('Download started…', 'ok');
    if (pendingBtn) {
      setTimeout(() => {
        pendingBtn.classList.remove('loading');
        pendingBtn.removeAttribute('aria-busy');
      }, 600);
    }
    pendingHref = null;
    pendingBtn  = null;
  }

  dlConfirm.addEventListener('click', () => {
    if (dlDontShow.checked) {
      localStorage.setItem('dl_skip_modal', '1');
    }
    startDownload();
  });

  [dlClose, dlCancel].forEach(el => el.addEventListener('click', closeDlModal));
  dlBackdrop.addEventListener('click', e => {
    if (e.target === dlBackdrop) closeDlModal();
  });

  document.querySelectorAll('.js-download').forEach(btn => {
    btn.addEventListener('click', function () {
      if (this.classList.contains('loading')) return;
      this.classList.add('loading');
      this.setAttribute('aria-busy', 'true');

      const href = this.getAttribute('data-href');
      let nice   = 'file';

      try {
        const url   = new URL(href, window.location.href);
        const isZip = url.searchParams.get('download') === 'all';
        if (isZip) {
          nice = 'All files (ZIP)';
        } else {
          const card  = this.closest('.card');
          const label = card?.querySelector('.meta[title]')?.getAttribute('title')
                     || card?.querySelector('.meta')?.textContent;
          if (label) nice = label.trim();
        }
      } catch(e) {}

      openDlModal(href, nice, this);
    }, { passive:true });
  });

  // data-open / data-nav helpers
  document.addEventListener('click', e => {
    const openBtn = e.target.closest('[data-open]');
    if (openBtn) {
      window.open(openBtn.getAttribute('data-open'), '_blank', 'noopener');
      return;
    }
    const navBtn = e.target.closest('[data-nav]');
    if (navBtn) {
      window.location.assign(navBtn.getAttribute('data-nav'));
      return;
    }
  });

  // Notes: click row to copy
  (function(){
    const notesPane = document.getElementById('pane-notes');
    if (!notesPane) return;

    notesPane.addEventListener('click', async e => {
      const tr = e.target.closest('tbody tr');
      if (!tr || !notesPane.contains(tr)) return;

      const td   = tr.querySelector('td:nth-child(2)');
      const raw  = tr.getAttribute('data-copy') || (td ? td.innerText : '');
      const text = (raw || '').trim();
      if (!text) return;

      try {
        await navigator.clipboard.writeText(text);
        tr.classList.add('copied');
        showToast('Note copied to clipboard', 'ok');
        setTimeout(() => tr.classList.remove('copied'), 800);
      } catch {
        if (td) {
          const sel = window.getSelection();
          const range = document.createRange();
          range.selectNodeContents(td);
          sel.removeAllRanges();
          sel.addRange(range);
          showToast('Press Ctrl+C to copy', 'warn');
        }
      }
    });
  })();

  // Zoom / pan controls for lightbox
  const z = {
    scale:1, min:1, max:6,
    tx:0, ty:0,
    dragging:false, lx:0, ly:0,
    pointers:new Map(),
    pinchDist:0, pinchStartScale:1
  };

  const zoomInBtn  = document.getElementById('zoomInBtn');
  const zoomOutBtn = document.getElementById('zoomOutBtn');
  const zoomFitBtn = document.getElementById('zoomFitBtn');
  const zoom100Btn = document.getElementById('zoom100Btn');

  function clamp(v, lo, hi){ return Math.max(lo, Math.min(hi, v)); }
  function applyZoom(){
    lbImg.style.transform = `translate(${z.tx}px, ${z.ty}px) scale(${z.scale})`;
    lbImg.style.cursor = (z.scale > 1 && !z.dragging) ? 'grab'
                       : (z.dragging ? 'grabbing' : 'default');
  }
  function resetZoom(){ z.scale=1; z.tx=0; z.ty=0; applyZoom(); }
  function scaleTo(s){ z.scale = clamp(s, z.min, z.max); applyZoom(); }
  function scaleBy(f){ scaleTo(z.scale * f); }

  lbImg.addEventListener('wheel', e => {
    if (lb.style.display !== 'flex') return;
    e.preventDefault();
    const factor = (e.deltaY < 0) ? 1.15 : (1 / 1.15);
    const old    = z.scale;
    scaleBy(factor);

    if (z.scale > 1 && old !== z.scale) {
      const rect = lbImg.getBoundingClientRect();
      const cx   = e.clientX - (rect.left + rect.width / 2);
      const cy   = e.clientY - (rect.top  + rect.height / 2);
      z.tx -= cx * (z.scale - old) * 0.08;
      z.ty -= cy * (z.scale - old) * 0.08;
      applyZoom();
    }
  }, { passive:false });

  lbImg.addEventListener('dblclick', () => {
    if (z.scale === 1) scaleTo(2); else resetZoom();
  });

  lbImg.addEventListener('pointerdown', e => {
    lbImg.setPointerCapture(e.pointerId);
    z.pointers.set(e.pointerId, { x:e.clientX, y:e.clientY });

    if (z.pointers.size === 1) {
      z.dragging = true;
      z.lx = e.clientX; z.ly = e.clientY;
      applyZoom();
    } else if (z.pointers.size === 2) {
      const pts = Array.from(z.pointers.values());
      z.pinchDist      = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
      z.pinchStartScale= z.scale;
    }
  });

  lbImg.addEventListener('pointermove', e => {
    if (!z.pointers.has(e.pointerId)) return;
    z.pointers.set(e.pointerId, { x:e.clientX, y:e.clientY });

    if (z.pointers.size === 2) {
      const pts = Array.from(z.pointers.values());
      const dist = Math.hypot(pts[0].x - pts[1].x, pts[0].y - pts[1].y);
      if (z.pinchDist > 0) {
        const ratio = dist / z.pinchDist;
        scaleTo(z.pinchStartScale * ratio);
      }
      return;
    }

    if (z.dragging && z.scale > 1) {
      const dx = e.clientX - z.lx;
      const dy = e.clientY - z.ly;
      z.tx += dx; z.ty += dy;
      z.lx = e.clientX; z.ly = e.clientY;
      applyZoom();
    }
  });

  function endPointer(e){
    if (z.pointers.has(e.pointerId)) z.pointers.delete(e.pointerId);
    if (z.pointers.size < 2) z.pinchDist = 0;
    if (z.pointers.size === 0) {
      z.dragging = false;
      applyZoom();
    }
  }

  lbImg.addEventListener('pointerup', endPointer);
  lbImg.addEventListener('pointercancel', endPointer);
  lbImg.addEventListener('pointerleave', e => {
    if (e.pointerType === 'mouse') endPointer(e);
  });

  zoomInBtn?.addEventListener('click', () => scaleBy(1.2));
  zoomOutBtn?.addEventListener('click', () => scaleBy(1/1.2));
  zoomFitBtn?.addEventListener('click', resetZoom);
  zoom100Btn?.addEventListener('click', () => scaleTo(1));

  document.addEventListener('keydown', e => {
    if (lb.style.display !== 'flex') return;
    if (e.key === '+') scaleBy(1.2);
    if (e.key === '-') scaleBy(1/1.2);
    if (e.key === '0') resetZoom();
  });

  x.addEventListener('click', resetZoom);
</script>

</body>

</html>
