<?php
// generate_certificate.php
// Liftek Iraq — Certificate of Thorough Examination generator
//
// IMPORTANT DESIGN RULE:
// Which template renders (LLG single-page vs Appliance/Crane dual-page) is
// decided ONLY by the job's Category field. It must NEVER be decided by
// whether a tbl_appliance_inspections row happens to exist — view_appliance_job.php
// auto-creates a blank appliance row for ANY job opened through it, so using
// "row exists" as the signal incorrectly forces LLG jobs into the appliance
// template. Category is the single source of truth for template selection.
//
// PDF MODE (?mode=pdf):
// When called with ?mode=pdf (by generate_certificate_pdf.php via loopback),
// this file changes its output in two ways:
//   1. Bootstrap CDN <link> is replaced with an inline CSS subset — Dompdf
//      cannot fetch external URLs (enable_remote=false is enforced in the PDF
//      endpoint to prevent SSRF), so the CDN link would silently fail.
//   2. All local image src paths are converted to base64 data URIs via
//      pdfImageSrc() — eliminates every outbound request from Dompdf and
//      prevents path-traversal attacks at the image layer.
//   3. QR codes are already server-side PNG data URIs (no JS required).
//   4. The print button bar is hidden (meaningless in a PDF context).

require_once 'db.php';
require_once __DIR__ . '/includes/checklist_helper.php';

ini_set('display_errors', 0);
error_reporting(0);

// PDF mode flag — set this BEFORE the function definitions so that
// renderCertLogo() and renderSignatureImage() can read it via 'global'.
// Only accepts the literal string 'pdf' — any other value is ignored.
$pdf_mode = isset($_GET['mode']) && $_GET['mode'] === 'pdf';

// BUG FIX: "Back" always went to reports.php no matter where the
// certificate was actually opened from (Reports page, or the job-detail
// popup opened from the main Register). Two related fixes:
//   1. The certificate is now opened inside a modal/iframe from every entry
//      point (see index.php / reports.php's openCertificateModal()), which
//      is passed &embed=1 — in that case there's no "Back" link at all, the
//      modal's own close button is the only way back, so the question is
//      moot by construction.
//   2. For the rare case of a direct/bookmarked visit to this URL (not
//      embedded), ?from= tells us which page to link back to. Whitelisted
//      to known-safe internal pages only — never trust it as a raw
//      redirect target (open-redirect prevention).
$embed_mode = isset($_GET['embed']) && $_GET['embed'] === '1';
$back_url   = (isset($_GET['from']) && $_GET['from'] === 'index') ? 'index.php' : 'reports.php';

// ARABIC TEXT SHAPING FOR PDF MODE ONLY: Dompdf has no real text-shaping/
// bidi engine, so raw Arabic Unicode renders as disconnected, mirrored
// glyphs in the PDF even though the exact same string is correct in the
// browser View. arabicShapeForPdf() (pure PHP, no external dependency)
// pre-converts Arabic text into already-shaped, already-reordered glyphs
// before it reaches Dompdf. Always loaded (no dependency on vendor/) but
// only ever CALLED when $pdf_mode is true — see renderCertLogo() and
// renderCertFooterBand() below.
require_once __DIR__ . '/includes/arabic_pdf_shaper.php';

// SHARED FOOTER RENDERING (pdfImageSrc, renderCertFooterImage,
// renderCertFooterBand) — extracted into its own file so
// certificate_settings.php's Live Preview panel can call the EXACT same
// rendering code the real certificate uses, not a hand-copied look-alike.
// See includes/cert_footer_render.php's own header comment for the full
// rationale. Guarded internally, safe to require_once from every entry
// point (including export_certificates_zip.php's per-job include() loop).
require_once __DIR__ . '/includes/cert_footer_render.php';

// SERVER-SIDE QR: load the helper only when vendor/autoload.php exists AND
// the endroid/qr-code library is actually installed (class is resolvable).
// This guards against two failure modes:
//   (a) composer install was never run  → vendor/autoload.php missing
//   (b) composer ran but endroid/qr-code wasn't installed (e.g. PHP version
//       constraint meant composer fell back and skipped it)
// In either case the certificate MUST still render — we fall back to
// client-side qrcodejs in browser mode, or omit the QR in PDF mode.
$qr_server_side = false;
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
    // Load the autoloader silently; suppress any output it might emit.
    require_once __DIR__ . '/vendor/autoload.php';
    // Check the actual QR class is resolvable before claiming server-side QR works.
    if (class_exists(\Endroid\QrCode\QrCode::class, true)) {
        $qr_server_side = true;
        require_once 'includes/qr_helper.php';
    }
}

// ---------------------------------------------------------------------------
// 0. GLOBAL CERTIFICATE BRANDING (Admin-controlled, applies to every
//    template equally). Safe defaults if the settings table/row doesn't
//    exist yet (e.g. migration not run) so certificates never break.
// ---------------------------------------------------------------------------
$certSettings = [
    'Header_Logo_Path' => null,
    'Disclaimer_Text' => '',
    'Footer_Mode' => 'text',
    'Footer_Image_Path' => null,
];
try {
    $row = $pdo->query("SELECT * FROM tbl_certificate_settings WHERE id = 1")->fetch(PDO::FETCH_ASSOC);
    if ($row) $certSettings = array_merge($certSettings, $row);
} catch (\PDOException $e) {}

// The bottom margin can hold any number of independently-formatted text
// lines now (added via certificate_settings.php) — not a single paragraph.
$certFooterTexts = [];
try {
    $certFooterTexts = $pdo->query("SELECT * FROM tbl_certificate_footer_texts ORDER BY Sort_Order ASC, id ASC")->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {}

// ---------------------------------------------------------------------------
// IMAGE PATH HELPER — converts a web-relative path to a base64 data URI
// when running in PDF mode, so Dompdf never makes outbound HTTP requests.
// In browser mode the original path is returned unchanged.
//
// SECURITY CONTROLS:
//   - realpath() resolves symlinks and normalises the path.
//   - strncmp containment check prevents path traversal (../../etc/passwd).
//   - mime_content_type() verifies it is actually an image before encoding.
//   - Returns empty string on any failure — the <img> simply renders blank.
// ---------------------------------------------------------------------------
// FUNCTION GUARD: all helper functions below are declared only once, even
// when this file is included multiple times in the same PHP process (e.g.
// export_certificates_zip.php iterates over N jobs, each time ob_start()ing
// this file). The define() acts as the idempotency sentinel.
// ---------------------------------------------------------------------------
if (!defined('CERT_RENDER_HELPERS_LOADED')) {
define('CERT_RENDER_HELPERS_LOADED', true);

/**
 * Renders a QR code for the given URL at the given pixel size.
 *
 * If vendor/autoload.php is present (composer install was run), uses the
 * server-side endroid/qr-code library to emit a self-contained PNG data URI
 * — works in both browser and Dompdf PDF mode (no JavaScript, no outbound
 * requests needed).
 *
 * If vendor is absent, falls back to a <div> placeholder that the page's
 * inline qrcodejs script will populate at runtime — this works only in
 * browser mode, so in PDF mode the QR slot is silently omitted (the cert
 * still renders cleanly; the QR is just not present).
 *
 * @param string $url       The URL to encode.
 * @param int    $px        Rendered size in pixels (display size; server-side
 *                          generates at 3× for crispness automatically).
 * @param string $elementId Unique HTML id for the client-side fallback div.
 */
function renderQr(string $url, int $px, string $elementId): void {
    global $qr_server_side, $pdf_mode;

    if ($qr_server_side) {
        // Server-side: emit a crisp PNG data URI (no outbound HTTP, no JS).
        $dataUri = generateQrCodeDataUri($url, $px * 3);
        echo '<img src="' . htmlspecialchars($dataUri) . '" style="width:' . $px . 'px; height:' . $px . 'px; display:block;">';
    } elseif (!$pdf_mode) {
        // Client-side fallback (browser only): qrcodejs will render into this div.
        echo '<div id="' . htmlspecialchars($elementId) . '"'
           . ' data-qr-url="' . htmlspecialchars($url) . '"'
           . ' data-qr-size="' . $px . '"'
           . ' style="width:' . $px . 'px; height:' . $px . 'px; overflow:hidden;"></div>';
    }
    // PDF mode + no vendor: output nothing — the cert renders without a QR
    // rather than crashing with a fatal error.
}

/**
 * Renders the wordmark area at the top of every certificate: the admin's
 * uploaded logo if one exists, otherwise the default "Liftek" text
 * wordmark — so a certificate is never left blank up top just because no
 * logo has been uploaded yet.
 */
function renderCertLogo(array $settings): void {
    global $pdf_mode;
    if (!empty($settings['Header_Logo_Path'])): ?>
        <img src="<?= pdfImageSrc($settings['Header_Logo_Path'], $pdf_mode); ?>" style="max-height: 22mm; max-width: 90mm; object-fit:contain; display:block;">
    <?php else: ?>
        <div>
            <h2 class="fw-bold m-0" style="color: #1a428a; font-size: 20px;">Liftek</h2>
            <small class="fw-bold rtl-text" style="font-size: 9px;"><?= $pdf_mode ? arabicShapeForPdf('تقنيات الرفع') : 'تقنيات الرفع'; ?></small>
        </div>
    <?php endif;
}

// renderCertFooterImage() and renderCertFooterBand() now live in
// includes/cert_footer_render.php (required near the top of this file) —
// see that file's header comment for why they were extracted (single
// source of truth shared with certificate_settings.php's Live Preview).

/**
 * Small "Page X of Y" label for multi-page certificate categories.
 * Client requirement: every category gets page numbers EXCEPT Training
 * (always a single page, so callers simply never call this for Training).
 * Placed as a direct sibling of the footer band (same safe pattern —
 * position:absolute on cert-paper itself, not nested inside a flow
 * wrapper), sitting just above the footer band's reserved 22mm strip so it
 * can never collide with it.
 */
function renderPageNumber(int $current, int $total): void {
    ?>
    <div style="position:absolute; bottom:23mm; right:15mm; font-size:7.5px; color:#666;">
        Page <?= $current; ?> of <?= $total; ?>
    </div>
    <?php
}

/**
 * Renders the legal disclaimer as part of the certificate's actual
 * CONTENT (small print near the signature block) — deliberately separate
 * from the margin, per explicit request. Only prints if the admin has
 * set some text via certificate_settings.php.
 *
 * BUG FIX (2026-08-21): on the Appliance template, this used to be appended
 * as brand-new trailing content AFTER an already-reserved empty spacer row
 * (see the Appliance page-1 signature block) and AFTER that, a "Document
 * Footer Codes" table — pushing the total page-1 content height past the
 * page's bottom-margin boundary (content wrapper reserves exactly 22mm at
 * the bottom, matching the footer band's own max-height with ZERO spare
 * clearance) whenever the disclaimer text was long enough. With real
 * production disclaimer text this visually collided with the footer band
 * (screenshot: "انظر كيف تداخل... الملاحظة القانونية مع الهامش"). Root cause
 * was extra height being ADDED rather than reused — the Appliance template
 * already reserves an empty spacer row right above this exact spot for
 * breathing room, so the fix is to render the disclaimer INSIDE that
 * already-budgeted spacer cell instead of as new trailing flow content
 * (see the Appliance page-1 signature block below). $inBox=true drops the
 * div's own top border/margin/padding, since the spacer <td> already
 * supplies the visual separation from the signature table above it.
 */
function renderCertDisclaimer(array $settings, bool $inBox = false): void {
    if (empty($settings['Disclaimer_Text'])) return;
    $style = $inBox
        ? 'font-size: 6.5px; line-height: 1.4; color: #666; text-align: justify;'
        : 'font-size: 6.5px; line-height: 1.5; color: #666; text-align: justify; margin-top: 10px; padding-top: 6px; border-top: 1px solid #ddd;';
    ?>
    <div style="<?= $style; ?>">
        <?= nl2br(htmlspecialchars($settings['Disclaimer_Text'])); ?>
    </div>
    <?php
}

/**
 * Serial numbers are sometimes entered as a comma-separated list when one
 * job covers several identical items (e.g. "c2250,c2251,c2252"). Displayed
 * that way on the certificate it reads as one long run-on value — this
 * renders each one on its own line instead:
 *   c2250
 *   c2251
 *   c2252
 * Safe on a single serial number too (no comma = no change other than
 * still being HTML-escaped).
 */
function formatSerialNumbersVertical(string $rawSerial): string {
    $parts = preg_split('/\s*,\s*/', trim($rawSerial));
    $parts = array_filter($parts, fn($p) => $p !== '');
    return implode('<br>', array_map('htmlspecialchars', $parts));
}

/**
 * Counts how many individual serial numbers are packed into the
 * comma-separated Serial No field (see formatSerialNumbersVertical() right
 * above — same split logic reused here so the displayed list and the
 * Quantity figure can never disagree). Used by the LLG certificate's
 * Quantity column, which used to be a hardcoded "1" regardless of how many
 * serial numbers were actually listed ("لازم يكون الكمية 17 قطعة" — client
 * report, an item with 17 comma-separated serials was still showing
 * Quantity: 1). An empty/whitespace-only field still counts as 1 item
 * (a single piece of equipment with no serial recorded is still one item).
 */
function countSerialNumbers(string $rawSerial): int {
    $parts = preg_split('/\s*,\s*/', trim($rawSerial));
    $parts = array_filter($parts, fn($p) => $p !== '');
    return max(1, count($parts));
}

/**
 * Renders an uploaded signature image if available, otherwise a blank
 * line for a physical signature — used identically everywhere a
 * signature is needed on any template.
 */
function renderSignatureImage(?string $path): void {
    global $pdf_mode;
    if (!empty($path)) {
        ?><img src="<?= pdfImageSrc($path, $pdf_mode); ?>" style="max-height: 35px; max-width: 120px; object-fit: contain; display: block;"><?php
    } else {
        ?><div style="height: 35px;">&nbsp;</div><?php
    }
}

} // end if (!defined('CERT_RENDER_HELPERS_LOADED'))

// ---------------------------------------------------------------------------
// 1. CAPTURE JOB IDENTIFIER + ACCESS CONTROL
//    SECURITY UPDATE (2026-08-31): the full printable/downloadable
//    certificate is no longer public under ANY path, including
//    ?token=XXXX. Previously the token path was left open by design so
//    verify.php's QR-scan page could link straight to it -- but that meant
//    anyone who scanned (or guessed) a certificate's QR code could open,
//    print, and save the full certificate with no login at all. verify.php
//    now shows a safe, read-only verification summary instead and no
//    longer links here, so this file can require a login unconditionally:
//    ?id=X   — the INTERNAL staff path (index.php, view_job.php, reports.php).
//    ?token= — kept ONLY as an alternate lookup key for staff who have the
//              token; it is no longer treated as a public/no-login route.
// ---------------------------------------------------------------------------
$job_id    = $_GET['id'] ?? 0;
$cert_token = $_GET['token'] ?? null;

// Login is now required to view this file regardless of which identifier
// (id or token) was used to look the job up.
require_once 'includes/auth.php';
require_login();

if (empty($job_id) && empty($cert_token)) {
    die("<div style='color:red; font-family:sans-serif; padding:20px;'><strong>Error:</strong> Invalid or missing Job ID.</div>");
}

// ---------------------------------------------------------------------------
// 2. FETCH THE BASE JOB ROW (tbl_jobs is the source of truth for Job_No,
//    Category, Approval, Engineer, Issue_Date)
// ---------------------------------------------------------------------------
$job = null;

if (!empty($cert_token)) {
    try {
        $stmt = $pdo->prepare("SELECT * FROM tbl_jobs WHERE CertToken = ? LIMIT 1");
        $stmt->execute([$cert_token]);
        $job = $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}
}

if (!$job && !empty($job_id)) {
    try {
        $stmt = $pdo->prepare("SELECT * FROM tbl_jobs WHERE ID = ? OR Job_No = ? LIMIT 1");
        $stmt->execute([$job_id, $job_id]);
        $job = $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}
}

if (!$job && !empty($job_id)) {
    try {
        $stmt = $pdo->prepare("SELECT * FROM qry_jobs_master WHERE ID = ? OR Job_No = ? LIMIT 1");
        $stmt->execute([$job_id, $job_id]);
        $job = $stmt->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}
}

if (!$job) {
    die("<div style='color:red; font-family:sans-serif; padding:20px;'><strong>Error:</strong> Job not found in database.</div>");
}

// SECURITY / POLICY FIX: "the certificate PDF is only ever shown after
// approval" was previously a hard block for everyone, including the
// person who just needs to see how it'll look before finalizing it. Now:
// Admin/Inspector (who can edit the job in the first place) may preview
// it via the internal ?id= path regardless of lock status — a real
// preview of the exact final output. The public ?token= path (QR/NFC)
// and the Coordinator role still require Is_Locked=1, since that's the
// "official, final" access path.
$__previewingUnlocked = false;
if ((int) ($job['Is_Locked'] ?? 0) !== 1) {
    require_once 'includes/auth.php';
    $__me = current_user();
    $__canPreview = $__me && in_array($__me['role'], ['admin', 'inspector'], true) && empty($cert_token);

    if (!$__canPreview) {
        die("<div style='font-family:sans-serif; padding:20px; background:#fff3cd; color:#664d03; border-radius:6px; max-width:500px; margin:60px auto; text-align:center;'>
                <strong>Certificate not yet available.</strong><br>
                This job has not been approved yet. It will be viewable here once an Admin approves and locks it.
             </div>");
    }
    $__previewingUnlocked = true;
}

$job_pk_id = $job['ID'] ?? $job['id'] ?? $job_id;
$job_no    = $job['Job_No'] ?? $job['job_no'] ?? ('JOB-' . $job_pk_id);
$work_order_ref = $job['Work_Order_Ref'] ?? null;
$work_order_ref_display = $work_order_ref ?: 'N/A';
$category  = trim($job['Category'] ?? $job['category'] ?? 'LLG');

// Lazy-generate a secure token for this job if it doesn't have one yet.
// Safe no-op if the CertToken column doesn't exist yet (migration not run) —
// wrapped in try/catch so it never breaks certificate rendering.
if (empty($job['CertToken']) && !empty($job_pk_id)) {
    try {
        // 128-bit token (32 hex chars) — still practically unguessable
        // (3.4×10^38 possibilities) while keeping the QR payload short
        // enough to scan quickly and reliably from a phone at small print
        // sizes. The original 256-bit (64-char) token made the QR matrix
        // noticeably denser than it needed to be for this use case.
        $newToken = bin2hex(random_bytes(16));
        $tokStmt = $pdo->prepare("UPDATE tbl_jobs SET CertToken = ? WHERE ID = ? AND CertToken IS NULL");
        $tokStmt->execute([$newToken, $job_pk_id]);
        $job['CertToken'] = $newToken;
    } catch (\Exception $e) {}
}

// ---------------------------------------------------------------------------
// BUILD PUBLIC VERIFICATION URL FOR THE QR CODE
//    Points to verify.php using the secure token (never the sequential ID).
//    This QR is meant to be scanned by anyone holding the physical/digital
//    certificate — no login required, by design. The only access control is
//    possession of this unguessable token, which is the correct model for a
//    public certificate-verification flow.
// ---------------------------------------------------------------------------
$scheme  = (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) !== 'off') ? 'https' : 'http';
$rawHost = $_SERVER['HTTP_HOST'] ?? 'localhost';
$safeHost = preg_replace('/[^a-zA-Z0-9\.\-:]/', '', $rawHost);

// BUG FIX: the project does not live at the web server root — it lives in
// a subfolder (e.g. http://localhost/mehok/). The previous version built
// the QR link as "$scheme://$safeHost/verify.php", which always points to
// the SERVER ROOT and silently drops the "/mehok" subfolder, producing a
// genuine Apache 404 ("verify.php not found") even though the file exists.
// SCRIPT_NAME always reflects the real path of the currently-running file
// (e.g. "/mehok/generate_certificate.php"), so deriving the base folder
// from it makes this work correctly regardless of subfolder name or depth.
$baseFolder = rtrim(str_replace('\\', '/', dirname($_SERVER['SCRIPT_NAME'] ?? '/')), '/');
$baseUrl = $scheme . '://' . $safeHost . $baseFolder;

if (!empty($job['CertToken'])) {
    $verify_url = $baseUrl . '/verify.php?token=' . urlencode($job['CertToken']);
} else {
    // Fallback if token generation failed for any reason — still verifiable by Job_No
    $verify_url = $baseUrl . '/verify.php?job=' . urlencode($job_no);
}

// ---------------------------------------------------------------------------
// 3. TEMPLATE SELECTION — CATEGORY IS THE ONLY SIGNAL
//    Training = dedicated training certificate template.
//    Default (empty / "LLG" / anything containing "Lifting Gear") = LLG template.
//    Everything else explicitly categorized (Appliance, Crane, etc.) = Appliance template.
// ---------------------------------------------------------------------------
$is_training = strcasecmp($category, 'Training') === 0;
$is_llg_category = !$is_training && (
    $category === '' ||
    strcasecmp($category, 'LLG') === 0 ||
    stripos($category, 'Lifting Gear') !== false
);
$is_appliance = !$is_training && !$is_llg_category;

// ---------------------------------------------------------------------------
// 3b. TRAINING-SPECIFIC DATA
// ---------------------------------------------------------------------------
$training_row = null;
if ($is_training) {
    try {
        $stmtTraining = $pdo->prepare("SELECT * FROM tbl_training_records WHERE JobID = ? LIMIT 1");
        $stmtTraining->execute([$job_pk_id]);
        $training_row = $stmtTraining->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}

    // The trainee's photo is just a regular inspection photo tagged with
    // Photo_Type = 'Trainee Photo' — reusing the existing photo pipeline
    // instead of a dedicated column/table. Most recent one wins.
    //
    // Fallback: any photos uploaded before the training-only uploader fix
    // (which removed the Type dropdown and forced this tag) may carry an
    // older tag like "Overview" instead. If no 'Trainee Photo'-tagged row
    // exists, fall back to the single most recent photo for the job so
    // older test/legacy data still displays correctly.
    $trainee_photo_url = null;
    try {
        $stmtPhoto = $pdo->prepare(
            "SELECT Photo_Path FROM tbl_inspection_photos
             WHERE JobID = ? AND Photo_Type = 'Trainee Photo'
             ORDER BY PhotoID DESC LIMIT 1"
        );
        $stmtPhoto->execute([$job_pk_id]);
        $trainee_photo_url = $stmtPhoto->fetchColumn() ?: null;

        if (!$trainee_photo_url) {
            $stmtPhotoFallback = $pdo->prepare(
                "SELECT Photo_Path FROM tbl_inspection_photos
                 WHERE JobID = ?
                 ORDER BY PhotoID DESC LIMIT 1"
            );
            $stmtPhotoFallback->execute([$job_pk_id]);
            $trainee_photo_url = $stmtPhotoFallback->fetchColumn() ?: null;
        }
    } catch (\PDOException $e) {}
}

// ---------------------------------------------------------------------------
// 4. LLG-SPECIFIC DATA (only fetched/used when rendering the LLG template)
// ---------------------------------------------------------------------------
$llg_row = null;
if (!$is_appliance && !$is_training) {
    try {
        $stmtLlg = $pdo->prepare("SELECT * FROM tbl_llg_inspections WHERE JobID = ? LIMIT 1");
        $stmtLlg->execute([$job_pk_id]);
        $llg_row = $stmtLlg->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}
}

// ---------------------------------------------------------------------------
// 5. APPLIANCE-SPECIFIC DATA (only fetched/used when rendering the Appliance template)
// ---------------------------------------------------------------------------
$appliance_row         = null;
$appliance_id          = null;
$appliance_test_params = [];

if ($is_appliance) {
    try {
        $stmtApp = $pdo->prepare("SELECT * FROM tbl_appliance_inspections WHERE JobID = ? LIMIT 1");
        $stmtApp->execute([$job_pk_id]);
        $appliance_row = $stmtApp->fetch(PDO::FETCH_ASSOC);
    } catch (\PDOException $e) {}

    $appliance_id = $appliance_row['Appliance_ID'] ?? $appliance_row['appliance_id'] ?? null;

    if ($appliance_id) {
        try {
            $stmtParams = $pdo->prepare("SELECT * FROM tbl_appliance_test_parameters WHERE inspection_id = ? ORDER BY `row_number` ASC LIMIT 4");
            $stmtParams->execute([$appliance_id]);
            $appliance_test_params = $stmtParams->fetchAll(PDO::FETCH_ASSOC);
        } catch (\Exception $e) {}
    }
}

// ---------------------------------------------------------------------------
// 5b. INSPECTION PHOTOS — shared across every category/template. Pulled once
//     here by JobID (category-agnostic on purpose) so Appliance, LLG, Misc,
//     Training and NDT certificates all show the same photo gallery with
//     zero extra per-category code.
// ---------------------------------------------------------------------------
$inspection_photos = [];
try {
    $stmtPhotos = $pdo->prepare(
        "SELECT PhotoID, Photo_Path, Photo_Type, Caption
         FROM tbl_inspection_photos
         WHERE JobID = ?
         ORDER BY PhotoID ASC"
    );
    $stmtPhotos->execute([$job_pk_id]);
    $inspection_photos = $stmtPhotos->fetchAll(PDO::FETCH_ASSOC);
} catch (\PDOException $e) {
    // Table missing or unreachable — certificate still renders, just without photos.
    $inspection_photos = [];
}

// ---------------------------------------------------------------------------
// 6. CLIENT NAME & ADDRESS — ROBUST RESOLUTION VIA tbl_clients
//    Priority: FK column on the job row (if your schema has one) -> name match
//    against tbl_clients (covers legacy jobs where only Client_Name text is
//    stored) -> raw text already on the job row -> final fallback.
// ---------------------------------------------------------------------------
$client_name    = $job['Client'] ?? $job['ClientName'] ?? $job['Client_Name'] ?? $job['Customer'] ?? $job['Customer_Name'] ?? null;
$client_address = $job['Client_Address'] ?? $job['Address'] ?? $job['client_address'] ?? null;

$client_fk_id = $job['ClientID'] ?? $job['client_id'] ?? $job['Client_ID'] ?? null;

if (!empty($client_fk_id)) {
    try {
        $cStmt = $pdo->prepare("SELECT * FROM tbl_clients WHERE ID = ? LIMIT 1");
        $cStmt->execute([$client_fk_id]);
        $clientData = $cStmt->fetch(PDO::FETCH_ASSOC);
        if ($clientData) {
            $client_name    = $clientData['ClientName'] ?? $clientData['client_name'] ?? $clientData['Client'] ?? $client_name;
            $client_address = $clientData['Address'] ?? $clientData['address'] ?? $clientData['Client_Address'] ?? $client_address;
        }
    } catch (\Exception $e) {}
}

// Name-based fallback when no FK resolved an address — this is what actually
// fixes most "Address = N/A" cases in this schema, since tbl_jobs stores
// Client_Name as free text rather than a real foreign key.
if (!empty($client_name) && empty($client_address)) {
    try {
        $cStmt2 = $pdo->prepare("SELECT * FROM tbl_clients WHERE ClientName = ? OR ClientName LIKE ? LIMIT 1");
        $cStmt2->execute([$client_name, "%$client_name%"]);
        $clientData2 = $cStmt2->fetch(PDO::FETCH_ASSOC);
        if ($clientData2) {
            $client_address = $clientData2['Address'] ?? $clientData2['address'] ?? $clientData2['Client_Address'] ?? $client_address;
            $client_name    = $clientData2['ClientName'] ?? $clientData2['client_name'] ?? $client_name;
        }
    } catch (\Exception $e) {}
}

$client_name    = $client_name ?: 'N/A';
$client_address = $client_address ?: 'Basra, Iraq';

// ---------------------------------------------------------------------------
// 7. MAP REMAINING FIELDS
//    Appliance jobs pull equipment specs from tbl_appliance_inspections first.
//    LLG jobs pull from tbl_jobs / tbl_llg_inspections, exactly as the
//    original dedicated LLG template expected.
// ---------------------------------------------------------------------------
$site_name = '-';
if (!empty($job['Site_ID'])) {
    try {
        $stmtSite = $pdo->prepare('SELECT Site_Name FROM tbl_client_sites WHERE SiteID = ? AND Deleted_At IS NULL LIMIT 1');
        $stmtSite->execute([$job['Site_ID']]);
        $siteVal = $stmtSite->fetchColumn();
        if ($siteVal !== false && trim((string)$siteVal) !== '') {
            $site_name = trim($siteVal);
        }
    } catch (\Throwable $e) { /* keep default '-' */ }
}
$sub_location    = trim($job['Sub_Location'] ?? '') ?: '-';
$inspection_type = trim($job['Inspection_Type'] ?? '') ?: 'In-Service Thorough Examination';

if ($is_appliance) {
    // BUG FIX ("where did the field I set disappear to?"): was a plain `??`
    // chain, which stops at the FIRST key that is merely present — even if
    // its value is an empty string. tbl_appliance_inspections.ItemName can
    // legitimately be '' (an inspector cleared the Appliance form's own
    // "Item Name" field and saved) while tbl_jobs.Item_Name still holds the
    // correct value shown on the dashboard/job list. firstNonEmpty() skips
    // blanks, not just NULLs, so the certificate falls back to the job's
    // real Item Name instead of silently printing "EQUIPMENT".
    $item_name     = firstNonEmpty(
        $appliance_row['ItemName'] ?? null,
        $appliance_row['item_name_description'] ?? null,
        $job['Item_Name'] ?? null,
        $job['ItemName'] ?? null,
        $job['item_name'] ?? null
    ) ?? 'EQUIPMENT';
    // FIX (admin explicit spec, 2026-08-25 — pointed directly at the two DB
    // columns in phpMyAdmin): the heading and the "ASSET DESCRIPTION" bar
    // are NOT the same value and must NOT share one computed/fallback
    // variable between them — every earlier round of this same complaint
    // came from exactly that conflation. Two independent sources now:
    //   - heading ("OF THOROUGH EXAMINATION OF ...")  <- item_name_description
    //   - "ASSET DESCRIPTION : ..." bar (and the page-2 annex's
    //     "Certificate Title" row)                     <- Certificate_Title
    // Neither one falls back into the other anymore.
    $rawItemNameDesc = trim((string) ($appliance_row['item_name_description'] ?? ''));
    // item_name_description is kept auto-synced to the visible "Item Name"
    // field by view_appliance_job.php's own JS on every keystroke/save, so
    // it should virtually always hold a real value. The only fallback here
    // is to $item_name itself (still an Item-Name concept, not
    // Certificate_Title) for old rows saved before that sync existed.
    $heading_display = ($rawItemNameDesc !== '') ? $rawItemNameDesc : $item_name;

    // BUG FIX (found on re-verification): tbl_appliance_inspections.Certificate_Title
    // has a schema DEFAULT of 'CERTIFICATE OF THOROUGH EXAMINATION' that gets
    // written automatically whenever a new appliance row is INSERTed without
    // explicitly naming that column. Treat that literal leftover default
    // text as blank rather than printing it as if an inspector typed it.
    $rawCertTitle = trim((string) ($appliance_row['Certificate_Title'] ?? ''));
    $asset_desc_display = (
        strcasecmp($rawCertTitle, 'CERTIFICATE OF THOROUGH EXAMINATION') === 0
    ) ? '' : $rawCertTitle;
    $raw_serial_no = $appliance_row['Serial_No'] ?? $appliance_row['serial_number']
                      ?? $job['Serial_No'] ?? $job['serial_no'] ?? $job['s_n'] ?? 'N/A';
    $mfg           = $appliance_row['brand'] ?? $appliance_row['Manufacturer']
                      ?? $job['brand'] ?? $job['Manufacturer'] ?? $job['Brand'] ?? '';
    $model         = $appliance_row['model'] ?? $appliance_row['Model_No']
                      ?? $job['model'] ?? $job['Model'] ?? '';
    $capacity      = $job['SWL_WLL'] ?? $job['Capacity'] ?? $job['SWL'] ?? '';
    $yom           = $appliance_row['Year_Of_Manufacture'] ?? $job['Year_Of_Manufacture'] ?? $job['YOM'] ?? '';
    $db_item_desc  = $appliance_row['item_description'] ?? $job['item_description'] ?? '';
    $approval      = strtoupper($job['Approval'] ?? $appliance_row['Approval_Status'] ?? 'PASS');
} else {
    // LLG: field mapping unchanged from the original dedicated LLG logic,
    // hardened the same empty-string-safe way as the Appliance branch above.
    $item_name     = firstNonEmpty(
        $job['item_name_description'] ?? null,
        $job['ItemName'] ?? null,
        $job['Item_Name'] ?? null,
        $job['item_name'] ?? null
    ) ?? 'EQUIPMENT';
    $raw_serial_no = $job['Serial_No'] ?? $job['serial_no'] ?? $job['SerialNo'] ?? $job['s_n'] ?? 'N/A';
    $mfg           = $job['brand'] ?? $job['Manufacturer'] ?? $job['Brand'] ?? $job['manufacturer'] ?? '';
    $model         = $job['model'] ?? $job['Model'] ?? $job['model_no'] ?? $job['Model_No'] ?? '';
    $capacity      = $job['SWL_WLL'] ?? $job['Capacity'] ?? $job['SWL'] ?? $llg_row['Marked_SWL'] ?? $job['Marked_SWL'] ?? '';
    $yom           = $job['Year_Of_Manufacture'] ?? $job['yom'] ?? $job['YOM'] ?? '';
    $db_item_desc  = $llg_row['Item_Description'] ?? $job['item_description'] ?? $job['Item_Description'] ?? '';
    $approval      = strtoupper($job['Approval'] ?? $llg_row['Approval_Status'] ?? 'PASS');
}

$issue_date = $job['Issue_Date'] ?? $job['issue_date'] ?? date('Y-m-d');
$inspector  = $job['Engineer'] ?? $job['engineer'] ?? 'Abdulla Qasim';

// Inspector's personal signature — matched by name to a real tbl_users
// account (Engineer is now always picked from a real Admin/Inspector
// dropdown, not free text, so this match is reliable). Counter Signature
// is the separate, fixed one from certificate_settings.php.
$inspector_signature_path = null;
try {
    $sigStmt = $pdo->prepare("SELECT Signature_Path FROM tbl_users WHERE Name = ? AND Deleted_At IS NULL LIMIT 1");
    $sigStmt->execute([$inspector]);
    $inspector_signature_path = $sigStmt->fetchColumn() ?: null;
} catch (\PDOException $e) {}
$counter_signature_path = $certSettings['Counter_Signature_Path'] ?? null;

$is_crane_equipment = (stripos($item_name, 'crane') !== false || stripos($item_name, 'loader') !== false);
$ref_standard = $job['Reference_Standard'] ?? $job['ref_standard']
    ?? ($is_crane_equipment ? 'ASME B30.5 / BS EN standards' : 'ASME B30.20 / BS EN 13155');
$procedure    = $job['Inspection_Procedure'] ?? $job['procedure'] ?? 'DOC/SH/005, ISSUE : 02/REV01';
$due_date     = $job['Expiry_Date'] ?? $job['due_date'] ?? date('Y-m-d', strtotime('+1 year'));

// BUG FIX: these 6 note fields previously always read from $job, which
// only ever contains tbl_jobs columns — they never actually existed there,
// so every certificate silently fell back to hardcoded defaults ("NIL",
// "N/A") no matter what an inspector entered. The real values live on
// $appliance_row (tbl_appliance_inspections) or $llg_row
// (tbl_llg_inspections) depending on category, so read from the correct
// source now that both tables actually have these columns.
$notes_source = $is_appliance ? ($appliance_row ?? []) : ($llg_row ?? []);
$prev_details  = $notes_source['Previous_Inspection_Details'] ?? 'Visual & Functional Exam';
$equipment_used = $notes_source['Equipment_Used'] ?? 'N/A';
$colour_code   = $notes_source['Colour_Code'] ?? 'Not Applicable';
$deviations    = $notes_source['Deviations'] ?? 'NIL';
$subcontracted = $notes_source['Subcontracted'] ?? 'NIL';
$comments      = $notes_source['Comments'] ?? 'Visual, functional, and load testing carried out as per standard specifications.';

// ---------------------------------------------------------------------------
// 8. BUILD DYNAMIC ASSET DESCRIPTION AND SPLIT EVENLY LEFT / RIGHT
//    (used by the Appliance template's split boxes; also used to build the
//    single "Description" cell in the LLG template)
// ---------------------------------------------------------------------------
$desc_lines = [];
if (!empty($mfg))           $desc_lines[] = "Manufacturer / Brand: " . $mfg;
if (!empty($model))         $desc_lines[] = "Model No: " . $model;
if (!empty($raw_serial_no)) $desc_lines[] = "Serial Number: " . preg_replace('/\s*,\s*/', "\n", trim((string) $raw_serial_no));
if (!empty($yom))           $desc_lines[] = "Year of Manufacture: " . $yom;
if (!empty($capacity))      $desc_lines[] = "Capacity / SWL: " . $capacity;
// FIX (admin screenshot annotation, 2026-08-25): removed the "Description /
// Notes: " label prefix per the admin's explicit mark-up — the free-text
// content itself (item_description, e.g. "Registration Number: ...") still
// prints exactly as typed, just without this extra label in front of it.
// Nothing else in this box changed.
if (!empty($db_item_desc))  $desc_lines[] = $db_item_desc;

if (empty($desc_lines)) {
    $desc_lines[] = "Manufacturer / Brand: ";
    $desc_lines[] = "Model No: ";
    $desc_lines[] = "Serial Number: N/A";
    $desc_lines[] = "Year of Manufacture: ";
    $desc_lines[] = "Capacity / SWL: ";
}

$full_description = implode("\n", $desc_lines);
$split_lines       = explode("\n", str_replace("\r", "", $full_description));
$mid_point         = ceil(count($split_lines) / 2);
$desc_left_text    = implode("\n", array_slice($split_lines, 0, $mid_point));
$desc_right_text   = implode("\n", array_slice($split_lines, $mid_point));

// LLG-only variant: strips the "Serial Number:" line specifically for the
// LLG template, since LLG (unlike Appliance) has its own dedicated Serial
// No column right next to this Description cell — repeating it inside the
// text itself was pure redundancy. Appliance has no separate Serial No
// display anywhere else, so $full_description (with the line included)
// stays untouched for Appliance's use.
$llg_desc_lines = array_filter($desc_lines, fn($line) => stripos($line, 'Serial Number:') !== 0);
$llg_full_description = implode("\n", $llg_desc_lines);

// ---------------------------------------------------------------------------
// 9. CHECKLIST ITEMS — separate lookup paths per template
// ---------------------------------------------------------------------------
$checklist_items = [];

if ($is_appliance) {
    if (!empty($appliance_id)) {
        try {
            $chkSql = "SELECT m.CheckItem, c.Result, c.Remarks, c.`row_number` AS DisplayOrder
                       FROM tbl_appliance_checklists c
                       JOIN tbl_master_appliance_checklists m ON m.ID = c.master_id
                       WHERE c.inspection_id = ?
                       ORDER BY c.`row_number` ASC";
            $chkStmt = $pdo->prepare($chkSql);
            $chkStmt->execute([$appliance_id]);
            $checklist_items = $chkStmt->fetchAll(PDO::FETCH_ASSOC);
        } catch (\Exception $e) {}
    }

    // BUG FIX (docx item 6, re-confirmed live on job LTG-IQ/00056 — "the
    // checklist not coming to the certificate"): this fallback used to run
    // its OWN separate query here, requiring an exact `m.Category =
    // 'Appliance'` match and an exact `m.item_name = ?` match. That's a
    // SECOND, divergent copy of the same matching logic already fixed (for
    // case/whitespace) in includes/checklist_helper.php's
    // getApplicableChecklistIds() — used by the edit form
    // (view_appliance_job.php) but never by this certificate page. Any
    // checklist row whose Category ended up spelled differently than the
    // literal string 'Appliance' (confirmed on job LTG-IQ/00056: rows
    // imported via manage_checklists.php's "Import Simple List" picked up
    // Category='LLG' due to a separate bug, now also fixed) — or whose
    // item_name had different capitalization/whitespace than this job's —
    // would silently vanish from the printed certificate while still
    // showing correctly on the edit form. Now calls the SAME hardened,
    // single source of truth the edit form uses, so what an inspector sees
    // while editing is guaranteed to be what prints.
    if (empty($checklist_items)) {
        try {
            $checklist_items = getChecklistItemsForDisplay($pdo, (int) $job_pk_id, $category, $item_name);
        } catch (\Exception $e) {}
    }
} else {
    // Same fix applied to LLG/Misc/other non-Appliance categories — was a
    // third, INNER-JOIN-only copy of this same matching logic (which also
    // meant any checklist item added to the master list AFTER this job's
    // checklist was last saved could never appear, on top of the
    // Category/whitespace matching problem).
    try {
        $checklist_items = getChecklistItemsForDisplay($pdo, (int) $job_pk_id, $category, $item_name);
    } catch (\Exception $e) {}
}
?>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8">
    
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Certificate - <?= htmlspecialchars((string)$job_no); ?></title>
    <?php if (!$pdf_mode): ?>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
    <?php else: ?>
    <?php
    // ARABIC TEXT SUPPORT FOR PDF MODE:
    // Dompdf's default font (DejaVu Sans) has no Arabic glyphs, so any Arabic
    // text (e.g. the "تقنيات الرفع" tagline, or Arabic footer/disclaimer
    // lines added via certificate_settings.php) renders as "????" in the PDF
    // even though the browser view shows it correctly via the OS's own fonts.
    //
    // This block is self-activating and safe: it only emits @font-face /
    // font-family rules once a real Arabic-capable .ttf is dropped into the
    // fonts/ folder. Until then it silently does nothing — no missing-file
    // errors, PDF still renders exactly as before.
    //
    // TO FIX: download a free Arabic font (e.g. "Noto Sans Arabic" — it
    // covers Latin glyphs too, so mixed English/Arabic lines render
    // correctly from a single font). From
    // https://fonts.google.com/noto/specimen/Noto+Sans+Arabic save the
    // Regular weight as: fonts/NotoSansArabic-Regular.ttf
    //
    // ROUND-2 BUG FIX — this was the actual reason Arabic still rendered as
    // "????" in the PDF even after a .ttf was downloaded onto the server:
    // TWO separate bugs stacked on top of each other:
    //
    //   1. WRONG PATH: Google Fonts' "Get font" download for a variable
    //      font family ships a top-level VARIABLE font
    //      (fonts/NotoSansArabic-VariableFont_wdth,wght.ttf — a single file
    //      covering every weight via font-variation-settings, which Dompdf
    //      cannot use) plus a fonts/static/ subfolder containing the
    //      individual static weights, e.g.
    //      fonts/static/NotoSansArabic-Regular.ttf. The code was only ever
    //      looking for fonts/NotoSansArabic-Regular.ttf directly — a path
    //      that never existed — so $hasArabicFont was always false and the
    //      whole @font-face block silently never activated. Fixed below by
    //      checking BOTH the flat path and the static/ subfolder path.
    //
    //   2. RELATIVE URL IN PDF MODE: the @font-face rule referenced the
    //      font with a plain relative URL (url('fonts/...')). That works
    //      for the browser View (resolved against the page's own address),
    //      but the PDF path feeds HTML into Dompdf via loadHtml() — a raw
    //      string, not a file — so Dompdf has no base URL to resolve a
    //      relative path against and silently fails to load the font,
    //      falling back to the default (Arabic-less) font. This is exactly
    //      why every image on the certificate (logo/photo/QR, via
    //      pdfImageSrc() below) is embedded as a base64 data URI instead of
    //      a file path. The Arabic font now gets the same treatment: in PDF
    //      mode only, it is embedded inline as a data URI so Dompdf never
    //      needs to resolve any path for it.
    $arabicFontCandidates = [
        __DIR__ . '/fonts/NotoSansArabic-Regular.ttf',         // flat layout
        __DIR__ . '/fonts/static/NotoSansArabic-Regular.ttf',  // Google Fonts "static/" layout
    ];
    $arabicFontPath = null;
    foreach ($arabicFontCandidates as $candidate) {
        if (file_exists($candidate)) { $arabicFontPath = $candidate; break; }
    }
    $hasArabicFont = $arabicFontPath !== null;
    $arabicFontSrc = $hasArabicFont
        ? 'fonts/' . ltrim(str_replace(__DIR__ . '/fonts/', '', $arabicFontPath), '/') // browser View: relative URL (cacheable).
        : '';
    if ($hasArabicFont && $pdf_mode) {
        $arabicFontSrc = 'data:font/ttf;base64,' . base64_encode(file_get_contents($arabicFontPath));
    }
    ?>
    <?php if ($hasArabicFont): ?>
    <style>
        @font-face {
            font-family: 'ArabicFont';
            src: url('<?= $arabicFontSrc; ?>') format('truetype');
            font-weight: normal;
            font-style: normal;
        }
        /* Applied narrowly (not on body/*) so existing Latin-text layout is
           unaffected — only elements explicitly marked .rtl-text opt in. */
        .rtl-text { font-family: 'ArabicFont', 'DejaVu Sans', sans-serif; direction: rtl; }
    </style>
    <?php endif; ?>
    <style>
        /* Minimal Bootstrap utility subset for Dompdf PDF rendering.
           Full Bootstrap CDN is omitted: Dompdf has enable_remote=false
           (SSRF prevention), so the CDN link would silently fail. */
        * { box-sizing: border-box; }
        body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; margin: 0; padding: 0; background: #fff; }
        .d-flex { display: flex; }
        .d-block { display: block; }
        .d-inline-block { display: inline-block; }
        .flex-direction-column { flex-direction: column; }
        .justify-content-between { justify-content: space-between; }
        .align-items-center { align-items: center; }
        .align-items-flex-end { align-items: flex-end; }
        .align-bottom { vertical-align: bottom !important; }
        .align-middle { vertical-align: middle !important; }
        .fw-bold { font-weight: bold; }
        .m-0 { margin: 0; }
        .mt-1 { margin-top: 0.25rem; }
        .mt-2 { margin-top: 0.5rem; }
        .mb-1 { margin-bottom: 0.25rem; }
        .mb-2 { margin-bottom: 0.5rem; }
        .mb-3 { margin-bottom: 1rem; }
        .me-1 { margin-right: 0.25rem; }
        .pb-2 { padding-bottom: 0.5rem; }
        .py-3 { padding-top: 1rem; padding-bottom: 1rem; }
        .text-center { text-align: center; }
        .text-end { text-align: right; }
        .text-start { text-align: left; }
        .text-justify { text-align: justify; }
        .text-muted { color: #6c757d; }
        .text-success { color: #198754; }
        .text-danger { color: #dc3545; }
        .text-warning { color: #856404; }
        .bg-light { background-color: #f8f9fa; }
        .w-100 { width: 100%; }
        .border-bottom { border-bottom: 1px solid #dee2e6; }
        .alert { padding: 0.75rem 1rem; border: 1px solid transparent; border-radius: 0.25rem; }
        .alert-warning { background-color: #fff3cd; border-color: #ffecb5; color: #664d03; }
        h2 { font-size: 1.5rem; }
        h3 { font-size: 1.25rem; }
        h4 { font-size: 1.1rem; }
        small { font-size: 85%; }
        strong { font-weight: bold; }
    </style>
    <?php endif; ?>
    <style>
        <?php
        // BUG FIX (extra near-blank page in PDF, reported again for both
        // Appliance AND Misc): the comment previously here claimed setting
        // Dompdf's `defaultMediaType` option to 'print' was enough to make
        // it apply the `@media print` block below (which zeroes out the
        // screen-only 20mm inter-page gap and 15px body padding). In
        // practice Dompdf's @media support did NOT reliably kick in for
        // this file, so every .cert-paper section was still ~20mm+15px
        // taller than one true A4 page in the actual generated PDF — just
        // enough overflow to spill onto a near-empty second page after
        // EVERY cert-paper block (Appliance page 1, Misc page 1, LLG page
        // 1, etc. — hence it appearing "everywhere").
        //
        // FIX: stop depending on Dompdf's media-query engine at all for
        // something this consequential. $pdf_mode is already known
        // precisely in PHP at render time (see top of file) — branch the
        // screen-only cosmetic values directly here, exactly the same
        // pattern already used for the Arabic font / image paths in this
        // file. The @media print block is kept below, unchanged, purely
        // for the browser's own window.print()/Ctrl+P output (the "🖨️
        // Print / Save PDF" button) — a completely different, real browser
        // context where @media print always works correctly.
        ?>
        body { background-color: <?= $pdf_mode ? '#fff' : '#525659'; ?>; font-family: 'Segoe UI', Arial, sans-serif; padding: <?= $pdf_mode ? '0' : '15px'; ?>; margin: 0; }
        @page { size: A4; margin: 0; }
        .cert-paper {
            background: #fff;
            width: 210mm;
            <?php if ($pdf_mode): ?>
            /* BUG FIX (phantom blank page — confirmed via direct PDF
               inspection of the actual downloaded PDF from
               generate_certificate_pdf.php, not just the browser
               preview): whenever a .cert-paper div's reserved height got
               close to the full @page A4 height (297mm), Dompdf inserted
               an extra, almost-entirely-blank continuation page right
               after it — independent of the div's ACTUAL content height
               (confirmed: content here is only ~200mm tall) and
               independent of any explicit page-break-before/after rule.
               297mm and 296mm both reproduced it. 285mm fixed it in
               initial testing but then intermittently failed on later,
               unhurried re-tests with byte-for-byte identical source —
               see claude/operating-notes-and-lessons.md for the full
               account. 270mm has since passed 5 consecutive real-PDF
               tests and is the current value; treat any change to this
               number as needing several repeated real-PDF tests (not
               just one) before trusting it, given the instability seen
               at 285mm. Fixed height (not min-height): content here
               never gets close to 270mm so nothing is at risk of being
               clipped. If a future template's content genuinely needs
               more than ~262mm (270mm minus the 8mm top/bottom padding),
               content will spill past the box visually (overflow is NOT
               hidden) and this same phantom-page bug may resurface — see
               the matching note on the @media print copy of this rule
               below, which is the one Dompdf's PDF renderer actually
               uses (defaultMediaType is forced to 'print' in
               generate_certificate_pdf.php / export_certificates_zip.php). */
            height: 270mm;
            margin: 0;
            padding: 8mm 10mm;
            box-shadow: none;
            border: none;
            <?php else: ?>
            min-height: 297mm;
            margin: 0 auto 20mm auto;
            padding: 10mm 12mm;
            box-shadow: 0 0 15px rgba(0,0,0,0.3);
            border: 1px solid #ccc;
            <?php endif; ?>
            box-sizing: border-box;
            position: relative;
        }
        .cert-table { border-collapse: collapse !important; width: 100%; }
        .cert-table th, .cert-table td {
            border: 1px solid #000 !important;
            padding: 4px 6px;
            font-size: 11px;
            background-color: #fff;
            vertical-align: middle;
        }
        /* BUG FIX: "الجدول الداخلي داخل المربع الأسود الكبير" — the label/value
           rows nested INSIDE a cert-table cell (Client's Name, Address, etc.)
           kept showing their own small border box around each row even
           though each <td> already had an inline `border:none`. Reason:
           `.cert-table th, .cert-table td` is a plain descendant selector —
           it matches ANY td nested anywhere under a .cert-table ancestor,
           regardless of how many tables are nested in between — and its
           `!important` always beats a plain (non-important) inline style.
           Inner tables now carry this class instead, and because this rule
           is declared AFTER `.cert-table td` above with the same selector
           specificity, it wins by source order and removes the inner
           borders/background, leaving only the one big rectangle around
           each left/right group that the client wants to keep. */
        .cert-table-plain, .cert-table-plain th, .cert-table-plain td {
            border: none !important;
            background: transparent !important;
        }
        .cert-title { font-weight: bold; font-size: 13px; text-transform: uppercase; margin: 6px 0; text-align: center; }
        .badge-pass { color: #198754; font-weight: bold; }
        .badge-fail { color: #dc3545; font-weight: bold; }
        .print-btn-bar { width: 210mm; margin: 0 auto 12px auto; }
        .page-break { page-break-before: always; break-before: page; }
        @media print {
            body { background: #fff; padding: 0; }
            /* BUG FIX: this is the block that ACTUALLY governs real PDF
               output — generate_certificate_pdf.php / export_certificates_zip.php
               set Dompdf's defaultMediaType to 'print', so Dompdf applies
               THIS rule (not the $pdf_mode-conditional .cert-paper rule
               above, which is only what the browser preview uses). Kept in
               sync with it: fixed height at 270mm — see the detailed
               comment on .cert-paper above for why. */
            .cert-paper { box-shadow: none; border: none; width: 210mm; height: 270mm; padding: 8mm 10mm; margin: 0; }
            .print-btn-bar { display: none !important; }
        }
    </style>
</head>
<body>

<?php if (!$pdf_mode): ?>
<div class="print-btn-bar d-flex justify-content-between align-items-center">
    <?php if ($embed_mode): ?>
        <span class="text-muted small fw-bold">Certificate Preview</span>
    <?php else: ?>
        <a href="<?= htmlspecialchars($back_url); ?>" class="btn btn-light text-dark fw-bold">← Back</a>
    <?php endif; ?>
    <button onclick="window.print()" class="btn btn-primary fw-bold">🖨️ Print / Save PDF (A4 Compact - 2 Pages)</button>
</div>
<?php endif; ?>

<?php if (!empty($__previewingUnlocked) && !$pdf_mode): ?>
<div class="print-btn-bar" style="background:#fff3cd; color:#664d03; text-align:center; padding:8px; font-weight:bold; border-radius:4px; margin-bottom:8px;">
    ⚠️ PREVIEW — this certificate is not approved yet. This is exactly how it will look once approved, but it is not the official document.
</div>
<?php endif; ?>

<?php if ($is_training): ?>
<!-- ==================== TRAINING CERTIFICATE TEMPLATE ====================
     BUG FIX (round 3): round 2 fixed the photo/text overlap by giving the
     middle content a big `margin-top: 70mm` to clear the absolutely
     positioned photo, and pinning the signature block with
     `position:absolute; bottom:12mm`. That introduced a NEW, worse bug:
     Dompdf's page-break calculation does not reliably handle a normal-flow
     block with a large top margin sitting inside a `position:relative`
     box that also contains `position:absolute` siblings — it split the
     wrapper across two pages, stranding the certifying text + topics list
     on page 2 while the logo/photo/QR/footer stayed on page 1.
     ("الصورة بصفحة والمعلومات بصفحة اخرى" — reported by the client.)

     Dompdf is only reliably correct with: (a) plain normal block flow, and
     (b) position:absolute for a single self-contained decorative element
     that does NOT need to coordinate its position against normal-flow
     siblings (the footer band, which is independent, still works fine).
     Mixing "absolute decorative elements + a flow element with a
     hand-tuned margin to dodge them" is exactly the combination that
     breaks. Round 3 removes ALL position:absolute from this template
     except the footer band: logo, cert info/QR, and the trainee photo are
     now laid out as ordinary flow rows (logo+QR side by side, photo in
     its own row below), so nothing can ever overlap by construction and
     nothing can trigger a bogus page split. -->
<div class="cert-paper" style="padding: 0; position: relative;">
    <?php renderCertFooterBand($certFooterTexts, $certSettings); ?>

    <!-- Certificate info + QR block — position:absolute, direct sibling of
         .cert-paper (exactly the same safe pattern as the footer band
         above it). BUG-FIX HISTORY: this used to be inside a
         `d-flex justify-content-between` row (collapsed left instead of
         reaching the right edge — Dompdf flex bug), then a table
         (correct, but only top-aligned with the logo). The client then
         hand-edited an exported PDF to show exactly where this block
         should sit and asked for the measurement to be applied precisely
         ("احسب مكان العبارات من كل جهه... بدقة"). Measured directly from
         that reference PDF via pdftotext -bbox (values in points,
         converted at 25.4/72 mm-per-point): the "Certificate No." line's
         top edge sits 38.45mm from the page top, and every line's right
         edge sits ~23.46mm from the page's right edge. Since this
         cert-paper's own box coincides exactly with the physical page in
         PDF mode (padding:0 inline override + margin:0 in $pdf_mode — see
         the .cert-paper CSS rule above), top/right here are page-relative
         numbers with no extra math needed, same as renderPageNumber()'s
         bottom:23mm;right:15mm below. -->
    <div style="position:absolute; top:38.5mm; right:23.5mm; text-align:right; font-size: 8.5px; line-height: 1.5; width: 150px;">
        <div><strong>Certificate No.:</strong> <?= htmlspecialchars($job_no); ?></div>
        <div><strong>Work Order Ref:</strong> <?= htmlspecialchars($work_order_ref_display); ?></div>
        <div><strong>Training Date:</strong> <?= htmlspecialchars($issue_date); ?></div>
        <div><strong>Expiry Date:</strong> <?= htmlspecialchars($due_date); ?></div>
        <div><strong>Site Name:</strong> <?= htmlspecialchars($site_name); ?></div>
        <div><strong>Sub Location:</strong> <?= htmlspecialchars($sub_location); ?></div>
        <div><strong>Type Of Inspection:</strong> <?= htmlspecialchars($inspection_type); ?></div>
        <!-- QR code: directly below the info block, same column. -->
        <div style="margin-top:6px; margin-left:auto;"><?php renderQr($verify_url, 60, 'qr-training'); ?></div>
        <div style="font-size:6px; font-weight:bold;">SCAN TO VERIFY</div>
    </div>

    <div style="margin: 15mm 15mm 22mm 15mm;">

        <!-- Logo: plain flow content — its own natural top-left (15mm,
             15mm from the page, i.e. this wrapper's own margin) already
             matches the reference PDF's measured logo position, so it
             needs no adjustment. The cert-info+QR block above is now
             independently absolutely positioned and no longer shares a
             row with this, so nothing here needs to "make room" for it. -->
        <?php renderCertLogo($certSettings); ?>

        <!-- Trainee photo: still plain, ordinary flow content — this
             wrapper no longer has any position:absolute children (the
             cert-info/QR block moved out to be a sibling of .cert-paper
             above), so a larger margin-top here does NOT reproduce the
             documented page-split bug (that bug was specifically about a
             flow element's margin-top interacting with position:absolute
             SIBLINGS inside the *same* relative parent — there are none
             here now). margin-top:40mm matches the reference PDF's
             measured photo position (74.87mm from page top) for a
             full-height (~20mm) logo. -->
        <div style="margin-top: 40mm;">
            <?php if ($trainee_photo_url): ?>
                <img src="<?= pdfImageSrc($trainee_photo_url, $pdf_mode); ?>" style="width:95px; height:115px; object-fit:cover; border:1px solid #999;">
            <?php else: ?>
                <div style="width:95px; height:115px; border:1px dashed #bbb; display:flex; align-items:center; justify-content:center; font-size:8px; color:#999; text-align:center;">No Photo Uploaded</div>
            <?php endif; ?>
        </div>

        <!-- Middle content: ordinary flow, directly follows the photo row.
             margin-top:10mm matches the reference PDF's measured position
             for "This is to certify that" (115.51mm from page top, ~10mm
             below the photo's measured bottom edge). -->
        <div style="margin-top: 10mm; text-align: center;">
            <?php if (!$training_row): ?>
                <div class="alert alert-warning d-inline-block" style="font-size: 12px;">
                    Training details have not been completed for this job yet.
                    Open it from the register and fill in the Trainee &amp; Course Details section first.
                </div>
            <?php else: ?>
                <p style="font-size: 13px; margin: 4px 0; color: #1a428a; font-weight: bold;">This is to certify that</p>
                <h3 class="fw-bold" style="font-size: 22px; margin: 6px 0;"><?= htmlspecialchars(strtoupper($training_row['Trainee_Name'] ?? 'N/A')); ?></h3>
                <p style="font-size: 12px; margin: 4px 0; color: #1a428a; font-weight: bold;">With Identification No.: <?= htmlspecialchars($training_row['Trainee_ID'] ?: 'N/A'); ?></p>
                <?php if (!empty($training_row['Nationality'])): ?>
                    <p style="font-size: 12px; margin: 4px 0; color: #1a428a; font-weight: bold;">Nationality: <?= htmlspecialchars($training_row['Nationality']); ?></p>
                <?php endif; ?>
                <p style="font-size: 12px; margin: 4px 0; color: #1a428a; font-weight: bold;">And Employed By <?= htmlspecialchars($client_name); ?></p>
                <p style="font-size: 12px; margin: 10px 0 4px; color: #1a428a; font-weight: bold;">Has attended classes and subsequently completed</p>
                <h4 class="fw-bold" style="font-size: 16px; margin: 6px 0;"><?= htmlspecialchars(strtoupper($training_row['Course_Name'] ?: $item_name)); ?></h4>

                <!-- Client requirement (2026-08-20 feedback): the block itself
                     was already horizontally centered via `margin:auto`, but
                     each line of TEXT inside it stayed left-aligned — with
                     lines of different length that produces a ragged left
                     edge that reads as "not centered" at a glance. Changed to
                     text-align:center to match Training_Parameters below it. -->
                <div style="text-align:center; max-width: 480px; margin: 8px auto 0; font-size: 10px; color: #333; line-height: 1.3;">
                    <?= nl2br(htmlspecialchars($training_row['Course_Content'] ?? '')); ?>
                </div>

                <?php if (!empty($training_row['Training_Parameters'])): ?>
                <!-- Client requirement: Training Parameters must be centered
                     on the page (was left-aligned like the topics list). -->
                <div style="text-align:center; max-width: 480px; margin: 8px auto 0;">
                    <div style="font-size: 10px; color: #333; line-height: 1.3;">
                        <?= nl2br(htmlspecialchars($training_row['Training_Parameters'])); ?>
                    </div>
                </div>
                <?php endif; ?>
            <?php endif; ?>
        </div>
        <!-- Legal inspection disclaimer intentionally omitted on Training
             certificates by request — that liability language is written
             for equipment thorough-examination certificates and doesn't
             apply to a training-completion certificate. -->
    </div>

    <!-- Signature block: client requirement is CM-precise fixed placement
         (trainer 3cm from the right, counter 3cm from the left, both at the
         same height, each signature 1cm above its own label, and the whole
         row 1cm above the footer band) — exactly what position:absolute is
         for. This is placed as a direct sibling of the wrapper above (same
         level as the footer band), NOT nested inside a flow container that
         needs a margin to dodge it — that specific combination (flow
         element + big margin + absolute siblings) is what caused the
         page-split bug fixed earlier in this template. A plain, independent
         absolute sibling — like the footer band already does successfully —
         does not have that problem. bottom:32mm = 1cm above the footer
         band's own 22mm reserved height. -->
    <!-- BUG FIX (client report 2026-08-20, "التواقيع بمكان غير مكانها
         الصحيح"): this row used `display:flex; justify-content:space-between`
         to place Counter Signature (left) and Trainer/Assessor (right) —
         exactly the same Dompdf flex bug already found and fixed elsewhere
         in this file (see the Dompdf lessons note near the top): instead
         of spreading the two columns apart, Dompdf collapsed them on top
         of each other, so the trainer's own signature image visually
         rendered under the "Counter Signature" label and the two blocks
         stacked instead of sitting side by side. Converted to a table,
         the one layout primitive Dompdf handles reliably here — same fix
         pattern already applied to the header rows above. -->
    <div style="position:absolute; left:0; right:0; bottom:32mm; padding: 0 30mm; box-sizing:border-box;">
        <table style="width:100%; border-collapse:collapse;" cellpadding="0" cellspacing="0">
            <tr>
                <td style="width:50%; text-align:center; vertical-align:bottom; border:none; padding:0 15mm 0 0;">
                    <div style="border-bottom: 1px solid #333; height: 40px; display:flex; align-items:flex-end; justify-content:center;"><?php renderSignatureImage($counter_signature_path); ?></div>
                    <div style="font-size: 10px; margin-top: 10mm;">Counter Signature</div>
                </td>
                <td style="width:50%; text-align:center; vertical-align:bottom; border:none; padding:0 0 0 15mm;">
                    <div style="border-bottom: 1px solid #333; height: 40px; display:flex; align-items:flex-end; justify-content:center;"><?php renderSignatureImage($inspector_signature_path); ?></div>
                    <div style="font-size: 10px; margin-top: 10mm; font-weight: bold;"><?= htmlspecialchars($inspector); ?></div>
                    <div style="font-size: 9px; color: #666;">Trainer/Assessor</div>
                </td>
            </tr>
        </table>
    </div>
</div>

<?php elseif ($is_appliance): ?>

<!-- ==================== PAGE 1: APPLIANCE / CRANE CERTIFICATE ==================== -->
<div class="cert-paper" style="padding: 0; position: relative;">
    <?php renderCertFooterBand($certFooterTexts, $certSettings); ?>
    <!-- BUG FIX (2026-08-21): this template (uniquely among the certificate
         pages) has its own trailing "Document Footer Codes" table in normal
         document flow, whose vertical position depends on how tall the
         content above it is (equipment description, comments, disclaimer
         text, etc. — all variable per job). renderPageNumber()'s fixed
         `position:absolute; bottom:23mm` sat only ~1mm above where that
         table could legitimately land, so a job with enough content pushed
         the two into direct visual collision (confirmed on a real PDF: the
         "24 02 2014" form-date text and "Page 1 of 2" literally overlapped
         at the same coordinates). Fix: for this page only, don't render the
         page number as a fixed absolute-position element at all — it now
         prints as part of that same footer-codes table row below (see
         "Document Footer Codes"), so its position always tracks the real
         content flow and can never collide with it again. -->
    <!-- Bottom margin reserved to match the footer band's fixed height (max
         22mm), so content never has to know its exact rendered size. -->
    <!-- Auto height (no fixed `height:`) — see the detailed bug-fix note on
         the LLG page 1 wrapper above: an exact-fit fixed height left zero
         tolerance for Dompdf's own box-model rounding and produced a
         near-blank overflow page. Auto height can never self-overflow. -->
    <div style="position: relative; margin: 15mm 15mm 22mm 15mm;">
    <div style="position:absolute; top:0; right:0; text-align:center; z-index:5;">
        <!-- QR code -->
        <?php renderQr($verify_url, 68, 'qr-appliance-p1'); ?>
        <div style="font-size:6.5px; font-weight:bold; margin-top:2px;">SCAN TO VERIFY</div>
        <div style="font-size:6px; color:#555;"><?= htmlspecialchars($job_no); ?></div>
    </div>
    <!-- BUG FIX: the logo used to be position:absolute top:0 left:0, which
         floated ON TOP of the client-info table right below it (that table
         started immediately with no space reserved for the logo's own
         ~20mm height/50mm width) — visually clipping "Client's Name" and
         other left-column labels ("اللوقو يغطي الجدول"). Moved into normal
         document flow (the same proven header pattern already used by the
         LLG and Checklist Annex templates) so it always pushes the title
         and everything after it down, and can never overlap later content.
         min-height:30mm keeps it clear of the QR block on the right too. -->
    <div class="d-flex align-items-center mb-1" style="min-height: 30mm;">
        <?php renderCertLogo($certSettings); ?>
    </div>
    <div class="cert-title mb-1" style="font-size: 14px; letter-spacing: 0.5px; padding: 0 20mm;">
        CERTIFICATE<br>
        <span style="font-size: 12px; font-weight: normal;">OF THOROUGH EXAMINATION OF <?= htmlspecialchars(strtoupper($heading_display)); ?></span>
    </div>

    <!-- Top Master Info Table -->
    <table class="cert-table mb-1">
        <tr>
            <td style="width: 50%; vertical-align: top;">
                <table class="w-100 cert-table-plain">
                    <tr><td style="width: 38%;" class="fw-bold">Client's Name</td><td style="width: 62%;">: <?= htmlspecialchars($client_name); ?></td></tr>
                    <tr><td class="fw-bold">Address</td><td>: <?= htmlspecialchars($client_address); ?></td></tr>
                    <tr><td class="fw-bold">Site Name</td><td>: <?= htmlspecialchars($site_name); ?></td></tr>
                    <tr><td class="fw-bold">Sub Location</td><td>: <?= htmlspecialchars($sub_location); ?></td></tr>
                    <tr><td class="fw-bold">Type Of Inspection</td><td>: <?= htmlspecialchars($inspection_type); ?></td></tr>
                </table>
            </td>
            <td style="width: 50%; vertical-align: top;">
                <table class="w-100 cert-table-plain">
                    <tr><td style="width: 42%;" class="fw-bold">Date of Inspection</td><td style="width: 58%;">: <?= htmlspecialchars($issue_date); ?></td></tr>
                    <tr><td class="fw-bold">Certificate No.</td><td>: <?= htmlspecialchars($job_no); ?></td></tr>
                    <tr><td class="fw-bold">Work Order Ref</td><td>: <?= htmlspecialchars($work_order_ref_display); ?></td></tr>
                    <tr><td class="fw-bold">Reference Standard</td><td>: <?= htmlspecialchars($ref_standard); ?></td></tr>
                    <tr><td class="fw-bold">Inspection Method</td><td>: <?= htmlspecialchars($procedure); ?></td></tr>
                    <tr><td class="fw-bold">&nbsp;</td><td>&nbsp;</td></tr>
                </table>
            </td>
        </tr>
    </table>

    <!-- FIX (admin explicit spec, 2026-08-25): sourced strictly from
         Certificate_Title now ($asset_desc_display, computed above) — no
         fallback to Item Name/item_name_description here anymore. If the
         inspector hasn't set a Certificate Title yet, this prints blank on
         purpose (signals "not set" rather than guessing a value). -->
    <div style="background: #f1f1f1; border: 1px solid #000; border-top: none; padding: 3px 6px; font-weight: bold; font-size: 11.5px;">
        ASSET DESCRIPTION : <?= htmlspecialchars(strtoupper($asset_desc_display)); ?>
    </div>

    <!-- Split Description Boxes -->
    <table class="cert-table mb-1" style="border-top: none !important;">
        <tr>
            <td style="width: 50%; height: 110px; vertical-align: top; white-space: pre-line;"><?= htmlspecialchars($desc_left_text); ?></td>
            <td style="width: 50%; height: 110px; vertical-align: top; white-space: pre-line;"><?= htmlspecialchars($desc_right_text); ?></td>
        </tr>
    </table>

    <!-- Test Parameters Header & Table — hidden entirely for Misc items,
         same reasoning as the form: no load chart applies to them. -->
    <?php if (strcasecmp($category, 'Mis') !== 0 /* FIX: hide Test Parameters on the printed certificate for MIS items too — DB category is 'Mis', not 'Misc' */): ?>
    <div class="text-center fw-bold" style="font-size: 11px; margin-bottom: 2px;">Test Parameters</div>
    <table class="cert-table text-center mb-1">
        <thead>
            <tr style="background-color: #f8f9fa;">
                <th style="width: 10%;">S.No</th>
                <th style="width: 22.5%;"><?= $is_crane_equipment ? 'Boom Length' : 'Test Stage'; ?></th>
                <th style="width: 22.5%;">Radius</th>
                <th style="width: 22.5%;">Tested Load</th>
                <th style="width: 22.5%;">Safe Working Load</th>
            </tr>
        </thead>
        <tbody>
            <?php
            $rows_to_show = 4;
            for ($i = 0; $i < $rows_to_show; $i++):
                $tp = $appliance_test_params[$i] ?? null;
            ?>
            <tr>
                <td class="fw-bold"><?= htmlspecialchars($tp['row_number'] ?? ($i + 1)); ?></td>
                <td><?= htmlspecialchars($tp['boom_length'] ?? ''); ?></td>
                <td><?= htmlspecialchars($tp['radius'] ?? ''); ?></td>
                <td><?= htmlspecialchars($tp['tested_load'] ?? ($i === 0 ? $capacity : '')); ?></td>
                <td class="fw-bold"><?= htmlspecialchars($tp['safe_working_load'] ?? $capacity); ?></td>
            </tr>
            <?php endfor; ?>
        </tbody>
    </table>
    <?php endif; ?>

    <!-- Equipment Used & Details Grid -->
    <table class="cert-table mb-1">
        <tr>
            <td style="width: 50%;"><strong>Equipment Used:</strong> <?= htmlspecialchars($equipment_used); ?></td>
            <td style="width: 50%;"><strong>Colour Code :</strong> <?= htmlspecialchars($colour_code); ?></td>
        </tr>
        <tr>
            <td><strong>Details of Deviations/Omissions from original scope of work:</strong><br><?= htmlspecialchars($deviations); ?></td>
            <td><strong>Details of Sub contracted work:</strong><br><?= htmlspecialchars($subcontracted); ?></td>
        </tr>
        <tr>
            <td colspan="2"><strong>Comments :</strong><br><?= htmlspecialchars($comments); ?></td>
        </tr>
    </table>

    <!-- Validity Notice Box -->
    <div style="border: 1px solid #000; border-top: none; padding: 3px 6px; font-size: 8.5px; text-align: center; background: #fff;" class="fw-bold mb-1">
        VALIDITY OF THIS CERTIFICATE CEASES IN THE EVENT THE EQUIPMENT UNDERGOES A MAJOR REPAIR OR ALTERATION, IN WHICH CASE IT MUST BE RETESTED.
    </div>

    <!-- Previous Inspection & Due Date & Result Bar -->
    <table class="cert-table mb-2">
        <tr>
            <td style="width: 50%;"><strong>Previous Inspection/Test Details:</strong><br><?= htmlspecialchars($prev_details); ?></td>
            <td style="width: 30%;"><strong>Due Date of Next Inspection:</strong><br><?= htmlspecialchars($due_date); ?></td>
            <td style="width: 20%; text-align: center;" class="align-middle">
                <strong>Result:</strong><br>
                <span class="<?= $approval === 'PASS' ? 'badge-pass' : 'badge-fail'; ?>" style="font-size: 13px;">
                    <?= htmlspecialchars($approval); ?>
                </span>
            </td>
        </tr>
    </table>

    <!-- Signature and Footer Box.
         BUG FIX: "Name and Signature of Inspector :" is a legal attestation
         label — the inspector's NAME must read immediately after it on the
         same line (standard certificate convention), with the actual
         signature mark below. Previously the signature image came first and
         the name was appended after it on the same line as the signature —
         visually correct-looking but not the legally expected order. -->
    <table class="cert-table" style="border-bottom: none;">
        <tr>
            <td style="width: 50%; height: 50px; vertical-align: top;">
                <strong>Name and Signature of Inspector :</strong> <?= htmlspecialchars($inspector); ?><br>
                <?php renderSignatureImage($inspector_signature_path); ?>
            </td>
            <td style="width: 50%; height: 50px; vertical-align: top;">
                <strong>Counter Signature :</strong><br>
                <?php renderSignatureImage($counter_signature_path); ?>
            </td>
        </tr>
    </table>
    <!-- BUG FIX (2026-08-21): the legal disclaimer used to render AFTER this
         row as brand-new trailing content (see the long note on
         renderCertDisclaimer() above) — with real, longer disclaimer text
         that pushed the "Document Footer Codes" line down far enough to
         visually collide with the footer band underneath, since the content
         wrapper's bottom margin (22mm) leaves zero spare clearance below the
         footer's own max-height (also 22mm). Fix: render the disclaimer
         INSIDE this already-budgeted empty row instead of appending it as
         new height — this cell's height is a minimum (`height` on a <td> is
         not a hard cap), so longer disclaimer text simply grows the row in
         place rather than adding a brand-new block after it. -->
    <table class="cert-table" style="border-top: none;">
        <tr>
            <td style="height: 35px; border-top: none; padding: 3px 6px; vertical-align: top;">
                <?php renderCertDisclaimer($certSettings, true); ?>
            </td>
        </tr>
    </table>

    <!-- Document Footer Codes.
         BUG FIX (2026-08-21): "Page 1 of 2" now prints here, inside this
         row's own right-hand cell, instead of as a separate
         position:absolute element floating over the page — see the long
         note above where renderPageNumber() used to be called for this
         template. Same visual spot (bottom-right, small grey text), but now
         it's part of the row's real flow so it can never overlap the form
         code/date next to it. -->
    <table class="w-100 mt-1" style="font-size: 9px;">
        <tr>
            <td style="border:none; width: 33.33%;">F/7.5/R64</td>
            <td style="border:none; width: 33.33%; text-align: center;">Issue : 02</td>
            <td style="border:none; width: 33.33%; text-align: right;">
                24 02 2014
                <span style="font-size: 7.5px; color:#666; margin-left: 8px;">Page 1 of 2</span>
            </td>
        </tr>
    </table>
    </div>
</div>

<!-- ==================== PAGE 2: APPLIANCE CHECKLIST ANNEX ==================== -->
<div class="cert-paper page-break" style="padding: 0; position: relative;">
    <?php renderCertFooterBand($certFooterTexts, $certSettings); ?>
    <?php renderPageNumber(2, 2); ?>
    <!-- Auto height (no fixed `height:`) — see the detailed bug-fix note on
         the LLG page 1 wrapper above: an exact-fit fixed height left zero
         tolerance for Dompdf's own box-model rounding and produced a
         near-blank overflow page. Auto height can never self-overflow. -->
    <div style="position: relative; margin: 15mm 15mm 22mm 15mm;">
    <div style="position:absolute; top:0; right:0; text-align:center; z-index:5;">
        <!-- QR code page 2 -->
        <?php renderQr($verify_url, 50, 'qr-misc-p2'); ?>
        <div style="font-size:6px; font-weight:bold; margin-top:2px;">SCAN TO VERIFY</div>
    </div>
    <div>
        <!-- BUG FIX: converted from a `d-flex justify-content-between`
             3-column row to a table — see the identical fix (and full
             explanation) on the Training certificate header above.
             Dompdf's justify-content:space-between is unreliable and was
             confirmed (by the client, via screenshot) to collapse flex
             items together instead of spreading them across the row. -->
        <table style="width:100%; border-collapse:collapse;" cellpadding="0" cellspacing="0" class="border-bottom pb-2 mb-2">
            <tr>
                <!-- BUG FIX: this label column used to be 35% wide, centered —
                     wide enough that its centered text reached the exact same
                     top-right corner the position:absolute QR block (above,
                     top:0;right:0) occupies, so the two visibly overlapped
                     (confirmed by the client from a real downloaded PDF).
                     Narrowed to 20% and added an empty 15% buffer column —
                     the SAME proven pattern already used for this exact QR-
                     clearance problem on this template's own page 1 header
                     (see the "buffer zone for the QR block" comment above). -->
                <td style="width:65%; vertical-align:middle; border:none; padding:0;">
                    <?php renderCertLogo($certSettings); ?>
                </td>
                <td style="width:20%; vertical-align:middle; border:none; padding:0; text-align:center;">
                    <small class="d-block fw-bold" style="font-size: 10px;">INSPECTION CHECKLIST ANNEX</small>
                    <small class="d-block text-muted" style="font-size: 8.5px;">Standard Verification Record</small>
                </td>
                <td style="width:15%; border:none; padding:0;"></td>
            </tr>
        </table>

        <table class="cert-table mb-3">
            <tr>
                <td style="width: 20%;" class="fw-bold">Certificate No.</td>
                <td style="width: 30%;"><?= htmlspecialchars($job_no); ?></td>
                <td style="width: 20%;" class="fw-bold">Issue Date</td>
                <td style="width: 30%;"><?= htmlspecialchars($issue_date); ?></td>
            </tr>
            <tr>
                <!-- FIX (docx item, 2026-08-31): Work Order Ref restored below per client request (previously removed 2026-08-25). Client\u2019s Name + Certificate Title kept as-is. -->
                <td class="fw-bold">Client's Name</td>
                <td><?= htmlspecialchars($client_name); ?></td>
                <td class="fw-bold">Certificate Title</td>
                <?php // FIX (admin explicit spec, 2026-08-25): this row is
                      // literally labeled "Certificate Title" — shows the
                      // same $asset_desc_display (raw Certificate_Title
                      // column) as the page-1 Asset Description bar, for
                      // consistency; no fallback to Item Name here either. ?>
                <td><?= htmlspecialchars($asset_desc_display); ?></td>
            </tr>
                <tr>
                    <td class="fw-bold">Work Order Ref</td>
                    <td colspan="3"><?= htmlspecialchars($work_order_ref_display); ?></td>
                </tr>
        </table>

        <div class="cert-title mb-3">
            THOROUGH EXAMINATION CHECKLIST RECORD
        </div>

        <table class="cert-table mb-3 align-middle">
            <thead>
                <tr style="background-color: #f8f9fa;" class="text-center">
                    <th style="width: 8%;">Item #</th>
                    <th style="width: 57%; text-align: left;">Inspection Criteria / Check Item</th>
                    <th style="width: 15%;">Result</th>
                    <th style="width: 20%;">Remarks / Notes</th>
                </tr>
            </thead>
            <tbody>
                <?php if (!empty($checklist_items)): ?>
                    <?php $i = 1; foreach ($checklist_items as $chk):
                        $res = strtoupper($chk['Result'] ?? 'PASS');
                    ?>
                        <tr>
                            <td class="text-center fw-bold"><?= $i++; ?></td>
                            <td><?= htmlspecialchars($chk['CheckItem']); ?></td>
                            <td class="text-center fw-bold <?= $res === 'PASS' ? 'text-success' : ($res === 'FAIL' ? 'text-danger' : 'text-warning'); ?>">
                                <?= htmlspecialchars($chk['Result'] ?? 'Pass'); ?>
                            </td>
                            <td><?= htmlspecialchars($chk['Remarks'] ?? '-'); ?></td>
                        </tr>
                    <?php endforeach; ?>
                <?php else: ?>
                    <tr>
                        <td colspan="4" class="text-center text-muted py-3">No specific checklist items recorded for this selected item.</td>
                    </tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>

    <div>
        <table class="cert-table mb-2">
            <tr>
                <td style="height: 45px;" class="align-bottom">
                    <strong>Inspector Name:</strong> <?= htmlspecialchars($inspector); ?>
                </td>
                <td style="height: 45px;" class="align-bottom text-end">
                    <strong>Signature:</strong> _____________________
                </td>
            </tr>
        </table>

        <div style="font-size: 8px; line-height: 1.8;" class="text-muted text-justify">
            This checklist annex forms an integral part of Certificate No. <?= htmlspecialchars($job_no); ?>. All inspection check points have been verified in accordance with approved quality management and international standards.
        </div>
    </div>
    </div>
</div>

<?php else: ?>
<!-- ==================== STANDARD LLG CERTIFICATE TEMPLATE ====================
     Redesigned to extend the full page (matching the Appliance template's
     layout philosophy) and now includes the same 6 certificate note fields.
     "Category" is intentionally removed from the visible certificate per
     request — the client-facing document shows Job No instead, which is
     what actually matters to them; Category remains an internal-only field
     used for routing/reporting. -->
<div class="cert-paper" style="padding: 0; position: relative;">
    <?php renderCertFooterBand($certFooterTexts, $certSettings); ?>
    <?php renderPageNumber(1, 2); ?>
    <!-- Bottom margin reserved to match the footer band's fixed height (max
         22mm), so content never has to know its exact rendered size. -->
    <!-- BUG FIX: this wrapper used to have a hard-coded `height: 260mm`
         (chosen to exactly fill one A4 page: 15mm + 260mm + 22mm = 297mm)
         plus `display:flex; flex-direction:column` with a `flex-grow:1`
         spacer below to push the signature block to the bottom. Dompdf's
         own box-model rounding (line-height/table-row metrics) has zero
         tolerance in an exact-fit fixed height — a few extra rendered
         pixels overflow into a second, almost-entirely-blank page, which is
         exactly the "توجد صفحة فارغة" bug reported on the Appliance annex.
         Auto height (no `height:` at all) can never self-overflow like
         this — the box is simply as tall as its content, whatever that is.
         The flex-grow spacer is removed too (same Dompdf flex reliability
         issue documented on the Training template above); the signature
         block below now just follows in normal flow with a fixed margin. -->
    <div style="position: relative; margin: 15mm 15mm 22mm 15mm;">
    <div style="position:absolute; top:0; right:0; text-align:center; z-index:5;">
        <!-- QR code -->
        <?php renderQr($verify_url, 68, 'qr-llg'); ?>
        <div style="font-size:6.5px; font-weight:bold; margin-top:2px;">SCAN TO VERIFY</div>
        <div style="font-size:6px; color:#555;"><?= htmlspecialchars($job_no); ?></div>
    </div>
    <!-- BUG FIX: converted from `d-flex justify-content-between` to a
         table — see the identical fix (with full explanation) on the
         Training certificate header. Dompdf's justify-content:space-between
         is unreliable and was confirmed (client screenshot) to collapse
         flex items together instead of spreading them across the row.
         min-height:22mm (kept on this wrapper div) reserves clearance
         below the absolutely-positioned QR block above. -->
    <div class="border-bottom pb-2 mb-2" style="min-height: 22mm;">
        <table style="width:100%; border-collapse:collapse;" cellpadding="0" cellspacing="0">
            <tr>
                <!-- Logo spans 65% — the remaining 35% is a buffer zone for the
                     position:absolute QR block (right:0) above to avoid overlap. -->
                <td style="width:65%; vertical-align:middle; border:none; padding:0;"><?php renderCertLogo($certSettings); ?></td>
                <td style="width:35%; border:none; padding:0;"></td>
            </tr>
        </table>
    </div>
    <!-- Defensive right/left padding: keeps long item-name titles from
         visually running under the absolutely-positioned QR box above. -->
    <div class="cert-title" style="padding: 0 18mm;">CERTIFICATE OF THOROUGH EXAMINATION OF <?= htmlspecialchars(strtoupper($item_name)); ?></div>

    <!-- BUG FIX: the LLG certificate was missing the client's Address
         entirely (present on the Appliance/Misc template, absent here) —
         added directly below Client's Name, matching standard certificate
         convention (name immediately followed by address). -->
    <table class="cert-table mb-2">
        <tr><td class="fw-bold" style="width:20%;">Client's Name</td><td style="width:30%;"><?= htmlspecialchars($client_name); ?></td><td class="fw-bold" style="width:20%;">Date</td><td style="width:30%;"><?= htmlspecialchars($issue_date); ?></td></tr>
        <tr><td class="fw-bold">Address</td><td colspan="3"><?= htmlspecialchars($client_address); ?></td></tr>
        <tr><td class="fw-bold">Site Name</td><td><?= htmlspecialchars($site_name); ?></td><td class="fw-bold">Sub Location</td><td><?= htmlspecialchars($sub_location); ?></td></tr>
        <tr><td class="fw-bold">Type Of Inspection</td><td colspan="3"><?= htmlspecialchars($inspection_type); ?></td></tr>
        <tr><td class="fw-bold">Certificate No.</td><td><?= htmlspecialchars($job_no); ?></td><td class="fw-bold">Work Order Ref</td><td><?= htmlspecialchars($work_order_ref_display); ?></td></tr>
        <tr><td class="fw-bold">Standard</td><td><?= htmlspecialchars($ref_standard); ?></td><td class="fw-bold">Due Date</td><td><?= htmlspecialchars($due_date); ?></td></tr>
        <tr><td class="fw-bold">Inspection Method</td><td colspan="3"><?= htmlspecialchars($procedure); ?></td></tr>
    </table>

    <div style="background: #f1f1f1; border: 1px solid #000; border-top: none; padding: 3px 6px; font-weight: bold; font-size: 11.5px; margin-top: 14px;">
        ASSET DESCRIPTION : <?= htmlspecialchars($item_name); ?>
    </div>

    <table class="cert-table text-center mb-2" style="border-top: none !important;">
        <tr class="bg-light"><th>Serial No</th><th>Description</th><th>Quantity</th><th>SWL</th><th>Proof Load</th><th>Result</th></tr>
        <tr>
            <td><?= formatSerialNumbersVertical((string) $raw_serial_no); ?></td>
            <td class="text-start" style="vertical-align: top; min-height: 190px; height: 190px; line-height: 1.35; padding-top: 6px;"><?= nl2br(htmlspecialchars($llg_full_description)); ?></td>
            <!-- BUG FIX: this was a hardcoded literal "1" regardless of how
                 many serial numbers were actually listed in Serial No — an
                 item with 17 comma-separated serials still showed
                 Quantity: 1 ("لازم يكون الكمية 17 قطعة" — client report).
                 countSerialNumbers() reuses the exact same comma-split
                 logic as the Serial No column above, so the two can never
                 disagree. -->
            <td><?= countSerialNumbers((string) $raw_serial_no); ?></td>
            <td><?= htmlspecialchars($capacity ?: 'N/A'); ?></td>
            <td><?= htmlspecialchars($llg_row['Proof_Load'] ?? 'N/A'); ?></td>
            <td class="fw-bold <?= $approval === 'PASS' ? 'badge-pass' : 'badge-fail'; ?>"><?= $approval; ?></td>
        </tr>
    </table>

    <!-- Certificate Notes — now real, DB-backed fields (see tbl_llg_inspections) -->
    <table class="cert-table mb-1">
        <tr>
            <td style="width: 50%;"><strong>Equipment Used:</strong> <?= htmlspecialchars($equipment_used); ?></td>
            <td style="width: 50%;"><strong>Colour Code :</strong> <?= htmlspecialchars($colour_code); ?></td>
        </tr>
        <tr>
            <td><strong>Details of Deviations/Omissions from original scope of work:</strong><br><?= htmlspecialchars($deviations); ?></td>
            <td><strong>Details of Sub contracted work:</strong><br><?= htmlspecialchars($subcontracted); ?></td>
        </tr>
        <tr>
            <td colspan="2"><strong>Comments :</strong><br><?= htmlspecialchars($comments); ?></td>
        </tr>
    </table>

    <table class="cert-table mb-2">
        <tr>
            <td style="width: 70%;"><strong>Previous Inspection/Test Details:</strong><br><?= htmlspecialchars($prev_details); ?></td>
            <td style="width: 30%; text-align: center;" class="align-middle">
                <strong>Result:</strong><br>
                <span class="<?= $approval === 'PASS' ? 'badge-pass' : 'badge-fail'; ?>" style="font-size: 13px;">
                    <?= htmlspecialchars($approval); ?>
                </span>
            </td>
        </tr>
    </table>

    <!-- BUG FIX: inspector's name now reads immediately after the label on
         the same line (legal attestation convention), signature below it —
         see matching note on the Appliance/Misc template. -->
    <table class="cert-table" style="border-bottom: none; margin-top: 10mm;">
        <tr>
            <td style="width: 50%; height: 50px; vertical-align: top;">
                <strong>Name and Signature of Inspector :</strong> <?= htmlspecialchars($inspector); ?><br>
                <?php renderSignatureImage($inspector_signature_path); ?>
            </td>
            <td style="width: 50%; height: 50px; vertical-align: top;">
                <strong>Counter Signature :</strong><br>
                <?php renderSignatureImage($counter_signature_path); ?>
            </td>
        </tr>
    </table>
    <?php renderCertDisclaimer($certSettings); ?>
    </div>
</div>

<!-- ==================== PAGE 2: LLG CHECKLIST ANNEX ====================
     BUG FIX: the checklist answers were always being fetched correctly
     ($checklist_items, populated further up) but the LLG template never
     actually had a page to display them on — only Appliance did. Same
     layout/pattern as the Appliance annex, just under the LLG branch too. -->
<div class="cert-paper page-break" style="padding: 0; position: relative;">
    <?php renderCertFooterBand($certFooterTexts, $certSettings); ?>
    <?php renderPageNumber(2, 2); ?>
    <!-- Auto height (no fixed `height:`) — see the detailed bug-fix note on
         the LLG page 1 wrapper above: an exact-fit fixed height left zero
         tolerance for Dompdf's own box-model rounding and produced a
         near-blank overflow page. Auto height can never self-overflow. -->
    <div style="position: relative; margin: 15mm 15mm 22mm 15mm;">
    <div style="position:absolute; top:0; right:0; text-align:center; z-index:5;">
        <!-- QR code page 2 -->
        <?php renderQr($verify_url, 50, 'qr-misc-p2'); ?>
        <div style="font-size:6px; font-weight:bold; margin-top:2px;">SCAN TO VERIFY</div>
    </div>
    <div>
        <!-- BUG FIX: converted from `d-flex justify-content-between` to a
             table — see the identical fix (full explanation) on the
             Training certificate header above. -->
        <table style="width:100%; border-collapse:collapse;" cellpadding="0" cellspacing="0" class="border-bottom pb-2 mb-2">
            <tr>
                <!-- BUG FIX: this label column used to be 35% wide, centered —
                     wide enough that its centered text reached the exact same
                     top-right corner the position:absolute QR block (above,
                     top:0;right:0) occupies, so the two visibly overlapped
                     (confirmed by the client from a real downloaded PDF).
                     Narrowed to 20% and added an empty 15% buffer column —
                     the SAME proven pattern already used for this exact QR-
                     clearance problem on this template's own page 1 header
                     (see the "buffer zone for the QR block" comment above). -->
                <td style="width:65%; vertical-align:middle; border:none; padding:0;">
                    <?php renderCertLogo($certSettings); ?>
                </td>
                <td style="width:20%; vertical-align:middle; border:none; padding:0; text-align:center;">
                    <small class="d-block fw-bold" style="font-size: 10px;">INSPECTION CHECKLIST ANNEX</small>
                    <small class="d-block text-muted" style="font-size: 8.5px;">Standard Verification Record</small>
                </td>
                <td style="width:15%; border:none; padding:0;"></td>
            </tr>
        </table>

        <table class="cert-table mb-3">
            <tr>
                <td style="width: 20%;" class="fw-bold">Certificate No.</td>
                <td style="width: 30%;"><?= htmlspecialchars($job_no); ?></td>
                <td style="width: 20%;" class="fw-bold">Work Order Ref</td>
                <td style="width: 30%;"><?= htmlspecialchars($work_order_ref_display); ?></td>
            </tr>
            <tr>
                <td class="fw-bold">Issue Date</td>
                <td><?= htmlspecialchars($issue_date); ?></td>
                <td class="fw-bold">Client / Item</td>
                <td><?= htmlspecialchars($client_name); ?> / <?= htmlspecialchars($item_name); ?></td>
            </tr>
        </table>

        <div class="cert-title mb-3">
            THOROUGH EXAMINATION CHECKLIST RECORD
        </div>

        <table class="cert-table mb-3 align-middle">
            <thead>
                <tr style="background-color: #f8f9fa;" class="text-center">
                    <th style="width: 8%;">Item #</th>
                    <th style="width: 57%; text-align: left;">Inspection Criteria / Check Item</th>
                    <th style="width: 15%;">Result</th>
                    <th style="width: 20%;">Remarks / Notes</th>
                </tr>
            </thead>
            <tbody>
                <?php if (!empty($checklist_items)): ?>
                    <?php $i = 1; foreach ($checklist_items as $chk):
                        $res = strtoupper($chk['Result'] ?? 'PASS');
                    ?>
                        <tr>
                            <td class="text-center fw-bold"><?= $i++; ?></td>
                            <td><?= htmlspecialchars($chk['CheckItem']); ?></td>
                            <td class="text-center fw-bold <?= $res === 'PASS' ? 'text-success' : ($res === 'FAIL' ? 'text-danger' : 'text-warning'); ?>">
                                <?= htmlspecialchars($chk['Result'] ?? 'Pass'); ?>
                            </td>
                            <td><?= htmlspecialchars($chk['Remarks'] ?? '-'); ?></td>
                        </tr>
                    <?php endforeach; ?>
                <?php else: ?>
                    <tr>
                        <td colspan="4" class="text-center text-muted py-3">No checklist items recorded for this job.</td>
                    </tr>
                <?php endif; ?>
            </tbody>
        </table>
    </div>

    <div>
        <table class="cert-table mb-2">
            <tr>
                <td style="height: 45px;" class="align-bottom">
                    <strong>Inspector Name:</strong> <?= htmlspecialchars($inspector); ?>
                </td>
                <td style="height: 45px;" class="align-bottom text-end">
                    <strong>Signature:</strong> _____________________
                </td>
            </tr>
        </table>
    </div>
    </div>
</div>
<?php endif; ?>

<?php if (!empty($inspection_photos) && !$is_training): ?>
<!-- ==================== INSPECTION PHOTOS ANNEX ====================
     Shown for Appliance, LLG, Misc, and NDT. NOT shown for Training: the
     portrait photo already appears directly on the certificate itself
     (top-left box), so a separate gallery page repeating that same one
     photo is redundant and was explicitly not wanted. -->
<div class="cert-paper page-break">
    <!-- BUG FIX: converted from `d-flex justify-content-between` to a
         table — see the identical fix (full explanation) on the Training
         certificate header above. -->
    <table style="width:100%; border-collapse:collapse;" cellpadding="0" cellspacing="0" class="border-bottom pb-2 mb-2">
        <tr>
            <td style="width:50%; vertical-align:middle; border:none; padding:0;"><?php renderCertLogo($certSettings); ?></td>
            <td style="width:50%; vertical-align:middle; border:none; padding:0; text-align:right;"><strong style="font-size: 11px;">Inspection Photos</strong></td>
        </tr>
    </table>
    <div class="cert-title" style="font-size:13px;">PHOTOGRAPHIC RECORD — CERTIFICATE NO. <?= htmlspecialchars($job_no); ?></div>

    <div style="display:flex; flex-wrap:wrap; gap:8px; margin-top:8px;">
        <?php foreach ($inspection_photos as $photo): ?>
            <?php
                $photoType    = htmlspecialchars($photo['Photo_Type'] ?: 'Overview');
                $photoCaption = htmlspecialchars($photo['Caption'] ?? '');
            ?>
            <div style="width:31%; border:1px solid #ccc; padding:4px; box-sizing:border-box; font-size:8px;">
                <img src="<?= pdfImageSrc($photo['Photo_Path'], $pdf_mode); ?>"
                     alt="<?= $photoType; ?>"
                     style="width:100%; height:120px; object-fit:cover; display:block;">
                <div class="fw-bold mt-1"><?= $photoType; ?></div>
                <?php if ($photoCaption !== ''): ?>
                    <div class="text-muted"><?= $photoCaption; ?></div>
                <?php endif; ?>
            </div>
        <?php endforeach; ?>
    </div>
</div>
<?php endif; ?>

<?php if (!$pdf_mode && !$qr_server_side): ?>
<!-- QR code client-side fallback: renders into placeholder divs emitted by
     renderQr() when vendor/autoload.php is absent or endroid/qr-code is not
     installed. Each div carries data-qr-url and data-qr-size attributes so
     this script can pick them up without any hardcoded IDs. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
<script>
(function () {
    // Find every placeholder div that renderQr() emitted.
    var containers = document.querySelectorAll('[data-qr-url]');
    containers.forEach(function (el) {
        var url  = el.getAttribute('data-qr-url');
        var size = parseInt(el.getAttribute('data-qr-size'), 10) || 60;
        if (!url) return;
        try {
            new QRCode(el, {
                text:          url,
                width:         size,
                height:        size,
                correctLevel:  QRCode.CorrectLevel.M
            });
        } catch (e) {
            // If qrcodejs fails for any reason, leave the div empty rather than crashing.
        }
    });
})();
</script>
<?php endif; ?>

</body>
</html>
