<?php
// ═══════════════════════════════════════════════════════════════
//  NTPPA CMS — Admin (ExpertAims-branded). Single-file dispatcher.
// ═══════════════════════════════════════════════════════════════
require __DIR__ . '/../app/bootstrap.php';
security_headers(true);   // admin responses: frame-deny + CSP (see app/helpers.php)

define('CMS_BUILD', 'v1.0');   // shown in Dashboard → System status + admin footer
// Deploy date: stamped once (first admin load) into settings['deploy_date'], shown in the footer.
function cms_deploy_date(){
  try {
    $d = trim((string) setting('deploy_date', ''));
    if ($d === '') {
      $d = date('Y-m-d');
      db()->prepare("INSERT INTO settings(skey,svalue) VALUES('deploy_date',?) ON DUPLICATE KEY UPDATE svalue=svalue")->execute([$d]);
    }
    return $d;
  } catch (\Throwable $e) { return ''; }
}

function sel($a, $b){ return (string)$a === (string)$b ? ' selected' : ''; }
function chk($c){ return $c ? ' checked' : ''; }

// In-place schema upgrades for live sites (idempotent — attempted once per session).
function ensure_schema(){
  if (!empty($_SESSION['schema_v11'])) return;
  try { db()->exec('ALTER TABLE users ADD COLUMN avatar VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE pages ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE submissions ADD COLUMN is_read TINYINT(1) NOT NULL DEFAULT 0'); } catch (\Throwable $e) { /* already there */ }
  // v5 (2026-07-09c): custom-page menu placement, spam flag, subscriber bell
  try { db()->exec("ALTER TABLE pages ADD COLUMN nav_label VARCHAR(120) DEFAULT NULL"); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec("ALTER TABLE pages ADD COLUMN menu_parent VARCHAR(20) DEFAULT NULL"); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE submissions ADD COLUMN is_spam TINYINT(1) NOT NULL DEFAULT 0'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE subscribers ADD COLUMN is_read TINYINT(1) NOT NULL DEFAULT 0'); } catch (\Throwable $e) { /* already there */ }
  // v6 (2026-07-10): flag spammy newsletter signups instead of dropping them
  try { db()->exec('ALTER TABLE subscribers ADD COLUMN is_spam TINYINT(1) NOT NULL DEFAULT 0'); } catch (\Throwable $e) { /* already there */ }
  // v5 (2026-07-12): per-post/per-page external "Source" link + multi-image hero
  try { db()->exec('ALTER TABLE posts ADD COLUMN source_url VARCHAR(500) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE posts ADD COLUMN source_label VARCHAR(160) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE pages ADD COLUMN source_url VARCHAR(500) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE pages ADD COLUMN source_label VARCHAR(160) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE pages ADD COLUMN hero_images TEXT DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  // v5 (2026-07-12): optional editable quote line on a page banner (with its own toggle)
  try { db()->exec('ALTER TABLE pages ADD COLUMN hero_quote VARCHAR(400) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE pages ADD COLUMN hero_quote_show TINYINT(1) NOT NULL DEFAULT 0'); } catch (\Throwable $e) { /* already there */ }
  // v1.0 (2026-07-13): editable homepage-hero CTA buttons (JSON list)
  try { db()->exec('ALTER TABLE pages ADD COLUMN hero_ctas TEXT DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  // v1.7 (2026-07-17): per-press-item image display — 'logo' (contain on a framed
  // light tile, the default) or 'photo' (cover, fills the frame edge-to-edge).
  try { db()->exec("ALTER TABLE pages ADD COLUMN media_fit VARCHAR(10) NOT NULL DEFAULT 'logo'"); } catch (\Throwable $e) { /* already there */ }
  // First run on a client install: seed its feature set from the theme's defaults.
  try {
    if ((string) setting('features', '') === '' && function_exists('theme_default_features')) {
      db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=svalue')
          ->execute(['features', json_encode(theme_default_features())]);
    }
  } catch (\Throwable $e) { /* settings table not ready yet */ }
  form_throttle_table();    // public-form rate limiting (self-creating)
  try { db()->exec(visits_table_ddl()); } catch (\Throwable $e) { /* no CREATE privilege — tracking stays off */ }
  try { db()->exec('CREATE TABLE IF NOT EXISTS subscribers (
    id INT AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(190) NOT NULL UNIQUE,
    page VARCHAR(160) DEFAULT NULL,
    is_read TINYINT(1) NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); } catch (\Throwable $e) { /* newsletter signups stay off */ }
  try { db()->exec('CREATE TABLE IF NOT EXISTS revisions (
    id INT AUTO_INCREMENT PRIMARY KEY,
    kind VARCHAR(10) NOT NULL,
    ref_id INT NOT NULL,
    snapshot MEDIUMTEXT,
    user_name VARCHAR(120) DEFAULT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    KEY idx_rev (kind, ref_id, id)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); } catch (\Throwable $e) { /* history stays off */ }
  // v1.1 (2026-07-14): career job APPLICATIONS inbox (built-in apply form).
  try { db()->exec('CREATE TABLE IF NOT EXISTS applications (
    id INT AUTO_INCREMENT PRIMARY KEY,
    job_title VARCHAR(200) DEFAULT NULL,
    first_name VARCHAR(80) DEFAULT NULL,
    last_name VARCHAR(80) DEFAULT NULL,
    phone VARCHAR(60) DEFAULT NULL,
    email VARCHAR(190) DEFAULT NULL,
    location VARCHAR(160) DEFAULT NULL,
    relocate VARCHAR(10) DEFAULT NULL,
    message TEXT DEFAULT NULL,
    video_url VARCHAR(300) DEFAULT NULL,
    page VARCHAR(160) DEFAULT NULL,
    is_spam TINYINT(1) NOT NULL DEFAULT 0,
    is_read TINYINT(1) NOT NULL DEFAULT 0,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    file1_name VARCHAR(255) DEFAULT NULL,
    file1_orig VARCHAR(255) DEFAULT NULL,
    file2_name VARCHAR(255) DEFAULT NULL,
    file2_orig VARCHAR(255) DEFAULT NULL,
    file3_name VARCHAR(255) DEFAULT NULL,
    file3_orig VARCHAR(255) DEFAULT NULL,
    KEY idx_app (is_read, is_spam, id)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4'); } catch (\Throwable $e) { /* applications stay off until the table exists */ }
  // v1.2 (2026-07-15): secure file attachments on the built-in apply form —
  // stored filename + original label per slot (Resume / Teaching philosophy /
  // Teaching-or-demo document). Files live under uploads/applications/ (web-denied).
  try { db()->exec('ALTER TABLE applications ADD COLUMN file1_name VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE applications ADD COLUMN file1_orig VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE applications ADD COLUMN file2_name VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE applications ADD COLUMN file2_orig VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE applications ADD COLUMN file3_name VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  try { db()->exec('ALTER TABLE applications ADD COLUMN file3_orig VARCHAR(255) DEFAULT NULL'); } catch (\Throwable $e) { /* already there */ }
  application_uploads_dir();   // ensure uploads/applications/ + its web-deny .htaccess exist
  login_attempts_table();   // creates the login rate-limit table if missing
  $_SESSION['schema_v11'] = 1;
}

// uploads/applications/ holds job-application attachments (resumes = PII). It must
// NOT be web-reachable: a .htaccess denies all direct access + script execution,
// so files are downloadable only via the authenticated ?p=application_file route.
// Self-healing: recreates the folder + guard file whenever the admin loads.
function application_uploads_dir(){
  $dir = dirname(__DIR__) . '/uploads/applications';
  if (!is_dir($dir)) @mkdir($dir, 0755, true);
  $ht = $dir . '/.htaccess';
  if (is_dir($dir) && !is_file($ht)) {
    @file_put_contents($ht,
      "# Job-application attachments (resumes = PII). NEVER web-accessible.\n"
      . "# Downloads happen only through the authenticated admin route\n"
      . "# (admin/?p=application_file). Deny every direct request + block scripts.\n"
      . "Options -Indexes -ExecCGI\n"
      . "<IfModule mod_authz_core.c>\n  Require all denied\n</IfModule>\n"
      . "<IfModule !mod_authz_core.c>\n  Order allow,deny\n  Deny from all\n</IfModule>\n"
      . "<IfModule mod_php.c>\n  php_flag engine off\n</IfModule>\n"
      . "RemoveHandler .php .phtml .php3 .php4 .php5 .php7 .php8 .phar\n"
      . "RemoveType   .php .phtml .php3 .php4 .php5 .php7 .php8 .phar\n");
  }
  return $dir;
}

// ─── Multi-image hero banner helpers ───
// A page's banner can hold several images that rotate as a slideshow, and each
// image may carry its OWN eyebrow / heading / sub-text (blank ⇒ the page's
// hero_* fields are used). Storage: hero_image = the primary/first image
// (previews, social sharing, LCP preload, full backward-compat); hero_images =
// a JSON list of {img,eyebrow,heading,subtext} once there are 2+ images or any
// per-slide text — otherwise it stays empty so a single banner behaves exactly
// as before. Both save paths (Banner tab + Banners manager) funnel through here.
function hero_fields_from_post(){
  if (isset($_POST['hero_images']) && is_array($_POST['hero_images'])) {
    $imgs = $_POST['hero_images'];
    $eyes = (array) ($_POST['hero_eyebrow_s'] ?? []);
    $hds  = (array) ($_POST['hero_heading_s'] ?? []);
    $subs = (array) ($_POST['hero_subtext_s'] ?? []);
    $list = []; $hasText = false;
    foreach ($imgs as $k => $v) {
      $img = trim((string) $v);
      if ($img === '') continue;
      $eye = mb_substr(trim((string) ($eyes[$k] ?? '')), 0, 200);
      $hd  = mb_substr(trim((string) ($hds[$k]  ?? '')), 0, 255);
      $sub = mb_substr(trim((string) ($subs[$k] ?? '')), 0, 500);
      if ($eye !== '' || $hd !== '' || $sub !== '') $hasText = true;
      $list[] = ['img' => $img, 'eyebrow' => $eye, 'heading' => $hd, 'subtext' => $sub];
    }
    $first  = $list[0]['img'] ?? '';
    // 2+ images OR any per-slide text ⇒ store JSON; a lone plain image stays "static".
    $joined = (count($list) > 1 || $hasText)
      ? (string) json_encode(array_values($list), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
      : '';
    return [$first, $joined];
  }
  // Legacy single-field forms (faculty "Profile photo") — no slideshow.
  return [trim((string) ($_POST['hero_image'] ?? '')), ''];
}

// Homepage-hero CTA buttons → JSON for pages.hero_ctas. Paired inputs
// hero_cta_text[] / hero_cta_url[] (only rows with text are kept). Returns NULL
// when the form has no CTA fields (every other page), so those pages are left
// untouched and fall back to the two brand-standard buttons at render time.
function hero_ctas_from_post(){
  if (!isset($_POST['hero_cta_text']) || !is_array($_POST['hero_cta_text'])) return null;
  $urls = (array) ($_POST['hero_cta_url'] ?? []);
  $out = [];
  foreach ($_POST['hero_cta_text'] as $k => $t) {
    $t = mb_substr(trim((string) $t), 0, 60);
    if ($t === '') continue;
    $u = mb_substr(trim((string) ($urls[$k] ?? '')), 0, 255);
    $out[] = ['text' => $t, 'url' => $u];
  }
  return $out ? (string) json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : null;
}

// One row of the banner-images repeater: an image line (badge, thumb, path input
// + media picker, reorder + remove) plus an OPTIONAL per-slide text line
// (eyebrow / heading / sub-text). $slide = ['img','eyebrow','heading','subtext'].
function hero_img_row_html($slide, $first = false){
  $img = (string) ($slide['img'] ?? '');
  $thumb = $img !== '' ? ' src="' . e(media_url($img)) . '"' : ' style="visibility:hidden"';
  return '<div class="hero-img-row">'
    . '<div class="hero-img-main">'
    . '<span class="hero-img-badge' . ($first ? ' is-first' : '') . '">' . ($first ? 'Main' : '') . '</span>'
    . '<span class="hero-img-thumb"><img' . $thumb . ' alt="" loading="lazy" onerror="this.style.visibility=\'hidden\'"></span>'
    . '<input class="inp hero-img-inp" name="hero_images[]" value="' . e($img) . '" placeholder="choose or paste an image path">'
    . '<button type="button" class="btn btn-ghost btn-sm cms-pick" title="Choose from Media Library"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg></button>'
    . '<button type="button" class="btn btn-ghost btn-sm hero-img-up" title="Move up" aria-label="Move up"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 19V5M5 12l7-7 7 7"/></svg></button>'
    . '<button type="button" class="btn btn-ghost btn-sm hero-img-down" title="Move down" aria-label="Move down"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12l7 7 7-7"/></svg></button>'
    . '<button type="button" class="btn btn-ghost btn-sm hero-img-del" title="Remove image" aria-label="Remove image"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M6 6 18 18M18 6 6 18"/></svg></button>'
    . '</div>'
    . '<div class="hero-img-text">'
    . '<input class="inp" name="hero_eyebrow_s[]" value="' . e((string) ($slide['eyebrow'] ?? '')) . '" placeholder="Eyebrow — optional, overrides the page" maxlength="200">'
    . '<input class="inp" name="hero_heading_s[]" value="' . e((string) ($slide['heading'] ?? '')) . '" placeholder="Heading — optional" maxlength="255">'
    . '<input class="inp" name="hero_subtext_s[]" value="' . e((string) ($slide['subtext'] ?? '')) . '" placeholder="Sub-text — optional" maxlength="500">'
    . '</div>'
    . '</div>';
}

// The full repeater for a page's banner images (used on the Banner tab and the
// Banners manager). $compact hides the help text on the dense Banners grid.
function hero_images_field_html($pg, $compact = false){
  $slides = hero_slides($pg);
  if (!$slides) $slides = [['img' => '', 'eyebrow' => '', 'heading' => '', 'subtext' => '']];
  $rows = '';
  foreach ($slides as $i => $sl) $rows .= hero_img_row_html($sl, $i === 0);
  return '<div class="hero-imgs" data-hero-imgs>'
    . '<div class="hero-imgs-list">' . $rows . '</div>'
    . '<button type="button" class="btn btn-ghost btn-sm hero-img-add"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg> Add image</button>'
    . ($compact ? '' : '<p class="hero-imgs-help">The <b>first</b> image is the main banner — it is used for link previews and social sharing. Add more to rotate them as a gentle slideshow. Each image can carry its own eyebrow / heading / sub-text (leave blank to reuse the page’s banner text). A single image with no text stays a normal static banner.</p>')
    . '</div>';
}

// ─── Job openings editor (friendly repeater for the 'jobs' section type) ───
// Storage stays a JSON array in the section body that app/render.php reads
// verbatim. As of v1.4 each opening is broken into LABELED, structured fields
// matching the live posting (Description, About Our Academy, Appointment
// Duration, Employment Start Date, Eligibility, Weekly Teaching Hours & Duties,
// Other Duties, Minimum Qualifications → Hard/Soft skills, General Position
// Benefits, Hiring Process) instead of one free-form "Details" box. Each is
// OPTIONAL — a blank field renders nothing. Rich fields use the shared Word-style
// [data-rich] editor (with paste cleanup); short fields are plain inputs. The
// editor decodes the JSON into cards and writes the SAME shape back through
//   sections[<id>][jobs][<idx>][<field>]
// fields; save_page() re-encodes them to JSON (jobs_json_from_post) before the
// UPDATE. Backward-compat: an older opening that only has the legacy single
// "details" value is migrated into Description on load, so nothing is lost.
// $idx is a numeric row index, or the "__IDX__" token for the JS "Add" template.
function job_row_html($sid, $idx, $job = []){
  if (!is_array($job)) $job = [];
  $val  = function ($k) use ($job){ return e((string) ($job[$k] ?? '')); };
  $name = function ($f) use ($sid, $idx){ return 'sections[' . $sid . '][jobs][' . $idx . '][' . $f . ']'; };
  $show = !array_key_exists('show', $job) || !empty($job['show']);   // default: enabled
  $am   = (($job['apply_method'] ?? '') === 'external') ? 'external' : 'form';   // default: built-in form

  // Backward-compat: a pre-v1.4 opening only carried one free-form "details"
  // field. Fold it into Description on load (only when Description is still empty)
  // so the client never loses content and, on their next save, it lands in the
  // structured shape.
  $descHtml = (string) ($job['description'] ?? '');
  if (trim($descHtml) === '' && trim((string) ($job['details'] ?? '')) !== '') $descHtml = (string) $job['details'];

  // A full-width rich-text field (Word-style toolbar via [data-rich]).
  $rich = function ($f, $label, $opt = '', $html = null) use ($name, $job){
    if ($html === null) $html = (string) ($job[$f] ?? '');
    return '<div class="jf full"><label>' . $label
      . ($opt !== '' ? ' <span class="jf-opt">' . $opt . '</span>' : '') . '</label>'
      . '<textarea class="inp" rows="3" name="' . $name($f) . '" data-rich data-sectype="jobs">' . e($html) . '</textarea></div>';
  };
  // A full-width single-line text field.
  $short = function ($f, $label, $opt = '', $ph = '') use ($name, $val){
    return '<div class="jf full"><label>' . $label
      . ($opt !== '' ? ' <span class="jf-opt">' . $opt . '</span>' : '') . '</label>'
      . '<input class="inp" name="' . $name($f) . '" value="' . $val($f) . '" placeholder="' . e($ph) . '"></div>';
  };

  return '<div class="jobrow' . ($show ? '' : ' is-off') . '" data-jobrow>'
    . '<input type="hidden" name="' . $name('show') . '" value="' . ($show ? '1' : '0') . '" data-job-show>'
    . '<div class="jobrow-hd">'
      . '<span class="jobrow-num" aria-hidden="true"></span>'
      . '<div class="jobrow-title-wrap">'
        . '<input class="inp jobrow-title" name="' . $name('title') . '" value="' . $val('title')
        . '" placeholder="Job title (required), e.g. Full-Stack Engineer" data-job-title>'
        . '<span class="jobrow-req">Add a title so this opening appears on the site.</span>'
      . '</div>'
      . '<button type="button" class="jobrow-toggle" data-job-toggle aria-pressed="' . ($show ? 'true' : 'false')
        . '" title="Show or hide this opening on the site">'
        . icon('eye', ['size' => 15]) . '<span class="jobrow-toggle-t">' . ($show ? 'Enabled' : 'Disabled') . '</span></button>'
      . '<button type="button" class="mini jobrow-up" title="Move up" aria-label="Move up">' . icon('chevron-up', ['size' => 18]) . '</button>'
      . '<button type="button" class="mini jobrow-down" title="Move down" aria-label="Move down">' . icon('chevron-down', ['size' => 18]) . '</button>'
      . '<button type="button" class="mini jobrow-del" title="Delete this opening" aria-label="Delete this opening">' . icon('trash', ['size' => 17]) . '</button>'
    . '</div>'
    . '<div class="jobrow-body">'
      . '<div class="jf full"><label>Summary <span class="jf-opt">(short one-line description shown on the card)</span></label>'
        . '<input class="inp" name="' . $name('summary') . '" value="' . $val('summary') . '" placeholder="A short summary shown on the card"></div>'
      // ── Structured posting sections (all optional; blank = omitted on the site) ──
      . $rich('description', 'Description', '(what the role involves)', $descHtml)
      . $rich('about', 'About Our Academy', '(a paragraph about NTPPA)')
      . $short('duration', 'Appointment Duration', '', 'e.g. 1 year, with renewal possibilities')
      . $short('start_date', 'Employment Start Date', '', 'e.g. January 3rd, 2026 or until filled')
      . $short('eligibility', 'Eligibility', '', 'e.g. Permanent resident or US citizen, OPT, O-1')
      . $rich('hours', 'Weekly Teaching Hours &amp; Duties', '(a list works well)')
      . $rich('other_duties', 'Other Duties')
      . '<div class="jf full jf-group"><span class="jf-group-lbl">Minimum Qualifications</span>'
        . $rich('hard_skills', 'Hard Skills', '(degrees / technical requirements)')
        . $rich('soft_skills', 'Soft Skills', '(personal qualities)')
      . '</div>'
      . $rich('benefits', 'General Position Benefits', '(usually a list)')
      . $rich('hiring_process', 'Hiring Process', '(how to apply &amp; what happens next)')
      . '<div class="jf full"><label>How people apply</label>'
        . '<select class="inp" name="' . $name('apply_method') . '" data-job-method>'
          . '<option value="form"' . sel($am, 'form') . '>Built-in application form (recommended)</option>'
          . '<option value="external"' . sel($am, 'external') . '>External link or email</option>'
        . '</select>'
        . '<span class="jf-opt" style="display:block;margin-top:4px">Built-in form: “Apply now” opens the on-site application form (name, email, resume &amp; files). External: “Apply now” links straight to the address below.</span></div>'
      . '<div class="jf full"><label>Apply link / fallback email <span class="jf-opt">(email, tel:, web address, or a page path)</span></label>'
        . '<input class="inp" name="' . $name('apply') . '" value="' . $val('apply') . '" placeholder="careers@newtampappa.com  ·  https://…  ·  /contact">'
        . '<span class="jf-opt" style="display:block;margin-top:4px">With the built-in form this email is only shown as a small “or email …” fallback line.</span></div>'
      . '<div class="jobrow-meta">'
        . '<div class="jf"><label>Location</label><input class="inp" name="' . $name('location') . '" value="' . $val('location') . '" placeholder="Tampa, FL"></div>'
        . '<div class="jf"><label>Type</label><input class="inp" name="' . $name('type') . '" value="' . $val('type') . '" placeholder="Full-time"></div>'
        . '<div class="jf"><label>Date</label><input class="inp" name="' . $name('date') . '" value="' . $val('date') . '" placeholder="Now hiring"></div>'
      . '</div>'
    . '</div>'
    . '</div>';
}

// The whole repeater for a jobs section: the current openings, an empty-state,
// the "Add job opening" button and a hidden <template> the JS clones for new rows.
function jobs_editor_html($sid, $body){
  $jobs = json_decode((string) $body, true);
  if (!is_array($jobs)) $jobs = [];
  $rows = ''; $i = 0;
  foreach ($jobs as $j){
    if (!is_array($j)) continue;
    $rows .= job_row_html($sid, $i, $j);
    $i++;
  }
  return '<div class="jobs-editor" data-jobs data-jobs-next="' . $i . '">'
    . '<div class="jobs-list">' . $rows . '</div>'
    . '<p class="jobrow-empty"' . ($i ? ' hidden' : '') . '>No openings yet. Click <b>Add job opening</b> to create one — '
      . 'or leave the list empty to show the friendly “no open positions” message on the site.</p>'
    . '<div class="jobrow-foot">'
      . '<button type="button" class="btn btn-ghost btn-sm jobrow-add">' . icon('plus', ['size' => 16]) . ' Add job opening</button>'
      . '<span class="jobrow-hint">Add, delete, reorder and enable/disable openings — no code, no JSON.</span>'
    . '</div>'
    . '<template class="jobrow-tpl">' . job_row_html($sid, '__IDX__', []) . '</template>'
    . '</div>';
}

// One fully-filled example opening (used to seed a new 'jobs' section) — the live
// "Junior Piano Faculty" posting, section by section, so the client sees exactly
// how a complete structured posting reads. Every field maps to a labeled editor
// field and to a headed section on the public page. Entities are used for
// apostrophes/ampersands so the value stays clean everywhere it is reused.
function example_job_json(){
  $job = [
    'title'       => 'Full-Stack Web Engineer',
    'location'    => 'Kuwait City, Kuwait',
    'type'        => 'Full-time',
    'date'        => 'Now hiring',
    'summary'     => 'Build and ship web and application projects end to end, from scoped brief to live product, alongside a small accountable team.',
    'description' => '<p>Design, build and maintain web and application projects for our clients, working across the stack from database to interface, with clear ownership from brief to launch.</p>',
    'about'       => '<p>ExpertAims is an IT consultancy and web and mobile studio delivering projects, cloud, security and AI across Kuwait, India and Malaysia since 2012. We work in small teams that own what they ship, before and after launch.</p>',
    'duration'    => 'Permanent, with growth into lead roles.',
    'start_date'  => 'As soon as the right person is found.',
    'eligibility' => 'Kuwait residency or a transferable visa preferred.',
    'hours'       => '<ul><li>Standard full-time hours.</li><li>Focused build time, minimal meetings.</li></ul>',
    'other_duties'=> '<ul><li>Code review and mentoring juniors.</li><li>Occasional client-facing scoping calls.</li></ul>',
    'hard_skills' => '<p>Strong PHP and JavaScript, comfortable with MySQL, REST APIs and a modern front-end. Bonus: cloud hosting and CI experience.</p>',
    'soft_skills' => '<p>Takes ownership, communicates honestly about timelines, and cares about the craft.</p>',
    'benefits'    => '<ul><li>Competitive salary.</li><li>Real projects with real ownership.</li><li>Learning budget and time.</li><li>A team that ships and stays for the journey.</li></ul>',
    'hiring_process' => '<p>To apply, send the following through the form below (or by email):</p><ol><li>A short CV or portfolio.</li><li>Links to work you have shipped, a repo, a live site or an app.</li><li>A line or two on what you want to build next.</li></ol><p>Shortlisted candidates have a short call, then a practical conversation with the team.</p>',
    'apply'       => (string) setting('email', 'careers@expertaims.com'),
    'show'        => true,
  ];
  return json_encode([$job], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}

// Turn the friendly jobs repeater's structured POST rows back into the canonical
// JSON array stored in the section body. Shape (v1.4):
//   {title, summary, description, about, duration, start_date, eligibility,
//    hours, other_duties, hard_skills, soft_skills, benefits, hiring_process,
//    apply, apply_method, location, type, date, show}
// Titleless rows are dropped (they never render); DOM order is preserved, so
// arrow-reordering the cards reorders openings. The legacy free-form "details"
// field is no longer written — the editor folds it into Description on load.
function jobs_json_from_post($rows){
  if (!is_array($rows)) return '[]';
  // Rich fields hold trusted admin HTML; an "empty" rich editor can still emit
  // markup like "<p></p>" or "<br>", so treat text-empty HTML as blank.
  $blank = function ($html){
    $t = trim(preg_replace('/&nbsp;|\xC2\xA0/u', ' ', strip_tags((string) $html)));
    return $t === '';
  };
  $richKeys = ['description', 'about', 'hours', 'other_duties', 'hard_skills', 'soft_skills', 'benefits', 'hiring_process'];
  $textKeys = ['summary', 'duration', 'start_date', 'eligibility', 'apply', 'location', 'type', 'date'];
  $out = [];
  foreach ($rows as $r){
    if (!is_array($r)) continue;
    $title = trim((string) ($r['title'] ?? ''));
    if ($title === '') continue;                 // no title ⇒ never shows on the site ⇒ drop it
    $job = ['title' => $title];
    foreach ($textKeys as $k){
      $v = trim((string) ($r[$k] ?? ''));
      if ($v !== '') $job[$k] = $v;
    }
    foreach ($richKeys as $k){
      $v = trim((string) ($r[$k] ?? ''));
      if ($v !== '' && !$blank($v)) $job[$k] = $v;   // trusted admin HTML from the rich editor
    }
    // Apply method: 'external' links out to the apply address; anything else
    // (default) uses the built-in on-site application form. Only stored when
    // 'external' so existing form-based openings need no migration.
    if (strtolower(trim((string) ($r['apply_method'] ?? ''))) === 'external') $job['apply_method'] = 'external';
    $job['show'] = !isset($r['show']) || $r['show'] === '1' || $r['show'] === 1 || $r['show'] === true;
    $out[] = $job;
  }
  return json_encode($out, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}

// ─── Media usage map (item: safe deletes) ───
// One pass over everything that can reference an upload; returns
// [path => [human descriptions…]] for every media row that is in use.
function media_usage_map(){
  $paths = [];
  try { foreach (db()->query('SELECT path FROM media') as $r) $paths[$r['path']] = []; } catch (\Throwable $e) { return []; }
  if (!$paths) return [];
  $check = function ($haystack, $label) use (&$paths){
    if ($haystack === null || $haystack === '') return;
    foreach ($paths as $p => $_){
      if (strpos($haystack, $p) !== false) $paths[$p][] = $label;
    }
  };
  try {
    foreach (db()->query('SELECT slug,title,hero_image,hero_images,og_image FROM pages') as $pg){
      $check((string) $pg['hero_image'] . ' ' . (string) ($pg['hero_images'] ?? '') . ' ' . (string) $pg['og_image'], 'Page: ' . $pg['title']);
    }
    foreach (db()->query('SELECT s.body, s.image, s.extra, p.title FROM sections s JOIN pages p ON p.id=s.page_id') as $s){
      $check((string) $s['body'] . ' ' . (string) $s['image'] . ' ' . (string) $s['extra'], 'Page: ' . $s['title']);
    }
    foreach (db()->query('SELECT title, banner, body FROM posts') as $po){
      $check((string) $po['banner'] . ' ' . (string) $po['body'], 'Post: ' . $po['title']);
    }
    foreach (db()->query('SELECT skey, svalue FROM settings') as $st){
      $check((string) $st['svalue'], 'Setting: ' . $st['skey']);
    }
    foreach (db()->query('SELECT name, avatar FROM users WHERE avatar IS NOT NULL') as $u){
      $check((string) $u['avatar'], 'Avatar: ' . $u['name']);
    }
  } catch (\Throwable $e) { /* best-effort — unknown tables are skipped */ }
  foreach ($paths as $p => $where) $paths[$p] = array_values(array_unique($where));
  return $paths;
}

// Resolve a media row's file on disk, locked to the uploads/ folder.
function media_disk_path($relPath){
  $rel = ltrim((string) $relPath, '/');
  if (strpos($rel, 'uploads/') !== 0) return null;             // only uploads/ is ours to delete
  $base = realpath(dirname(__DIR__) . '/uploads');
  $full = realpath(dirname(__DIR__) . '/' . $rel);
  if ($base === false || $full === false) return null;         // file already gone
  if (strpos($full, $base . DIRECTORY_SEPARATOR) !== 0) return null; // traversal guard
  return $full;
}

// ─── Revision history — the undo safety net for page/post saves. ───
// Every save (and every restore) snapshots the PREVIOUS state first, so any
// version of the last 10 can be brought back from the editor's History menu.
function snapshot_revision($kind, $refId){
  try {
    if ($kind === 'page') {
      $pg = page_by_id($refId); if (!$pg) return;
      $snap = ['page' => $pg, 'sections' => page_sections($refId)];
    } else {
      $po = find_post_by_id($refId); if (!$po) return;
      $snap = ['post' => $po];
    }
    db()->prepare('INSERT INTO revisions(kind, ref_id, snapshot, user_name) VALUES(?,?,?,?)')
      ->execute([$kind, (int) $refId, json_encode($snap, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), (string) (current_user()['name'] ?? '')]);
    // keep only the newest 10 versions per item
    $st = db()->prepare('SELECT id FROM revisions WHERE kind=? AND ref_id=? ORDER BY id DESC');
    $st->execute([$kind, (int) $refId]);
    $ids = $st->fetchAll(\PDO::FETCH_COLUMN);
    if (count($ids) > 10) {
      $old = array_slice($ids, 10);
      db()->exec('DELETE FROM revisions WHERE id IN (' . implode(',', array_map('intval', $old)) . ')');
    }
  } catch (\Throwable $e) { /* history is best-effort — it never blocks a save */ }
}
function revisions_for($kind, $refId){
  try {
    $st = db()->prepare('SELECT id, user_name, created_at FROM revisions WHERE kind=? AND ref_id=? ORDER BY id DESC LIMIT 10');
    $st->execute([$kind, (int) $refId]);
    return $st->fetchAll();
  } catch (\Throwable $e) { return []; }
}
// Compact "History" dropdown for the page/post editor headers. The editor
// header lives INSIDE the editor's <form>, so the Restore buttons reference
// an external form via the HTML5 form="" attribute (nested forms are invalid)
// — echo revisions_form() once, after the editor form closes.
function revisions_dropdown($kind, $refId){
  $revs = revisions_for($kind, $refId);
  $h = '<details class="rev-dd"><summary title="Restore an earlier saved version"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M3 12a9 9 0 1 0 3-6.7L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l3 3"/></svg> History</summary><div class="rev-menu">';
  $h .= '<div class="rev-h">Saved versions <span>newest first · last 10</span></div>';
  if (!$revs) {
    $h .= '<div class="rev-empty">No saved versions yet — one is stored every time you press Save.</div>';
  } else {
    foreach ($revs as $r) {
      $t = strtotime((string) $r['created_at']);
      $h .= '<div class="rev-row"><div class="rev-meta"><b>' . e(rel_time($t)) . '</b><span>' . e(date('M j, Y · g:i A', $t)) . ((string) $r['user_name'] !== '' ? ' — ' . e($r['user_name']) : '') . '</span></div>'
          . '<button class="btn btn-ghost btn-sm" type="submit" form="revRestoreForm" name="id" value="' . (int) $r['id'] . '" '
          . 'onclick="return confirm(\'Restore this version? Your current state is saved to History first, so you can switch back.\')">Restore</button></div>';
    }
  }
  $h .= '<div class="rev-tip">Tip: <b>Ctrl/Cmd + Z</b> undoes typing in any box (the visual editor also has ↺ ↻ toolbar buttons). History brings back whole saved versions.</div>';
  return $h . '</div></details>';
}
function revisions_form(){
  return '<form id="revRestoreForm" method="post" style="display:none">' . csrf_field() . '<input type="hidden" name="action" value="restore_revision"></form>';
}

// ─── Dashboard chart primitives — inline SVG, no libraries. ───
// Tiny 30-day sparkline for the stat cards (real daily counts only).
function dash_spark(array $vals, $color = '#AE1626'){
  $w = 140; $h = 40;
  if (count($vals) < 2) $vals = array_pad($vals, 2, 0);
  $n = count($vals);
  $max = 0; foreach ($vals as $v){ if ($v > $max) $max = $v; }
  if ($max < 1) $max = 1;
  $pts = ''; $i = 0;
  foreach ($vals as $v){
    $x = round(2 + $i * ($w - 4) / ($n - 1), 1);
    $y = round($h - 3 - ($v / $max) * ($h - 9), 1);
    $pts .= ($pts === '' ? '' : ' ') . $x . ',' . $y;
    $i++;
  }
  static $sgid = 0; $sgid++;
  $base = $h - 3;
  return '<svg class="spark" viewBox="0 0 ' . $w . ' ' . $h . '" preserveAspectRatio="none" aria-hidden="true">'
    . '<defs><linearGradient id="dsg' . $sgid . '" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="' . $color . '" stop-opacity=".22"/><stop offset="1" stop-color="' . $color . '" stop-opacity="0"/></linearGradient></defs>'
    . '<polygon points="2,' . $base . ' ' . $pts . ' ' . ($w - 2) . ',' . $base . '" fill="url(#dsg' . $sgid . ')"/>'
    . '<polyline points="' . $pts . '" fill="none" stroke="' . $color . '" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linecap="round" stroke-linejoin="round"/></svg>';
}
// Donut from real counts via stroke-dasharray segments. $segs = [[label, value, cssColor], …]
function dash_donut(array $segs, $size = 132, $stroke = 15, $center = '', $sub = ''){
  $total = 0; foreach ($segs as $s){ $total += max(0, (int) $s[1]); }
  $c = $size / 2; $r = ($size - $stroke) / 2 - 1;
  $circ = 2 * M_PI * $r;
  $out = '<svg class="donut" viewBox="0 0 ' . $size . ' ' . $size . '" width="' . $size . '" height="' . $size . '" aria-hidden="true">'
       . '<circle cx="' . $c . '" cy="' . $c . '" r="' . round($r, 2) . '" fill="none" style="stroke:var(--line-2)" stroke-width="' . $stroke . '"/>';
  if ($total > 0){
    $out .= '<g transform="rotate(-90 ' . $c . ' ' . $c . ')">';
    $off = 0;
    foreach ($segs as $s){
      $v = max(0, (int) $s[1]); if (!$v) continue;
      $len = $v / $total * $circ;
      $out .= '<circle cx="' . $c . '" cy="' . $c . '" r="' . round($r, 2) . '" fill="none" style="stroke:' . $s[2] . '" stroke-width="' . $stroke . '" stroke-dasharray="' . round($len, 2) . ' ' . round($circ - $len, 2) . '" stroke-dashoffset="' . round(-$off, 2) . '"/>';
      $off += $len;
    }
    $out .= '</g>';
  }
  if ($center !== '') $out .= '<text class="donut-n" x="' . $c . '" y="' . ($c + ($sub !== '' ? 2 : 7)) . '" text-anchor="middle">' . e($center) . '</text>';
  if ($sub !== '')    $out .= '<text class="donut-s" x="' . $c . '" y="' . ($c + 18) . '" text-anchor="middle">' . e($sub) . '</text>';
  return $out . '</svg>';
}

// Slugs the site's menus link to — never deletable from the admin.
function cms_protected_slugs(){
  // EAC's real pages — never deletable from the admin.
  return ['home','services','projects','about','clients','innovation','news','careers','companies','products','contact','hosting'];
}

// Sidebar/topbar avatar chip: uploaded photo, falling back to the user's initials.
function user_avatar_html($u){
  $av        = trim((string) ($u['avatar'] ?? ''));
  $logo      = admin_favicon_url();
  $logoStyle = 'object-fit:contain;background:#fff;padding:14%;box-sizing:border-box';
  if ($av !== '') {
    // Uploaded photo. If the file is missing (a stale DB path), the onerror
    // handler swaps it for the brand logo so we never show a broken "?" image.
    $onerr = $logo !== ''
      ? "this.onerror=null;this.src='" . e($logo) . "';this.style.cssText='" . $logoStyle . "'"
      : "this.onerror=null;this.style.display='none'";
    return '<img src="' . e(media_url($av)) . '" alt="" onerror="' . $onerr . '">';
  }
  // No photo → show the client's brand logo on a light tile instead of red initials.
  if ($logo !== '') return '<img src="' . e($logo) . '" alt="" style="' . $logoStyle . '">';
  return e(strtoupper(substr((string) ($u['name'] ?? '?'), 0, 2)));
}

// Feeds the media picker in the editor: uploaded images as {name, path (relative, for fields), url (absolute, for preview/inline)}.
function cms_media_script(){
  $rows = db()->query('SELECT filename,path FROM media ORDER BY id DESC')->fetchAll();
  $media = array_map(function($m){ return ['name'=>$m['filename'], 'path'=>$m['path'], 'url'=>base_url($m['path'])]; }, $rows);
  return '<script>window.CMS_BASE=' . json_encode(base_url('')) . ';window.CMS_MEDIA=' . json_encode($media, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG) . ';</script>';
}

// Reorder news stories (priority) by swapping the `sort` of adjacent news-* pages.
function move_news($do){
  $dir = strpos($do, 'nup:') === 0 ? -1 : 1;
  $pid = (int) substr($do, strpos($do, ':') + 1);
  $rows = db()->query("SELECT id,sort FROM pages WHERE slug LIKE 'news-%' ORDER BY sort,id")->fetchAll();
  if (!$rows) return;
  $base = max(1, (int) $rows[0]['sort']);
  $u = db()->prepare('UPDATE pages SET sort=? WHERE id=?');
  $i = $base; foreach ($rows as $r){ $u->execute([$i++, $r['id']]); }
  $rows = db()->query("SELECT id,sort FROM pages WHERE slug LIKE 'news-%' ORDER BY sort,id")->fetchAll();
  $ids = array_column($rows, 'id');
  $idx = array_search($pid, $ids); if ($idx === false) return;
  $j = $idx + $dir; if ($j < 0 || $j >= count($rows)) return;
  $u->execute([$rows[$j]['sort'], $rows[$idx]['id']]);
  $u->execute([$rows[$idx]['sort'], $rows[$j]['id']]);
}

function renumber_sections($pid){
  $i = 1; $u = db()->prepare('UPDATE sections SET sort=? WHERE id=?');
  foreach (page_sections($pid) as $s){ $u->execute([$i++, $s['id']]); }
}
function move_section($pid, $do){
  $dir = strpos($do, 'up:') === 0 ? -1 : 1;
  $sid = (int) substr($do, strpos($do, ':') + 1);
  renumber_sections($pid);
  $secs = page_sections($pid); $ids = array_column($secs, 'id');
  $idx = array_search($sid, $ids); if ($idx === false) return;
  $j = $idx + $dir; if ($j < 0 || $j >= count($secs)) return;
  $u = db()->prepare('UPDATE sections SET sort=? WHERE id=?');
  $u->execute([$secs[$j]['sort'], $secs[$idx]['id']]);
  $u->execute([$secs[$idx]['sort'], $secs[$j]['id']]);
}

// Build a full, portable SQL backup (schema + data) of the CMS database via PDO.
// Works on any host (no mysqldump binary needed). Re-importable in phpMyAdmin.
function db_dump(){
  $pdo = db();
  $tables = $pdo->query('SHOW TABLES')->fetchAll(\PDO::FETCH_COLUMN);
  $out  = "-- ═══════════════════════════════════════════════════════════════\n";
  $out .= "--  NTPPA CMS — database backup\n";
  $out .= "--  Generated: " . date('Y-m-d H:i:s') . "\n";
  $out .= "--  Restore: phpMyAdmin → your DB → Import → choose this file.\n";
  $out .= "-- ═══════════════════════════════════════════════════════════════\n";
  $out .= "SET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n";
  foreach ($tables as $t){
    $create = $pdo->query('SHOW CREATE TABLE `' . str_replace('`','',$t) . '`')->fetch(\PDO::FETCH_ASSOC);
    $ddl = $create['Create Table'] ?? '';
    $out .= "DROP TABLE IF EXISTS `$t`;\n" . $ddl . ";\n\n";
    $rows = $pdo->query('SELECT * FROM `' . str_replace('`','',$t) . '`')->fetchAll(\PDO::FETCH_ASSOC);
    if ($rows){
      $cols = '`' . implode('`,`', array_keys($rows[0])) . '`';
      foreach ($rows as $row){
        $vals = array_map(function($v) use ($pdo){ return $v === null ? 'NULL' : $pdo->quote((string) $v); }, array_values($row));
        $out .= "INSERT INTO `$t` ($cols) VALUES (" . implode(',', $vals) . ");\n";
      }
      $out .= "\n";
    }
  }
  $out .= "SET FOREIGN_KEY_CHECKS = 1;\n";
  return $out;
}

// ─────────────── POST actions ───────────────
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
  csrf_check();
  $action = $_POST['action'] ?? '';

  if ($action === 'login') {
    $email = (string) ($_POST['email'] ?? '');
    if (login_locked($email)) { flash('Too many failed attempts. Please wait a few minutes and try again.'); redirect(admin_url('?p=login')); }
    if (attempt_login($email, $_POST['password'] ?? '')) {
      login_reset($email);
      // Self-healing recovery key: if no ADMIN_RESET_KEY is configured yet,
      // provision one now and show it ONCE on the next screen (item: remove
      // the manual .env step behind admin/reset.php).
      if ((current_user()['role'] ?? '') === 'admin' && !recovery_key_configured()) {
        provision_recovery_key();
      }
      redirect(admin_url('?p=dashboard'));
    }
    login_fail($email);
    flash('Invalid email or password.'); redirect(admin_url('?p=login'));
  }

  require_login();
  ensure_schema();

  if ($action === 'logout') { $_SESSION = []; session_destroy(); redirect(admin_url('?p=login')); }

  // ── Security monitor: block / release an IP, save the alert recipient ──
  if ($action === 'sec_block') {
    require_cap('settings');
    [$ok, $msg] = security_block($_POST['ip'] ?? '', $_POST['reason'] ?? 'Manually blocked from admin', current_user()['name'] ?? 'admin');
    flash($msg); redirect(admin_url('?p=security'));
  }
  if ($action === 'sec_release') {
    require_cap('settings');
    [$ok, $msg] = security_release($_POST['ip'] ?? '', current_user()['name'] ?? 'admin');
    flash($msg); redirect(admin_url('?p=security'));
  }
  if ($action === 'sec_save') {
    require_cap('settings');
    $rcpt = mb_substr(trim((string) ($_POST['security_alert_email'] ?? '')), 0, 255);
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
      ->execute(['security_alert_email', $rcpt]);
    flash($rcpt === '' ? 'Alert recipient cleared — email alerts are now OFF.' : 'Security alert recipient saved.');
    redirect(admin_url('?p=security'));
  }

  // ── Associate partners: add / edit (with optional logo upload) / delete ──
  if ($action === 'partner_save') {
    require_cap('settings'); partners_table();
    $id    = (int) ($_POST['id'] ?? 0);
    $name  = mb_substr(trim((string) ($_POST['name'] ?? '')), 0, 120);
    $url   = mb_substr(trim((string) ($_POST['url'] ?? '')), 0, 255);
    $blurb = mb_substr(trim((string) ($_POST['blurb'] ?? '')), 0, 400);
    $plate = ($_POST['plate'] ?? 'light') === 'dark' ? 'dark' : 'light';
    $sort  = (int) ($_POST['sort'] ?? 0);
    $vis   = empty($_POST['visible']) ? 0 : 1;
    $logo  = mb_substr(trim((string) ($_POST['logo'] ?? '')), 0, 255);
    if ($name === '') { flash('Partner needs a name.'); redirect(admin_url('?p=partners')); }
    if ($url !== '' && !preg_match('~^https?://~i', $url)) $url = 'https://' . $url;
    // optional logo upload → uploads/ (web-deny-exec already set there)
    if (!empty($_FILES['logofile']['name']) && ($_FILES['logofile']['error'] ?? 4) === 0) {
      $ext = strtolower(pathinfo($_FILES['logofile']['name'], PATHINFO_EXTENSION));
      if (in_array($ext, ['png','svg','jpg','jpeg','webp','gif'], true) && (int)$_FILES['logofile']['size'] <= 3*1024*1024) {
        $fn = 'partner-' . preg_replace('/[^a-z0-9\-]/i','_', strtolower($name)) . '-' . substr(bin2hex(random_bytes(4)),0,6) . '.' . $ext;
        if (move_uploaded_file($_FILES['logofile']['tmp_name'], __DIR__ . '/../uploads/' . $fn)) $logo = 'uploads/' . $fn;
      } else { flash('Logo must be PNG/SVG/JPG/WebP/GIF under 3 MB.'); redirect(admin_url('?p=partners')); }
    }
    if ($id > 0) {
      db()->prepare('UPDATE partners SET name=?,url=?,blurb=?,plate=?,sort=?,visible=?' . ($logo!==''?',logo=?':'') . ' WHERE id=?')
        ->execute($logo!=='' ? [$name,$url,$blurb,$plate,$sort,$vis,$logo,$id] : [$name,$url,$blurb,$plate,$sort,$vis,$id]);
      flash('Partner updated.');
    } else {
      db()->prepare('INSERT INTO partners(name,url,blurb,plate,sort,visible,logo) VALUES(?,?,?,?,?,?,?)')
        ->execute([$name,$url,$blurb,$plate,$sort,$vis,$logo]);
      flash('Partner added.');
    }
    redirect(admin_url('?p=partners'));
  }
  if ($action === 'partner_delete') {
    require_cap('settings'); partners_table();
    db()->prepare('DELETE FROM partners WHERE id=?')->execute([(int) ($_POST['id'] ?? 0)]);
    flash('Partner removed.');
    redirect(admin_url('?p=partners'));
  }

  // ── Hosting: plans CRUD + reseller handoff URLs ──
  if ($action === 'hosting_urls') {
    require_cap('settings');
    $set = db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)');
    foreach (['hosting_store_url','hosting_portal_url'] as $k) {
      $v = mb_substr(trim((string) ($_POST[$k] ?? '')), 0, 255);
      if ($v !== '' && !preg_match('~^https?://~i', $v)) $v = 'https://' . $v;
      $set->execute([$k, $v]);
    }
    $enabled = empty($_POST['hosting_page_enabled']) ? '0' : '1';
    $set->execute(['hosting_page_enabled', $enabled]);
    flash($enabled === '1' ? 'Hosting page is ON. Links saved.' : 'Hosting page turned OFF — the menu now links to your external hosting site.');
    redirect(admin_url('?p=hosting'));
  }
  if ($action === 'plan_save') {
    require_cap('settings'); hosting_plans_table();
    $id = (int) ($_POST['id'] ?? 0);
    $f = [
      'name'      => mb_substr(trim((string)($_POST['name'] ?? '')), 0, 80),
      'tagline'   => mb_substr(trim((string)($_POST['tagline'] ?? '')), 0, 160),
      'price'     => mb_substr(trim((string)($_POST['price'] ?? '')), 0, 40),
      'period'    => mb_substr(trim((string)($_POST['period'] ?? '/mo')), 0, 40),
      'features'  => mb_substr(trim((string)($_POST['features'] ?? '')), 0, 2000),
      'cta_url'   => mb_substr(trim((string)($_POST['cta_url'] ?? '')), 0, 255),
      'cta_label' => mb_substr(trim((string)($_POST['cta_label'] ?? 'Order now')), 0, 60) ?: 'Order now',
      'featured'  => empty($_POST['featured']) ? 0 : 1,
      'sort'      => (int) ($_POST['sort'] ?? 0),
      'visible'   => empty($_POST['visible']) ? 0 : 1,
    ];
    if ($f['name'] === '') { flash('Plan needs a name.'); redirect(admin_url('?p=hosting')); }
    if ($f['cta_url'] !== '' && !preg_match('~^https?://~i', $f['cta_url'])) $f['cta_url'] = 'https://' . $f['cta_url'];
    if ($id > 0) {
      db()->prepare('UPDATE hosting_plans SET name=?,tagline=?,price=?,period=?,features=?,cta_url=?,cta_label=?,featured=?,sort=?,visible=? WHERE id=?')
        ->execute([$f['name'],$f['tagline'],$f['price'],$f['period'],$f['features'],$f['cta_url'],$f['cta_label'],$f['featured'],$f['sort'],$f['visible'],$id]);
      flash('Plan updated.');
    } else {
      db()->prepare('INSERT INTO hosting_plans(name,tagline,price,period,features,cta_url,cta_label,featured,sort,visible) VALUES(?,?,?,?,?,?,?,?,?,?)')
        ->execute([$f['name'],$f['tagline'],$f['price'],$f['period'],$f['features'],$f['cta_url'],$f['cta_label'],$f['featured'],$f['sort'],$f['visible']]);
      flash('Plan added.');
    }
    redirect(admin_url('?p=hosting'));
  }
  if ($action === 'plan_delete') {
    require_cap('settings'); hosting_plans_table();
    db()->prepare('DELETE FROM hosting_plans WHERE id=?')->execute([(int) ($_POST['id'] ?? 0)]);
    flash('Plan removed.'); redirect(admin_url('?p=hosting'));
  }

  if ($action === 'delete_page') {
    require_cap('pages');
    $pgRow = page_by_id((int) ($_POST['id'] ?? 0));
    if (!$pgRow) {
      flash('That page no longer exists — it may already have been deleted.');
    } elseif (in_array($pgRow['slug'], cms_protected_slugs(), true)) {
      flash('“' . $pgRow['title'] . '” is a built-in menu page and can’t be deleted.');
    } elseif (!user_can_page($pgRow['slug'])) {
      flash('You don’t have permission to delete “' . $pgRow['title'] . '”.');
    } else {
      // Clear any menu placement first so the live nav can never keep a stale
      // item, then delete the page (sections cascade via the foreign key).
      try { db()->prepare('UPDATE pages SET menu_parent=NULL, nav_label=NULL WHERE id=?')->execute([$pgRow['id']]); } catch (\Throwable $e) {}
      db()->prepare('DELETE FROM pages WHERE id=?')->execute([$pgRow['id']]);
      try { db()->prepare("DELETE FROM revisions WHERE kind='page' AND ref_id=?")->execute([$pgRow['id']]); } catch (\Throwable $e) {}
      flash('“' . $pgRow['title'] . '” was deleted — removed from the Pages list and the site menu.');
    }
    redirect(admin_url('?p=pages'));
  }

  // ── Navigation manager (admin only): publish/place a custom page into a menu,
  //    or unpublish/remove it — the single source of truth is the pages row, so
  //    the per-page editor and this control stay perfectly in sync. ──
  if ($action === 'nav_save') {
    require_cap('pages');
    if ((current_user()['role'] ?? '') !== 'admin') { flash('Menu placement is an administrator setting.'); redirect(admin_url('?p=nav')); }
    $pgRow = page_by_id((int) ($_POST['id'] ?? 0));
    if ($pgRow && !in_array($pgRow['slug'], cms_protected_slugs(), true)) {
      $status = ($_POST['status'] ?? 'draft') === 'published' ? 'published' : 'draft';
      $mp = in_array($_POST['menu_parent'] ?? '', ['', 'main', 'services', 'company'], true) ? $_POST['menu_parent'] : '';
      $nl = mb_substr(trim((string) ($_POST['nav_label'] ?? '')), 0, 120);
      db()->prepare('UPDATE pages SET status=?, menu_parent=?, nav_label=? WHERE id=?')
        ->execute([$status, $mp, ($nl !== '' ? $nl : null), $pgRow['id']]);
      flash('“' . $pgRow['title'] . '” updated.');
    }
    redirect(admin_url('?p=nav'));
  }
  if ($action === 'nav_move') {
    require_cap('pages');
    if ((current_user()['role'] ?? '') !== 'admin') { redirect(admin_url('?p=nav')); }
    $pgRow = page_by_id((int) ($_POST['id'] ?? 0));
    $dir = ($_POST['dir'] ?? 'up') === 'down' ? 1 : -1;
    if ($pgRow && ($pgRow['menu_parent'] ?? '') !== '' && $pgRow['menu_parent'] !== null) {
      // Reorder only within the same menu group, using the shared sort column.
      $peers = db()->prepare('SELECT id, sort FROM pages WHERE menu_parent=? ORDER BY sort, id');
      $peers->execute([$pgRow['menu_parent']]);
      $rows = $peers->fetchAll(); $ids = array_column($rows, 'id');
      $idx = array_search($pgRow['id'], $ids);
      if ($idx !== false) {
        $j = $idx + $dir;
        if ($j >= 0 && $j < count($rows)) {
          // ensure distinct, monotonically increasing sort values first
          $base = 10; $u = db()->prepare('UPDATE pages SET sort=? WHERE id=?');
          foreach ($rows as $k => $r) { $rows[$k]['sort'] = $base + $k; $u->execute([$base + $k, $r['id']]); }
          $a = $rows[$idx]['sort']; $b = $rows[$j]['sort'];
          $u->execute([$b, $rows[$idx]['id']]);
          $u->execute([$a, $rows[$j]['id']]);
        }
      }
    }
    redirect(admin_url('?p=nav'));
  }

  if ($action === 'save_page') {
    require_cap('pages');
    $id = (int) ($_POST['page_id'] ?? 0);
    $pgRow = page_by_id($id);
    if ($pgRow && user_can_page($pgRow['slug'])) {
      snapshot_revision('page', $id);   // History: keep the state we are about to replace
      [$heroImage, $heroImages] = hero_fields_from_post();   // multi-image banner → first + full list
      $srcUrl   = safe_ext_url($_POST['source_url'] ?? '');  // external "Source" link (http/https only)
      $srcLabel = mb_substr(trim((string) ($_POST['source_label'] ?? '')), 0, 160);
      $heroQuote = mb_substr(trim((string) ($_POST['hero_quote'] ?? '')), 0, 400);   // optional banner quote
      $heroQuoteShow = isset($_POST['hero_quote_show']) ? 1 : 0;
      $heroCtas = hero_ctas_from_post();   // homepage-hero CTA buttons → JSON (null when unused)
      // Press image display: 'photo' (cover, fills the frame) or 'logo' (contain,
      // framed light tile — the default). A hidden field round-trips the value on
      // non-press pages so it is never clobbered.
      $mediaFit = in_array(($_POST['media_fit'] ?? ''), ['logo', 'photo'], true) ? $_POST['media_fit'] : 'logo';
      try {
        db()->prepare('UPDATE pages SET title=?,hero_eyebrow=?,hero_heading=?,hero_subtext=?,hero_image=?,hero_images=?,hero_show=?,meta_title=?,meta_description=?,meta_keywords=?,canonical=?,og_image=?,robots=?,status=?,source_url=?,source_label=?,hero_quote=?,hero_quote_show=?,hero_ctas=?,media_fit=? WHERE id=?')
          ->execute([
            trim($_POST['title'] ?? ''), $_POST['hero_eyebrow'] ?? '', $_POST['hero_heading'] ?? '', $_POST['hero_subtext'] ?? '',
            $heroImage, $heroImages, isset($_POST['hero_show']) ? 1 : 0,
            $_POST['meta_title'] ?? '', $_POST['meta_description'] ?? '', $_POST['meta_keywords'] ?? '',
            $_POST['canonical'] ?? '', $_POST['og_image'] ?? '', $_POST['robots'] ?? 'index,follow',
            $_POST['status'] ?? 'published', ($srcUrl !== '' ? $srcUrl : null), ($srcLabel !== '' ? $srcLabel : null),
            ($heroQuote !== '' ? $heroQuote : null), $heroQuoteShow, $heroCtas, $mediaFit, $id,
          ]);
      } catch (\Throwable $e) {   // pre-upgrade DB missing the new columns — save the rest, keep working
        db()->prepare('UPDATE pages SET title=?,hero_eyebrow=?,hero_heading=?,hero_subtext=?,hero_image=?,hero_show=?,meta_title=?,meta_description=?,meta_keywords=?,canonical=?,og_image=?,robots=?,status=? WHERE id=?')
          ->execute([
            trim($_POST['title'] ?? ''), $_POST['hero_eyebrow'] ?? '', $_POST['hero_heading'] ?? '', $_POST['hero_subtext'] ?? '',
            $heroImage, isset($_POST['hero_show']) ? 1 : 0,
            $_POST['meta_title'] ?? '', $_POST['meta_description'] ?? '', $_POST['meta_keywords'] ?? '',
            $_POST['canonical'] ?? '', $_POST['og_image'] ?? '', $_POST['robots'] ?? 'index,follow',
            $_POST['status'] ?? 'published', $id,
          ]);
      }
      // "Show in menu" — ADMIN ONLY (editors can save pages, but only an
      // administrator may put a page into the site's navigation). Custom
      // pages only; the 20 built-in slugs are already hard-wired in the menu.
      if ((current_user()['role'] ?? '') === 'admin' && isset($_POST['menu_parent'])
          && !in_array($pgRow['slug'], cms_protected_slugs(), true)) {
        $mp = in_array($_POST['menu_parent'], ['', 'main', 'services', 'company'], true) ? $_POST['menu_parent'] : '';
        $nl = mb_substr(trim((string) ($_POST['nav_label'] ?? '')), 0, 120);
        try {
          db()->prepare('UPDATE pages SET menu_parent=?, nav_label=? WHERE id=?')
            ->execute([$mp, ($nl !== '' ? $nl : null), $id]);
        } catch (\Throwable $e) { /* column not migrated yet — sign out/in or run upgrade-2026-07-09c.sql */ }
      }
      $typeMap = [];
      foreach (page_sections($id) as $ss) $typeMap[$ss['id']] = $ss['type'];
      foreach (($_POST['sections'] ?? []) as $sid => $sf) {
        $t = $typeMap[(int) $sid] ?? 'richtext';
        if ($t === 'jobs') {
          // Friendly repeater → the SAME JSON array the render reads (storage unchanged).
          $body = jobs_json_from_post($sf['jobs'] ?? []);
        } else {
          $body = $sf['body'] ?? '';
          $rawTypes = ['html', 'videos', 'gallery', 'carousel', 'certs', 'sponsors'];
          if (feature_on('quote_forms')) $rawTypes[] = 'quote';   // band bullets are plain lines
          if (!in_array($t, $rawTypes, true)) $body = clean_richtext($body); // client rich-text
        }
        db()->prepare('UPDATE sections SET heading=?,subheading=?,body=?,image=?,button_text=?,button_url=?,extra=?,visible=? WHERE id=? AND page_id=?')
          ->execute([
            $sf['heading'] ?? '', $sf['subheading'] ?? '', $body, $sf['image'] ?? '',
            $sf['button_text'] ?? '', $sf['button_url'] ?? '', $sf['extra'] ?? null,
            !empty($sf['visible']) ? 1 : 0, (int) $sid, $id,
          ]);
      }
      $do = $_POST['do'] ?? 'save';
      if (strpos($do, 'addsec:') === 0) {
        $type = preg_replace('/[^a-z_]/', '', substr($do, 7)) ?: 'richtext';
        $sort = (int) db()->query("SELECT COALESCE(MAX(sort),0)+1 FROM sections WHERE page_id=$id")->fetchColumn();
        // 'jobs' sections start with ONE richly-structured example opening so the
        // repeater editor isn't empty and the client sees how to lay out a full
        // posting (Description / About / Duration / Eligibility / Weekly Hours /
        // Qualifications / Benefits / Hiring Process). Apply method defaults to the
        // built-in on-site form; the "apply" email is only the small fallback line.
        $newHead = $type === 'jobs' ? 'Open Positions' : ucfirst(str_replace('_', ' ', $type)) . ' section';
        $newBody = $type === 'jobs' ? example_job_json() : '';
        db()->prepare('INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,?,?,1,?,?)')
          ->execute([$id, $type, $sort, $newHead, $newBody]);
        flash('Section added.');
      } elseif (strpos($do, 'splitphoto:') === 0) {
        // Pull a full-width photo (figure.wideband) out of a Custom HTML block into
        // its own \'photo\' section — gets an Enable/Disable switch + image picker.
        $sid2 = (int) substr($do, 11);
        $st2 = db()->prepare('SELECT * FROM sections WHERE id=? AND page_id=?'); $st2->execute([$sid2, $id]);
        $sec2 = $st2->fetch();
        if ($sec2 && preg_match('#<figure class="wideband">\s*<img[^>]*src="([^"]*)"[^>]*/?\s*>\s*(?:<figcaption>(.*?)</figcaption>)?\s*</figure>#s', (string) $sec2['body'], $m2)) {
          $src2 = trim($m2[1]);
          $bu2  = rtrim((string) config('base_url'), '/');
          if ($bu2 !== '' && strpos($src2, $bu2 . '/') === 0) $src2 = substr($src2, strlen($bu2) + 1);
          $src2 = ltrim($src2, '/');
          $cap2 = trim(strip_tags($m2[2] ?? ''));
          $pos2 = strpos((string) $sec2['body'], $m2[0]);
          $before2 = trim(substr((string) $sec2['body'], 0, $pos2));
          $after2  = trim(substr((string) $sec2['body'], $pos2 + strlen($m2[0])));
          db()->prepare('UPDATE sections SET sort = sort + 2 WHERE page_id=? AND sort > ?')->execute([$id, (int) $sec2['sort']]);
          db()->prepare('UPDATE sections SET body=? WHERE id=?')->execute([$before2, $sid2]);
          db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,subheading,image,body) VALUES(?,'photo',?,1,'Full-width photo',?,?,'')")
            ->execute([$id, (int) $sec2['sort'] + 1, $cap2, $src2]);
          if ($after2 !== '')
            db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,'html',?,1,?,?)")
              ->execute([$id, (int) $sec2['sort'] + 2, ($sec2['heading'] ?: 'Content') . ' (continued)', $after2]);
          renumber_sections($id);
          flash('Photo moved to its own section — it now has an Enabled/Disabled switch and image picker.');
        } else {
          flash('No full-width photo found in that block.');
        }
      } elseif (strpos($do, 'delsec:') === 0) {
        db()->prepare('DELETE FROM sections WHERE id=? AND page_id=?')->execute([(int) substr($do, 7), $id]);
        flash('Section removed.');
      } elseif (strpos($do, 'up:') === 0 || strpos($do, 'down:') === 0) {
        move_section($id, $do); flash('Section reordered.');
      } else { flash('Page saved.'); }
      redirect(admin_url('?p=edit&id=' . $id));
    }
    redirect(admin_url('?p=pages'));
  }

  // Banners aggregate view — writes to the same pages.hero_* fields as the
  // per-page Banner tab (single source of truth; no duplicate storage).
  if ($action === 'save_banner') {
    require_cap('pages');
    $id = (int) ($_POST['id'] ?? 0);
    $pgRow = page_by_id($id);
    if ($pgRow && user_can_page($pgRow['slug'])) {
      [$heroImage, $heroImages] = hero_fields_from_post();   // multi-image banner → first + full list
      try {
        db()->prepare('UPDATE pages SET hero_image=?, hero_images=?, hero_eyebrow=?, hero_heading=?, hero_subtext=?, hero_show=? WHERE id=?')
          ->execute([
            $heroImage, $heroImages, $_POST['hero_eyebrow'] ?? '', $_POST['hero_heading'] ?? '',
            $_POST['hero_subtext'] ?? '', isset($_POST['hero_show']) ? 1 : 0, $id,
          ]);
      } catch (\Throwable $e) {   // pre-upgrade DB without hero_images
        db()->prepare('UPDATE pages SET hero_image=?, hero_eyebrow=?, hero_heading=?, hero_subtext=?, hero_show=? WHERE id=?')
          ->execute([
            $heroImage, $_POST['hero_eyebrow'] ?? '', $_POST['hero_heading'] ?? '',
            $_POST['hero_subtext'] ?? '', isset($_POST['hero_show']) ? 1 : 0, $id,
          ]);
      }
      flash('Banner updated for “' . $pgRow['title'] . '”.');
    }
    redirect(admin_url('?p=banners'));
  }

  if ($action === 'add_page') {
    require_cap('pages');
    $title  = trim($_POST['title'] ?? '');
    $prefix = preg_replace('/[^a-z\-]/', '', $_POST['prefix'] ?? '');
    $seed   = preg_replace('/[^a-z_]/', '', $_POST['seed'] ?? '');   // optional content seed (e.g. 'gallery')
    if ($title !== '') {
      $slug = $prefix . slugify($title);
      $st = db()->prepare('SELECT id FROM pages WHERE slug=?'); $st->execute([$slug]);
      if ($st->fetch()) { flash('A page with that name already exists.'); redirect(admin_url('?p=pages')); }
      db()->prepare("INSERT INTO pages (slug,title,nav_label,hero_show,meta_title,meta_description,meta_keywords,canonical,og_image,robots,status,sort) VALUES (?,?,NULL,0,'','','','','','index,follow','draft',95)")
        ->execute([$slug, $title]);
      $nid = (int) db()->lastInsertId();
      if ($seed === 'news') {
        // News stories show their banner (= the card image) and carry "Category · Date" in the eyebrow.
        db()->prepare("UPDATE pages SET hero_show=1, hero_eyebrow=? WHERE id=?")->execute(['News · ' . date('d / m / Y'), $nid]);
        db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,'richtext',1,1,'Story',?)")->execute([$nid, '<p>Write the news story here…</p>']);
        flash('News story created as a draft. Set the Banner image, the "Category · Date" eyebrow, write the story, then publish under SEO & Meta.');
      } elseif ($seed === 'gallery') {
        // Gallery pages start ready to use: a photo gallery + a video gallery section.
        db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,'gallery',1,1,'Photo Gallery','')")->execute([$nid]);
        db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,'videos',2,1,'Video Gallery','')")->execute([$nid]);
        flash('Gallery page created as a draft — add image paths to the Photo Gallery and YouTube links to the Video Gallery (one per line), then publish under SEO & Meta.');
      } else {
        db()->prepare("INSERT INTO sections(page_id,type,sort,visible,heading,body) VALUES(?,'richtext',1,1,?,'')")->execute([$nid, $title]);
        flash('Page created as a draft — add your content, then set status to published under SEO & Meta.');
      }
      redirect(admin_url('?p=edit&id=' . $nid));
    }
    redirect(admin_url('?p=pages'));
  }

  if ($action === 'backup_db') {
    try { db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')->execute(['last_backup_at', date('c')]); } catch (\Throwable $e) {}
    require_cap('settings');
    $sql = db_dump();
    // remember when the last backup was taken (shown in Dashboard → System status)
    try { db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')->execute(['last_backup_at', date('Y-m-d H:i:s')]); } catch (\Throwable $e) {}
    $fn  = 'ntppa-backup-' . date('Y-m-d_His') . '.sql';
    header('Content-Type: application/sql; charset=utf-8');
    header('Content-Disposition: attachment; filename="' . $fn . '"');
    header('Content-Length: ' . strlen($sql));
    header('X-Content-Type-Options: nosniff');
    echo $sql;
    exit;
  }

  // Theme-provided admin actions (client-specific screens' save handlers).
  if (function_exists('theme_admin_action') && theme_admin_action($action)) { /* handled + redirected by the theme */ }

  if ($action === 'save_settings') {
    require_cap('settings');
    $setKeys = ['site_name','tagline','phone','email','address','facebook','youtube','client_logo','base_url','popup_enabled','popup_image','popup_link','ga4_id','head_code','body_code','timezone'];
    if (function_exists('theme_settings_fields')) $setKeys = array_merge($setKeys, array_keys(theme_settings_fields()));
    foreach ($setKeys as $k) {
      if (!isset($_POST[$k])) continue;
      $v = (string) $_POST[$k];
      if ($k === 'timezone' && $v !== '' && !in_array($v, timezone_identifiers_list(), true)) continue;  // only real timezones
      if ($k === 'ga4_id') $v = trim($v);
      db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')->execute([$k, $v]);
    }
    // checkbox toggles
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
        ->execute(['force_https', isset($_POST['force_https']) ? '1' : '0']);
    flash('Settings saved.'); redirect(admin_url('?p=settings'));
  }

  if ($action === 'save_features') {
    require_cap('settings');
    if (($user_role = (current_user()['role'] ?? '')) !== 'admin') { flash('Only administrators can change features.'); redirect(admin_url('?p=settings')); }
    $on = $_POST['feat'] ?? [];
    $map = [];
    foreach (feature_registry() as $k => $def) $map[$k] = !empty($on[$k]) ? 1 : 0;
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
        ->execute(['features', json_encode($map)]);
    flash('Features updated — the CMS menu and site reflect the change immediately.');
    redirect(admin_url('?p=settings'));
  }

  if ($action === 'save_certs') {
    require_cap('pages');
    if (!feature_on('certs')) redirect(admin_url('?p=dashboard'));
    $code=$_POST['code']??[]; $name=$_POST['name']??[]; $pdf=$_POST['pdf']??[]; $en=$_POST['en']??[];
    $lines = [];
    foreach ($code as $i => $c) {
      $c = trim($c); $n = trim($name[$i] ?? ''); $p2 = trim($pdf[$i] ?? '');
      if ($c === '' && $n === '') continue;
      $line = $c . ' | ' . $n . ' | ' . $p2;
      if (($en[$i] ?? '1') === '0') $line .= ' | off';
      $lines[] = $line;
    }
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
        ->execute(['certificates', implode("\n", $lines)]);
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
        ->execute(['cert_watermark', trim((string) ($_POST['cert_watermark'] ?? ''))]);
    flash('Certificates saved.'); redirect(admin_url('?p=certs'));
  }

  if ($action === 'save_sponsors') {
    require_cap('pages');
    if (!feature_on('sponsors')) redirect(admin_url('?p=dashboard'));
    $logo=$_POST['logo']??[]; $alt=$_POST['alt']??[];
    $lines = [];
    foreach ($logo as $i => $l) {
      $l = trim($l); if ($l === '') continue;
      $a = trim($alt[$i] ?? '');
      $lines[] = $a !== '' ? $l . ' | ' . $a : $l;
    }
    db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)')
        ->execute(['sponsors', implode("\n", $lines)]);
    flash('Sponsors saved.'); redirect(admin_url('?p=sponsors'));
  }

  if ($action === 'reorder_news') {
    require_cap('pages');
    if (!feature_on('news')) redirect(admin_url('?p=dashboard'));
    if (!empty($_POST['do'])) move_news($_POST['do']);
    redirect(admin_url('?p=news'));
  }

  if ($action === 'save_popup') {
    require_cap('settings');
    $vals = [
      'popup_enabled'    => (($_POST['popup_enabled'] ?? '0') === '1') ? '1' : '0',
      'popup_image'      => trim((string) ($_POST['popup_image'] ?? '')),
      'popup_link'       => trim((string) ($_POST['popup_link'] ?? '')),
      'popup_title'      => mb_substr(trim((string) ($_POST['popup_title'] ?? '')), 0, 160),
      'popup_title_show' => isset($_POST['popup_title_show']) ? '1' : '0',
      'popup_text'       => clean_richtext($_POST['popup_text'] ?? ''),
      'popup_newsletter' => isset($_POST['popup_newsletter']) ? '1' : '0',
    ];
    $st = db()->prepare('INSERT INTO settings(skey,svalue) VALUES(?,?) ON DUPLICATE KEY UPDATE svalue=VALUES(svalue)');
    foreach ($vals as $k => $v) $st->execute([$k, $v]);
    flash('Popup saved — it shows once per visitor session on the live site.');
    redirect(admin_url('?p=popup'));
  }

  if ($action === 'delete_subscriber') {
    require_cap('settings');
    try { db()->prepare('DELETE FROM subscribers WHERE id=?')->execute([(int) ($_POST['id'] ?? 0)]); } catch (\Throwable $e) {}
    flash('Subscriber removed.'); redirect(admin_url('?p=popup'));
  }

  if ($action === 'export_subscribers') {
    require_cap('settings');
    $rows = [];
    try { $rows = db()->query('SELECT email, page, created_at FROM subscribers ORDER BY id')->fetchAll(); } catch (\Throwable $e) {}
    // guard the first character so spreadsheet apps never treat a cell as a formula
    $cell = function($v){ $v = (string) $v; if ($v !== '' && strpos('=+-@', $v[0]) !== false) $v = "'" . $v; return '"' . str_replace('"', '""', $v) . '"'; };
    $csv = "email,signed_up,from_page\r\n";
    foreach ($rows as $r) $csv .= $cell($r['email']) . ',' . $cell($r['created_at']) . ',' . $cell($r['page']) . "\r\n";
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="ntppa-subscribers-' . date('Y-m-d') . '.csv"');
    header('X-Content-Type-Options: nosniff');
    echo $csv;
    exit;
  }

  if ($action === 'save_post') {
    require_cap('blog');
    $id = (int) ($_POST['id'] ?? 0);
    $slug = slugify($_POST['slug'] ?: $_POST['title']);
    $srcUrl   = safe_ext_url($_POST['source_url'] ?? '');   // external "Source" link (http/https only)
    $srcLabel = mb_substr(trim((string) ($_POST['source_label'] ?? '')), 0, 160);
    $base = [trim($_POST['title']), $slug, $_POST['author'] ?? '', $_POST['excerpt'] ?? '', clean_richtext($_POST['body'] ?? ''),
             $_POST['banner'] ?? '', isset($_POST['featured']) ? 1 : 0, $_POST['meta_title'] ?? '', $_POST['meta_description'] ?? '',
             $_POST['status'] ?? 'published', ($_POST['published_at'] ?: date('Y-m-d'))];
    $fields = array_merge($base, [($srcUrl !== '' ? $srcUrl : null), ($srcLabel !== '' ? $srcLabel : null)]);
    if ($id) snapshot_revision('post', $id);   // History: keep the state we are about to replace
    try {
      if ($id) {
        db()->prepare('UPDATE posts SET title=?,slug=?,author=?,excerpt=?,body=?,banner=?,featured=?,meta_title=?,meta_description=?,status=?,published_at=?,source_url=?,source_label=? WHERE id=?')
          ->execute(array_merge($fields, [$id]));
      } else {
        db()->prepare('INSERT INTO posts(title,slug,author,excerpt,body,banner,featured,meta_title,meta_description,status,published_at,source_url,source_label) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?)')->execute($fields);
      }
    } catch (\Throwable $e) {   // pre-upgrade DB without the source columns — save the rest
      if ($id) {
        db()->prepare('UPDATE posts SET title=?,slug=?,author=?,excerpt=?,body=?,banner=?,featured=?,meta_title=?,meta_description=?,status=?,published_at=? WHERE id=?')
          ->execute(array_merge($base, [$id]));
      } else {
        db()->prepare('INSERT INTO posts(title,slug,author,excerpt,body,banner,featured,meta_title,meta_description,status,published_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)')->execute($base);
      }
    }
    flash('Post saved.'); redirect(admin_url('?p=blog'));
  }
  if ($action === 'delete_post') { require_cap('blog'); db()->prepare('DELETE FROM posts WHERE id=?')->execute([(int) $_POST['id']]); flash('Post deleted.'); redirect(admin_url('?p=blog')); }

  if ($action === 'restore_revision') {
    $rid = (int) ($_POST['id'] ?? 0);
    $rev = null;
    try { $st = db()->prepare('SELECT * FROM revisions WHERE id=?'); $st->execute([$rid]); $rev = $st->fetch() ?: null; } catch (\Throwable $e) {}
    if (!$rev) { flash('That version is no longer available.'); redirect(admin_url('?p=dashboard')); }
    $snap = json_decode((string) $rev['snapshot'], true) ?: [];
    $when = date('M j \a\t g:i A', strtotime((string) $rev['created_at']));

    if ($rev['kind'] === 'page') {
      require_cap('pages');
      $pgRow = page_by_id((int) $rev['ref_id']);
      if (!$pgRow || !user_can_page($pgRow['slug']) || empty($snap['page'])) { flash('Could not restore that version.'); redirect(admin_url('?p=pages')); }
      snapshot_revision('page', $pgRow['id']);   // a restore is itself undoable
      $pv = $snap['page'];
      try {
        db()->prepare('UPDATE pages SET title=?,hero_eyebrow=?,hero_heading=?,hero_subtext=?,hero_image=?,hero_images=?,hero_show=?,meta_title=?,meta_description=?,meta_keywords=?,canonical=?,og_image=?,robots=?,status=?,source_url=?,source_label=?,hero_quote=?,hero_quote_show=? WHERE id=?')
          ->execute([
            (string) ($pv['title'] ?? $pgRow['title']),
            (string) ($pv['hero_eyebrow'] ?? ''), (string) ($pv['hero_heading'] ?? ''), (string) ($pv['hero_subtext'] ?? ''),
            (string) ($pv['hero_image'] ?? ''), (string) ($pv['hero_images'] ?? ''), (int) ($pv['hero_show'] ?? 1),
            (string) ($pv['meta_title'] ?? ''), (string) ($pv['meta_description'] ?? ''), (string) ($pv['meta_keywords'] ?? ''),
            (string) ($pv['canonical'] ?? ''), (string) ($pv['og_image'] ?? ''), (string) ($pv['robots'] ?? 'index,follow'),
            (string) ($pv['status'] ?? 'published'),
            ($pv['source_url'] ?? null) ?: null, ($pv['source_label'] ?? null) ?: null,
            ($pv['hero_quote'] ?? null) ?: null, (int) ($pv['hero_quote_show'] ?? 0), $pgRow['id'],
          ]);
      } catch (\Throwable $e) {   // pre-upgrade DB without the new columns
        db()->prepare('UPDATE pages SET title=?,hero_eyebrow=?,hero_heading=?,hero_subtext=?,hero_image=?,hero_show=?,meta_title=?,meta_description=?,meta_keywords=?,canonical=?,og_image=?,robots=?,status=? WHERE id=?')
          ->execute([
            (string) ($pv['title'] ?? $pgRow['title']),
            (string) ($pv['hero_eyebrow'] ?? ''), (string) ($pv['hero_heading'] ?? ''), (string) ($pv['hero_subtext'] ?? ''),
            (string) ($pv['hero_image'] ?? ''), (int) ($pv['hero_show'] ?? 1),
            (string) ($pv['meta_title'] ?? ''), (string) ($pv['meta_description'] ?? ''), (string) ($pv['meta_keywords'] ?? ''),
            (string) ($pv['canonical'] ?? ''), (string) ($pv['og_image'] ?? ''), (string) ($pv['robots'] ?? 'index,follow'),
            (string) ($pv['status'] ?? 'published'), $pgRow['id'],
          ]);
      }
      db()->prepare('DELETE FROM sections WHERE page_id=?')->execute([$pgRow['id']]);
      $ins = db()->prepare('INSERT INTO sections(page_id,type,sort,visible,heading,subheading,body,image,button_text,button_url,extra) VALUES(?,?,?,?,?,?,?,?,?,?,?)');
      foreach (($snap['sections'] ?? []) as $sv) {
        $ins->execute([$pgRow['id'], (string) ($sv['type'] ?? 'richtext'), (int) ($sv['sort'] ?? 0), (int) ($sv['visible'] ?? 1),
          $sv['heading'] ?? '', $sv['subheading'] ?? '', $sv['body'] ?? '', $sv['image'] ?? '',
          $sv['button_text'] ?? '', $sv['button_url'] ?? '', $sv['extra'] ?? null]);
      }
      flash('Version from ' . $when . ' restored. The replaced state was saved to History, so you can switch back.');
      redirect(admin_url('?p=edit&id=' . $pgRow['id']));
    }

    if ($rev['kind'] === 'post') {
      require_cap('blog');
      $po = find_post_by_id((int) $rev['ref_id']);
      if (!$po || empty($snap['post'])) { flash('Could not restore that version.'); redirect(admin_url('?p=blog')); }
      snapshot_revision('post', $po['id']);      // a restore is itself undoable
      $pv = $snap['post'];
      try {
        db()->prepare('UPDATE posts SET title=?,author=?,excerpt=?,body=?,banner=?,featured=?,meta_title=?,meta_description=?,status=?,published_at=?,source_url=?,source_label=? WHERE id=?')
          ->execute([
            (string) ($pv['title'] ?? $po['title']), $pv['author'] ?? '', $pv['excerpt'] ?? '', $pv['body'] ?? '',
            $pv['banner'] ?? '', (int) ($pv['featured'] ?? 0), $pv['meta_title'] ?? '', $pv['meta_description'] ?? '',
            (string) ($pv['status'] ?? 'published'), ($pv['published_at'] ?? null) ?: date('Y-m-d'),
            ($pv['source_url'] ?? null) ?: null, ($pv['source_label'] ?? null) ?: null, $po['id'],
          ]);
      } catch (\Throwable $e) {   // pre-upgrade DB without the source columns
        db()->prepare('UPDATE posts SET title=?,author=?,excerpt=?,body=?,banner=?,featured=?,meta_title=?,meta_description=?,status=?,published_at=? WHERE id=?')
          ->execute([
            (string) ($pv['title'] ?? $po['title']), $pv['author'] ?? '', $pv['excerpt'] ?? '', $pv['body'] ?? '',
            $pv['banner'] ?? '', (int) ($pv['featured'] ?? 0), $pv['meta_title'] ?? '', $pv['meta_description'] ?? '',
            (string) ($pv['status'] ?? 'published'), ($pv['published_at'] ?? null) ?: date('Y-m-d'), $po['id'],
          ]);
      }
      flash('Version from ' . $when . ' restored. The replaced state was saved to History, so you can switch back.');
      redirect(admin_url('?p=post&id=' . $po['id']));
    }
    redirect(admin_url('?p=dashboard'));
  }

  if ($action === 'delete_submission') { require_cap('pages'); db()->prepare('DELETE FROM submissions WHERE id=?')->execute([(int) $_POST['id']]); flash('Enquiry deleted.'); redirect(admin_url('?p=enquiries')); }

  if ($action === 'delete_application') {
    require_cap('pages');
    $aid = (int) ($_POST['id'] ?? 0);
    // remove any attached files from disk first (jailed to uploads/applications/)
    try {
      $st = db()->prepare('SELECT * FROM applications WHERE id=?'); $st->execute([$aid]); $row = $st->fetch();
      if ($row) {
        $dir = realpath(dirname(__DIR__) . '/uploads/applications');
        foreach ([1, 2, 3] as $n) {
          $fn = basename((string) ($row['file' . $n . '_name'] ?? ''));
          if ($fn === '' || !preg_match('/^[a-f0-9]{32}\.[A-Za-z0-9]{1,5}$/', $fn)) continue;
          $full = $dir !== false ? realpath($dir . '/' . $fn) : false;
          if ($full !== false && strpos($full, $dir . DIRECTORY_SEPARATOR) === 0 && is_file($full)) @unlink($full);
        }
      }
    } catch (\Throwable $e) { /* files best-effort — the row is removed regardless */ }
    db()->prepare('DELETE FROM applications WHERE id=?')->execute([$aid]);
    flash('Application deleted.'); redirect(admin_url('?p=applications'));
  }

  if ($action === 'export_applications') {
    require_cap('pages');
    $rows = [];
    try { $rows = db()->query('SELECT job_title,first_name,last_name,email,phone,location,relocate,video_url,message,page,is_spam,created_at FROM applications ORDER BY id DESC')->fetchAll(); } catch (\Throwable $e) {}
    // guard the first character so spreadsheet apps never treat a cell as a formula
    $cell = function($v){ $v = (string) $v; if ($v !== '' && strpos('=+-@', $v[0]) !== false) $v = "'" . $v; return '"' . str_replace('"', '""', $v) . '"'; };
    $csv = "job,first_name,last_name,email,phone,location,relocate,youtube,message,page,spam,received\r\n";
    foreach ($rows as $r) $csv .= $cell($r['job_title']) . ',' . $cell($r['first_name']) . ',' . $cell($r['last_name']) . ',' . $cell($r['email']) . ',' . $cell($r['phone']) . ',' . $cell($r['location']) . ',' . $cell($r['relocate']) . ',' . $cell($r['video_url']) . ',' . $cell($r['message']) . ',' . $cell($r['page']) . ',' . $cell(!empty($r['is_spam']) ? 'spam' : '') . ',' . $cell($r['created_at']) . "\r\n";
    header('Content-Type: text/csv; charset=utf-8');
    header('Content-Disposition: attachment; filename="ntppa-applications-' . date('Y-m-d') . '.csv"');
    header('X-Content-Type-Options: nosniff');
    echo $csv;
    exit;
  }

  if ($action === 'upload_media' && !empty($_FILES['file']['name'])) {
    require_cap('media');
    $names = (array) $_FILES['file']['name'];      // supports multiple files (name="file[]")
    $ins = db()->prepare('INSERT INTO media(filename,path,size) VALUES(?,?,?)');
    $ok = 0; $skip = 0;
    foreach ($names as $i => $orig) {
      if ($orig === '' || ($_FILES['file']['error'][$i] ?? 4) !== 0 || empty($_FILES['file']['tmp_name'][$i])) { continue; }
      $ext = strtolower(pathinfo($orig, PATHINFO_EXTENSION));
      $size = (int) $_FILES['file']['size'][$i];
      $isImg = in_array($ext, ['jpg','jpeg','png','webp','gif','svg']); $isPdf = ($ext === 'pdf');
      $max = $isPdf ? 20 * 1024 * 1024 : 6 * 1024 * 1024;
      if (($isImg || $isPdf) && $size <= $max) {
        $name = preg_replace('/[^a-z0-9\-_]/i', '_', pathinfo($orig, PATHINFO_FILENAME)) . '-' . substr(bin2hex(random_bytes(4)), 0, 6) . '.' . $ext;
        if (move_uploaded_file($_FILES['file']['tmp_name'][$i], __DIR__ . '/../uploads/' . $name)) {
          $ins->execute([$name, 'uploads/' . $name, $size]); $ok++;
        } else $skip++;
      } else $skip++;
    }
    if ($ok)  flash($ok . ' file' . ($ok === 1 ? '' : 's') . ' uploaded' . ($skip ? " · $skip skipped (wrong type or too large)" : '') . '.');
    else      flash('Nothing uploaded — use JPG, PNG, WebP, GIF, SVG (≤6 MB) or PDF (≤20 MB), and ensure uploads/ is writable (chmod 755).');
    redirect(admin_url('?p=media'));
  }

  if ($action === 'regen_reset_key') {
    if ((current_user()['role'] ?? '') !== 'admin') { flash('Only administrators can manage the recovery key.'); redirect(admin_url('?p=dashboard')); }
    provision_recovery_key();   // stages the one-time reveal panel
    redirect(admin_url('?p=users'));
  }

  if ($action === 'delete_media') {
    require_cap('media');
    $mid = (int) ($_POST['id'] ?? 0);
    $st = db()->prepare('SELECT * FROM media WHERE id=?'); $st->execute([$mid]);
    $row = $st->fetch();
    if (!$row) { flash('That file is no longer in the library.'); redirect(admin_url('?p=media')); }
    // where is it used? (server-side re-check — the tile's chip is advisory only)
    $usage = media_usage_map()[$row['path']] ?? [];
    if ($usage && empty($_POST['force'])) {
      flash('“' . $row['filename'] . '” is still used by: ' . implode(' · ', array_slice($usage, 0, 4))
        . (count($usage) > 4 ? ' +' . (count($usage) - 4) . ' more' : '')
        . '. Tick “delete anyway” on the tile to remove it regardless (those spots will show a broken image).');
      redirect(admin_url('?p=media'));
    }
    $disk = media_disk_path($row['path']);
    $fileGone = true;
    if ($disk !== null && is_file($disk)) $fileGone = @unlink($disk);
    db()->prepare('DELETE FROM media WHERE id=?')->execute([$mid]);
    flash('“' . $row['filename'] . '” deleted' . ($fileGone ? '' : ' from the library (the file itself could not be removed — check folder permissions)') . '.');
    redirect(admin_url('?p=media'));
  }

  // Replace-in-place: overwrite an existing media file at the SAME path with new
  // bytes, so every page/section/hero/press/faculty/gallery reference that points
  // at that path shows the new image immediately (the ?v=<mtime> cache-buster,
  // now applied to all uploads/ image URLs, forces browsers to refetch). Path is
  // jailed to uploads/ (media_disk_path realpath check); CSRF is enforced above.
  if ($action === 'replace_media') {
    require_cap('media');
    $mid = (int) ($_POST['id'] ?? 0);
    $st = db()->prepare('SELECT * FROM media WHERE id=?'); $st->execute([$mid]);
    $row = $st->fetch();
    if (!$row) { flash('That file is no longer in the library.'); redirect(admin_url('?p=media')); }
    $target = media_disk_path($row['path']);   // jailed to uploads/, must already exist
    if ($target === null) { flash('That file can’t be replaced — only images inside uploads/ are editable.'); redirect(admin_url('?p=media')); }
    $f = $_FILES['newfile'] ?? null;
    if (empty($_POST) && (int) ($_SERVER['CONTENT_LENGTH'] ?? 0) > 0) { flash('That image was too large to upload — please choose a smaller file.'); redirect(admin_url('?p=media')); }
    if (!$f || ($f['error'] ?? 4) === UPLOAD_ERR_NO_FILE || empty($f['tmp_name']) || !is_uploaded_file((string) $f['tmp_name'])) {
      flash((($f['error'] ?? 0) === UPLOAD_ERR_INI_SIZE) ? 'That image was too large to upload.' : 'No replacement image was chosen.');
      redirect(admin_url('?p=media'));
    }
    $newExt = strtolower(pathinfo((string) $f['name'], PATHINFO_EXTENSION));
    $tgtExt = strtolower(pathinfo($target, PATHINFO_EXTENSION));
    $imgExt = ['jpg', 'jpeg', 'png', 'webp', 'gif', 'svg'];
    if (!in_array($newExt, $imgExt, true)) { flash('Please choose an image file — JPG, PNG, WebP, GIF or SVG.'); redirect(admin_url('?p=media')); }
    if ((int) $f['size'] <= 0 || (int) $f['size'] > 6 * 1024 * 1024) { flash('The replacement image must be between 1 byte and 6 MB.'); redirect(admin_url('?p=media')); }
    // MIME sanity check (finfo) — extension whitelist alone isn't enough.
    $finfo = function_exists('finfo_open') ? finfo_open(FILEINFO_MIME_TYPE) : null;
    $mime  = $finfo ? (string) @finfo_file($finfo, (string) $f['tmp_name']) : '';
    if ($finfo) finfo_close($finfo);
    $mimeOk = [
      'jpg' => ['image/jpeg'], 'jpeg' => ['image/jpeg'], 'png' => ['image/png'],
      'webp' => ['image/webp'], 'gif' => ['image/gif'],
      'svg' => ['image/svg+xml', 'text/xml', 'application/xml', 'text/plain', 'text/html'],
    ];
    if ($mime !== '' && isset($mimeOk[$newExt]) && !in_array($mime, $mimeOk[$newExt], true)) {
      flash('That file doesn’t look like a valid image — nothing was changed.'); redirect(admin_url('?p=media'));
    }
    // Overwrite the EXISTING path (keep its filename + extension so every reference
    // keeps resolving; browsers sniff content, so a PNG saved at a .jpg path shows).
    if (!@move_uploaded_file((string) $f['tmp_name'], $target)) {
      flash('Could not save the image — check that the uploads/ folder is writable (755 or 775).'); redirect(admin_url('?p=media'));
    }
    @chmod($target, 0644);
    @touch($target);                    // fresh mtime → new ?v= so browsers refetch
    clearstatcache(true, $target);
    try { db()->prepare('UPDATE media SET size=? WHERE id=?')->execute([(int) @filesize($target), $mid]); } catch (\Throwable $e) {}
    $note = ($newExt !== $tgtExt && $newExt !== '') ? ' (kept the original “.' . $tgtExt . '” name so every page still finds it)' : '';
    flash('“' . $row['filename'] . '” replaced — the new image now shows everywhere it’s used' . $note . '.');
    redirect(admin_url('?p=media'));
  }

  if ($action === 'cleanup_media') {
    require_cap('media');
    // "Clear old clutter" — delete library rows whose file no longer exists on
    // disk (broken tiles). Only uploads/-scoped rows are considered; absolute
    // URLs (remote images) are left alone.
    $gone = [];
    foreach (db()->query('SELECT id, path, filename FROM media') as $m2) {
      $rel = ltrim((string) $m2['path'], '/');
      if (strpos($rel, 'uploads/') !== 0) continue;
      if (!is_file(dirname(__DIR__) . '/' . $rel)) $gone[] = (int) $m2['id'];
    }
    if ($gone) {
      db()->exec('DELETE FROM media WHERE id IN (' . implode(',', $gone) . ')');
      flash(count($gone) . ' broken librar' . (count($gone) === 1 ? 'y entry' : 'y entries') . ' cleaned up (their files were already gone).');
    } else {
      flash('Nothing to clean — every library entry’s file exists.');
    }
    redirect(admin_url('?p=media'));
  }

  if ($action === 'save_user') {
    require_cap('users');
    $uid   = (int) ($_POST['uid'] ?? 0);
    $name  = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $role  = ($_POST['role'] ?? 'editor') === 'admin' ? 'admin' : 'editor';
    $pass  = (string) ($_POST['password'] ?? '');
    $caps  = $_POST['cap'] ?? [];
    $perms = json_encode([
      'pages'    => !empty($caps['pages'])    ? 1 : 0,
      'blog'     => !empty($caps['blog'])     ? 1 : 0,
      'media'    => !empty($caps['media'])    ? 1 : 0,
      'settings' => !empty($caps['settings']) ? 1 : 0,
      'users'    => 0,                       // only admins manage users
      'scope'    => trim($_POST['scope'] ?? 'all') ?: 'all',
    ]);
    if ($name === '' || !filter_var($email, FILTER_VALIDATE_EMAIL)) { flash('Enter a name and a valid email.'); redirect(admin_url('?p=users')); }
    $st = db()->prepare('SELECT id FROM users WHERE email=? AND id<>?'); $st->execute([$email, $uid]);
    if ($st->fetch()) { flash('Another user already uses that email.'); redirect(admin_url('?p=users')); }

    // Profile photo / avatar → uploads/avatars/ (square image, ≤2 MB). Keeps the
    // current photo unless a new file is chosen or "Remove photo" is ticked.
    $avatar = trim((string) ($_POST['avatar_current'] ?? ''));
    if (!empty($_POST['avatar_remove'])) $avatar = '';
    if (!empty($_FILES['avatar_file']['tmp_name']) && is_uploaded_file($_FILES['avatar_file']['tmp_name'])) {
      $af  = $_FILES['avatar_file'];
      $ext = strtolower(pathinfo($af['name'], PATHINFO_EXTENSION));
      if (!in_array($ext, ['jpg','jpeg','png','webp','gif'], true) || (int) $af['size'] > 2 * 1024 * 1024) {
        flash('Profile photo must be a JPG, PNG, WebP or GIF under 2 MB.');
        redirect(admin_url('?p=users' . ($uid ? '&edit=' . $uid : '')));
      }
      $dir = __DIR__ . '/../uploads/avatars';
      if (!is_dir($dir)) @mkdir($dir, 0755, true);
      $fn = 'avatar-' . ($uid ?: 'new') . '-' . substr(bin2hex(random_bytes(4)), 0, 6) . '.' . $ext;
      if (move_uploaded_file($af['tmp_name'], $dir . '/' . $fn)) $avatar = 'uploads/avatars/' . $fn;
      else { flash('Photo upload failed — make sure uploads/ is writable (chmod 755).'); redirect(admin_url('?p=users' . ($uid ? '&edit=' . $uid : ''))); }
    }

    if ($uid) {
      if ($role !== 'admin') {  // never demote the last administrator
        $cur = db()->prepare('SELECT role FROM users WHERE id=?'); $cur->execute([$uid]);
        $admins = (int) db()->query("SELECT COUNT(*) FROM users WHERE role='admin'")->fetchColumn();
        if ($cur->fetchColumn() === 'admin' && $admins <= 1) { flash('You can’t remove the last administrator.'); redirect(admin_url('?p=users')); }
      }
      if ($pass !== '') {
        if (strlen($pass) < 8) { flash('Password must be at least 8 characters.'); redirect(admin_url('?p=users')); }
        db()->prepare('UPDATE users SET name=?,email=?,role=?,perms=?,avatar=?,password_hash=? WHERE id=?')
          ->execute([$name, $email, $role, $perms, $avatar, password_hash($pass, PASSWORD_DEFAULT), $uid]);
      } else {
        db()->prepare('UPDATE users SET name=?,email=?,role=?,perms=?,avatar=? WHERE id=?')->execute([$name, $email, $role, $perms, $avatar, $uid]);
      }
      // keep the signed-in session copy fresh when you edit your own profile
      if ($uid === (int) (current_user()['id'] ?? 0)) {
        $st = db()->prepare('SELECT * FROM users WHERE id=?'); $st->execute([$uid]);
        if ($row = $st->fetch()) {
          $_SESSION['user'] = [
            'id' => $row['id'], 'name' => $row['name'], 'email' => $row['email'], 'role' => $row['role'],
            'avatar' => (string) ($row['avatar'] ?? ''),
            'perms' => (isset($row['perms']) && $row['perms'] !== '' ? (json_decode($row['perms'], true) ?: []) : []),
          ];
        }
      }
      flash('Team member updated.');
    } else {
      if (strlen($pass) < 8) { flash('New members need a password of at least 8 characters.'); redirect(admin_url('?p=users')); }
      db()->prepare('INSERT INTO users(name,email,password_hash,role,perms,avatar) VALUES(?,?,?,?,?,?)')
        ->execute([$name, $email, password_hash($pass, PASSWORD_DEFAULT), $role, $perms, $avatar]);
      flash('Team member added.');
    }
    redirect(admin_url('?p=users'));
  }

  if ($action === 'delete_user') {
    require_cap('users');
    $uid = (int) ($_POST['uid'] ?? 0);
    if ($uid === (int) (current_user()['id'] ?? 0)) { flash('You can’t delete your own account.'); redirect(admin_url('?p=users')); }
    $cur = db()->prepare('SELECT role FROM users WHERE id=?'); $cur->execute([$uid]);
    if ($cur->fetchColumn() === 'admin') {
      $admins = (int) db()->query("SELECT COUNT(*) FROM users WHERE role='admin'")->fetchColumn();
      if ($admins <= 1) { flash('You can’t delete the last administrator.'); redirect(admin_url('?p=users')); }
    }
    db()->prepare('DELETE FROM users WHERE id=?')->execute([$uid]);
    flash('Team member removed.'); redirect(admin_url('?p=users'));
  }
}

// ─────────────── Router ───────────────
$p = $_GET['p'] ?? 'dashboard';

if ($p === 'login') {
  if (current_user()) redirect(admin_url('?p=dashboard'));
  $msg = flash();
  ?><!DOCTYPE html><html lang="en" data-theme="light"><head><meta charset="UTF-8"><script>(function(){try{var t=localStorage.getItem("ea-cms-theme");if(!t&&window.matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches)t="dark";if(t==="dark")document.documentElement.setAttribute("data-theme","dark");}catch(e){}})();</script><meta name="viewport" content="width=device-width,initial-scale=1"><title>Sign in · AimsHub</title><link rel="icon" href="<?= admin_favicon_url() ?>"><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="<?= asset_url('assets/admin.css') ?>"></head><body>
  <div id="login" style="display:grid">
    <div class="brandside">
      <div class="bs-top"><img src="<?= base_url('assets/ea-wordmark.png') ?>" alt="ExpertAims" style="filter:brightness(0) invert(1)"> <span style="opacity:.4">|</span> <span style="font-size:12px;letter-spacing:.14em;text-transform:uppercase;color:#fff;font-weight:700">AimsHub</span></div>
      <div class="bs-mid">
        <h1>One dashboard to run<br>the <span>entire website.</span></h1>
        <p>Edit every page, section, banner and SEO tag — no code. Built by ExpertAims for <?= e(setting('site_name', 'your business')) ?>.</p>
        <div class="bs-feat">
          <div><span class="dot"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg></span> Edit every page, section &amp; banner</div>
          <div><span class="dot"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg></span> Media library, SEO &amp; analytics built in</div>
          <div><span class="dot"><svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 5 5L20 7"/></svg></span> Secure, role-based team access</div>
        </div>
      </div>
      <div class="bs-bot"><span class="bs-brandline">AimsHub<span>·</span>by ExpertAims</span>© 2012–<?= date('Y') ?> ExpertAims Consulting · Technology · Outsourcing</div>
    </div>
    <div class="formside"><form class="lform" method="post" action="<?= admin_url('?p=login') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="login">
      <?php $clLogin = client_logo_url(); ?>
      <div class="lclient">
        <span class="lclient-mark<?= $clLogin === '' ? ' is-letter' : '' ?>"><?php if ($clLogin !== ''): ?><img src="<?= e($clLogin) ?>" alt="" onerror="var p=this.parentNode;p.classList.add('is-letter');p.textContent=<?= json_encode(strtoupper(substr((string) setting('site_name', 'C'), 0, 1))) ?>;"><?php else: ?><?= e(strtoupper(substr((string) setting('site_name', 'C'), 0, 1))) ?><?php endif; ?></span>
        <span class="lclient-name"><b><?= e(setting('site_name', 'Your Website')) ?></b><span>Content Management</span></span>
      </div>
      <h2>Sign in to your dashboard</h2><div class="sub">Manage your website content.</div>
      <?php if ($msg): ?><div class="demo" style="background:var(--bad-soft);color:var(--bad)"><?= e($msg) ?></div><?php endif; ?>
      <div class="field"><label>Email address</label><input class="inp" type="email" name="email" required autofocus></div>
      <div class="field"><label>Password</label><input class="inp" type="password" name="password" required></div>
      <button class="btn btn-primary btn-block" type="submit" style="margin-top:8px">Sign in</button>
      <div class="lforgot"><a href="<?= admin_url('reset.php') ?>">Forgot password?</a></div>
      <div class="lpower">Powered by <img src="<?= base_url('assets/ea-mark.png') ?>" alt=""> <b>ExpertAims</b></div>
    </form></div>
  </div><button type="button" class="theme-t" id="themeToggle" style="position:fixed;top:16px;right:16px;z-index:200" title="Light / dark mode"><?= icon('sun', ['class'=>'ic t-sun']) ?><?= icon('moon', ['class'=>'ic t-moon']) ?></button><script>document.addEventListener("click",function(e){var b=e.target.closest("#themeToggle");if(!b)return;var el=document.documentElement;var d=el.getAttribute("data-theme")==="dark";el.setAttribute("data-theme",d?"light":"dark");try{localStorage.setItem("ea-cms-theme",d?"light":"dark");}catch(err){}});</script></body></html><?php
  exit;
}

require_login();
ensure_schema();
$user = current_user();
$msg = flash();

// ─── Authenticated download of an application attachment (resumes = PII) ───
// The ONLY way an uploaded file leaves the server. Requires the 'pages' cap,
// looks up the stored (random) filename for the requested row + slot, jails the
// path to uploads/applications/, and streams it as an attachment with the right
// mime and the ORIGINAL filename. Files are never web-reachable directly (the
// folder's .htaccess denies all), so this route is the privacy boundary.
if (($_GET['p'] ?? '') === 'application_file') {
  require_cap('pages');
  $id = (int) ($_GET['id'] ?? 0);
  $n  = (int) ($_GET['n'] ?? 0);
  if ($id <= 0 || $n < 1 || $n > 3) { http_response_code(404); exit('Not found'); }
  $row = null;
  try { $st = db()->prepare('SELECT * FROM applications WHERE id=?'); $st->execute([$id]); $row = $st->fetch(); } catch (\Throwable $e) { $row = null; }
  if (!$row) { http_response_code(404); exit('Not found'); }
  $stored = (string) ($row['file' . $n . '_name'] ?? '');
  $orig   = (string) ($row['file' . $n . '_orig'] ?? '');
  // basename() + a strict character check jail the lookup to the folder — no
  // path traversal, no absolute paths, only files this route created.
  $stored = basename($stored);
  if ($stored === '' || !preg_match('/^[a-f0-9]{32}\.[A-Za-z0-9]{1,5}$/', $stored)) { http_response_code(404); exit('Not found'); }
  $dir  = realpath(dirname(__DIR__) . '/uploads/applications');
  $full = $dir !== false ? realpath($dir . '/' . $stored) : false;
  if ($full === false || strpos($full, $dir . DIRECTORY_SEPARATOR) !== 0 || !is_file($full)) { http_response_code(404); exit('Not found'); }
  $ext  = strtolower(pathinfo($stored, PATHINFO_EXTENSION));
  $mimes = ['pdf' => 'application/pdf', 'doc' => 'application/msword',
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'txt' => 'text/plain', 'rtf' => 'application/rtf'];
  $mime = $mimes[$ext] ?? 'application/octet-stream';
  // download filename = the applicant name + slot + original extension (sanitised)
  $who  = trim(($row['first_name'] ?? '') . ' ' . ($row['last_name'] ?? ''));
  $slotName = [1 => 'resume', 2 => 'teaching-philosophy', 3 => 'sample'][$n];
  $dl = preg_replace('/[^A-Za-z0-9._-]+/', '-', trim(($who !== '' ? $who . '-' : '') . $slotName)) . '.' . $ext;
  $dl = trim($dl, '-') ?: ('application-file.' . $ext);
  while (ob_get_level() > 0) ob_end_clean();
  header('Content-Type: ' . $mime);
  header('Content-Disposition: attachment; filename="' . $dl . '"');
  header('Content-Length: ' . filesize($full));
  header('X-Content-Type-Options: nosniff');
  header('Cache-Control: private, no-store');
  readfile($full);
  exit;
}

// Layout header
function layout_top($active, $title, $crumb = '') {
  global $user;
  // Notification bell: unread enquiries + unread job applications (spam-flagged
  // ones don't ring the bell — they wait quietly in the inbox) + unread signups.
  $navUnread = 0; $unreadEnq = 0; $unreadSub = 0; $unreadApp = 0;
  if (user_can('pages')) {
    try { $unreadEnq = (int) db()->query('SELECT COUNT(*) FROM submissions WHERE is_read=0 AND COALESCE(is_spam,0)=0')->fetchColumn(); }
    catch (\Throwable $e) { try { $unreadEnq = (int) db()->query('SELECT COUNT(*) FROM submissions WHERE is_read=0')->fetchColumn(); } catch (\Throwable $e2) {} }
    try { $unreadApp = (int) db()->query('SELECT COUNT(*) FROM applications WHERE is_read=0 AND COALESCE(is_spam,0)=0')->fetchColumn(); }
    catch (\Throwable $e) { try { $unreadApp = (int) db()->query('SELECT COUNT(*) FROM applications WHERE is_read=0')->fetchColumn(); } catch (\Throwable $e2) {} }
  }
  if (user_can('settings')) {
    try { $unreadSub = (int) db()->query('SELECT COUNT(*) FROM subscribers WHERE is_read=0 AND COALESCE(is_spam,0)=0')->fetchColumn(); }
    catch (\Throwable $e) { try { $unreadSub = (int) db()->query('SELECT COUNT(*) FROM subscribers WHERE is_read=0')->fetchColumn(); } catch (\Throwable $e2) {} }
  }
  $navUnread = $unreadEnq + $unreadApp + $unreadSub;
  $bellTitle = $navUnread
    ? trim(implode(' · ', array_filter([
        $unreadEnq ? $unreadEnq . ' new enquir' . ($unreadEnq === 1 ? 'y' : 'ies') : '',
        $unreadApp ? $unreadApp . ' new application' . ($unreadApp === 1 ? '' : 's') : '',
        $unreadSub ? $unreadSub . ' new subscriber' . ($unreadSub === 1 ? '' : 's') : '',
      ])))
    : 'Notifications — new form submissions appear here';
  $bellHref = admin_url($unreadApp && !$unreadEnq ? '?p=applications' : ($unreadSub && !$unreadEnq && !$unreadApp ? '?p=popup' : '?p=enquiries')); ?>
<!DOCTYPE html><html lang="en" data-theme="light"><head><meta charset="UTF-8"><script>(function(){try{var t=localStorage.getItem("ea-cms-theme");if(!t&&window.matchMedia&&matchMedia("(prefers-color-scheme: dark)").matches)t="dark";if(t==="dark")document.documentElement.setAttribute("data-theme","dark");}catch(e){}})();</script><meta name="viewport" content="width=device-width,initial-scale=1"><title><?= e($title) ?> · AimsHub</title><link rel="icon" href="<?= admin_favicon_url() ?>"><link rel="preconnect" href="https://fonts.googleapis.com"><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet"><link rel="stylesheet" href="<?= asset_url('assets/admin.css') ?>"></head><body>
<div id="app" class="on">
  <aside class="sidebar">
    <div class="sb-brand"><div class="mark"><img src="<?= base_url('assets/ea-mark.png') ?>" alt=""></div><div class="txt"><b>AimsHub</b><span>by ExpertAims</span></div></div>
    <?php $sbLogo = client_logo_url(); ?>
    <a class="sb-site" href="<?= base_url() ?>" target="_blank"><?php if ($sbLogo !== ''): ?><span class="fav fav-img"><img src="<?= e($sbLogo) ?>" alt="" onerror="this.parentNode.className='fav';this.parentNode.textContent=<?= json_encode(strtoupper(substr(setting('site_name','N'),0,1))) ?>;"></span><?php else: ?><div class="fav"><?= e(strtoupper(substr(setting('site_name','N'),0,1))) ?></div><?php endif; ?><div class="n"><b><?= e(setting('site_name','NTPPA')) ?></b><span>View live site ↗</span></div></a>
    <nav class="sb-nav">
      <div class="sb-group">Content</div>
      <?php
      // sidebar nav icons — drawn from the central icon() registry (app/icons.php)
      $nav = ['dashboard'=>['Dashboard','dashboard'],
        'pages'=>['Pages','pages'],
        'banners'=>['Banners','banners'],
        'blog'=>['Blog Posts','blog'],
        'media'=>['Media Library','media'],
        'certificates'=>['Certificates','certificate'],
        'news'=>['News Stories','doc'],
        'certs'=>['Cert Badges','medal'],
        'sponsors'=>['Sponsors','medal'],
        'popup'=>['Homepage Popup','megaphone']];
      $canNav = ['dashboard'=>true, 'pages'=>user_can('pages'), 'banners'=>user_can('pages'), 'blog'=>(user_can('blog') && feature_on('blog')), 'media'=>user_can('media'), 'certificates'=>user_can('pages'), 'popup'=>(user_can('settings') && feature_on('popup')), 'news'=>(user_can('pages') && feature_on('news')), 'certs'=>(user_can('pages') && feature_on('certs')), 'sponsors'=>(user_can('pages') && feature_on('sponsors'))];
      foreach ($nav as $k => $v){ if (empty($canNav[$k])) continue; echo '<a class="nav-i' . ($active === $k || ($active==='edit'&&$k==='pages') || ($active==='post'&&$k==='blog') ? ' on' : '') . '" href="' . admin_url('?p=' . $k) . '">' . icon($v[1], ['class'=>'ic']) . ' ' . $v[0] . '</a>'; }
      ?>
      <?php if (user_can('pages') || user_can('settings') || user_can('users')): ?>
      <div class="sb-group">System</div>
      <?php if (($user['role'] ?? '') === 'admin'): ?><a class="nav-i<?= $active==='nav'?' on':'' ?>" href="<?= admin_url('?p=nav') ?>"><?= icon('nav-list', ['class'=>'ic']) ?> Navigation</a><?php endif; ?>
      <?php if (user_can('pages')): ?><a class="nav-i<?= $active==='enquiries'?' on':'' ?>" href="<?= admin_url('?p=enquiries') ?>"><?= icon('mail', ['class'=>'ic']) ?> Enquiries</a><?php endif; ?>
      <?php if (user_can('pages') && feature_on('applications')): ?><a class="nav-i<?= $active==='applications'?' on':'' ?>" href="<?= admin_url('?p=applications') ?>"><?= icon('briefcase', ['class'=>'ic']) ?> Applications<?= $unreadApp ? ' <span class="badge">' . ($unreadApp > 99 ? '99+' : $unreadApp) . '</span>' : '' ?></a><?php endif; ?>
      <?php if (user_can('users')): ?><a class="nav-i<?= $active==='users'?' on':'' ?>" href="<?= admin_url('?p=users') ?>"><?= icon('users', ['class'=>'ic']) ?> Team &amp; Access</a><?php endif; ?>
      <?php if (user_can('settings')): ?><a class="nav-i<?= $active==='settings'?' on':'' ?>" href="<?= admin_url('?p=settings') ?>"><?= icon('gear', ['class'=>'ic']) ?> Settings</a>
      <a class="nav-i<?= $active==='backup'?' on':'' ?>" href="<?= admin_url('?p=backup') ?>"><?= icon('database', ['class'=>'ic']) ?> Backup &amp; Restore</a>
      <a class="nav-i<?= $active==='security'?' on':'' ?>" href="<?= admin_url('?p=security') ?>"><?= icon('lock', ['class'=>'ic']) ?> Security<?php $sec_hi = function_exists('security_recent_events') ? count(array_filter(security_recent_events(30), function($e){ return (int)$e['severity']>=2; })) : 0; if ($sec_hi): ?> <span class="badge"><?= $sec_hi>99?'99+':$sec_hi ?></span><?php endif; ?></a>
      <a class="nav-i<?= $active==='partners'?' on':'' ?>" href="<?= admin_url('?p=partners') ?>"><?= icon('briefcase', ['class'=>'ic']) ?> Partners</a>
      <a class="nav-i<?= $active==='hosting'?' on':'' ?>" href="<?= admin_url('?p=hosting') ?>"><?= icon('database', ['class'=>'ic']) ?> Hosting</a><?php endif; ?>
      <?php endif; ?>
    </nav>
    <?php $avImg = user_avatar_html($user); ?>
    <div class="sb-user"><div class="avatar"><?= $avImg ?></div><div class="n"><b><?= e($user['name']) ?></b><span><?= e(ucfirst($user['role'])) ?></span></div><form method="post" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="logout"><button title="Sign out"><?= icon('logout', ['class'=>'ic']) ?></button></form></div>
  </aside>
  <div class="sb-ovl" id="sbOvl" hidden></div>
  <div class="main">
    <div class="topbar"><button class="hbg-a" id="sbToggle" type="button" aria-label="Open menu"><?= icon('menu', ['class'=>'ic']) ?></button><div><div class="tb-title"><?= e($title) ?></div><?php if ($crumb): ?><div class="tb-crumb"><?= e($crumb) ?></div><?php endif; ?></div><div class="tb-right"><div class="tb-clock" title="Site date &amp; time"><?= icon('clock', ['class'=>'ic ic-sm']) ?><span id="tbClock">…</span></div><?php if (user_can('pages')): ?><a class="tb-find" id="tbSearch" href="<?= admin_url('?p=pages&focus=search') ?>" title="Search pages (Ctrl/⌘ + K)"><?= icon('search', ['class'=>'ic ic-sm']) ?><kbd id="tbFindK">⌘K</kbd></a><a class="icon-btn tb-bell" href="<?= $bellHref ?>" title="<?= e($bellTitle) ?>" aria-label="Notifications<?= $navUnread ? ' — ' . e($bellTitle) : '' ?>"><?= icon('bell', ['class'=>'ic']) ?><?= $navUnread ? '<span class="bell-n">' . ($navUnread > 99 ? '99+' : $navUnread) . '</span>' : '' ?></a><?php endif; ?><button type="button" class="theme-t" id="themeToggle" title="Light / dark mode" aria-label="Toggle light and dark mode"><?= icon('sun', ['class'=>'ic t-sun']) ?><?= icon('moon', ['class'=>'ic t-moon']) ?></button><a class="tb-live" href="<?= base_url() ?>" target="_blank"><span class="live"></span> View live site</a><div class="avatar" style="width:38px;height:38px;border-radius:10px"><?= $avImg ?></div></div></div>
    <div class="content">
<?php if ($GLOBALS['msg'] ?? null): ?><div class="toastmsg" role="status"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="m5 12 5 5L20 7"/></svg> <?= e($GLOBALS['msg']) ?></div><?php endif; ?>
<?php
  // ── ONE-TIME recovery-key reveal (provisioned at sign-in / setup / regenerate).
  //    Rendered exactly once, then forgotten — the key is never logged or re-shown.
  $reveal = $_SESSION['reveal_reset_key'] ?? null;
  if ($reveal && ($user['role'] ?? '') === 'admin') {
    unset($_SESSION['reveal_reset_key']);
    $rk = (string) ($reveal['key'] ?? ''); $rw = !empty($reveal['written']);
    echo '<div class="card card-p reveal-key" role="alert">'
       . '<div class="rk-head"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>'
       . '<b>' . ($rw ? 'Your password recovery key — save it now (shown only this once)' : 'Recovery key generated — one manual step needed') . '</b></div>'
       . ($rw
          ? '<p>A new recovery key protects <code>admin/reset.php</code> (the “forgot password” page). It has been stored in the site’s <code>.env</code> file automatically. Copy it somewhere safe (password manager) — if every login is ever lost, this key is how you get back in. It will <b>not</b> be shown again.</p>'
          : '<p>The site’s <code>.env</code> file isn’t writable, so the key below could <b>not</b> be stored automatically. Copy the whole line and add it to <code>.env</code> in the site root (cPanel → File Manager) to activate the reset page — then keep the key somewhere safe.</p>')
       . '<div class="rk-row"><code id="rkVal">' . ($rw ? e($rk) : 'ADMIN_RESET_KEY=' . e($rk)) . '</code>'
       . '<button type="button" class="btn btn-primary btn-sm" onclick="var t=document.getElementById(\'rkVal\').textContent;(navigator.clipboard?navigator.clipboard.writeText(t):Promise.reject()).then(()=>{this.textContent=\'Copied ✓\';},()=>{var r=document.createRange();r.selectNodeContents(document.getElementById(\'rkVal\'));var s=getSelection();s.removeAllRanges();s.addRange(r);document.execCommand(\'copy\');this.textContent=\'Copied ✓\';});">Copy</button></div>'
       . '<p class="rk-foot">Manage it any time under <b>Team &amp; Access → Password recovery key</b>.</p>'
       . '</div>';
  }
}
function layout_bottom(){
  echo '<footer class="adm-foot"><span>AimsHub <span style="opacity:.6">by ExpertAims</span> <span class="adm-foot-build">' . e(CMS_BUILD) . '</span>' . (cms_deploy_date() !== '' ? ' <span class="adm-foot-build" style="opacity:.7">· deployed ' . e(cms_deploy_date()) . '</span>' : '') . '</span>'
     . '<span class="adm-foot-legal">Copyrighted to <a href="https://expertaims.com" target="_blank" rel="noopener noreferrer">ExpertAims Consultancy</a> · 2012–' . date('Y') . '</span></footer>';
  echo '</div></div></div>' . icons_js() . '<script>window.CMS_ASSETS=' . json_encode(base_url('assets/')) . ';window.CMS_TZ=' . json_encode(date_default_timezone_get()) . ';</script><script src="' . asset_url('assets/admin.js') . '"></script></body></html>';
}

// ─────────────── Views ───────────────
if ($p === 'dashboard') {
  // ═══ Dashboard v3 — analytics overview. Every number is real data from
  //     this site's own database; charts are inline SVG (no libraries).
  layout_top('dashboard', 'Dashboard', 'Overview of your site');
  $hr = (int) date('G');
  $greet = $hr < 12 ? 'Good morning' : ($hr < 17 ? 'Good afternoon' : 'Good evening');

  // ── content counts ──
  $pub = 0; $draPages = 0;
  foreach (db()->query("SELECT status, COUNT(*) c FROM pages GROUP BY status") as $r){
    if ($r['status'] === 'published') $pub = (int) $r['c']; else $draPages += (int) $r['c'];
  }
  $nb = count_rows('posts'); $nm = count_rows('media');
  $draPosts = 0;
  try { $draPosts = (int) db()->query("SELECT COUNT(*) FROM posts WHERE status='draft'")->fetchColumn(); } catch (\Throwable $e) {}
  $drafts = $draPages + $draPosts;

  // ── 30-day windows in the site timezone (previous 30 days = change baseline) ──
  $todayYmd = date('Y-m-d');
  $d30 = [];
  for ($i = 29; $i >= 0; $i--) $d30[date('Y-m-d', strtotime('-' . $i . ' days'))] = 0;
  $keys30 = array_keys($d30);
  $from30 = $keys30[0];
  $from60 = date('Y-m-d', strtotime('-59 days'));

  // visits & page views — first-party `visits` table (see track_visit in app/helpers.php)
  $viewsByDay = $d30; $visitsByDay = $d30;
  $curViews = 0; $curVisits = 0; $prevViews = 0; $prevVisits = 0;
  $topPaths = []; $hasHistory = false; $trackingLive = feature_on('visits');
  try {
    if (!$trackingLive) throw new \RuntimeException('visits feature off');
    $st = db()->prepare('SELECT day, path, hits FROM visits WHERE day >= ?');
    $st->execute([$from60]);
    foreach ($st as $r){
      $d = (string) $r['day']; $h = (int) $r['hits'];
      if ($d >= $from30){
        if ($r['path'] === '~visit'){ $curVisits += $h; if (isset($visitsByDay[$d])) $visitsByDay[$d] += $h; }
        else { $curViews += $h; if (isset($viewsByDay[$d])) $viewsByDay[$d] += $h; $topPaths[$r['path']] = ($topPaths[$r['path']] ?? 0) + $h; }
        if ($d < $todayYmd) $hasHistory = true;
      } elseif ($r['path'] === '~visit'){ $prevVisits += $h; } else { $prevViews += $h; }
    }
  } catch (\Throwable $e) { $trackingLive = false; }

  // enquiries per day (created_at) + unread count
  $enqByDay = $d30; $curEnq = 0; $prevEnq = 0; $unreadEnq = 0;
  try {
    $st = db()->prepare('SELECT DATE(created_at) d, COUNT(*) c FROM submissions WHERE created_at >= ? GROUP BY DATE(created_at)');
    $st->execute([$from60 . ' 00:00:00']);
    foreach ($st as $r){
      $d = (string) $r['d']; $c = (int) $r['c'];
      if ($d >= $from30){ $curEnq += $c; if (isset($enqByDay[$d])) $enqByDay[$d] += $c; }
      else $prevEnq += $c;
    }
    $unreadEnq = (int) db()->query('SELECT COUNT(*) FROM submissions WHERE is_read=0')->fetchColumn();
  } catch (\Throwable $e) {}

  // blog posts per day (published_at)
  $postByDay = $d30; $curPosts = 0; $prevPosts = 0;
  try {
    $st = db()->prepare("SELECT published_at d, COUNT(*) c FROM posts WHERE status='published' AND published_at BETWEEN ? AND ? GROUP BY published_at");
    $st->execute([$from60, $todayYmd]);
    foreach ($st as $r){
      $d = (string) $r['d']; $c = (int) $r['c'];
      if ($d >= $from30){ $curPosts += $c; if (isset($postByDay[$d])) $postByDay[$d] += $c; }
      else $prevPosts += $c;
    }
  } catch (\Throwable $e) {}

  // % change vs the previous 30 days — real comparisons only, never invented
  $chip = function($cur, $prev){
    if ($prev > 0){
      $pc = (int) round(($cur - $prev) / $prev * 100);
      $cls = $pc > 0 ? 'up' : ($pc < 0 ? 'dn' : 'neu');
      $ar = $pc > 0 ? '<svg class="ic" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg>'
          : ($pc < 0 ? '<svg class="ic" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg>' : '');
      return '<span class="chip ' . $cls . '" title="vs previous 30 days">' . $ar . ($pc > 0 ? '+' : '') . $pc . '%</span>';
    }
    if ($cur > 0) return '<span class="chip up" title="No data in the previous 30 days to compare against">new</span>';
    return '<span class="chip neu" title="No data yet">—</span>';
  };

  echo '<div class="page-head"><div><h1>' . $greet . ', ' . e(explode(' ', $user['name'])[0]) . '</h1><p>' . date('l, F j') . ' · ' . e(setting('site_name')) . '</p></div><div class="sp"></div><div class="row-btns"><a class="btn btn-ghost btn-sm" href="' . base_url() . '" target="_blank">View live site</a><a class="btn btn-primary btn-sm" href="' . admin_url('?p=pages') . '">Edit pages</a></div></div>';

  // ── 1 · stat cards with 30-day sparklines ──
  $trackNote = !$trackingLive ? 'needs the DB upgrade'
             : (!$hasHistory ? ($curViews + $curVisits > 0 ? 'tracking started today' : 'tracking starts with the first visit') : 'last 30 days');
  $cards = [
    ['Site visits', $curVisits, $chip($curVisits, $prevVisits), array_values($visitsByDay), '#AE1626', 'var(--ea-red-soft)', 'var(--ea-red)',
     '<path d="M2 12s3.5-7 10-7 10 7 10 7-3.5 7-10 7S2 12 2 12z"/><circle cx="12" cy="12" r="3"/>', $trackNote],
    ['Page views', $curViews, $chip($curViews, $prevViews), array_values($viewsByDay), '#1a56db', 'var(--blue-soft)', 'var(--blue)',
     '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>', $trackNote],
    ['Enquiries', $curEnq, $chip($curEnq, $prevEnq), array_values($enqByDay), '#12885E', 'var(--ok-soft)', 'var(--ok)',
     '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>', 'last 30 days' . ($unreadEnq ? ' · ' . $unreadEnq . ' unread' : '')],
    ['Blog posts', $nb, ($curPosts > 0 ? '<span class="chip up" title="Published in the last 30 days">+' . $curPosts . '</span>' : '<span class="chip neu" title="Nothing published in the last 30 days">0 new</span>'), array_values($postByDay), '#B7791F', 'var(--warn-soft)', 'var(--warn)',
     '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>', 'total · ' . $curPosts . ' this month'],
  ];
  echo '<div class="db-stats">';
  foreach ($cards as $c){
    echo '<div class="card stat2"><div class="stat2-top"><span class="stat2-ico" style="background:' . $c[5] . ';color:' . $c[6] . '"><svg class="ic" viewBox="0 0 24 24">' . $c[7] . '</svg></span>' . $c[2] . '</div>'
       . '<div><div class="stat2-n">' . number_format((int) $c[1]) . '</div><div class="stat2-l">' . $c[0] . ' · ' . e($c[8]) . '</div></div>'
       . dash_spark($c[3], $c[4]) . '</div>';
  }
  echo '</div>';

  // ── 1b · quick actions — one slim strip right under the numbers (the
  //        most-used jumps sit before the charts now; dashboard v3.1 layout) ──
  $qa = [
    ['Add new post',   '?p=post',      'blog',     '<path d="M12 5v14M5 12h14"/>'],
    ['Upload media',   '?p=media',     'media',    '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/>'],
    ['Create page',    '?p=pages',     'pages',    '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6M12 18v-6M9 15h6"/>'],
    ['View enquiries', '?p=enquiries', 'pages',    '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>'],
    ['Manage team',    '?p=users',     'users',    '<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>'],
    ['Site settings',  '?p=settings',  'settings', '<circle cx="12" cy="12" r="3"/><path d="M12 1v4M12 19v4M4.2 4.2l2.8 2.8M17 17l2.8 2.8M1 12h4M19 12h4M4.2 19.8 7 17M17 7l2.8-2.8"/>'],
  ];
  echo '<div class="card card-p qa-strip"><span class="qa-strip-l">Quick actions</span><div class="qa-row">';
  foreach ($qa as $q){
    if (!user_can($q[2])) continue;
    echo '<a class="qa-tile" href="' . admin_url($q[1]) . '"><span class="qa-tic"><svg class="ic" viewBox="0 0 24 24">' . $q[3] . '</svg></span><b>' . $q[0] . '</b></a>';
  }
  echo '</div></div>';

  // ── 2 · site analytics: 30-day line chart + top-pages donut ──
  $vw = 660; $vh = 240; $gx0 = 42; $gx1 = $vw - 16; $gy0 = 18; $gy1 = $vh - 32;
  $maxRaw = max(1, max(max($viewsByDay), max($visitsByDay)));
  $mag = pow(10, max(0, (int) floor(log10($maxRaw))));
  $step = $maxRaw > 10 ? max(1, (int) ($mag / 5)) : 1;
  $maxY = (int) (ceil(($maxRaw * 1.15) / $step) * $step);
  if ($maxY < 4) $maxY = 4;
  if ($maxY % 2) $maxY++;
  $n30 = count($keys30);
  $ptsV = ''; $ptsS = ''; $i = 0;
  foreach ($keys30 as $k){
    $x = round($gx0 + $i * ($gx1 - $gx0) / ($n30 - 1), 1);
    $ptsV .= ($ptsV === '' ? '' : ' ') . $x . ',' . round($gy1 - ($viewsByDay[$k] / $maxY) * ($gy1 - $gy0), 1);
    $ptsS .= ($ptsS === '' ? '' : ' ') . $x . ',' . round($gy1 - ($visitsByDay[$k] / $maxY) * ($gy1 - $gy0), 1);
    $i++;
  }
  $hasChart = ($curViews + $curVisits) > 0;
  echo '<div class="db-row db-row-a">';
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Site analytics <span class="badge b-neu" style="font-size:10px">30 days</span></div><div class="lgd-row"><span class="lgd"><i style="background:var(--ea-red)"></i>Page views</span><span class="lgd"><i style="background:var(--blue)"></i>Visits</span></div></div>';
  if ($hasChart){
    echo '<div class="db-chart-wrap" id="dbChartWrap"><svg id="dbChart" viewBox="0 0 ' . $vw . ' ' . $vh . '" role="img" aria-label="Daily page views and visits over the last 30 days">';
    echo '<defs><linearGradient id="dbGradV" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#AE1626" stop-opacity=".16"/><stop offset="1" stop-color="#AE1626" stop-opacity="0"/></linearGradient></defs>';
    for ($g = 0; $g <= 4; $g++){
      $gy = round($gy1 - $g * ($gy1 - $gy0) / 4, 1);
      echo '<line class="chart-grid" x1="' . $gx0 . '" y1="' . $gy . '" x2="' . $gx1 . '" y2="' . $gy . '"/>';
      if ($g % 2 === 0) echo '<text class="chart-lbl" x="' . ($gx0 - 7) . '" y="' . ($gy + 3.5) . '" text-anchor="end">' . number_format($maxY * $g / 4) . '</text>';
    }
    foreach ([0, 7, 14, 21, 29] as $ix){
      $lx = round($gx0 + $ix * ($gx1 - $gx0) / ($n30 - 1), 1);
      echo '<text class="chart-lbl" x="' . $lx . '" y="' . ($vh - 12) . '" text-anchor="middle">' . date('M j', strtotime($keys30[$ix])) . '</text>';
    }
    echo '<polygon points="' . $gx0 . ',' . $gy1 . ' ' . $ptsV . ' ' . $gx1 . ',' . $gy1 . '" fill="url(#dbGradV)"/>';
    echo '<polyline class="chart-line-s" points="' . $ptsS . '"/>';
    echo '<polyline class="chart-line-v" points="' . $ptsV . '"/>';
    echo '<line id="dbVline" class="chart-vline" x1="0" x2="0" y1="' . $gy0 . '" y2="' . $gy1 . '" style="display:none"/>';
    echo '<circle id="dbDotV" class="chart-dot-v" r="4" style="display:none"/><circle id="dbDotS" class="chart-dot-s" r="4" style="display:none"/>';
    echo '</svg><div class="db-tip" id="dbTip" hidden></div></div>';
    $dashJs = ['vw' => $vw, 'x0' => $gx0, 'x1' => $gx1, 'y0' => $gy0, 'y1' => $gy1, 'max' => $maxY,
               'days' => array_map(function($k){ return date('M j', strtotime($k)); }, $keys30),
               'views' => array_values($viewsByDay), 'visits' => array_values($visitsByDay)];
    echo '<script>window.CMS_DASH=' . json_encode($dashJs, JSON_HEX_TAG) . ';</script>';
  } else {
    echo '<div class="db-empty">' . ($trackingLive
      ? 'No visit data yet — <b>tracking started today</b>.<br>The chart fills in as visitors browse the public site.'
      : 'Visit tracking needs the database upgrade.<br>Import <code>database/upgrade-2026-07-09b.sql</code> once, or sign out and back in to apply it automatically.')
      . '</div>';
  }
  echo '</div>';

  // top pages donut (top 5 paths by hits + "Other" so proportions stay true)
  arsort($topPaths);
  $tp = array_slice($topPaths, 0, 5, true);
  $tpTotal = array_sum($topPaths);
  $tpOther = $tpTotal - array_sum($tp);
  $palette = ['#AE1626', '#1a56db', '#12885E', '#B7791F', '#7C3AED'];
  $titleBySlug = []; foreach (all_pages() as $pgx) $titleBySlug[$pgx['slug']] = $pgx['title'];
  $segs = []; $legend = []; $li = 0;
  foreach ($tp as $pathK => $hitsN){
    $col = $palette[$li % 5]; $li++;
    $label = isset($titleBySlug[$pathK]) ? $titleBySlug[$pathK]
           : (strpos($pathK, 'blog-') === 0 ? 'Blog: ' . ucwords(str_replace('-', ' ', substr($pathK, 5))) : '/' . $pathK);
    $segs[] = [$label, $hitsN, $col];
    $legend[] = [$label, $hitsN, $col, $tpTotal ? round($hitsN / $tpTotal * 100) : 0];
  }
  if ($tpOther > 0){ $segs[] = ['Other', $tpOther, 'var(--muted-2)']; $legend[] = ['Other pages', $tpOther, 'var(--muted-2)', $tpTotal ? round($tpOther / $tpTotal * 100) : 0]; }
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Top pages</div><span class="badge b-neu" style="font-size:10px">30 days</span></div>';
  if ($tpTotal > 0){
    echo '<div class="donut-wrap">' . dash_donut($segs, 132, 15, number_format($tpTotal), 'views') . '<div class="dlegend">';
    foreach ($legend as $L) echo '<div class="dlegend-r"><i style="background:' . $L[2] . '"></i><b>' . e($L[0]) . '</b><span>' . number_format($L[1]) . ' · ' . $L[3] . '%</span></div>';
    echo '</div></div>';
  } else {
    echo '<div class="db-empty">No page views recorded yet.<br>Your most-visited pages will rank here.</div>';
  }
  echo '</div></div>';

  echo '<div class="db-row db-row-b">';

  // ── 3 · recent activity — real events merged from enquiries / pages / posts / media ──
  $events = [];
  try { foreach (db()->query('SELECT name, subject, created_at FROM submissions ORDER BY id DESC LIMIT 6') as $r)
    $events[] = [(int) strtotime((string) $r['created_at']), 'New enquiry — <b>' . e($r['name'] ?: '(no name)') . '</b>' . ($r['subject'] ? ' · ' . e($r['subject']) : ''), '?p=enquiries', '<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>'];
  } catch (\Throwable $e) {}
  try { foreach (db()->query('SELECT id, title, updated_at FROM pages WHERE updated_at IS NOT NULL ORDER BY updated_at DESC LIMIT 6') as $r)
    $events[] = [(int) strtotime((string) $r['updated_at']), 'Page updated — <b>' . e($r['title']) . '</b>', '?p=edit&id=' . (int) $r['id'], '<path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/>'];
  } catch (\Throwable $e) {}
  try { foreach (db()->query("SELECT id, title, published_at FROM posts WHERE status='published' AND published_at <= CURDATE() ORDER BY published_at DESC, id DESC LIMIT 6") as $r)
    $events[] = [(int) strtotime((string) $r['published_at'] . ' 00:00'), 'Post published — <b>' . e($r['title']) . '</b>', '?p=post&id=' . (int) $r['id'], '<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/>'];
  } catch (\Throwable $e) {}
  try { foreach (db()->query('SELECT filename, created_at FROM media ORDER BY id DESC LIMIT 6') as $r)
    $events[] = [(int) strtotime((string) $r['created_at']), 'Media uploaded — <b>' . e($r['filename']) . '</b>', '?p=media', '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/>'];
  } catch (\Throwable $e) {}
  usort($events, function($a, $b){ return $b[0] <=> $a[0]; });
  $events = array_slice($events, 0, 8);
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Recent activity</div></div><div class="act-list">';
  if (!$events) echo '<div class="db-empty" style="padding:24px 6px">Activity appears here as pages are edited, posts publish, media uploads and enquiries arrive.</div>';
  foreach ($events as $ev){
    echo '<a class="act-item" href="' . admin_url($ev[2]) . '"><span class="dot"><svg class="ic ic-sm" viewBox="0 0 24 24">' . $ev[3] . '</svg></span><div class="b"><p>' . $ev[1] . '</p><span>' . e(rel_time($ev[0])) . '</span></div></a>';
  }
  echo '</div></div>';

  // continue editing (localStorage — kept from the previous dashboard)
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Continue editing</div><a class="link" href="' . admin_url('?p=pages') . '">All pages</a></div><div id="dashRecent" class="rec-list"><div class="rec-empty">Open any page and it will appear here — pick up right where you left off.</div></div></div>';
  echo '</div>';

  // ── 5 · latest posts + 6 · content overview ──
  echo '<div class="db-row db-row-c">';
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Latest posts</div><a class="link" href="' . admin_url('?p=blog') . '">View all &rarr;</a></div><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Post</th><th>Author</th><th>Status</th><th>Date</th></tr></thead><tbody>';
  $lastPosts = db()->query('SELECT id, title, author, status, published_at FROM posts ORDER BY published_at DESC, id DESC LIMIT 5')->fetchAll();
  if (!$lastPosts) echo '<tr><td colspan="4" style="color:var(--muted)">No blog posts yet — <a class="link" href="' . admin_url('?p=post') . '">write the first one</a>.</td></tr>';
  foreach ($lastPosts as $po){
    $pt = $po['published_at'] ? strtotime((string) $po['published_at']) : 0;
    echo '<tr onclick="location.href=\'' . admin_url('?p=post&id=' . (int) $po['id']) . '\'"><td style="max-width:340px"><b style="white-space:normal">' . e($po['title']) . '</b></td><td>' . e($po['author'] ?: '—') . '</td><td>'
       . ($po['status'] === 'published' ? '<span class="badge b-ok"><span class="d"></span>Published</span>' : '<span class="badge b-neu"><span class="d"></span>Draft</span>')
       . '</td><td style="white-space:nowrap">' . ($pt ? e(date('M j, Y', $pt)) : '—') . '</td></tr>';
  }
  echo '</tbody></table></div></div>';

  $ov = [
    ['Published pages', $pub, '#AE1626'],
    ['Blog posts', max(0, $nb - $draPosts), '#1a56db'],
    ['Media files', $nm, '#12885E'],
    ['Drafts', $drafts, '#B7791F'],
  ];
  $ovTotal = 0; foreach ($ov as $o) $ovTotal += $o[1];
  echo '<div class="card"><div class="card-hd"><div class="sec-title">Content overview</div><span class="badge b-neu" style="font-size:10px">live counts</span></div>';
  if ($ovTotal > 0){
    echo '<div class="donut-wrap">' . dash_donut($ov, 132, 15, number_format($ovTotal), 'items') . '<div class="dlegend">';
    foreach ($ov as $o) echo '<div class="dlegend-r"><i style="background:' . $o[2] . '"></i><b>' . e($o[0]) . '</b><span>' . number_format($o[1]) . '</span></div>';
    echo '</div></div>';
  } else {
    echo '<div class="db-empty">Nothing here yet — create your first page or post.</div>';
  }
  echo '</div></div>';

  // ── 7 · system status — live server facts, nothing invented ──
  try { $myV = (string) db()->query('SELECT VERSION()')->fetchColumn(); } catch (\Throwable $e) { $myV = '—'; }
  $srv = trim((string) ($_SERVER['SERVER_SOFTWARE'] ?? '')); if ($srv === '') $srv = 'PHP ' . PHP_SAPI;
  $httpsNow = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off')
           || (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https')
           || (($_SERVER['SERVER_PORT'] ?? '') == 443);
  $forceH = ((string) setting('force_https', '0') === '1');
  if ($httpsNow && $forceH)      { $sslV = 'Active + enforced';      $sslD = 'ok';   $sslS = 'HTTPS + HSTS on every visit'; }
  elseif ($httpsNow)             { $sslV = 'Certificate active';     $sslD = 'ok';   $sslS = 'Force HTTPS is off (Settings)'; }
  elseif ($forceH)               { $sslV = 'Enforced — on HTTP now'; $sslD = 'warn'; $sslS = 'this request was not secure'; }
  else                           { $sslV = 'Not active';             $sslD = 'warn'; $sslS = 'enable after cPanel AutoSSL'; }
  $upBytes = 0; $upFiles = 0;
  try {
    $upDir = dirname(__DIR__) . '/uploads';
    if (is_dir($upDir)){
      $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($upDir, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::LEAVES_ONLY);
      foreach ($it as $f){ if ($f->isFile()){ $upBytes += $f->getSize(); $upFiles++; } }
    }
  } catch (\Throwable $e) {}
  $upMb = $upBytes >= 1048576 ? number_format($upBytes / 1048576, 1) . ' MB' : number_format($upBytes / 1024) . ' KB';
  $lastBk = (string) setting('last_backup_at', '');
  $lastBkT = $lastBk !== '' ? strtotime($lastBk) : 0;
  $mods = ['Pages' => user_can('pages'), 'Blog' => user_can('blog'), 'Media' => user_can('media'), 'Enquiries' => user_can('pages'), 'Team' => user_can('users'), 'Settings' => user_can('settings'), 'Backup' => user_can('settings')];
  $modOn = count(array_filter($mods));
  $phpOk = version_compare(PHP_VERSION, '7.4', '>=');
  $sys = [
    ['PHP', PHP_VERSION, $phpOk ? 'ok' : 'warn', $phpOk ? 'healthy' : 'ask your host to upgrade'],
    ['Database', $myV !== '' ? $myV : '—', ($myV !== '' && $myV !== '—') ? 'ok' : 'warn', 'MySQL / MariaDB'],
    ['Web server', $srv, 'ok', 'reported by the host'],
    ['SSL certificate', $sslV, $sslD, $sslS],
    ['Storage used', $upMb, 'ok', 'uploads · ' . $upFiles . ' file' . ($upFiles === 1 ? '' : 's')],
    ['Last backup', $lastBkT ? rel_time($lastBkT) : '—', $lastBkT ? 'ok' : 'warn', $lastBkT ? date('M j, Y g:i A', $lastBkT) : 'Backup &amp; Restore → download'],
    ['Build', CMS_BUILD, 'ok', 'AimsHub by ExpertAims'],
    ['Modules', $modOn . ' of ' . count($mods) . ' active', 'ok', ($user['role'] ?? '') === 'admin' ? 'full access' : 'for your role'],
  ];
  echo '<div class="card card-p db-sys"><div class="sec-title">System status</div><div class="sys-grid">';
  foreach ($sys as $s2){
    echo '<div class="sys-i"><span class="l"><span class="sys-d' . ($s2[2] === 'ok' ? '' : ' ' . $s2[2]) . '"></span>' . $s2[0] . '</span><b class="v" title="' . e($s2[1]) . '">' . e($s2[1]) . '</b><span class="s">' . $s2[3] . '</span></div>';
  }
  echo '</div></div>';

  echo '<script>(function(){try{var r=JSON.parse(localStorage.getItem("cms-recent")||"[]");if(!r.length)return;'
     . 'var h="";r.slice(0,5).forEach(function(x){h+="<a class=\"rec-row\" href=\"?p=edit&id="+encodeURIComponent(x.i)+"\"><b>"+String(x.t).replace(/[<>&]/g,"")+"</b><span>/"+String(x.s).replace(/[<>&]/g,"")+"</span></a>";});'
     . 'document.getElementById("dashRecent").innerHTML=h;}catch(e){}})();</script>';
  layout_bottom();
  exit;
}

if ($p === 'pages') {
  require_cap('pages');
  layout_top('pages', 'Pages', 'Grouped by website menu — click a page to edit');

  // one query for section counts + types per page
  $secinfo = [];
  foreach (db()->query('SELECT page_id, COUNT(*) c, GROUP_CONCAT(DISTINCT type) t FROM sections GROUP BY page_id') as $r) $secinfo[$r['page_id']] = $r;

  // 'mini' groups render as compact cards in a shared top row; the rest pack
  // into a masonry column layout so tall cards never leave empty gaps.
  // Every group has an Add box ('prefix' + optional 'seed' for pre-built sections).
  $groups = [
    'Home'      => ['mini'=>1,'desc'=>'Landing page','tint'=>'#AE1626','soft'=>'var(--ea-red-soft)','slugs'=>['home'],'prefix'=>'','addph'=>'New standalone page…',
                   'ic'=>'<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><path d="M9 22V12h6v10"/>'],
    'Main menu' => ['mini'=>1,'desc'=>'Top-level menu items','tint'=>'#5A6069','soft'=>'var(--line-2)','slugs'=>['services','projects','hosting','contact'],'prefix'=>'','addph'=>'New standalone page…',
                   'ic'=>'<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>'],
    'Company'   => ['desc'=>'Company dropdown menu','tint'=>'#1a56db','soft'=>'#E8F0FC','slugs'=>['about','clients','innovation','news','careers','companies','products'],'prefix'=>'','addph'=>'New company page…',
                   'ic'=>'<path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-3"/><path d="M9 9v.01M9 12v.01M9 15v.01M9 18v.01"/>'],
  ];

  $bySlug = []; foreach (all_pages() as $pg) $bySlug[$pg['slug']] = $pg;
  $used = [];

  $row = function($pg) use ($secinfo) {
    $si = $secinfo[$pg['id']] ?? null;
    $types = $si ? (',' . $si['t'] . ',') : '';
    $chips = '';
    if (strpos($types, ',videos,') !== false)  $chips .= '<span class="mchip">video</span>';
    if (strpos($types, ',gallery,') !== false) $chips .= '<span class="mchip g">photos</span>';
    // Non-protected pages get an inline delete button (same guards as the editor's
    // "Delete this page"). A stretched .pgrow-link keeps the whole row clickable
    // for editing while the delete button (higher z-index) captures its own click.
    $isProt = in_array($pg['slug'], cms_protected_slugs(), true);
    $del = $isProt ? '' :
      '<form method="post" class="pgrow-del" onsubmit="return confirm(' . json_encode('Delete “' . $pg['title'] . '” and all of its sections permanently?', JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ')">'
      . csrf_field() . '<input type="hidden" name="action" value="delete_page"><input type="hidden" name="id" value="' . (int) $pg['id'] . '">'
      . '<button type="submit" class="pgrow-delb" title="Delete this page" aria-label="Delete ' . e($pg['title']) . '"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button></form>';
    return '<div class="pgrow' . ($isProt ? '' : ' has-del') . '">'
      . '<a class="pgrow-link" href="' . admin_url('?p=edit&id=' . $pg['id']) . '" aria-label="Edit ' . e($pg['title']) . '"></a>'
      . '<span class="pdot' . ($pg['status'] === 'published' ? '' : ' draft') . '" title="' . ($pg['status'] === 'published' ? 'Published' : 'Draft') . '"></span>'
      . '<b>' . e($pg['title']) . '</b>' . $chips
      . '<span class="slug">/' . e($pg['slug']) . '</span>'
      . '<svg class="ic ic-sm pen" viewBox="0 0 24 24"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>'
      . $del . '</div>';
  };

  // one card = header + page rows (+ optional collapse) + note + Add box
  $card = function($label, $g, $items) use ($row) {
    $h = '<div class="card pgc' . (!empty($g['mini']) ? ' pgc-mini' : '') . '">'
       . '<div class="pgc-hd"><div class="pgc-ico" style="background:' . $g['soft'] . ';color:' . $g['tint'] . '"><svg class="ic" viewBox="0 0 24 24">' . $g['ic'] . '</svg></div>'
       . '<div><b>' . $label . '</b><span class="sub">' . $g['desc'] . '</span></div>'
       . '<span class="pgc-cnt">' . count($items) . ' ' . (count($items) === 1 ? 'page' : 'pages') . '</span></div>';

    $cut = $g['collapse'] ?? 0;
    if ($cut && count($items) > $cut + 2) {
      foreach (array_slice($items, 0, $cut) as $pg) $h .= $row($pg);
      $h .= '<details class="pmore"><summary>Show all ' . count($items) . ' pages…</summary>';
      foreach (array_slice($items, $cut) as $pg) $h .= $row($pg);
      $h .= '</details>';
    } else {
      foreach ($items as $pg) $h .= $row($pg);
    }

    if (!empty($g['note'])) {
      $h .= '<a class="pgrow pgc-note" href="' . admin_url($g['note'][1]) . '" style="color:var(--muted);font-size:12.5px">' . $g['note'][0] . ' <b style="color:var(--ea-red)">' . $g['note'][2] . ' &rarr;</b></a>';
    }
    $h .= '<form method="post" class="addrow">' . csrf_field() . '<input type="hidden" name="action" value="add_page"><input type="hidden" name="prefix" value="' . e($g['prefix'] ?? '') . '">'
        . (!empty($g['seed']) ? '<input type="hidden" name="seed" value="' . e($g['seed']) . '">' : '')
        . '<input class="inp" name="title" placeholder="' . e($g['addph'] ?? 'New page title…') . '" required><button class="btn btn-ghost btn-sm" type="submit">Add</button></form>';
    return $h . '</div>';
  };

  // collect each group's pages once, so the search counter knows the total
  $minis = []; $fulls = []; $total = 0;
  foreach ($groups as $label => $g) {
    $items = [];
    foreach ($g['slugs'] as $s) { if (isset($bySlug[$s])) { $items[] = $bySlug[$s]; $used[$s] = 1; } }
    $extra = [];
    if (!empty($g['like'])) {
      foreach ($bySlug as $s => $pg) {
        if ($s !== $g['like'] && !in_array($s, $g['slugs'], true) && strpos($s, $g['like']) === 0) { $extra[] = $pg; $used[$s] = 1; }
      }
      usort($extra, function($a, $b){ return strcasecmp($a['title'], $b['title']); });
    }
    $items = array_merge($items, $extra);
    $total += count($items);
    $html = $card($label, $g, $items);
    if (!empty($g['mini'])) $minis[] = $html; else $fulls[] = $html;
  }

  // anything not matched above → "Other pages" card (also gets an Add box)
  $left = [];
  foreach ($bySlug as $s => $pg) if (empty($used[$s])) $left[] = $pg;
  $total += count($left);
  if ($left) $fulls[] = $card('Other pages', [
    'desc'=>'Not linked from the main menu','tint'=>'var(--text-2)','soft'=>'var(--line-2)','prefix'=>'','addph'=>'New page title…',
    'ic'=>'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/>',
  ], $left);

  echo '<div class="page-head"><div><h1>Pages</h1><p>Each website menu is a card — the pages inside it match the dropdown on the live site.</p></div></div>';
  echo '<div id="pgWrap" data-total="' . $total . '">';
  echo '<div class="pg-tools"><div class="pg-search"><svg class="ic" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg>'
     . '<input class="inp" type="search" id="pgSearch" placeholder="Search pages by name or address…" autocomplete="off" aria-label="Search pages"><kbd>/</kbd></div>'
     . '<span class="pg-search-n" id="pgSearchN">' . $total . ' pages</span></div>';
  echo '<div class="pg-minis">' . implode('', $minis) . '</div>';
  echo '<div class="pg-masonry">' . implode('', $fulls) . '</div>';
  echo '<div class="pg-noresults" id="pgNoResults" hidden>No pages match — try a different word, or add the page below its group.</div>';
  echo '</div>';
  layout_bottom();
  exit;
}

// ═══ NAVIGATION MANAGER (?p=nav) — administrators only ═══════════════════════
// One control center for the site menu: publish/unpublish any custom page and
// place it in a menu group, or reorder within a group. Same pages row that the
// per-page Activation bar writes — one source of truth, always in sync.
if ($p === 'nav') {
  require_cap('pages');
  if (($user['role'] ?? '') !== 'admin') { flash('The Navigation manager is available to administrators.'); redirect(admin_url('?p=pages')); }
  layout_top('nav', 'Navigation', 'Activate custom pages into the site menu');

  $custom = [];
  foreach (all_pages() as $pg2) {
    $s = $pg2['slug'];
    if (in_array($s, cms_protected_slugs(), true) || strpos($s, 'faculty-') === 0 || strpos($s, 'press-') === 0) continue;
    $custom[] = $pg2;
  }
  $groups = ['company' => 'Company dropdown', 'services' => 'Services dropdown', 'main' => 'Main menu (top level)'];
  $inMenu = ['company' => [], 'services' => [], 'main' => []]; $off = [];
  foreach ($custom as $r) { $mp = (string) ($r['menu_parent'] ?? ''); if (isset($inMenu[$mp])) $inMenu[$mp][] = $r; else $off[] = $r; }

  // one page row: status + menu group + label + save, with optional reorder
  $navRow = function($r, $withOrder = false, $idx = 0, $count = 0){
    $mp   = (string) ($r['menu_parent'] ?? '');
    $pub  = $r['status'] === 'published';
    $label = e($r['nav_label'] ?: '');
    $h  = '<div class="nvrow">';
    $h .= '<div class="nvrow-main"><span class="pdot' . ($pub ? '' : ' draft') . '" title="' . ($pub ? 'Published' : 'Draft') . '"></span>'
        . '<div class="nvrow-t"><b>' . e($r['title']) . '</b><span class="slug">/' . e($r['slug']) . '</span></div>'
        . '<a class="btn btn-ghost btn-sm nvrow-edit" href="' . admin_url('?p=edit&id=' . $r['id']) . '">Open editor</a></div>';
    if ($withOrder) {
      $h .= '<div class="nvrow-ord">';
      $up = '<form method="post" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="nav_move"><input type="hidden" name="id" value="' . (int) $r['id'] . '"><input type="hidden" name="dir" value="up"><button class="mini"' . ($idx === 0 ? ' disabled' : '') . ' title="Move up"><svg class="ic" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg></button></form>';
      $dn = '<form method="post" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="nav_move"><input type="hidden" name="id" value="' . (int) $r['id'] . '"><input type="hidden" name="dir" value="down"><button class="mini"' . ($idx >= $count - 1 ? ' disabled' : '') . ' title="Move down"><svg class="ic" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></button></form>';
      $h .= $up . $dn . '</div>';
    }
    $sel = function($v, $c){ return (string) $v === (string) $c ? ' selected' : ''; };
    $h .= '<form method="post" class="nvrow-form">' . csrf_field() . '<input type="hidden" name="action" value="nav_save"><input type="hidden" name="id" value="' . (int) $r['id'] . '">'
        . '<select class="inp" name="status" aria-label="Status"><option value="published"' . $sel($r['status'],'published') . '>Published</option><option value="draft"' . $sel($r['status'],'draft') . '>Draft</option></select>'
        . '<select class="inp" name="menu_parent" aria-label="Menu group"><option value=""' . $sel($mp,'') . '>Not in menu</option><option value="about"' . $sel($mp,'about') . '>About Us</option><option value="programs"' . $sel($mp,'programs') . '>Programs</option><option value="main"' . $sel($mp,'main') . '>Main menu</option></select>'
        . '<input class="inp" name="nav_label" value="' . $label . '" placeholder="' . e($r['title']) . '" aria-label="Menu label">'
        . '<button class="btn btn-primary btn-sm" type="submit">Save</button></form>';
    return $h . '</div>';
  };

  echo '<div class="page-head"><div><h1>Navigation</h1><p>Publish a custom page and choose which site menu it appears in — the changes go live on the site instantly. Built-in pages, faculty and press items are managed on their own screens.</p></div></div>';

  if (!$custom) {
    echo '<div class="card card-p nv-empty"><b>No custom pages yet.</b><p style="color:var(--muted);font-size:13px;margin-top:6px">Add a page from the <a class="link" href="' . admin_url('?p=pages') . '">Pages</a> screen — new custom pages appear here so you can publish them and place them in a menu.</p></div>';
    layout_bottom(); exit;
  }

  echo '<div class="nv-wrap">';
  foreach ($groups as $gk => $glabel) {
    $items = $inMenu[$gk];
    echo '<div class="card nvcard"><div class="nvcard-hd"><b>' . e($glabel) . '</b><span class="pgc-cnt">' . count($items) . '</span></div>';
    if ($items) { $c = count($items); foreach ($items as $i => $r) echo $navRow($r, true, $i, $c); }
    else echo '<div class="nvrow nvrow-empty">No pages in this menu yet — set a page below to “' . e($glabel) . '”.</div>';
    echo '</div>';
  }
  echo '<div class="card nvcard"><div class="nvcard-hd"><b>Not in any menu</b><span class="pgc-cnt">' . count($off) . '</span><span class="nvcard-note">Live at their web address only (or still a draft)</span></div>';
  if ($off) foreach ($off as $r) echo $navRow($r, false);
  else echo '<div class="nvrow nvrow-empty">Every custom page is currently placed in a menu.</div>';
  echo '</div>';
  echo '</div>';
  layout_bottom();
  exit;
}

// ═══ BANNERS (?p=banners) — aggregate hero-banner editor over the pages table ══
// A convenience view: review and update every page's hero banner in one place.
// Writes go to the same pages.hero_* fields the per-page Banner tab uses.
if ($p === 'banners') {
  if (function_exists('theme_banners_view')) { require_cap('pages'); theme_banners_view($user); exit; }
  require_cap('pages');
  layout_top('banners', 'Banners', 'Every page hero banner in one place');
  $pglist = [];
  foreach (all_pages() as $pg2) { if (user_can_page($pg2['slug'])) $pglist[] = $pg2; }
  echo '<div class="page-head"><div><h1>Banners</h1><p>The hero banner shown at the top of each page. Update the image, headings and on/off state here — it is the same banner as each page’s Banner tab.</p></div></div>';
  echo '<div class="bnr-wrap">';
  foreach ($pglist as $r) {
    $isFacRow = strpos($r['slug'], 'faculty-') === 0;
    $imgList  = hero_images_list($r);
    $img = trim((string) ($imgList[0] ?? ''));   // thumbnail = the primary/first banner
    $prev = $img !== '' ? '<img src="' . e(media_url($img)) . '" alt="" loading="lazy" onerror="this.style.display=\'none\';this.parentNode.classList.add(\'no-img\')">'
          . (count($imgList) > 1 ? '<span class="bnr-count" title="' . count($imgList) . ' banner images rotate as a slideshow">' . count($imgList) . ' images</span>' : '') : '';
    echo '<form method="post" class="card bnr-card" action="' . admin_url('?p=banners') . '">' . csrf_field()
       . '<input type="hidden" name="action" value="save_banner"><input type="hidden" name="id" value="' . (int) $r['id'] . '">'
       . '<div class="bnr-thumb' . ($img === '' ? ' no-img' : '') . '">' . $prev . '</div>'
       . '<div class="bnr-body">'
       . '<div class="bnr-hd"><div><b>' . e($r['title']) . '</b><span class="slug">/' . e($r['slug']) . '</span></div>'
       . '<a class="btn btn-ghost btn-sm" href="' . admin_url('?p=edit&id=' . $r['id']) . '#banner">Full editor →</a></div>'
       . '<div class="bnr-grid">'
       . '<div class="field full" style="margin:0"><label>' . ($isFacRow ? 'Profile photo' : 'Banner image' . (count($imgList) > 1 ? 's' : '')) . '</label>'
       . ($isFacRow
           ? '<div class="cms-pickrow"><input class="inp" name="hero_image" value="' . e($r['hero_image']) . '" placeholder="choose or paste a path"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div>'
           : hero_images_field_html($r, true))
       . '</div>'
       . '<div class="field" style="margin:0"><label>Eyebrow</label><input class="inp" name="hero_eyebrow" value="' . e($r['hero_eyebrow']) . '"></div>'
       . '<div class="field" style="margin:0"><label>Heading</label><input class="inp" name="hero_heading" value="' . e($r['hero_heading']) . '"></div>'
       . '<div class="field full" style="margin:0"><label>Sub-text</label><input class="inp" name="hero_subtext" value="' . e($r['hero_subtext']) . '"></div>'
       . '</div>'
       . '<div class="bnr-foot"><label class="bnr-show"><input type="checkbox" name="hero_show" value="1"' . chk($r['hero_show']) . ' style="accent-color:var(--ea-red)"> Show banner on this page</label>'
       . '<button class="btn btn-primary btn-sm" type="submit">Save banner</button></div>'
       . '</div></form>';
  }
  echo '</div>';
  echo cms_media_script();
  layout_bottom();
  exit;
}

if ($p === 'edit') {
  require_cap('pages');
  $pg = page_by_id((int) ($_GET['id'] ?? 0));
  if (!$pg || !user_can_page($pg['slug'])) { redirect(admin_url('?p=pages')); }
  $secs = page_sections($pg['id']);
  // Faculty profile pages: the Banner tab is the ONE place a teacher's photo,
  // role line and short bio are edited — every appearance follows it.
  $isFac = strpos($pg['slug'], 'faculty-') === 0;
  // "Activatable" custom pages get the top Activation bar (publish + menu place).
  // Built-in menu pages, faculty and press items are managed elsewhere.
  $isProtected = in_array($pg['slug'], cms_protected_slugs(), true);
  $isPress     = strpos($pg['slug'], 'press-') === 0;
  $activatable = !$isProtected && !$isFac && !$isPress;
  $isAdminRole = (($user['role'] ?? '') === 'admin');
  layout_top('edit', 'Editing: ' . $pg['title'], '/' . $pg['slug']);
  ?>
  <script>try{var _r=JSON.parse(localStorage.getItem("cms-recent")||"[]");_r=_r.filter(function(x){return String(x.i)!=="<?= (int) $pg['id'] ?>";});_r.unshift({i:<?= (int) $pg['id'] ?>,t:<?= json_encode($pg['title']) ?>,s:<?= json_encode($pg['slug']) ?>});localStorage.setItem("cms-recent",JSON.stringify(_r.slice(0,8)));}catch(e){}</script>
  <?php $upd = trim((string) ($pg['updated_at'] ?? '')); ?>
  <form method="post" action="<?= admin_url('?p=edit&id=' . $pg['id']) ?>" data-guard><?= csrf_field() ?>
  <input type="hidden" name="action" value="save_page"><input type="hidden" name="page_id" value="<?= $pg['id'] ?>">
  <div class="page-head"><div style="display:flex;align-items:center;gap:12px"><a class="mini" href="<?= admin_url('?p=pages') ?>" style="width:36px;height:36px;border:1px solid var(--line)"><svg class="ic" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg></a><div><h1>Editing: <?= e($pg['title']) ?></h1><p>newtampappa.com/<?= e($pg['slug']==='home'?'':$pg['slug']) ?> · <a class="link" href="<?= base_url($pg['slug']==='home'?'':$pg['slug']) ?>" target="_blank">Open ↗</a><?= $upd !== '' ? ' · <span title="When this page was last saved">Last edited ' . e(date('M j, Y \a\t g:i A', strtotime($upd))) . '</span>' : '' ?></p></div></div><div class="sp"></div><div class="row-btns"><?= revisions_dropdown('page', $pg['id']) ?><button class="btn btn-primary btn-sm" name="do" value="save"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="m5 12 5 5L20 7"/></svg> Save changes</button></div></div>

  <?php if ($activatable): $curMp = (string) ($pg['menu_parent'] ?? ''); $isPub = ($pg['status'] === 'published'); ?>
  <!-- ACTIVATION BAR — one clear place to publish a custom page and put it in a menu -->
  <input type="hidden" id="statusField" name="status" value="<?= e($pg['status']) ?>">
  <div class="card act-bar <?= $isPub ? 'is-pub' : 'is-draft' ?>">
    <div class="act-hd">
      <div class="act-state">
        <span class="act-dot" aria-hidden="true"></span>
        <div><b class="act-now"><?= $isPub ? 'Published — live on the site' : 'Draft — not visible to visitors' ?></b>
        <span class="act-sub"><?= $isPub ? 'Anyone can open this page at its web address.' : 'Only you (signed in) can see this page until it is published.' ?></span></div>
      </div>
      <div class="act-btns">
        <?php if ($isPub): ?>
          <button type="submit" class="btn btn-ghost btn-sm" name="do" value="save" onclick="document.getElementById('statusField').value='draft'">Switch to draft</button>
        <?php else: ?>
          <button type="submit" class="btn btn-primary btn-sm" name="do" value="save" onclick="document.getElementById('statusField').value='published'"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M5 12h14M13 6l6 6-6 6"/></svg> Publish now</button>
        <?php endif; ?>
      </div>
    </div>
    <?php if ($isAdminRole): ?>
    <div class="act-menu">
      <div class="field" style="margin:0"><label>Show in menu</label>
        <select class="inp" name="menu_parent">
          <option value=""<?= sel($curMp,'') ?>>Not in the menu — web address only</option>
          <option value="about"<?= sel($curMp,'about') ?>>About Us dropdown</option>
          <option value="programs"<?= sel($curMp,'programs') ?>>Programs dropdown</option>
          <option value="main"<?= sel($curMp,'main') ?>>Main menu (top level)</option>
        </select>
      </div>
      <div class="field" style="margin:0"><label>Menu label <span style="color:var(--muted);font-weight:400">(blank = page title)</span></label>
        <input class="inp" name="nav_label" value="<?= e($pg['nav_label'] ?? '') ?>" placeholder="<?= e($pg['title']) ?>">
      </div>
    </div>
    <?php else: ?>
    <p class="act-editor-note"><b>Menu placement is an administrator setting.</b> <?= $curMp !== '' ? 'This page currently appears in the site menu.' : 'This page is reachable by its web address only — ask an administrator to add it to a menu.' ?></p>
    <?php endif; ?>
    <p class="act-help"><b>Draft</b> = only you can see it. &nbsp;<b>Published</b> = live at <code>/<?= e($pg['slug']) ?></code>. &nbsp;<b>Show in menu</b> = also appears in the site navigation.<?= $isAdminRole ? ' Pick a menu group above, then Publish — it applies together in one save.' : '' ?></p>
  </div>
  <?php endif; ?>

  <div class="ed-tabs"><div class="ed-tab on" data-tab="sections"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="7" rx="1"/><rect x="3" y="14" width="18" height="7" rx="1"/></svg> Content &amp; Sections <span class="cnt"><?= count($secs) ?></span></div><div class="ed-tab" data-tab="banner"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3-3-6 6"/></svg> <?= $isFac ? 'Profile Photo &amp; Banner' : 'Banner' ?></div><div class="ed-tab" data-tab="seo"><svg class="ic ic-sm" viewBox="0 0 24 24"><circle cx="11" cy="11" r="7"/><path d="m21 21-4.3-4.3"/></svg> SEO &amp; Meta</div></div>

  <!-- SECTIONS -->
  <div class="ed-panel on" data-panel="sections"><div class="blocks">
  <?php foreach ($secs as $s): ?>
    <div class="block <?= $s['visible'] ? '' : 'hidden-sec' ?>">
      <div class="block-hd"><div class="bi"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg></div>
        <div class="bt"><b><?= e($s['heading'] ?: 'Untitled section') ?> <span class="ty"><?= e($s['type']) ?></span></b><span><?= e(mb_strimwidth(strip_tags($s['body']), 0, 60, '…')) ?></span></div>
        <div class="ctrls">
          <span class="mini" title="Show or hide this section on the site" style="width:auto;padding:0 10px;gap:9px;cursor:default">
            <label style="display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:600;cursor:pointer"><input type="radio" name="sections[<?= $s['id'] ?>][visible]" value="1"<?= $s['visible'] ? ' checked' : '' ?> style="accent-color:var(--ea-red)" onchange="this.closest('.block').classList.remove('hidden-sec')"> Enabled</label>
            <label style="display:inline-flex;align-items:center;gap:4px;font-size:11.5px;font-weight:600;cursor:pointer"><input type="radio" name="sections[<?= $s['id'] ?>][visible]" value="0"<?= $s['visible'] ? '' : ' checked' ?> style="accent-color:var(--ea-red)" onchange="this.closest('.block').classList.add('hidden-sec')"> Disabled</label>
          </span>
          <?php if ($s['type'] === 'html' && strpos((string) $s['body'], 'class="wideband"') !== false): ?>
          <button class="btn btn-ghost btn-sm" name="do" value="splitphoto:<?= $s['id'] ?>" title="Move the full-width photo into its own section with an Enable/Disable switch" style="white-space:nowrap">Split photo out</button>
          <?php endif; ?>
          <button class="mini" name="do" value="up:<?= $s['id'] ?>" title="Move up"><svg class="ic" viewBox="0 0 24 24"><path d="m18 15-6-6-6 6"/></svg></button>
          <button class="mini" name="do" value="down:<?= $s['id'] ?>" title="Move down"><svg class="ic" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></button>
          <button class="mini" name="do" value="delsec:<?= $s['id'] ?>" title="Delete" onclick="return confirm('Delete this section?')"><svg class="ic" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button>
          <button type="button" class="mini" onclick="this.closest('.block').classList.toggle('open')"><svg class="ic chev" viewBox="0 0 24 24"><path d="m6 9 6 6 6-6"/></svg></button>
        </div>
      </div>
      <?php
        // Show ONLY the fields this section type actually uses (keeps the editor clean).
        $t = $s['type'];
        $showHeading = !in_array($t, ['html','stats','photo','newslist'], true);
        $showSub     = in_array($t, ['richtext','image_text','cards','videos','gallery','slider','jobs','photo','sponsors'], true) || ($t === 'quote' && feature_on('quote_forms'));
        $showBody    = !in_array($t, ['cards','photo','newslist'], true);
        $showImage   = in_array($t, ['image_text','photo'], true);
        $showButtons = in_array($t, ['image_text','cta'], true);
        $showExtra   = in_array($t, ['cards','cta'], true);
        $extraLabel  = $t === 'cta'
          ? 'Second button <span style="color:var(--muted);font-weight:400">(optional — outline style)</span>'
          : 'Cards <span style="color:var(--muted);font-weight:400">(one card per entry — title, text, image)</span>';
        $extraPh     = $t === 'cta' ? '{"button2_text":"View Programs","button2_url":"programs.html"}'
                                    : '[{"title":"…","text":"…","image":"uploads/…"}]';
        $isRich   = in_array($t, ['richtext','image_text','html'], true);
        if ($t === 'carousel') $bodyHint2 = 'one image per line:  path | caption | add "off" to hide it — shows as a 3-per-view sliding strip';
        elseif ($t === 'certs') $bodyHint2 = 'leave empty to use the global list (Certificates screen), or one per line: Code | Name | pdf-path';
        elseif ($t === 'sponsors') $bodyHint2 = 'leave empty to use the global list (Sponsors screen), or one per line: logo-path | name';
        elseif ($t === 'newslist') $bodyHint2 = 'no content needed — the listing is built automatically from your News Stories';
        elseif ($t === 'quote' && feature_on('quote_forms')) $bodyHint2 = 'one selling-point bullet per line (shown beside the quotation form)';
        else $bodyHint2 = null;
        $bodyHint = $bodyHint2 !== null ? $bodyHint2 : ($t==='videos' ? 'one YouTube link per line — optional title after a | (link | My title)'
                  : ($t==='gallery' ? 'one image path per line — optional caption after a | (path | My caption)'
                  : ($t==='slider' ? 'one slide per line — image path, optional caption after a | (path | My caption)'
                  : ($t==='stats' ? 'one stat per line — number | label (e.g. 11+ | Years Serving Tampa)'
                  : ($t==='quote' ? 'one quote per line — optional attribution after a | (Quote text | — Dr. Judith Jain). Add several to rotate them; use the Enabled/Disabled toggle above to show or hide.'
                  : ($isRich ? 'write here — formatting works like Word' : ($t==='cta' ? 'short call-to-action text' : 'short plain text')))))));
        // 'jobs' uses the friendly repeater editor below, not this Body textarea.
        $bodyRows = (($t==='videos'||$t==='gallery'||$t==='stats'||$t==='slider'||$t==='quote'||$t==='carousel'||$t==='certs'||$t==='sponsors') ? 6 : 4);
        $sid = $s['id'];
        // hidden inputs preserve values of fields we hide for this type, so saving never blanks them
        $keep = function($f) use ($sid, $s){ echo '<input type="hidden" name="sections['.$sid.']['.$f.']" value="'.e($s[$f]).'">'; };
      ?>
      <div class="block-body"><div class="fgrid">
        <?php if ($showHeading): ?><div class="field full" style="margin:0"><label>Heading</label><input class="inp" name="sections[<?= $sid ?>][heading]" value="<?= e($s['heading']) ?>"></div><?php else: $keep('heading'); endif; ?>
        <?php if ($showSub): ?><div class="field full" style="margin:0"><label>Sub-heading / eyebrow</label><input class="inp" name="sections[<?= $sid ?>][subheading]" value="<?= e($s['subheading']) ?>"></div><?php else: $keep('subheading'); endif; ?>
        <?php if ($t === 'jobs'): // friendly repeater — no JSON hand-editing; body is rebuilt server-side ?>
          <div class="field full" style="margin:0"><label>Openings <span style="color:var(--muted);font-weight:400">(each becomes a card on the page — add, delete, reorder, enable/disable)</span></label>
          <?= jobs_editor_html($sid, $s['body']) ?>
          </div>
        <?php elseif ($showBody): ?><div class="field full" style="margin:0"><label>Body <span style="color:var(--muted);font-weight:400">(<?= e($bodyHint) ?>)</span></label><textarea class="inp" rows="<?= $bodyRows ?>" name="sections[<?= $sid ?>][body]"<?= $isRich ? ' data-rich data-sectype="' . e($t) . '"' : '' ?>><?= e($s['body']) ?></textarea>
          <?php if ($t === 'gallery' || $t === 'slider'): ?><button type="button" class="btn btn-ghost btn-sm cms-pick-line" style="margin-top:8px"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Add image from Media Library</button><?php endif; ?>
        </div><?php else: $keep('body'); endif; ?>
        <?php if ($showImage): ?><div class="field full" style="margin:0"><label>Image</label><div class="cms-pickrow"><input class="inp" name="sections[<?= $sid ?>][image]" value="<?= e($s['image']) ?>" placeholder="choose or paste a path"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div></div><?php else: $keep('image'); endif; ?>
        <?php if ($showButtons): ?>
          <div class="field" style="margin:0"><label>Button text</label><input class="inp" name="sections[<?= $sid ?>][button_text]" value="<?= e($s['button_text']) ?>"></div>
          <div class="field" style="margin:0"><label>Button link</label><input class="inp" name="sections[<?= $sid ?>][button_url]" value="<?= e($s['button_url']) ?>" placeholder="/contact"></div>
        <?php else: $keep('button_text'); $keep('button_url'); endif; ?>
        <?php if ($showExtra): ?><div class="field full" style="margin:0"><label><?= $extraLabel ?></label><input class="inp" name="sections[<?= $sid ?>][extra]" value="<?= e($s['extra']) ?>" placeholder='<?= e($extraPh) ?>'></div><?php else: $keep('extra'); endif; ?>
      </div></div>
    </div>
  <?php endforeach; ?>
  </div>
  <div style="margin-top:12px;display:flex;gap:8px;flex-wrap:wrap;align-items:center"><span style="font-size:12.5px;color:var(--muted);font-weight:600">Add section:</span>
    <?php
      $secTypes = ['richtext'=>'Text','image_text'=>'Image + Text','photo'=>'Full-width Photo','cards'=>'Cards','stats'=>'Stats bar',
                   'quote'=>(feature_on('quote_forms') ? 'Quote Form' : 'Quote'),'jobs'=>'Job Openings','slider'=>'Image Slider',
                   'carousel'=>'Photo Carousel','videos'=>'Video Gallery','gallery'=>'Photo Gallery','cta'=>'Call-to-action','html'=>'Custom HTML'];
      if (feature_on('certs'))    $secTypes['certs'] = 'Certifications';
      if (feature_on('sponsors')) $secTypes['sponsors'] = 'Sponsors';
      if (feature_on('news'))     $secTypes['newslist'] = 'News Listing';
    ?>
    <?php foreach ($secTypes as $t => $lbl): ?>
      <button class="btn btn-ghost btn-sm" name="do" value="addsec:<?= $t ?>"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg> <?= $lbl ?></button>
    <?php endforeach; ?>
  </div></div>

  <!-- BANNER (faculty pages: the single "Profile photo" edit point) -->
  <div class="ed-panel" data-panel="banner"><div class="ed-layout"><div>
    <?php if ($isFac): ?>
    <div class="card card-p" style="margin-bottom:16px;border-left:3px solid var(--ea-red)">
      <b style="font-size:13.5px">This is a faculty profile page</b>
      <p style="color:var(--muted);font-size:12.5px;line-height:1.6;margin-top:5px">The <b>Profile photo</b> below is the one photo used <b>everywhere this teacher appears</b> — the homepage carousel, the Faculty page card, the photo on this page, and the social-share image. Change it here once and every spot updates together. The <b>Role / title</b> and <b>Short bio</b> fields feed the faculty cards the same way.</p>
    </div>
    <?php endif; ?>
    <div class="banner-prev" style="margin-bottom:16px"><img src="<?= e(link_url($pg['hero_image'])) ?>" alt="" onerror="this.style.display='none'"><div class="ov"><span class="eb"><?= e($pg['hero_eyebrow']) ?></span><h3><?= e($pg['hero_heading'] ?: $pg['title']) ?></h3><p><?= e($pg['hero_subtext']) ?></p></div></div>
  </div><div class="card card-p" style="position:sticky;top:20px">
    <?php if ($isFac): ?>
    <div class="field"><label>Profile photo — used everywhere this teacher appears</label><div class="cms-pickrow"><input class="inp" name="hero_image" value="<?= e($pg['hero_image']) ?>" placeholder="choose or paste a path"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div></div>
    <?php else: ?>
    <div class="field"><label>Banner image<?= count(hero_images_list($pg)) > 1 ? 's <span style="color:var(--muted);font-weight:400">(slideshow)</span>' : '' ?></label><?= hero_images_field_html($pg) ?></div>
    <?php endif; ?>
    <div class="field"><label><?= $isFac ? 'Role / title — shown under the name on faculty cards' : 'Eyebrow' ?></label><input class="inp" name="hero_eyebrow" value="<?= e($pg['hero_eyebrow']) ?>"></div>
    <div class="field"><label>Heading</label><input class="inp" name="hero_heading" value="<?= e($pg['hero_heading']) ?>"></div>
    <div class="field"><label><?= $isFac ? 'Short bio — shown on the Faculty page card' : 'Sub-text' ?></label><textarea class="inp" rows="2" name="hero_subtext"><?= e($pg['hero_subtext']) ?></textarea></div>
    <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><input type="checkbox" name="hero_show"<?= chk($pg['hero_show']) ?> style="accent-color:var(--ea-red)"> Show banner<?= $isFac ? ' <span style="color:var(--muted);font-weight:400">(faculty pages keep this off — the photo is used in the layout, not as a banner)</span>' : '' ?></label>
    <?php if (!$isFac): ?>
    <div style="margin-top:18px;padding-top:16px;border-top:1px dashed var(--line)">
      <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;margin-bottom:8px"><input type="checkbox" name="hero_quote_show"<?= chk($pg['hero_quote_show'] ?? 0) ?> style="accent-color:var(--ea-red)"> Show a quote on the banner</label>
      <div class="field" style="margin-bottom:0"><input class="inp" name="hero_quote" value="<?= e($pg['hero_quote'] ?? '') ?>" placeholder="e.g. All students may learn how to play the piano well, given the right instruction." maxlength="400"></div>
      <p style="color:var(--muted);font-size:12px;line-height:1.5;margin:6px 0 0">An optional short italic quote shown over the banner. Toggle it on/off without losing the text.</p>
    </div>
    <?php else: ?>
    <input type="hidden" name="hero_quote" value="<?= e($pg['hero_quote'] ?? '') ?>">
    <input type="hidden" name="hero_quote_show" value="<?= (int) ($pg['hero_quote_show'] ?? 0) ?>"<?= ($pg['hero_quote_show'] ?? 0) ? '' : ' disabled' ?>>
    <?php endif; ?>
    <?php if ($pg['slug'] === 'home'):
      $ctaRows = hero_ctas($pg);
      while (count($ctaRows) < 3) $ctaRows[] = ['text' => '', 'url' => ''];
      $ctaRows = array_slice($ctaRows, 0, 4);
    ?>
    <div style="margin-top:18px;padding-top:16px;border-top:1px dashed var(--line)">
      <div class="sec-title" style="font-size:12.5px;margin-bottom:4px">Hero buttons</div>
      <p style="color:var(--muted);font-size:12px;line-height:1.55;margin:0 0 10px">The call-to-action buttons on the homepage hero. The <b>first</b> button is solid purple, the rest are outlined. Leave a row blank to remove that button. Links can be a page (<code>contact.html</code>) or a full URL.</p>
      <?php foreach ($ctaRows as $c): ?>
      <div class="cms-pickrow" style="margin-bottom:8px">
        <input class="inp" name="hero_cta_text[]" value="<?= e($c['text'] ?? '') ?>" placeholder="Button text — e.g. Request Free Interview" maxlength="60">
        <input class="inp" name="hero_cta_url[]" value="<?= e($c['url'] ?? '') ?>" placeholder="contact.html" maxlength="255" style="max-width:180px">
      </div>
      <?php endforeach; ?>
      <p style="color:var(--muted);font-size:12px;line-height:1.55;margin:8px 0 0"><b>Tip:</b> the <b>Eyebrow</b> is the small label above a section title, and the <b>Heading</b> is the section title, e.g. <code>What we do</code> above <code>Seven services, one accountable team</code>.</p>
    </div>
    <?php endif; ?>
    <?php if ($isPress): $mf = (($pg['media_fit'] ?? 'logo') === 'photo') ? 'photo' : 'logo'; ?>
    <div style="margin-top:18px;padding-top:16px;border-top:1px dashed var(--line)">
      <div class="sec-title" style="font-size:12.5px;margin-bottom:4px">Image display</div>
      <p style="color:var(--muted);font-size:12px;line-height:1.55;margin:0 0 10px">How this item’s image appears on the <b>Press listing</b>. Choose <b>Logo</b> for a publication logo or crest (fit whole, on a light framed tile) or <b>Photo</b> for a photograph or poster (fills the frame edge-to-edge).</p>
      <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;margin-bottom:7px"><input type="radio" name="media_fit" value="logo"<?= $mf === 'logo' ? ' checked' : '' ?> style="accent-color:var(--ea-red)"> Logo <span style="color:var(--muted);font-weight:400">— fit &amp; frame (default)</span></label>
      <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><input type="radio" name="media_fit" value="photo"<?= $mf === 'photo' ? ' checked' : '' ?> style="accent-color:var(--ea-red)"> Photo <span style="color:var(--muted);font-weight:400">— fill the frame</span></label>
    </div>
    <div style="margin-top:18px;padding-top:16px;border-top:1px dashed var(--line)">
      <div class="sec-title" style="font-size:12.5px;margin-bottom:4px">External source</div>
      <p style="color:var(--muted);font-size:12px;line-height:1.55;margin:0 0 12px">Link this press item to the original coverage (e.g. the newspaper feature or PDF). When set, a tasteful <b>“Read the original ↗”</b> button appears on the press page.</p>
      <div class="field"><label>Source link</label><input class="inp" type="url" name="source_url" value="<?= e($pg['source_url'] ?? '') ?>" placeholder="https://example.com/article"></div>
      <div class="field" style="margin-bottom:0"><label>Source link text <span style="color:var(--muted);font-weight:400">(defaults to “Read the original”)</span></label><input class="inp" name="source_label" value="<?= e($pg['source_label'] ?? '') ?>" placeholder="Read the Tampa Beacon feature" maxlength="160"></div>
    </div>
    <?php else: ?>
    <input type="hidden" name="media_fit" value="<?= e(($pg['media_fit'] ?? 'logo') === 'photo' ? 'photo' : 'logo') ?>">
    <input type="hidden" name="source_url" value="<?= e($pg['source_url'] ?? '') ?>">
    <input type="hidden" name="source_label" value="<?= e($pg['source_label'] ?? '') ?>">
    <?php endif; ?>
  </div></div></div>

  <!-- SEO -->
  <div class="ed-panel" data-panel="seo"><div class="ed-layout"><div class="card card-p">
    <div class="sec-title" style="margin-bottom:16px">Search engine listing</div>
    <div class="field"><label>Page title <span class="counter" id="ctTitle"></span></label><input class="inp" id="seoTitle" name="meta_title" value="<?= e($pg['meta_title']) ?>" oninput="seoUpdate()"></div>
    <div class="field"><label>Meta description <span class="counter" id="ctDesc"></span></label><textarea class="inp" id="seoDesc" name="meta_description" rows="3" oninput="seoUpdate()"><?= e($pg['meta_description']) ?></textarea></div>
    <div class="fgrid" style="padding:0;grid-template-columns:1fr 1fr">
      <div class="field" style="margin:0"><label>Focus keyword</label><input class="inp" id="seoKw" name="meta_keywords" value="<?= e($pg['meta_keywords']) ?>" oninput="seoUpdate()"></div>
      <div class="field" style="margin:0"><label>Robots</label><select class="inp" name="robots"><option<?= sel($pg['robots'],'index,follow') ?>>index,follow</option><option<?= sel($pg['robots'],'noindex,follow') ?>>noindex,follow</option><option<?= sel($pg['robots'],'noindex,nofollow') ?>>noindex,nofollow</option></select></div>
      <div class="field" style="margin:0"><label>Canonical URL</label><input class="inp" name="canonical" value="<?= e($pg['canonical']) ?>"></div>
      <div class="field" style="margin:0"><label>OG / share image<?= $isFac ? ' <span style="color:var(--muted);font-weight:400">(auto — faculty pages share the Profile photo)</span>' : '' ?></label><input class="inp" name="og_image" value="<?= e($pg['og_image']) ?>"></div>
      <?php if (!$activatable): ?>
      <div class="field" style="margin:0"><label>Page status</label><select class="inp" name="status"><option<?= sel($pg['status'],'published') ?>>published</option><option<?= sel($pg['status'],'draft') ?>>draft</option></select></div>
      <?php else: ?>
      <div class="field" style="margin:0"><label>Page status</label><div class="inp act-ro"><?= $pg['status']==='published' ? 'Published' : 'Draft' ?> · set in the <b>Activation</b> bar at the top</div></div>
      <?php endif; ?>
      <div class="field" style="margin:0"><label>Page title (H1 / nav)</label><input class="inp" name="title" value="<?= e($pg['title']) ?>"></div>
    </div>
  </div><div class="grid" style="align-content:start;gap:16px;position:sticky;top:20px">
    <div class="card"><div class="card-hd" style="padding:12px 16px"><div class="sec-title" style="font-size:12.5px">Google preview</div><span class="badge b-neu" style="font-size:10px">Live</span></div><div class="serp"><div class="u"><div class="fav">N</div><div class="ut"><b><?= e(setting('site_name')) ?></b><span>https://newtampappa.com/<?= e($pg['slug']==='home'?'':$pg['slug']) ?></span></div></div><div class="st" id="pvTitle"></div><div class="sd" id="pvDesc"></div></div></div>
    <div class="card card-p"><div class="sec-title" style="font-size:12.5px;margin-bottom:12px">SEO checklist <span class="badge b-ok" id="seoPct"></span></div><div class="checks" id="seoChecks"></div></div>
  </div></div></div>
  </form>
  <?= revisions_form() ?>
  <?php if (!in_array($pg['slug'], cms_protected_slugs(), true)): ?>
  <form method="post" action="<?= admin_url('?p=pages') ?>" onsubmit="return confirm('Delete this page and all of its sections permanently?')" style="margin-top:14px"><?= csrf_field() ?><input type="hidden" name="action" value="delete_page"><input type="hidden" name="id" value="<?= (int) $pg['id'] ?>"><button class="btn btn-ghost btn-sm" style="color:var(--bad)">Delete this page</button></form>
  <?php endif; ?>
  <?= cms_media_script() ?>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'blog') {
  if (!feature_on('blog')) redirect(admin_url('?p=dashboard'));
  require_cap('blog');
  layout_top('blog', 'Blog Posts', 'Articles shown on the blog page');
  echo '<div class="page-head"><div><h1>Blog Posts</h1></div><div class="sp"></div><a class="btn btn-primary btn-sm" href="' . admin_url('?p=post') . '"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg> New post</a></div><div class="card"><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Post</th><th>Author</th><th>Date</th><th>Status</th><th style="text-align:right">Actions</th></tr></thead><tbody>';
  foreach (all_posts() as $po) echo '<tr onclick="location.href=\'' . admin_url('?p=post&id=' . $po['id']) . '\'"><td style="max-width:420px"><b style="white-space:normal">' . e($po['title']) . '</b></td><td>' . e($po['author'] ?: '—') . '</td><td>' . e($po['published_at']) . '</td><td>' . ($po['status']==='published'?'<span class="badge b-ok"><span class="d"></span>Published</span>':'<span class="badge b-neu"><span class="d"></span>Draft</span>') . '</td><td><div class="act-btns"><a class="mini" href="' . admin_url('?p=post&id=' . $po['id']) . '"><svg class="ic" viewBox="0 0 24 24"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></a></div></td></tr>';
  echo '</tbody></table></div></div>';
  layout_bottom();
  exit;
}

if ($p === 'post') {
  if (!feature_on('blog')) redirect(admin_url('?p=dashboard'));
  require_cap('blog');
  $po = ($_GET['id'] ?? 0) ? find_post_by_id((int) $_GET['id']) : null;
  $po = $po ?: ['id'=>'','title'=>'','slug'=>'','author'=>'','excerpt'=>'','body'=>'','banner'=>'','featured'=>0,'meta_title'=>'','meta_description'=>'','status'=>'published','published_at'=>date('Y-m-d'),'source_url'=>'','source_label'=>''];
  layout_top('post', $po['id'] ? 'Edit post' : 'New post');
  ?><form method="post" action="<?= admin_url('?p=post') ?>" data-guard><?= csrf_field() ?><input type="hidden" name="action" value="save_post"><input type="hidden" name="id" value="<?= e($po['id']) ?>">
  <div class="page-head"><div style="display:flex;align-items:center;gap:12px"><a class="mini" href="<?= admin_url('?p=blog') ?>" style="width:36px;height:36px;border:1px solid var(--line)"><svg class="ic" viewBox="0 0 24 24"><path d="M19 12H5M12 19l-7-7 7-7"/></svg></a><h1><?= $po['id'] ? 'Edit post' : 'New post' ?></h1></div><div class="sp"></div><div class="row-btns"><?= $po['id'] ? revisions_dropdown('post', (int) $po['id']) : '' ?><button class="btn btn-primary btn-sm">Save post</button></div></div>
  <div class="ed-layout"><div class="card card-p">
    <div class="field"><label>Title</label><input class="inp" name="title" value="<?= e($po['title']) ?>" required></div>
    <div class="field"><label>URL slug <span style="color:var(--muted);font-weight:400">(leave blank to auto-generate)</span></label><input class="inp" name="slug" value="<?= e($po['slug']) ?>"></div>
    <div class="field"><label>Excerpt</label><textarea class="inp" rows="2" name="excerpt"><?= e($po['excerpt']) ?></textarea></div>
    <div class="field"><label>Body <span style="color:var(--muted);font-weight:400">(write here — formatting works like Word)</span></label><textarea class="inp" rows="10" name="body" data-rich data-sectype="richtext"><?= e($po['body']) ?></textarea></div>
  </div><div class="card card-p" style="position:sticky;top:20px">
    <div class="field"><label>Author</label><input class="inp" name="author" value="<?= e($po['author']) ?>"></div>
    <div class="field"><label>Publish date</label><input class="inp" type="date" name="published_at" value="<?= e($po['published_at']) ?>"></div>
    <div class="field"><label>Banner image</label><div class="cms-pickrow"><input class="inp" name="banner" value="<?= e($po['banner']) ?>" placeholder="choose or paste a URL"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div></div>
    <div class="field"><label>Source link <span style="color:var(--muted);font-weight:400">(optional — link to the original article/feature)</span></label><input class="inp" type="url" name="source_url" value="<?= e($po['source_url'] ?? '') ?>" placeholder="https://example.com/article"></div>
    <div class="field"><label>Source link text <span style="color:var(--muted);font-weight:400">(defaults to “Read the original”)</span></label><input class="inp" name="source_label" value="<?= e($po['source_label'] ?? '') ?>" placeholder="Read the original" maxlength="160"></div>
    <div class="field"><label>Status</label><select class="inp" name="status"><option<?= sel($po['status'],'published') ?>>published</option><option<?= sel($po['status'],'draft') ?>>draft</option></select></div>
    <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;margin-bottom:14px"><input type="checkbox" name="featured"<?= chk($po['featured']) ?> style="accent-color:var(--ea-red)"> Featured (top of blog)</label>
    <div class="field"><label>Meta description</label><textarea class="inp" rows="2" name="meta_description"><?= e($po['meta_description']) ?></textarea></div>
  </div></div></form>
  <?php if ($po['id']) echo revisions_form(); ?>
  <?php if ($po['id']): ?><form method="post" action="<?= admin_url('?p=blog') ?>" onsubmit="return confirm('Delete this post permanently?')" style="margin-top:14px"><?= csrf_field() ?><input type="hidden" name="action" value="delete_post"><input type="hidden" name="id" value="<?= e($po['id']) ?>"><button class="btn btn-ghost btn-sm" style="color:var(--bad)">Delete post</button></form><?php endif;
  echo cms_media_script();
  layout_bottom();
  exit;
}

if ($p === 'media') {
  require_cap('media');
  layout_top('media', 'Media Library', 'Upload and manage images');
  // usage map (safe deletes) + broken-entry count (cleanup) in one pass
  $usageMap = media_usage_map();
  $broken = 0;
  $rowsM = db()->query('SELECT * FROM media ORDER BY id DESC')->fetchAll();
  foreach ($rowsM as $m2) {
    $rel = ltrim((string) $m2['path'], '/');
    if (strpos($rel, 'uploads/') === 0 && !is_file(dirname(__DIR__) . '/' . $rel)) $broken++;
  }
  echo '<div class="page-head"><div><h1>Media Library</h1><p>' . count($rowsM) . ' file' . (count($rowsM) === 1 ? '' : 's') . ' · hover a tile to delete — files still used on a page are protected</p></div>';
  if ($broken) {
    echo '<div class="sp"></div><form method="post" onsubmit="return confirm(\'Remove ' . $broken . ' broken librar' . ($broken === 1 ? 'y entry' : 'y entries') . '? Their image files are already missing from the uploads folder, so nothing visible changes.\')">' . csrf_field()
       . '<input type="hidden" name="action" value="cleanup_media"><button class="btn btn-ghost btn-sm" type="submit" title="Delete library entries whose file no longer exists on the server"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg> Clean up ' . $broken . ' broken entr' . ($broken === 1 ? 'y' : 'ies') . '</button></form>';
  }
  echo '</div>';
  echo '<form method="post" enctype="multipart/form-data" action="' . admin_url('?p=media') . '" style="margin-bottom:18px">' . csrf_field() . '<input type="hidden" name="action" value="upload_media"><label class="dropzone" style="display:block"><svg class="ic" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12"/></svg><b style="color:var(--text)">Choose images (or PDFs) to upload</b><br><span style="color:var(--muted);font-size:12px">Select one or several files — JPG/PNG/WebP/GIF/SVG images, or PDFs (e.g. a recital programme).</span><br><input type="file" name="file[]" multiple accept="image/*,application/pdf" onchange="this.form.submit()" style="margin-top:8px"></label></form>';
  echo '<div class="media-grid">';
  foreach ($rowsM as $mm) {
    $ispdf = strtolower(pathinfo($mm['path'], PATHINFO_EXTENSION)) === 'pdf';
    $thumb = $ispdf
      ? '<a href="' . base_url(e($mm['path'])) . '" target="_blank" style="display:flex;align-items:center;justify-content:center;height:100%;background:var(--ea-red-soft);color:var(--ea-red);font:700 13px/1 sans-serif;text-decoration:none">PDF</a>'
      : '<img src="' . base_url(e($mm['path'])) . '" loading="lazy">';
    $used = $usageMap[$mm['path']] ?? [];
    $rel = ltrim((string) $mm['path'], '/');
    $isBroken = strpos($rel, 'uploads/') === 0 && !is_file(dirname(__DIR__) . '/' . $rel);
    $usedTitle = $used ? 'Used by: ' . implode(' · ', array_slice($used, 0, 6)) . (count($used) > 6 ? ' +' . (count($used) - 6) . ' more' : '') : 'Not referenced by any page, post or setting';
    // per-tile delete: in-use files require the extra "delete anyway" consent
    $delMsg = $used
      ? 'This file is STILL USED by: ' . implode(', ', array_slice($used, 0, 4)) . (count($used) > 4 ? ' +' . (count($used) - 4) . ' more' : '') . '.\n\nDelete anyway? Those spots will show a broken image until you pick a new one.'
      : 'Delete “' . $mm['filename'] . '” permanently? It isn’t used on any page.';
    echo '<div class="media-i' . ($isBroken ? ' m-broken' : '') . '"><div class="th">' . $thumb
       . '<form method="post" class="m-del" onsubmit="return confirm(' . json_encode($delMsg, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ')">' . csrf_field()
       . '<input type="hidden" name="action" value="delete_media"><input type="hidden" name="id" value="' . (int) $mm['id'] . '">'
       . ($used ? '<input type="hidden" name="force" value="1">' : '')
       . '<button type="submit" class="m-del-b" title="Delete this file"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button></form>'
       . ($isBroken ? '<span class="m-flag bad" title="The image file is missing from the uploads folder — use “Clean up broken entries”">file missing</span>'
          : ($used ? '<span class="m-flag" title="' . e($usedTitle) . '">in use · ' . count($used) . '</span>' : ''))
       . '</div><div class="mm"><b>' . e($mm['filename']) . '</b><span>' . e($mm['path']) . '</span>'
       . '<div class="m-acts">'
       . (!$ispdf
          ? '<form method="post" enctype="multipart/form-data" class="m-rep" action="' . admin_url('?p=media') . '">' . csrf_field()
            . '<input type="hidden" name="action" value="replace_media"><input type="hidden" name="id" value="' . (int) $mm['id'] . '">'
            . '<label class="m-act" title="Upload a new image to replace this one — it updates everywhere this file is used">'
            . icon('upload', ['size' => 13]) . '<span>Replace</span>'
            . '<input type="file" name="newfile" accept="image/jpeg,image/png,image/webp,image/gif,image/svg+xml,.jpg,.jpeg,.png,.webp,.gif,.svg" style="display:none" onchange="if(this.files.length){this.closest(\'.media-i\').classList.add(\'m-busy\');this.form.submit();}"></label>'
            . '</form>'
          : '')
       . '<button type="button" class="m-act m-copy" data-path="' . e($mm['path']) . '" title="Copy this file’s path to paste into a page banner or image field">' . icon('link', ['size' => 13]) . '<span>Copy path</span></button>'
       . '</div></div></div>';
  }
  echo '</div>';
  if (!count_rows('media')) echo '<p style="color:var(--muted)">No images yet — upload one above, then paste its path (e.g. <code>uploads/…</code>) into a page banner or section.</p>';
  // Recommended sizes — one reference for every image slot on the site.
  echo '<div class="card card-p" style="max-width:860px;margin-top:22px"><div class="sec-title" style="margin-bottom:12px">Recommended image sizes</div>'
     . '<p style="color:var(--muted);font-size:12.5px;margin:0 0 12px">Bigger is fine — the site crops and scales automatically. JPG for photos, transparent PNG for logos. Keep photos under ~1&nbsp;MB for fast loading.</p>'
     . '<table style="width:100%;border-collapse:collapse;font-size:13px">';
  foreach ([
    ['Page banner (Banner tab)', '1920 × 800 px', 'wide landscape JPG'],
    ['Blog post banner', '1920 × 800 px', 'wide landscape JPG'],
    ['Image + Text section', '1000 × 750 px', 'landscape JPG'],
    ['Photo Gallery images', '800 × 600 px', '4:3 JPG'],
    ['Cards section images', '600 × 400 px', 'landscape JPG'],
    ['Announcement popup image (Settings)', '900 × 1100 px', 'portrait JPG/PNG'],
    ['Team profile photo (Team & Access)', '256 × 256 px', 'square JPG/PNG'],
    ['OG / share image (SEO tab)', '1200 × 630 px', 'JPG — used by WhatsApp / social previews'],
  ] as $r) {
    echo '<tr style="border-top:1px solid var(--line)"><td style="padding:8px 10px 8px 0;color:var(--text)">' . $r[0] . '</td><td style="padding:8px 10px;white-space:nowrap"><b>' . $r[1] . '</b></td><td style="padding:8px 0;color:var(--muted)">' . $r[2] . '</td></tr>';
  }
  echo '</table></div>';
  // Recommended sizes — one reference for every image slot on the site.
  echo '<div class="card card-p" style="max-width:860px;margin-top:22px"><div class="sec-title" style="margin-bottom:12px">Recommended image sizes</div>'
     . '<p style="color:var(--muted);font-size:12.5px;margin:0 0 12px">Bigger is fine — the site crops and scales automatically. JPG for photos, transparent PNG for logos. Keep photos under ~1&nbsp;MB for fast loading.</p>'
     . '<table style="width:100%;border-collapse:collapse;font-size:13px">';
  $sizeRows = [
    ['Page banner / story banner / default banner', '1920 × 800 px', 'wide landscape JPG'],
    ['Full-width Photo section', '1920 × 700 px', 'wide landscape JPG'],
    ['Image + Text section', '1000 × 750 px', 'landscape JPG'],
    ['Photo Carousel / Slider images', '900 × 640 px', 'landscape JPG'],
    ['Photo Gallery images', '800 × 600 px', '4:3 JPG'],
    ['Partner / sponsor logos', '400 × 240 px', 'transparent PNG'],
    ['Site & client logos (Settings)', '512 × 512 px or larger', 'transparent PNG'],
    ['OG / share image (SEO tab)', '1200 × 630 px', 'JPG — WhatsApp / social previews'],
  ];
  foreach ($sizeRows as $r) {
    echo '<tr style="border-top:1px solid var(--line)"><td style="padding:8px 10px 8px 0;color:var(--text)">' . $r[0] . '</td><td style="padding:8px 10px;white-space:nowrap"><b>' . $r[1] . '</b></td><td style="padding:8px 0;color:var(--muted)">' . $r[2] . '</td></tr>';
  }
  echo '</table></div>';
  layout_bottom();
  exit;
}

if ($p === 'certificates') {
  require_cap('pages');
  $action = $_GET['action'] ?? 'list';
  $cert_id = (int) ($_GET['id'] ?? 0);

  // Handle certificate generation form submission
  if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'generate_certificate') {
    csrf_check();

    $name = mb_substr(trim((string) ($_POST['name'] ?? '')), 0, 160);
    $email = mb_substr(trim((string) ($_POST['email'] ?? '')), 0, 190);
    $education = mb_substr(trim((string) ($_POST['education'] ?? '')), 0, 255);
    $institution = mb_substr(trim((string) ($_POST['institution'] ?? '')), 0, 255);
    $country = mb_substr(trim((string) ($_POST['country'] ?? '')), 0, 120);
    $role = mb_substr(trim((string) ($_POST['role'] ?? '')), 0, 160);
    $location = mb_substr(trim((string) ($_POST['location'] ?? '')), 0, 160);
    $signatory_name = mb_substr(trim((string) ($_POST['signatory_name'] ?? '')), 0, 160);
    $signatory_title = mb_substr(trim((string) ($_POST['signatory_title'] ?? '')), 0, 160);
    $start_date = trim((string) ($_POST['start_date'] ?? ''));
    $end_date = trim((string) ($_POST['end_date'] ?? ''));
    $issued_date = trim((string) ($_POST['issued_date'] ?? date('Y-m-d')));
    $description = trim((string) ($_POST['description'] ?? ''));
    $status = ($_POST['status'] ?? 'published') === 'draft' ? 'draft' : 'published';

    // Validation
    if ($name === '') {
      flash('Internee name is required');
      redirect(admin_url('?p=certificates&action=new'));
    }

    try {
      new DateTime($start_date);
      new DateTime($end_date);
      new DateTime($issued_date);
    } catch (\Throwable $e) {
      flash('Invalid date format');
      redirect(admin_url('?p=certificates&action=new'));
    }

    if (strtotime($end_date) < strtotime($start_date)) {
      flash('End date must be after or equal to start date');
      redirect(admin_url('?p=certificates&action=new'));
    }

    // Generate certificate ID
    $cert_id = generate_cert_id();

    // Prepare data
    $data = [
      'internee_name' => $name,
      'email' => $email,
      'education' => $education,
      'institution' => $institution,
      'country' => $country,
      'role' => $role,
      'company' => 'ExpertAims Consultancy LLP',
      'location' => $location,
      'start_date' => $start_date,
      'end_date' => $end_date,
      'issued_date' => $issued_date,
      'description' => $description,
      'signatory_name' => $signatory_name,
      'signatory_title' => $signatory_title,
      'cert_id' => $cert_id
    ];

    // Generate PDF
    $pdf_result = generate_certificate_pdf($data);

    if (!$pdf_result['success']) {
      flash('Error generating certificate: ' . $pdf_result['message']);
      redirect(admin_url('?p=certificates&action=new'));
    }

    // Save to database
    $db_result = save_certificate_to_database($data, $pdf_result['path']);

    if (!$db_result['success']) {
      flash('Error saving certificate: ' . $db_result['message']);
      redirect(admin_url('?p=certificates&action=new'));
    }

    // Go to the certificate's page with an inline preview of what was generated
    $newId = (int) db()->lastInsertId();
    flash('✓ Certificate ' . $db_result['cert_id'] . ' generated and saved.');
    redirect(admin_url('?p=certificates&action=view&id=' . $newId . '&generated=1'));
  }

  // Serve/preview the generated certificate file (auth-gated; the certificates/
  // folder itself is blocked from direct web access by its .htaccess).
  if ($action === 'download' && $cert_id > 0) {
    $cert = db()->prepare('SELECT * FROM certificates WHERE id = ?');
    $cert->execute([$cert_id]); $cert = $cert->fetch();
    $file = $cert ? dirname(__DIR__) . '/certificates/' . basename((string) $cert['pdf_file_path']) : '';
    if (!$cert || $cert['pdf_file_path'] === '' || !is_file($file)) { http_response_code(404); exit('Certificate file not found.'); }
    $inline = isset($_GET['inline']);
    $isPdf = strtolower(pathinfo($file, PATHINFO_EXTENSION)) === 'pdf';
    header('Content-Type: ' . ($isPdf ? 'application/pdf' : 'text/html; charset=utf-8'));
    header('Content-Disposition: ' . ($inline ? 'inline' : 'attachment') . '; filename="' . basename($file) . '"');
    header('Content-Length: ' . filesize($file));
    header('X-Content-Type-Options: nosniff');
    readfile($file);
    exit;
  }

  // Handle certificate deletion (hard delete + remove the generated PDF file)
  if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'delete_certificate') {
    csrf_check();
    $del_id = (int) ($_POST['id'] ?? 0);
    $row = $del_id ? db()->prepare('SELECT certificate_id, pdf_file_path FROM certificates WHERE id = ?') : null;
    if ($row) { $row->execute([$del_id]); $row = $row->fetch(); }
    if (!$row) {
      flash('That certificate no longer exists.');
      redirect(admin_url('?p=certificates'));
    }
    // remove the row from the DB
    db()->prepare('DELETE FROM certificates WHERE id = ?')->execute([$del_id]);
    // remove the generated PDF/HTML file if present (traversal-guarded to the certificates/ folder)
    $pf = (string) ($row['pdf_file_path'] ?? '');
    if ($pf !== '') {
      $dir = dirname(__DIR__) . '/certificates';
      $full = realpath($dir . '/' . basename($pf));
      if ($full !== false && strpos($full, realpath($dir) . DIRECTORY_SEPARATOR) === 0 && is_file($full)) @unlink($full);
    }
    flash('Certificate ' . e((string) $row['certificate_id']) . ' deleted.');
    redirect(admin_url('?p=certificates'));
  }

  // List certificates
  if ($action === 'list') {
    layout_top('certificates', 'Certificates', 'Generate and manage internship certificates');

    $certs = db()->query('SELECT * FROM certificates ORDER BY issued_date DESC')->fetchAll();

    echo '<div class="page-head"><div><h1>Internship Certificates</h1><p>' . count($certs) . ' certificate' . (count($certs) === 1 ? '' : 's') . '</p></div>';
    echo '<a href="' . admin_url('?p=certificates&action=new') . '" class="btn btn-primary" style="margin-left:auto"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M12 5v14M5 12h14"/></svg> New Certificate</a>';
    echo '</div>';

    if (empty($certs)) {
      echo '<p style="color:var(--muted)">No certificates yet. <a href="' . admin_url('?p=certificates&action=new') . '">Generate one</a></p>';
    } else {
      echo '<table class="table" style="width:100%"><thead><tr><th>Certificate ID</th><th>Internee</th><th>Issue Date</th><th>Status</th><th style="text-align:right">Actions</th></tr></thead><tbody>';
      foreach ($certs as $c) {
        $status_badge = $c['status'] === 'published' ? '<span style="color:var(--ok);font-weight:500">Published</span>' : '<span style="color:var(--muted)">Draft</span>';
        echo '<tr><td><b>' . e($c['certificate_id']) . '</b></td><td>' . e($c['internee_name']) . '</td><td>' . (new DateTime($c['issued_date']))->format('d M Y') . '</td><td>' . $status_badge . '</td><td style="text-align:right">';
        echo '<a href="' . admin_url('?p=certificates&action=view&id=' . (int) $c['id']) . '" class="btn btn-ghost btn-sm">View</a> ';
        echo '<a href="' . admin_url('?p=certificates&action=edit&id=' . (int) $c['id']) . '" class="btn btn-ghost btn-sm">Edit</a> ';
        $delMsg = 'Delete certificate ' . $c['certificate_id'] . ' for ' . $c['internee_name'] . '?\n\nThis permanently removes it from the database and deletes its PDF. Anyone verifying this ID afterwards will see \'not found\'. This cannot be undone.';
        echo '<form method="post" style="display:inline" onsubmit="return confirm(' . json_encode($delMsg, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP) . ')">' . csrf_field()
           . '<input type="hidden" name="action" value="delete_certificate"><input type="hidden" name="id" value="' . (int) $c['id'] . '">'
           . '<button type="submit" class="btn btn-ghost btn-sm" style="color:var(--ea-red,#c41e3a)">Delete</button></form>';
        echo '</td></tr>';
      }
      echo '</tbody></table>';
    }

    layout_bottom();
    exit;
  }

  // Generate new certificate form
  if ($action === 'new') {
    layout_top('certificates', 'New Certificate', 'Generate a new internship certificate');

    $next_id = generate_cert_id();

    echo '<div class="card" style="max-width:600px"><div style="padding:20px">';
    echo '<h2 style="margin:0 0 20px">Generate New Certificate</h2>';
    echo '<form method="post" style="display:grid;gap:15px">';
    echo csrf_field();
    echo '<input type="hidden" name="action" value="generate_certificate">';

    // Internee details section
    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px">';
    echo '<div style="font-weight:600;margin-bottom:10px;font-size:13px;color:var(--muted)">INTERNEE DETAILS</div>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Internee Name *</span><input type="text" name="name" required style="width:100%;" placeholder="e.g., Aafrah Hajira"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Email (optional)</span><input type="email" name="email" style="width:100%;" placeholder="internee@example.com"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Education *</span><input type="text" name="education" required style="width:100%;" placeholder="e.g., B.Tech in Electrical & Electronics Engineering"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Institution *</span><input type="text" name="institution" required style="width:100%;" placeholder="e.g., National Institute of Technology Karnataka"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Country *</span><input type="text" name="country" required list="country-list" style="width:100%;" placeholder="e.g., India"></label>';
    echo cert_country_datalist();
    echo '</div>';

    // Internship details section
    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px">';
    echo '<div style="font-weight:600;margin-bottom:10px;font-size:13px;color:var(--muted)">INTERNSHIP DETAILS</div>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Role *</span><input type="text" name="role" required style="width:100%;" placeholder="e.g., Summer Internship"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Location *</span>';
    echo '<select name="location" required style="width:100%;padding:6px 8px;border:1px solid var(--line);border-radius:4px;background:var(--input-bg);color:var(--text)">';
    echo '<option value="">Select a location</option>';
    echo '<option value="Kuwait Office">Kuwait Office</option>';
    echo '<option value="India Office">India Office</option>';
    echo '<option value="Malaysia Office">Malaysia Office</option>';
    echo '</select></label>';
    echo '</div>';

    // Dates section
    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px">';
    echo '<div style="font-weight:600;margin-bottom:10px;font-size:13px;color:var(--muted)">INTERNSHIP PERIOD</div>';
    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Start Date *</span><input type="date" name="start_date" required style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">End Date *</span><input type="date" name="end_date" required style="width:100%;"></label>';
    echo '</div>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Issue Date</span><input type="date" name="issued_date" value="' . date('Y-m-d') . '" style="width:100%;"></label>';
    echo '</div>';

    // Authorised signatory section (which director signs this certificate)
    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px">';
    echo '<div style="font-weight:600;margin-bottom:10px;font-size:13px;color:var(--muted)">AUTHORISED SIGNATORY</div>';
    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Signatory Name *</span><input type="text" name="signatory_name" required value="Deepu Soman" list="signatory-list" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Designation *</span><input type="text" name="signatory_title" required value="Managing Partner" style="width:100%;"></label>';
    echo '</div>';
    echo cert_signatory_datalist();
    echo '<p style="margin:8px 0 0;font-size:11px;color:var(--muted)">The director who signs this certificate. Their name &amp; designation appear above the signature line.</p>';
    echo '</div>';

    // Description
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Description (optional)</span><textarea name="description" style="width:100%;min-height:80px;padding:8px;border:1px solid var(--line);border-radius:4px;background:var(--input-bg);color:var(--text);font-family:inherit" placeholder="Brief description of achievements during internship..."></textarea></label>';

    // Certificate ID (read-only)
    echo '<div style="padding:12px;background:var(--ok-soft);border-left:3px solid var(--ok);border-radius:4px">';
    echo '<span style="font-size:12px;color:var(--muted)">Certificate ID (auto-generated)</span><br>';
    echo '<span style="font-weight:600;color:var(--text);font-size:16px">' . e($next_id) . '</span>';
    echo '</div>';

    // Status
    echo '<div><span style="display:block;margin-bottom:8px;font-weight:500;font-size:13px">Status</span>';
    echo '<label style="margin-right:20px"><input type="radio" name="status" value="published" checked> Published (visible on /verify page)</label>';
    echo '<label><input type="radio" name="status" value="draft"> Draft (hidden)</label>';
    echo '</div>';

    // Submit
    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:20px;border-top:1px solid var(--line);padding-top:15px">';
    echo '<a href="' . admin_url('?p=certificates') . '" class="btn btn-ghost">Cancel</a>';
    echo '<button type="submit" class="btn btn-primary">Generate Certificate</button>';
    echo '</div>';

    echo '</form></div></div>';

    layout_bottom();
    exit;
  }

  // View certificate
  if ($action === 'view' && $cert_id > 0) {
    $cert = db()->prepare('SELECT * FROM certificates WHERE id = ?')->execute([$cert_id])->fetch();
    if (!$cert) {
      flash('Certificate not found');
      redirect(admin_url('?p=certificates'));
    }

    layout_top('certificates', 'Certificate Details', e($cert['internee_name']));

    echo '<div class="page-head"><div><h1>' . e($cert['internee_name']) . '</h1><p>Certificate ID: <b>' . e($cert['certificate_id']) . '</b></p></div>';
    echo '<div style="display:flex;gap:10px;margin-left:auto">';
    if ($cert['pdf_file_path']) echo '<a href="' . admin_url('?p=certificates&action=download&id=' . $cert['id']) . '" class="btn btn-primary"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download PDF</a>';
    echo '<a href="' . admin_url('?p=certificates&action=edit&id=' . $cert['id']) . '" class="btn btn-secondary">Edit</a>';
    echo '<a href="' . admin_url('?p=certificates') . '" class="btn btn-ghost">Back</a>';
    echo '</div></div>';

    if (isset($_GET['generated'])) {
      echo '<div style="padding:12px 16px;background:var(--ok-soft);border-left:3px solid var(--ok);border-radius:6px;margin-bottom:16px;font-size:14px"><b>Certificate generated.</b> Preview below — scan the QR with your phone to test verification, or download the PDF.</div>';
    }

    // ── Inline preview of the generated certificate ──
    if ($cert['pdf_file_path']) {
      $isPdf = strtolower(pathinfo((string) $cert['pdf_file_path'], PATHINFO_EXTENSION)) === 'pdf';
      $src = admin_url('?p=certificates&action=download&inline=1&id=' . $cert['id']);
      echo '<div class="card" style="padding:0;overflow:hidden;margin-bottom:18px;background:#525659">';
      if ($isPdf) {
        echo '<object data="' . e($src) . '#view=FitH&toolbar=1" type="application/pdf" style="width:100%;height:62vh;display:block;border:0">';
        echo '<iframe src="' . e($src) . '" style="width:100%;height:62vh;border:0" title="Certificate preview"></iframe></object>';
      } else {
        echo '<iframe src="' . e($src) . '" style="width:100%;height:62vh;border:0;background:#fff" title="Certificate preview"></iframe>';
      }
      echo '</div>';
    }

    echo '<div class="card" style="max-width:820px"><div class="sec-title" style="padding:14px 16px 0;font-weight:600">Certificate details</div><table style="width:100%;border-collapse:collapse;font-size:14px;padding:16px">';
    $fields = [
      'internee_name' => 'Internee Name',
      'email' => 'Email',
      'education' => 'Education',
      'institution' => 'Institution',
      'country' => 'Country',
      'role' => 'Role',
      'location' => 'Location',
      'start_date' => 'Start Date',
      'end_date' => 'End Date',
      'issued_date' => 'Issue Date',
      'signatory_name' => 'Signed By',
      'signatory_title' => 'Designation',
      'description' => 'Description',
      'status' => 'Status',
    ];

    foreach ($fields as $key => $label) {
      $value = $cert[$key] ?? '';
      if (in_array($key, ['start_date', 'end_date', 'issued_date']) && $value) {
        $value = (new DateTime($value))->format('d F Y');
      }
      if ($key === 'status') {
        $value = $value === 'published' ? '<span style="color:var(--ok);font-weight:500">Published</span>' : '<span style="color:var(--muted)">Draft</span>';
      }
      echo '<tr style="border-top:1px solid var(--line)"><td style="padding:12px 10px 12px 16px;width:150px;font-weight:500;color:var(--muted)">' . e($label) . '</td><td style="padding:12px 16px 12px 0">' . ($key === 'status' ? $value : e($value)) . '</td></tr>';
    }
    echo '</table></div>';

    layout_bottom();
    exit;
  }

  // Edit certificate
  if ($action === 'edit' && $cert_id > 0) {
    $cert = db()->prepare('SELECT * FROM certificates WHERE id = ?')->execute([$cert_id])->fetch();
    if (!$cert) {
      flash('Certificate not found');
      redirect(admin_url('?p=certificates'));
    }

    if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'update_certificate') {
      csrf_check();

      $name = mb_substr(trim((string) ($_POST['name'] ?? '')), 0, 160);
      $email = mb_substr(trim((string) ($_POST['email'] ?? '')), 0, 190);
      $education = mb_substr(trim((string) ($_POST['education'] ?? '')), 0, 255);
      $institution = mb_substr(trim((string) ($_POST['institution'] ?? '')), 0, 255);
      $country = mb_substr(trim((string) ($_POST['country'] ?? '')), 0, 120);
      $role = mb_substr(trim((string) ($_POST['role'] ?? '')), 0, 160);
      $location = mb_substr(trim((string) ($_POST['location'] ?? '')), 0, 160);
      $signatory_name = mb_substr(trim((string) ($_POST['signatory_name'] ?? '')), 0, 160) ?: 'Deepu Soman';
      $signatory_title = mb_substr(trim((string) ($_POST['signatory_title'] ?? '')), 0, 160) ?: 'Managing Partner';
      $start_date = trim((string) ($_POST['start_date'] ?? ''));
      $end_date = trim((string) ($_POST['end_date'] ?? ''));
      $issued_date = trim((string) ($_POST['issued_date'] ?? date('Y-m-d')));
      $description = trim((string) ($_POST['description'] ?? ''));
      $status = ($_POST['status'] ?? 'published') === 'draft' ? 'draft' : 'published';

      if ($name === '') {
        flash('Internee name is required');
        redirect(admin_url('?p=certificates&action=edit&id=' . $cert['id']));
      }

      try {
        new DateTime($start_date);
        new DateTime($end_date);
        new DateTime($issued_date);
      } catch (\Throwable $e) {
        flash('Invalid date format');
        redirect(admin_url('?p=certificates&action=edit&id=' . $cert['id']));
      }

      // Regenerate the PDF so the file reflects the edits
      $pdf_result = generate_certificate_pdf([
        'internee_name' => $name, 'education' => $education, 'institution' => $institution,
        'country' => $country, 'role' => $role, 'location' => $location, 'start_date' => $start_date,
        'end_date' => $end_date, 'issued_date' => $issued_date, 'description' => $description,
        'signatory_name' => $signatory_name, 'signatory_title' => $signatory_title, 'cert_id' => $cert['certificate_id'],
      ]);
      $pdf_path = $pdf_result['success'] ? basename($pdf_result['path']) : (string) $cert['pdf_file_path'];

      // Update database
      $st = db()->prepare('
        UPDATE certificates
        SET internee_name=?, email=?, education=?, institution=?, country=?, role=?, location=?, start_date=?, end_date=?, issued_date=?, description=?, signatory_name=?, signatory_title=?, pdf_file_path=?, status=?
        WHERE id = ?
      ');

      $st->execute([$name, $email, $education, $institution, $country, $role, $location, $start_date, $end_date, $issued_date, $description, $signatory_name, $signatory_title, $pdf_path, $status, $cert['id']]);

      flash('Certificate updated and PDF regenerated.');
      redirect(admin_url('?p=certificates&action=view&id=' . $cert['id'] . '&generated=1'));
    }

    layout_top('certificates', 'Edit Certificate', e($cert['internee_name']));

    echo '<div class="page-head"><div><h1>Edit Certificate</h1><p>' . e($cert['certificate_id']) . ' — ' . e($cert['internee_name']) . '</p></div>';
    echo '<a href="' . admin_url('?p=certificates&action=view&id=' . $cert['id']) . '" class="btn btn-ghost" style="margin-left:auto">Back to details</a>';
    echo '</div>';

    echo '<div class="card" style="max-width:600px"><div style="padding:20px">';
    echo '<form method="post" style="display:grid;gap:15px">';
    echo csrf_field();
    echo '<input type="hidden" name="action" value="update_certificate">';

    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Internee Name *</span><input type="text" name="name" required value="' . e($cert['internee_name']) . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Email</span><input type="email" name="email" value="' . e($cert['email'] ?? '') . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Education *</span><input type="text" name="education" required value="' . e($cert['education']) . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Institution *</span><input type="text" name="institution" required value="' . e($cert['institution']) . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Country *</span><input type="text" name="country" required list="country-list" value="' . e($cert['country'] ?? '') . '" style="width:100%;" placeholder="e.g., India"></label>';
    echo cert_country_datalist();

    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px">';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Role *</span><input type="text" name="role" required value="' . e($cert['role']) . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;margin-top:10px;font-weight:500;font-size:13px">Location *</span>';
    echo '<select name="location" required style="width:100%;padding:6px 8px;border:1px solid var(--line);border-radius:4px;background:var(--input-bg);color:var(--text)">';
    echo '<option value="">Select a location</option>';
    foreach (['Kuwait Office', 'India Office', 'Malaysia Office'] as $loc) {
      echo '<option value="' . e($loc) . '"' . ($cert['location'] === $loc ? ' selected' : '') . '>' . e($loc) . '</option>';
    }
    echo '</select></label>';
    echo '</div>';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Start Date</span><input type="date" name="start_date" value="' . e($cert['start_date']) . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">End Date</span><input type="date" name="end_date" value="' . e($cert['end_date']) . '" style="width:100%;"></label>';
    echo '</div>';

    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Issue Date</span><input type="date" name="issued_date" value="' . e($cert['issued_date']) . '" style="width:100%;"></label>';

    echo '<div style="padding:15px;background:var(--bg2);border-radius:6px"><div style="font-weight:600;margin-bottom:10px;font-size:13px;color:var(--muted)">AUTHORISED SIGNATORY</div>';
    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Signatory Name *</span><input type="text" name="signatory_name" required list="signatory-list" value="' . e($cert['signatory_name'] ?? 'Deepu Soman') . '" style="width:100%;"></label>';
    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Designation *</span><input type="text" name="signatory_title" required value="' . e($cert['signatory_title'] ?? 'Managing Partner') . '" style="width:100%;"></label>';
    echo '</div>' . cert_signatory_datalist() . '</div>';

    echo '<label><span style="display:block;margin-bottom:4px;font-weight:500;font-size:13px">Description</span><textarea name="description" style="width:100%;min-height:80px;padding:8px;border:1px solid var(--line);border-radius:4px;background:var(--input-bg);color:var(--text);font-family:inherit">' . e($cert['description'] ?? '') . '</textarea></label>';

    echo '<div><span style="display:block;margin-bottom:8px;font-weight:500;font-size:13px">Status</span>';
    echo '<label style="margin-right:20px"><input type="radio" name="status" value="published"' . ($cert['status'] === 'published' ? ' checked' : '') . '> Published</label>';
    echo '<label><input type="radio" name="status" value="draft"' . ($cert['status'] === 'draft' ? ' checked' : '') . '> Draft</label>';
    echo '</div>';

    echo '<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:20px;border-top:1px solid var(--line);padding-top:15px">';
    echo '<a href="' . admin_url('?p=certificates&action=view&id=' . $cert['id']) . '" class="btn btn-ghost">Cancel</a>';
    echo '<button type="submit" class="btn btn-primary">Save Changes</button>';
    echo '</div>';

    echo '</form></div></div>';

    layout_bottom();
    exit;
  }

  redirect(admin_url('?p=certificates'));
}

if ($p === 'certs') {
  require_cap('pages');
  if (!feature_on('certs')) redirect(admin_url('?p=dashboard'));
  layout_top('certs', 'Certificates', 'Badges shown on the site + the protected PDF vault');
  $rows = [];
  foreach (preg_split('/\r\n|\r|\n/', (string) setting('certificates')) as $line) {
    $line = trim($line); if ($line === '') continue;
    $c = array_map('trim', explode('|', $line));
    $rows[] = ['code'=>$c[0]??'', 'name'=>$c[1]??'', 'pdf'=>$c[2]??'', 'on'=>!(isset($c[3]) && strtolower($c[3])==='off')];
  }
  if (!$rows) $rows[] = ['code'=>'','name'=>'','pdf'=>'','on'=>true];
  $pic = icon('media', ['class'=>'ic ic-sm']) . ' Choose';
  ?>
  <div class="page-head"><div><h1>Certificates</h1><p>Each row is one certificate. Attach a PDF to include it in the protected vault; use Shown / Hidden to toggle it without deleting.</p></div></div>
  <form method="post" action="<?= admin_url('?p=certs') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="save_certs">
  <div class="card card-p" style="max-width:980px">
    <div class="tbl-wrap"><table class="tbl"><thead><tr><th style="width:20%">Code</th><th style="width:30%">Name</th><th>PDF (optional)</th><th style="width:110px">Status</th></tr></thead><tbody id="certRows">
    <?php foreach ($rows as $r): ?>
      <tr class="cert-row">
        <td><input class="inp" name="code[]" value="<?= e($r['code']) ?>" placeholder="ISO 9001:2015"></td>
        <td><input class="inp" name="name[]" value="<?= e($r['name']) ?>" placeholder="Quality Management"></td>
        <td><div class="cms-pickrow"><input class="inp" name="pdf[]" value="<?= e($r['pdf']) ?>" placeholder="uploads/cert.pdf"><button type="button" class="btn btn-ghost btn-sm cms-pick"><?= $pic ?></button></div></td>
        <td><select class="inp" name="en[]"><option value="1"<?= $r['on'] ? ' selected' : '' ?>>Shown</option><option value="0"<?= $r['on'] ? '' : ' selected' ?>>Hidden</option></select></td>
      </tr>
    <?php endforeach; ?>
    </tbody></table></div>
    <div style="display:flex;gap:10px;align-items:center;margin-top:12px"><button type="button" class="btn btn-ghost btn-sm" id="addCert"><?= icon('plus', ['class'=>'ic ic-sm']) ?> Add certificate</button><span style="color:var(--muted);font-size:12px">Leave Code and Name blank to remove a row.</span></div>
    <div class="set-row" style="margin-top:14px"><div class="lab"><b>Watermark text</b><span>Stamped over PDF previews in the vault.</span></div><div class="ctl"><input class="inp" name="cert_watermark" value="<?= e(setting('cert_watermark')) ?>"></div></div>
    <div style="padding-top:14px"><button class="btn btn-primary btn-sm">Save certificates</button></div>
  </div></form>
  <script>(function(){var b=document.getElementById('addCert');if(!b)return;var w=document.getElementById('certRows');b.addEventListener('click',function(){var last=w.querySelector('.cert-row:last-child');if(!last)return;var c=last.cloneNode(true);c.querySelectorAll('input').forEach(function(i){i.value='';});c.querySelectorAll('select').forEach(function(x){x.value='1';});w.appendChild(c);var f=c.querySelector('input');if(f)f.focus();});})();</script>
  <?= cms_media_script() ?>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'sponsors') {
  require_cap('pages');
  if (!feature_on('sponsors')) redirect(admin_url('?p=dashboard'));
  layout_top('sponsors', 'Sponsors', 'Partner logos shown in the scrolling strip');
  $rows = [];
  foreach (preg_split('/\r\n|\r|\n/', (string) setting('sponsors')) as $line) {
    $line = trim($line); if ($line === '') continue;
    $parts = explode('|', $line, 2);
    $rows[] = ['logo'=>trim($parts[0]), 'alt'=>trim($parts[1] ?? '')];
  }
  if (!$rows) $rows[] = ['logo'=>'','alt'=>''];
  $pic = icon('media', ['class'=>'ic ic-sm']) . ' Choose';
  ?>
  <div class="page-head"><div><h1>Sponsors</h1><p>One row per partner logo (transparent PNG works best, roughly 400×240px). Logos scroll continuously wherever a Sponsors section is placed.</p></div></div>
  <form method="post" action="<?= admin_url('?p=sponsors') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="save_sponsors">
  <div class="card card-p" style="max-width:900px">
    <div class="tbl-wrap"><table class="tbl"><thead><tr><th>Logo image</th><th style="width:34%">Name (alt text)</th></tr></thead><tbody id="spRows">
    <?php foreach ($rows as $r): ?>
      <tr class="sp-row">
        <td><div class="cms-pickrow"><input class="inp" name="logo[]" value="<?= e($r['logo']) ?>" placeholder="uploads/partner.png"><button type="button" class="btn btn-ghost btn-sm cms-pick"><?= $pic ?></button></div></td>
        <td><input class="inp" name="alt[]" value="<?= e($r['alt']) ?>" placeholder="Partner name"></td>
      </tr>
    <?php endforeach; ?>
    </tbody></table></div>
    <div style="display:flex;gap:10px;align-items:center;margin-top:12px"><button type="button" class="btn btn-ghost btn-sm" id="addSp"><?= icon('plus', ['class'=>'ic ic-sm']) ?> Add sponsor</button><span style="color:var(--muted);font-size:12px">Clear a row\'s logo to remove it.</span></div>
    <div style="padding-top:14px"><button class="btn btn-primary btn-sm">Save sponsors</button></div>
  </div></form>
  <script>(function(){var b=document.getElementById('addSp');if(!b)return;var w=document.getElementById('spRows');b.addEventListener('click',function(){var last=w.querySelector('.sp-row:last-child');if(!last)return;var c=last.cloneNode(true);c.querySelectorAll('input').forEach(function(i){i.value='';});w.appendChild(c);var f=c.querySelector('input');if(f)f.focus();});})();</script>
  <?= cms_media_script() ?>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'news') {
  require_cap('pages');
  if (!feature_on('news')) redirect(admin_url('?p=dashboard'));
  layout_top('news', 'News Stories', 'Add, prioritise & manage the stories on your News page');
  $items = news_items(false); // include drafts
  ?>
  <div class="page-head"><div><h1>News Stories</h1><p>News listings are built from these stories automatically — each card\'s image &amp; title stay in sync. Reorder with the arrows; the top story is featured.</p></div>
    <div class="sp"></div>
    <form method="post" action="<?= admin_url('?p=pages') ?>" style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
      <?= csrf_field() ?><input type="hidden" name="action" value="add_page"><input type="hidden" name="prefix" value="news-"><input type="hidden" name="seed" value="news">
      <input class="inp" name="title" placeholder="New story title…" required style="width:220px">
      <button class="btn btn-primary btn-sm"><?= icon('plus', ['class'=>'ic ic-sm']) ?> Add news story</button>
    </form>
  </div>
  <div class="badge b-neu" style="display:block;padding:10px 14px;margin-bottom:14px;line-height:1.5;max-width:940px">Set each story\'s <b>Category · Date</b> in its editor under <b>Banner → Eyebrow</b> (e.g. <code>Staff · 22 / 09 / 2026</code>) — the category powers the filter buttons. Use <b>Banner → Banner image</b> for the card/story image.</div>
  <?php if (!$items): ?>
    <div class="card card-p" style="max-width:940px"><p style="color:var(--muted)">No news stories yet. Add your first one above.</p></div>
  <?php else: foreach ($items as $k => $it): ?>
    <div class="card card-p" style="display:flex;gap:14px;align-items:center;margin-bottom:10px;max-width:940px">
      <img src="<?= e(media_url($it['img'])) ?>" alt="" onerror="this.style.visibility='hidden'" style="width:86px;height:60px;object-fit:cover;border-radius:8px;flex:none;background:var(--line)">
      <div style="flex:1;min-width:0">
        <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap">
          <b style="font-size:14px"><?= e($it['title']) ?></b>
          <?php if ($k === 0): ?><span class="badge b-ok" style="font-size:10px">Featured</span><?php endif; ?>
          <?php if ($it['status'] !== 'published'): ?><span class="badge b-neu" style="font-size:10px">Draft</span><?php endif; ?>
        </div>
        <span style="color:var(--muted);font-size:12px"><?= e(trim(($it['cat'] ?: 'No category') . ($it['date'] ? ' · ' . $it['date'] : ''))) ?> · /<?= e($it['slug']) ?></span>
      </div>
      <div class="row-btns" style="flex:none;display:flex;gap:6px;align-items:center">
        <form method="post" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="reorder_news"><button class="mini" name="do" value="nup:<?= $it['id'] ?>" title="Higher priority"<?= $k === 0 ? ' disabled' : '' ?>><?= icon('chevron-up', ['class'=>'ic']) ?></button></form>
        <form method="post" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="reorder_news"><button class="mini" name="do" value="ndown:<?= $it['id'] ?>" title="Lower priority"<?= $k === count($items)-1 ? ' disabled' : '' ?>><?= icon('chevron-down', ['class'=>'ic']) ?></button></form>
        <a class="btn btn-ghost btn-sm" href="<?= admin_url('?p=edit&id=' . $it['id']) ?>">Edit</a>
        <a class="btn btn-ghost btn-sm" href="<?= base_url($it['slug']) ?>" target="_blank" title="View">↗</a>
        <form method="post" style="margin:0" onsubmit="return confirm('Delete this story permanently?')"><?= csrf_field() ?><input type="hidden" name="action" value="delete_page"><input type="hidden" name="id" value="<?= $it['id'] ?>"><button class="mini" title="Delete" style="color:var(--bad)"><?= icon('trash', ['class'=>'ic']) ?></button></form>
      </div>
    </div>
  <?php endforeach; endif; ?>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'settings') {
  require_cap('settings');
  layout_top('settings', 'Settings', 'Global site identity — feeds header & footer');
  // Common US timezones first (the client is in Tampa), then a general fallback set.
  $tzList = ['America/New_York'=>'Eastern — New York / Tampa','America/Chicago'=>'Central — Chicago','America/Denver'=>'Mountain — Denver','America/Phoenix'=>'Arizona — Phoenix','America/Los_Angeles'=>'Pacific — Los Angeles','America/Anchorage'=>'Alaska — Anchorage','Pacific/Honolulu'=>'Hawaii — Honolulu','UTC'=>'UTC'];
  $curTz = trim((string) setting('timezone', 'America/New_York')) ?: 'America/New_York';
  if (!isset($tzList[$curTz]) && in_array($curTz, timezone_identifiers_list(), true)) $tzList = [$curTz => $curTz] + $tzList;
  ?><div class="page-head"><div><h1>Settings</h1><p>These values appear in the header and footer of every page.</p></div></div>
  <form method="post" action="<?= admin_url('?p=settings') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="save_settings">
  <div class="card card-p" style="max-width:760px">
    <div class="sec-title" style="margin-bottom:4px">Site identity &amp; contact</div>
    <?php foreach (['site_name'=>'Site name','tagline'=>'Tagline','phone'=>'Phone','email'=>'Email','address'=>'Address','facebook'=>'Facebook URL','youtube'=>'YouTube URL'] as $k => $lbl): ?>
    <div class="set-row"><div class="lab"><b><?= $lbl ?></b></div><div class="ctl"><input class="inp" name="<?= $k ?>" value="<?= e(setting($k)) ?>"></div></div>
    <?php endforeach; ?>
    <div class="set-row"><div class="lab"><b>Client logo</b><span>Small logo shown in the dashboard’s site chip and on the sign-in screen. Leave blank to use the site’s round logo. PNG/SVG with a transparent background works best.</span></div><div class="ctl">
      <div class="cms-pickrow"><input class="inp" name="client_logo" value="<?= e(setting('client_logo')) ?>" placeholder="choose or paste an image path"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div>
      <?php $clp = client_logo_url(); if ($clp !== ''): ?><div class="set-logo-prev"><img src="<?= e($clp) ?>" alt="Current client logo"></div><?php endif; ?>
    </div></div>

    <div class="sec-title" style="margin:22px 0 4px">Homepage popup</div>
    <div class="set-row"><div class="lab"><b>Announcement popup</b><span>Poster image, title bar, message and newsletter signup — managed on its own screen now.</span></div><div class="ctl"><a class="btn btn-ghost btn-sm" href="<?= admin_url('?p=popup') ?>">Open Homepage Popup &rarr;</a></div></div>

    <div class="sec-title" style="margin:22px 0 4px">Analytics &amp; custom code</div>
    <div class="set-row"><div class="lab"><b>Google Analytics (GA4)</b><span>Paste just the Measurement ID (Admin → Data streams), e.g. <code>G-XXXXXXXXXX</code>. The tracking snippet is added to every public page automatically.</span></div><div class="ctl"><input class="inp" name="ga4_id" value="<?= e(setting('ga4_id')) ?>" placeholder="G-XXXXXXXXXX"></div></div>
    <div class="set-row"><div class="lab"><b>Extra head code</b><span>Site-verification meta tags, Search Console, or any snippet a provider asks you to put in &lt;head&gt;.</span></div><div class="ctl"><textarea class="inp" name="head_code" rows="4" placeholder="&lt;meta name=&quot;…-site-verification&quot; content=&quot;…&quot;&gt;" style="font-family:ui-monospace,Menlo,monospace;font-size:12px"><?= e(setting('head_code')) ?></textarea></div></div>
    <div class="set-row"><div class="lab"><b>Code before &lt;/body&gt;</b><span>For live-chat widgets or badges that must load at the end of the page.</span></div><div class="ctl"><textarea class="inp" name="body_code" rows="3" placeholder="&lt;!-- chat widget / badge --&gt;" style="font-family:ui-monospace,Menlo,monospace;font-size:12px"><?= e(setting('body_code')) ?></textarea></div></div>

    <div class="sec-title" style="margin:22px 0 4px">Advanced</div>
    <div class="set-row"><div class="lab"><b>Timezone</b><span>Used for “last edited” stamps, blog dates and the dashboard clock.</span></div><div class="ctl">
      <select class="inp" name="timezone"><?php foreach ($tzList as $tzv => $tzl): ?><option value="<?= e($tzv) ?>"<?= sel($curTz, $tzv) ?>><?= e($tzl) ?></option><?php endforeach; ?></select>
    </div></div>
    <div class="set-row"><div class="lab"><b>Force HTTPS</b><span>Redirects every http:// visit to https:// and sends an HSTS header. Turn on only <b>after</b> the certificate works (cPanel → SSL/TLS Status → AutoSSL) — see DEPLOY-CPANEL.md.</span></div><div class="ctl">
      <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><input type="checkbox" name="force_https" value="1"<?= chk(setting('force_https','0') === '1') ?> style="accent-color:var(--ea-red)"> Redirect all visits to HTTPS</label>
    </div></div>
    <div class="set-row"><div class="lab"><b>Base URL</b><span>Blank if the site is at the domain root; <code>/ntppa</code> if in a subfolder.</span></div><div class="ctl"><input class="inp" name="base_url" value="<?= e(setting('base_url')) ?>"></div></div>
    <div style="padding-top:16px"><button class="btn btn-primary btn-sm">Save settings</button></div>
  </div></form>

  <?php if (function_exists('theme_settings_fields')): $tf = theme_settings_fields(); if ($tf): ?>
  <!-- ─── Client theme settings (declared by app/render.php's theme_settings_fields) ─── -->
  <form method="post" action="<?= admin_url('?p=settings') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="save_settings">
  <div class="card card-p" style="max-width:760px;margin-top:16px">
    <div class="sec-title" style="margin-bottom:4px">Site design &amp; branding</div>
    <p style="color:var(--muted);font-size:12.5px;margin:0 0 14px">Fields specific to this client's website design.</p>
    <?php foreach ($tf as $tk => $td): $tlabel = is_array($td) ? ($td[0] ?? $tk) : (string) $td; $ttype = is_array($td) ? ($td[1] ?? 'text') : 'text'; ?>
    <div class="set-row"><div class="lab"><b><?= e($tlabel) ?></b></div><div class="ctl">
      <?php if ($ttype === 'image'): ?><div class="cms-pickrow"><input class="inp" name="<?= e($tk) ?>" value="<?= e(setting($tk)) ?>" placeholder="choose or paste a path"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div>
      <?php elseif ($ttype === 'textarea'): ?><textarea class="inp" name="<?= e($tk) ?>" rows="4" style="font-family:ui-monospace,Menlo,monospace;font-size:12px"><?= e(setting($tk)) ?></textarea>
      <?php else: ?><input class="inp" name="<?= e($tk) ?>" value="<?= e(setting($tk)) ?>"><?php endif; ?>
    </div></div>
    <?php endforeach; ?>
    <div style="padding-top:16px"><button class="btn btn-primary btn-sm">Save design settings</button></div>
  </div></form>
  <?php endif; endif; ?>

  <?php if (($user['role'] ?? '') === 'admin'): ?>
  <!-- ─── Features — the per-client switchboard (one engine, per-client modules) ─── -->
  <form method="post" action="<?= admin_url('?p=settings') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="save_features">
  <div class="card card-p" style="max-width:760px;margin-top:16px">
    <div class="sec-title" style="margin-bottom:4px">Features</div>
    <p style="color:var(--muted);font-size:12.5px;margin:0 0 14px">Switch whole modules on or off for this client. A switched-off module disappears from the CMS menu, the page editor and the live website — nothing is deleted, so it can be switched back on any time.</p>
    <?php $feats = features(); foreach (feature_registry() as $fk => $fd): ?>
    <div class="set-row"><div class="lab"><b><?= e($fd[0]) ?></b><span><?= e($fd[1]) ?></span></div><div class="ctl" style="display:flex;align-items:center">
      <label style="display:inline-flex;align-items:center;gap:8px;font-size:13px;font-weight:600;cursor:pointer">
        <input type="checkbox" name="feat[<?= $fk ?>]" value="1"<?= !empty($feats[$fk]) ? ' checked' : '' ?> style="accent-color:var(--ea-red);width:17px;height:17px">
        <span><?= !empty($feats[$fk]) ? 'Enabled' : 'Disabled' ?></span>
      </label>
    </div></div>
    <?php endforeach; ?>
    <div style="padding-top:16px"><button class="btn btn-primary btn-sm">Save features</button></div>
  </div></form>
  <?php endif; ?>

  <?php if (user_can('users')): ?>
  <a class="card card-p" href="<?= admin_url('?p=users') ?>" style="max-width:760px;margin-top:16px;display:flex;align-items:center;gap:14px">
    <div class="pgc-ico" style="background:var(--ea-red-soft);color:var(--ea-red)"><svg class="ic" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M22 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/></svg></div>
    <div style="flex:1"><b style="display:block">Team &amp; Access</b><span style="color:var(--muted);font-size:12.5px">Create logins for your team and choose what each person can edit.</span></div>
    <svg class="ic" viewBox="0 0 24 24" style="color:var(--muted)"><path d="m9 18 6-6-6-6"/></svg>
  </a>
  <?php endif; ?>
  <?= cms_media_script() ?>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'users') {
  require_cap('users');
  layout_top('users', 'Team & Access', 'Create logins and choose what each person can edit');
  $editId = (int) ($_GET['edit'] ?? 0);
  $editU = null;
  if ($editId) { $st = db()->prepare('SELECT * FROM users WHERE id=?'); $st->execute([$editId]); $editU = $st->fetch() ?: null; }
  $isEdit = (bool) $editU;
  $ep = ($editU && !empty($editU['perms'])) ? (json_decode($editU['perms'], true) ?: []) : [];
  $capLabels = ['pages'=>'Pages & sections', 'blog'=>'Blog posts', 'media'=>'Media library', 'settings'=>'Settings'];
  $meId = (int) (current_user()['id'] ?? 0);

  echo '<div class="page-head"><div><h1>Team &amp; Access</h1><p>Add logins for your team and control exactly what each person can edit.</p></div></div>';
  echo '<div class="ed-layout"><div class="card"><div class="card-hd"><div class="sec-title">People with access</div></div><div class="tbl-wrap"><table class="tbl"><thead><tr><th>Name</th><th>Role</th><th>Can edit</th><th style="text-align:right">Actions</th></tr></thead><tbody>';
  foreach (db()->query('SELECT * FROM users ORDER BY role, name') as $uu){
    $me = ((int) $uu['id'] === $meId);
    if ($uu['role'] === 'admin') { $access = '<span class="badge b-ok">Everything</span>'; }
    else {
      $pp = !empty($uu['perms']) ? (json_decode($uu['perms'], true) ?: []) : []; $on = [];
      foreach ($capLabels as $ck => $cl) if (!empty($pp[$ck])) $on[] = explode(' ', $cl)[0];
      $access = $on ? e(implode(', ', $on)) : '<span style="color:var(--muted)">— none yet —</span>';
      if (!empty($pp['scope']) && $pp['scope'] !== 'all') $access .= ' <span class="mchip">limited pages</span>';
    }
    echo '<tr><td><div style="display:flex;align-items:center;gap:10px"><div class="avatar" style="width:32px;height:32px;font-size:11px">' . user_avatar_html($uu) . '</div><div><b>' . e($uu['name']) . '</b>' . ($me ? ' <span class="mchip">you</span>' : '') . '<br><span style="color:var(--muted);font-size:12px">' . e($uu['email']) . '</span></div></div></td>'
       . '<td>' . ($uu['role']==='admin' ? '<span class="badge b-ok"><span class="d"></span>Admin</span>' : '<span class="badge b-neu"><span class="d"></span>Editor</span>') . '</td>'
       . '<td style="font-size:12.5px">' . $access . '</td>'
       . '<td style="text-align:right;white-space:nowrap"><a class="mini" href="' . admin_url('?p=users&edit=' . $uu['id']) . '" title="Edit"><svg class="ic" viewBox="0 0 24 24"><path d="M12 20h9M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg></a>';
    if (!$me) echo ' <form method="post" style="display:inline" onsubmit="return confirm(\'Remove this person\\047s access?\')">' . csrf_field() . '<input type="hidden" name="action" value="delete_user"><input type="hidden" name="uid" value="' . $uu['id'] . '"><button class="mini" title="Remove" style="color:var(--bad)"><svg class="ic" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button></form>';
    echo '</td></tr>';
  }
  echo '</tbody></table></div></div>';
  ?>
  <div class="card card-p" style="align-self:start;position:sticky;top:20px">
    <div class="sec-title" style="margin-bottom:14px"><?= $isEdit ? 'Edit ' . e($editU['name']) : 'Add a team member' ?></div>
    <form method="post" action="<?= admin_url('?p=users') ?>" enctype="multipart/form-data"><?= csrf_field() ?><input type="hidden" name="action" value="save_user"><input type="hidden" name="uid" value="<?= $isEdit ? (int) $editU['id'] : 0 ?>">
      <div class="field"><label>Name</label><input class="inp" name="name" value="<?= e($editU['name'] ?? '') ?>" required></div>
      <div class="field"><label>Email <span style="color:var(--muted);font-weight:400">(this is their login)</span></label><input class="inp" type="email" name="email" value="<?= e($editU['email'] ?? '') ?>" required></div>
      <div class="field"><label>Password <?= $isEdit ? '<span style="color:var(--muted);font-weight:400">(leave blank to keep current)</span>' : '<span style="color:var(--muted);font-weight:400">(min 8 characters)</span>' ?></label><input class="inp" type="password" name="password" autocomplete="new-password"<?= $isEdit ? '' : ' required minlength="8"' ?>></div>
      <?php $curAv = trim((string) ($editU['avatar'] ?? '')); ?>
      <div class="field"><label>Profile photo <span style="color:var(--muted);font-weight:400">(square, ~256×256px — shown in the sidebar &amp; top bar)</span></label>
        <input type="hidden" name="avatar_current" value="<?= e($curAv) ?>">
        <div style="display:flex;align-items:center;gap:12px">
          <div class="avatar avatar-lg" style="width:52px;height:52px;border-radius:14px;font-size:14px"><?= user_avatar_html($isEdit ? $editU : []) ?></div>
          <div style="flex:1;min-width:0">
            <input class="inp" type="file" name="avatar_file" accept="image/*" style="height:auto;padding:9px 12px;font-size:12.5px">
            <?php if ($curAv !== ''): ?><label style="display:inline-flex;align-items:center;gap:6px;font-size:12px;color:var(--muted);margin-top:7px;cursor:pointer"><input type="checkbox" name="avatar_remove" value="1" style="accent-color:var(--ea-red)"> Remove current photo (back to initials)</label><?php endif; ?>
          </div>
        </div>
      </div>
      <div class="field"><label>Role</label>
        <select class="inp" name="role" id="uRole" onchange="uRoleSync()">
          <option value="editor"<?= (!$isEdit || $editU['role']==='editor') ? ' selected' : '' ?>>Editor — limited access</option>
          <option value="admin"<?= ($isEdit && $editU['role']==='admin') ? ' selected' : '' ?>>Administrator — full access</option>
        </select>
      </div>
      <div id="uPerms" class="uperms">
        <div class="uperms-h">This editor is allowed to manage:</div>
        <?php foreach ($capLabels as $ck => $cl): ?>
          <label class="ucap"><input type="checkbox" name="cap[<?= $ck ?>]" value="1"<?= !empty($ep[$ck]) ? ' checked' : '' ?>> <span><?= e($cl) ?></span></label>
        <?php endforeach; ?>
        <div class="field" style="margin:12px 0 0"><label>Limit to specific pages <span style="color:var(--muted);font-weight:400">(optional)</span></label>
          <input class="inp" name="scope" value="<?= e($ep['scope'] ?? 'all') ?>" placeholder="all">
          <div style="font-size:11.5px;color:var(--muted);margin-top:6px">Use <b>all</b> for every page, or a comma-separated list of page URLs, e.g. <code>about, why-ntppa, contact</code>.</div>
        </div>
      </div>
      <div style="display:flex;gap:8px;margin-top:8px"><button class="btn btn-primary btn-sm"><?= $isEdit ? 'Save changes' : 'Add member' ?></button><?php if ($isEdit): ?><a class="btn btn-ghost btn-sm" href="<?= admin_url('?p=users') ?>">Cancel</a><?php endif; ?></div>
    </form>
  </div></div>
  <?php if (($user['role'] ?? '') === 'admin'): $rkOn = recovery_key_configured(); ?>
  <div class="card card-p" style="max-width:820px;margin-top:16px">
    <div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap">
      <div class="pgc-ico" style="background:<?= $rkOn ? 'var(--ok-soft)' : 'var(--warn-soft)' ?>;color:<?= $rkOn ? 'var(--ok)' : 'var(--warn)' ?>"><svg class="ic" viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div>
      <div style="flex:1;min-width:240px">
        <b style="display:block">Password recovery key <?= $rkOn ? '<span class="badge b-ok" style="margin-left:6px"><span class="d"></span>Configured</span>' : '<span class="badge b-warn" style="margin-left:6px"><span class="d"></span>Not configured</span>' ?></b>
        <span style="color:var(--muted);font-size:12.5px;line-height:1.6;display:block;margin-top:3px">
          Protects the “forgot password” page (<code>admin/reset.php</code>). <?= $rkOn
            ? 'Regenerating creates a fresh key, stores it in the site’s <code>.env</code> and shows it to you once — the old key stops working immediately.'
            : 'Generate one now — it is stored in the site’s <code>.env</code> and shown to you once. (A key is also created automatically the next time an administrator signs in.)' ?>
        </span>
      </div>
      <form method="post" style="margin:0" onsubmit="return confirm('<?= $rkOn ? 'Generate a NEW recovery key? The current key stops working immediately, and the new one is shown only once.' : 'Generate the recovery key now? It will be shown only once — keep it somewhere safe.' ?>')"><?= csrf_field() ?>
        <input type="hidden" name="action" value="regen_reset_key">
        <button class="btn btn-ghost btn-sm" type="submit"><?= $rkOn ? 'Regenerate key' : 'Generate key' ?></button>
      </form>
    </div>
  </div>
  <?php endif; ?>
  <script>function uRoleSync(){var r=document.getElementById('uRole'),p=document.getElementById('uPerms');if(r&&p)p.style.display=r.value==='admin'?'none':'';}uRoleSync();</script>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'popup') {
  if (!feature_on('popup')) redirect(admin_url('?p=dashboard'));
  require_cap('settings');
  layout_top('popup', 'Homepage Popup', 'Announcement shown once per visit on the live site');
  $pe  = ((string) setting('popup_enabled', '0') === '1');
  $img = trim((string) setting('popup_image'));
  $ttl = (string) setting('popup_title');
  $tts = ((string) setting('popup_title_show', '0') === '1');
  $txt = (string) setting('popup_text');
  $nws = ((string) setting('popup_newsletter', '0') === '1');
  $subs = null;
  try { $subs = db()->query('SELECT * FROM subscribers ORDER BY id DESC')->fetchAll(); } catch (\Throwable $e) { $subs = null; }
  // opening this screen clears the subscriber part of the top-bar bell;
  // rows fetched above still carry the pre-clear is_read, so new ones get a chip
  if ($subs !== null) { try { db()->exec('UPDATE subscribers SET is_read=1 WHERE is_read=0'); } catch (\Throwable $e) {} }
  ?>
  <div class="page-head"><div><h1>Homepage Popup</h1><p>A poster announcement visitors see ~1.5&nbsp;seconds after the page loads — once per browsing session. Every piece below is optional except the image.</p></div></div>
  <div class="ed-layout">
    <form method="post" action="<?= admin_url('?p=popup') ?>" class="card card-p"><?= csrf_field() ?><input type="hidden" name="action" value="save_popup">
      <div class="sec-title" style="margin-bottom:4px">Popup content</div>
      <div class="set-row"><div class="lab"><b>Show the popup</b><span>Master switch for the live site.</span></div><div class="ctl">
        <select class="inp" name="popup_enabled"><option value="1"<?= sel($pe ? '1' : '0', '1') ?>>Shown</option><option value="0"<?= sel($pe ? '1' : '0', '0') ?>>Hidden</option></select>
      </div></div>
      <div class="set-row"><div class="lab"><b>Poster image</b><span>Upload in Media Library, then Choose — portrait works best.</span></div><div class="ctl"><div class="cms-pickrow"><input class="inp" name="popup_image" value="<?= e($img) ?>" placeholder="uploads/…"><button type="button" class="btn btn-ghost btn-sm cms-pick"><svg class="ic ic-sm" viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.1-3.1a2 2 0 0 0-2.8 0L6 21"/></svg> Choose</button></div></div></div>
      <div class="set-row"><div class="lab"><b>Click-through link</b><span>Where tapping the poster goes, e.g. <code>/summer-programs</code>. Blank = not clickable.</span></div><div class="ctl"><input class="inp" name="popup_link" value="<?= e(setting('popup_link')) ?>" placeholder="/summer-programs"></div></div>
      <div class="set-row"><div class="lab"><b>Title bar</b><span>A coloured strip above the poster. <b>Tip:</b> keep it hidden and the poster gets the full screen height — no scrolling.</span></div><div class="ctl">
        <input class="inp" name="popup_title" value="<?= e($ttl) ?>" placeholder="Summer 2026 enrollment is open!" maxlength="160">
        <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600;margin-top:9px"><input type="checkbox" name="popup_title_show" value="1"<?= chk($tts) ?> style="accent-color:var(--ea-red)"> Show the title bar</label>
      </div></div>
      <div class="set-row"><div class="lab"><b>Message under the image</b><span>Optional short rich text, e.g. “New students: schedule a FREE interview…”.</span></div><div class="ctl"><textarea class="inp" rows="3" name="popup_text" data-rich data-sectype="richtext"><?= e($txt) ?></textarea></div></div>
      <div class="set-row"><div class="lab"><b>Newsletter signup</b><span>Adds an email box to the popup (spam-protected: hidden honeypot + a simple sum question). Signups collect below.</span></div><div class="ctl">
        <label style="display:flex;align-items:center;gap:8px;font-size:13px;font-weight:600"><input type="checkbox" name="popup_newsletter" value="1"<?= chk($nws) ?> style="accent-color:var(--ea-red)"> Collect email addresses in the popup</label>
      </div></div>
      <div style="padding-top:16px"><button class="btn btn-primary btn-sm">Save popup</button></div>
    </form>
    <div class="card card-p" style="position:sticky;top:20px">
      <div class="sec-title" style="margin-bottom:12px">Preview <span class="badge b-neu" style="font-size:10px"><?= $pe ? 'live' : 'hidden' ?></span></div>
      <div class="pop-prev">
        <div class="pop-prev-card">
          <?php if ($tts && $ttl !== ''): ?><div class="pop-prev-title"><?= e($ttl) ?></div><?php endif; ?>
          <?php if ($img !== ''): ?><img src="<?= e(media_url($img)) ?>" alt="" onerror="this.style.display='none'"><?php else: ?><div class="pop-prev-none">No poster image chosen yet</div><?php endif; ?>
          <?php if (trim($txt) !== ''): ?><div class="pop-prev-text"><?= $txt ?></div><?php endif; ?>
          <?php if ($nws): ?><div class="pop-prev-news"><span class="inp-fake">you@email.com</span><span class="btn-fake">Sign up</span></div><?php endif; ?>
          <span class="pop-prev-x"><?= icon('close', ['size'=>12]) ?></span>
        </div>
      </div>
      <p style="font-size:12px;color:var(--muted);line-height:1.6;margin-top:12px">The live popup always fits the visitor’s screen — the poster scales down whenever the title, message or signup form is switched on. Shown once per browsing session.</p>
    </div>
  </div>
  <?php
  echo '<div class="card" style="max-width:940px;margin-top:16px"><div class="card-hd"><div class="sec-title">Newsletter subscribers';
  if ($subs !== null) echo ' <span class="badge b-neu">' . count($subs) . '</span>';
  echo '</div>';
  if ($subs) {
    echo '<form method="post" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="export_subscribers"><button class="btn btn-ghost btn-sm" type="submit"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Export CSV</button></form>';
  }
  echo '</div>';
  if ($subs === null) {
    echo '<div class="card-p" style="color:var(--muted);font-size:13px">The subscribers table isn’t in your database yet — import <code>database/upgrade-2026-07-09b.sql</code> once, or sign out and back in to apply it automatically.</div>';
  } elseif (!$subs) {
    echo '<div class="card-p" style="color:var(--muted);font-size:13px">No signups yet. Turn on <b>Newsletter signup</b> above and emails collected through the popup will appear here.</div>';
  } else {
    echo '<div class="tbl-wrap"><table class="tbl"><thead><tr><th>Email</th><th>Signed up</th><th>From page</th><th style="text-align:right">Actions</th></tr></thead><tbody>';
    foreach ($subs as $sb) {
      echo '<tr style="cursor:default"><td><a class="link" href="mailto:' . e($sb['email']) . '">' . e($sb['email']) . '</a>' . (empty($sb['is_read']) ? ' <span class="mchip">new</span>' : '') . '</td>'
         . '<td style="white-space:nowrap">' . e(rel_time($sb['created_at'])) . '</td>'
         . '<td style="color:var(--muted)">' . ($sb['page'] !== '' && $sb['page'] !== null ? '/' . e($sb['page']) : '—') . '</td>'
         . '<td style="text-align:right"><form method="post" style="display:inline" onsubmit="return confirm(\'Remove this subscriber?\')">' . csrf_field()
         . '<input type="hidden" name="action" value="delete_subscriber"><input type="hidden" name="id" value="' . (int) $sb['id'] . '">'
         . '<button class="mini" title="Remove" style="color:var(--bad)"><svg class="ic" viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/></svg></button></form></td></tr>';
    }
    echo '</tbody></table></div>';
  }
  echo '</div>';
  echo cms_media_script();
  layout_bottom();
  exit;
}

if ($p === 'enquiries') {
  require_cap('pages');
  $subs = null;
  try { $subs = db()->query('SELECT * FROM submissions ORDER BY id DESC')->fetchAll(); } catch (\Throwable $e) { $subs = null; }
  // opening the inbox marks everything read (clears the top-bar bell); rows
  // fetched above still carry the pre-clear is_read, so fresh ones get a chip
  if ($subs !== null) { try { db()->exec('UPDATE submissions SET is_read=1 WHERE is_read=0'); } catch (\Throwable $e) {} }
  layout_top('enquiries', 'Enquiries', 'Contact-form submissions from your website');
  if ($subs === null) { echo '<div class="card card-p" style="max-width:820px"><b>Run the latest database import</b><p style="color:var(--muted);margin-top:6px">The enquiries table isn’t in your database yet. Run <code>database/add-enquiries.sql</code> once (phpMyAdmin → SQL) to enable enquiry capture.</p></div>'; layout_bottom(); exit; }
  $spamN = 0; foreach ($subs as $s) { if (!empty($s['is_spam'])) $spamN++; }
  echo '<div class="page-head"><div><h1>Enquiries</h1><p>' . count($subs) . ' submission' . (count($subs) === 1 ? '' : 's') . ' from the website contact form'
     . ($spamN ? ' · <span style="color:var(--warn)">' . $spamN . ' flagged as likely spam</span> (kept for review — the notification bell ignores them)' : '') . '.</p></div></div>';
  if (!$subs) echo '<div class="card card-p" style="max-width:820px"><p style="color:var(--muted)">No enquiries yet — requests sent through the Contact form will appear here.</p></div>';
  foreach ($subs as $s) {
    echo '<div class="card card-p" style="margin-bottom:12px;max-width:820px' . (!empty($s['is_spam']) ? ';border-left:3px solid var(--warn);opacity:.85' : '') . '">'
      . '<div style="display:flex;gap:10px;align-items:baseline;flex-wrap:wrap"><b style="font-size:14px">' . e($s['name'] ?: '(no name)') . '</b>'
      . (!empty($s['is_spam']) ? '<span class="badge b-warn" title="Flagged by the spam filter (too many links / junk keywords). Nothing is ever auto-deleted — delete it below if it really is spam.">likely spam</span>' : '')
      . (empty($s['is_read']) ? '<span class="mchip">new</span>' : '')
      . ($s['subject'] ? '<span class="badge b-neu">' . e($s['subject']) . '</span>' : '')
      . '<span style="color:var(--muted);font-size:12px;margin-left:auto">' . e($s['created_at']) . '</span></div>'
      . '<div style="font-size:12.5px;color:var(--muted);margin:5px 0 9px">'
      . ($s['email'] ? '<a class="link" href="mailto:' . e($s['email']) . '">' . e($s['email']) . '</a>' : '')
      . ($s['phone'] ? ' &middot; <a class="link" href="tel:' . e($s['phone']) . '">' . e($s['phone']) . '</a>' : '')
      . ($s['page'] ? ' &middot; /' . e($s['page']) : '') . '</div>'
      . '<div style="white-space:pre-wrap;font-size:13.5px;line-height:1.55">' . e($s['message']) . '</div>'
      . '<form method="post" style="margin-top:10px" onsubmit="return confirm(\'Delete this enquiry?\')">' . csrf_field()
      . '<input type="hidden" name="action" value="delete_submission"><input type="hidden" name="id" value="' . (int) $s['id'] . '">'
      . '<button class="btn btn-ghost btn-sm" style="color:var(--bad)">Delete</button></form></div>';
  }
  layout_bottom();
  exit;
}

// Download chips for an application's attachments — each links to the authed
// ?p=application_file streaming route (files are never directly web-reachable).
function application_files_html($a){
  $slots = [1 => 'Resume', 2 => 'Teaching philosophy', 3 => 'Teaching / demo document'];
  $chips = '';
  foreach ($slots as $n => $label) {
    $stored = (string) ($a['file' . $n . '_name'] ?? '');
    if ($stored === '') continue;
    $orig = trim((string) ($a['file' . $n . '_orig'] ?? ''));
    $href = admin_url('?p=application_file&id=' . (int) $a['id'] . '&n=' . $n);
    $chips .= '<a class="app-file" href="' . e($href) . '" title="' . e($orig !== '' ? $orig : $label) . '">'
      . icon('download', ['size' => 14]) . '<span><b>' . e($label) . '</b>' . ($orig !== '' ? ' &middot; ' . e($orig) : '') . '</span></a>';
  }
  if ($chips === '') return '';
  return '<div class="app-files"><span class="app-files-h">' . icon('briefcase', ['size' => 13]) . ' Attachments</span>' . $chips . '</div>';
}

if ($p === 'applications') {
  if (!feature_on('applications')) redirect(admin_url('?p=dashboard'));
  require_cap('pages');
  $apps = null;
  try { $apps = db()->query('SELECT * FROM applications ORDER BY id DESC')->fetchAll(); } catch (\Throwable $e) { $apps = null; }
  // opening the inbox marks everything read (clears its share of the bell); the
  // rows fetched above still carry the pre-clear is_read, so new ones get a chip
  if ($apps !== null) { try { db()->exec('UPDATE applications SET is_read=1 WHERE is_read=0'); } catch (\Throwable $e) {} }
  layout_top('applications', 'Applications', 'Career applications submitted from your website');
  if ($apps === null) {
    echo '<div class="card card-p" style="max-width:820px"><b>Run the latest database import</b><p style="color:var(--muted);margin-top:6px">The applications table isn’t in your database yet. Sign out and back in to apply it automatically, or run <code>database/upgrade-2026-07-14.sql</code> once (phpMyAdmin → SQL).</p></div>';
    layout_bottom(); exit;
  }
  $spamN = 0; foreach ($apps as $a) { if (!empty($a['is_spam'])) $spamN++; }
  echo '<div class="page-head"><div><h1>Applications</h1><p>' . count($apps) . ' application' . (count($apps) === 1 ? '' : 's') . ' from the career page apply form'
     . ($spamN ? ' · <span style="color:var(--warn)">' . $spamN . ' flagged as likely spam</span> (kept for review — the notification bell ignores them)' : '') . '.</p></div>';
  if ($apps) echo '<form method="post" style="margin:0">' . csrf_field() . '<input type="hidden" name="action" value="export_applications"><button class="btn btn-ghost btn-sm" type="submit">' . icon('download', ['size'=>16]) . ' Export CSV</button></form>';
  echo '</div>';
  if (!$apps) { echo '<div class="card card-p" style="max-width:820px"><p style="color:var(--muted)">No applications yet — submissions from the career page apply form will appear here.</p></div>'; layout_bottom(); exit; }
  foreach ($apps as $a) {
    $name = trim(($a['first_name'] ?? '') . ' ' . ($a['last_name'] ?? ''));
    echo '<div class="card card-p" style="margin-bottom:12px;max-width:820px' . (!empty($a['is_spam']) ? ';border-left:3px solid var(--warn);opacity:.85' : '') . '">'
      . '<div style="display:flex;gap:10px;align-items:baseline;flex-wrap:wrap"><b style="font-size:14px">' . e($name !== '' ? $name : '(no name)') . '</b>'
      . ($a['job_title'] ? '<span class="badge b-neu" title="Applied for">' . e($a['job_title']) . '</span>' : '')
      . (!empty($a['is_spam']) ? '<span class="badge b-warn" title="Flagged by the spam filter. Nothing is ever auto-deleted — delete it below if it really is spam.">likely spam</span>' : '')
      . (empty($a['is_read']) ? '<span class="mchip">new</span>' : '')
      . '<span style="color:var(--muted);font-size:12px;margin-left:auto">' . e(rel_time($a['created_at'])) . '</span></div>'
      . '<div style="font-size:12.5px;color:var(--muted);margin:5px 0 9px">'
      . ($a['email'] ? '<a class="link" href="mailto:' . e($a['email']) . '">' . e($a['email']) . '</a>' : '')
      . ($a['phone'] ? ' &middot; <a class="link" href="tel:' . e($a['phone']) . '">' . e($a['phone']) . '</a>' : '')
      . ($a['location'] ? ' &middot; ' . e($a['location']) : '')
      . (($a['relocate'] ?? '') !== '' ? ' &middot; Relocate: <b>' . e($a['relocate']) . '</b>' : '')
      . ($a['page'] ? ' &middot; /' . e($a['page']) : '') . '</div>'
      . (($a['video_url'] ?? '') !== '' ? '<div style="margin:0 0 9px;font-size:12.5px">' . icon('play', ['size'=>14]) . ' <a class="link" href="' . e(safe_ext_url($a['video_url'])) . '" target="_blank" rel="noopener noreferrer">' . e($a['video_url']) . '</a></div>' : '')
      . (($a['message'] ?? '') !== '' ? '<div style="white-space:pre-wrap;font-size:13.5px;line-height:1.55">' . e($a['message']) . '</div>' : '<div style="font-size:13px;color:var(--muted)">(no message)</div>')
      . application_files_html($a)
      . '<form method="post" style="margin-top:10px" onsubmit="return confirm(\'Delete this application? Any attached files are deleted too.\')">' . csrf_field()
      . '<input type="hidden" name="action" value="delete_application"><input type="hidden" name="id" value="' . (int) $a['id'] . '">'
      . '<button class="btn btn-ghost btn-sm" style="color:var(--bad)">Delete</button></form></div>';
  }
  layout_bottom();
  exit;
}

if ($p === 'hosting') {
  require_cap('settings'); hosting_plans_table();
  layout_top('hosting', 'Hosting', 'Manage the plans on the Hosting page and where the buttons hand off');
  $rows = hosting_plans_all(false);
  $edit = null;
  if (($eid = (int) ($_GET['edit'] ?? 0)) > 0) { foreach ($rows as $r) if ((int)$r['id'] === $eid) $edit = $r; }
  ?>
  <div class="page-head"><div><h1>Hosting</h1><p>The Hosting page sells on-site and hands off to your reseller platform for search, checkout and the client portal (they handle logins and payments securely).</p></div></div>

  <?php $hostOn = (string) setting('hosting_page_enabled','1') === '1'; ?>
  <div class="card card-p" style="max-width:820px;margin-bottom:18px">
    <div class="sec-title" style="margin-bottom:4px">Hosting page</div>
    <p style="color:var(--muted);font-size:13px;margin:2px 0 14px">When ON, the menu "Hosting" opens this on-site page. When OFF, the menu links straight to your external hosting site (<?= e(setting('hosting_url','https://expertaims.xyz')) ?>), the way it did before, and this page shows a short pointer there.</p>
    <form method="post" action="<?= admin_url('?p=hosting') ?>" style="display:grid;gap:12px;max-width:640px"><?= csrf_field() ?><input type="hidden" name="action" value="hosting_urls">
      <label style="display:inline-flex;gap:10px;align-items:center;font-weight:600"><input type="checkbox" name="hosting_page_enabled" value="1" <?= $hostOn?'checked':'' ?> style="width:18px;height:18px"> Show the Hosting page on the site <span class="chip <?= $hostOn?'up':'neu' ?>" style="margin-left:6px"><?= $hostOn?'ON':'OFF' ?></span></label>
      <div class="sec-title" style="margin:8px 0 0;font-size:13px">Reseller handoff links</div>
      <p style="color:var(--muted);font-size:12px;margin:0">Where the domain search, plan buttons and client login point. From your ResellerClub / SuperSite account.</p>
      <label style="display:grid;gap:5px"><span>Store / SuperSite URL <span style="color:var(--muted);font-size:12px">(domain search + default order button)</span></span><input type="text" name="hosting_store_url" value="<?= e(setting('hosting_store_url','')) ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <label style="display:grid;gap:5px"><span>Client portal URL <span style="color:var(--muted);font-size:12px">(OrderBox login, blank hides the login button)</span></span><input type="text" name="hosting_portal_url" value="<?= e(setting('hosting_portal_url','')) ?>" placeholder="https://…" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <div><button class="btn btn-acc" type="submit">Save links</button></div>
    </form>
  </div>

  <div class="card" style="margin-bottom:18px">
    <div class="card-hd"><b>Plans</b><span class="chip neu"><?= count($rows) ?> total</span></div>
    <div class="card-p" style="overflow-x:auto">
      <?php if (!$rows): ?><p style="color:var(--muted);margin:0">No plans yet, add one below.</p><?php else: ?>
      <table class="tbl"><thead><tr><th>Name</th><th>Price</th><th>Featured</th><th>Order link</th><th>Sort</th><th>Shown</th><th></th></tr></thead><tbody>
        <?php foreach ($rows as $r): ?>
        <tr>
          <td><b><?= e($r['name']) ?></b><?php if(!empty($r['tagline'])): ?><div style="color:var(--muted);font-size:12px"><?= e($r['tagline']) ?></div><?php endif; ?></td>
          <td><?= e($r['price']) ?> <span style="color:var(--muted);font-size:12px"><?= e($r['period']) ?></span></td>
          <td><?= $r['featured'] ? '<span class="chip up">yes</span>' : '<span class="chip neu">—</span>' ?></td>
          <td style="font-size:12px"><?= !empty($r['cta_url']) ? '<span class="chip up">custom</span>' : '<span class="chip neu">store</span>' ?></td>
          <td><?= (int)$r['sort'] ?></td>
          <td><?= $r['visible'] ? '<span class="chip up">yes</span>' : '<span class="chip neu">hidden</span>' ?></td>
          <td style="text-align:right;white-space:nowrap">
            <a class="btn" href="<?= admin_url('?p=hosting&edit='.(int)$r['id']) ?>" style="padding:5px 12px;font-size:12px">Edit</a>
            <form method="post" action="<?= admin_url('?p=hosting') ?>" style="display:inline" onsubmit="return confirm('Remove this plan?')"><?= csrf_field() ?><input type="hidden" name="action" value="plan_delete"><input type="hidden" name="id" value="<?= (int)$r['id'] ?>"><button class="btn" type="submit" style="padding:5px 12px;font-size:12px">Delete</button></form>
          </td>
        </tr>
        <?php endforeach; ?>
      </tbody></table>
      <?php endif; ?>
    </div>
  </div>

  <div class="card card-p" style="max-width:760px">
    <div class="sec-title" style="margin-bottom:12px"><?= $edit ? 'Edit plan' : 'Add a plan' ?></div>
    <form method="post" action="<?= admin_url('?p=hosting') ?>" style="display:grid;gap:14px"><?= csrf_field() ?>
      <input type="hidden" name="action" value="plan_save"><input type="hidden" name="id" value="<?= $edit ? (int)$edit['id'] : 0 ?>">
      <label style="display:grid;gap:5px"><span>Plan name</span><input type="text" name="name" required value="<?= e($edit['name'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <label style="display:grid;gap:5px"><span>Tagline</span><input type="text" name="tagline" value="<?= e($edit['tagline'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <div style="display:flex;gap:12px;flex-wrap:wrap">
        <label style="display:grid;gap:5px;flex:1;min-width:140px"><span>Price</span><input type="text" name="price" placeholder="KWD 5" value="<?= e($edit['price'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
        <label style="display:grid;gap:5px;width:140px"><span>Period</span><input type="text" name="period" value="<?= e($edit['period'] ?? '/mo') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      </div>
      <label style="display:grid;gap:5px"><span>Features <span style="color:var(--muted);font-size:12px">(one per line)</span></span><textarea name="features" rows="6" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"><?= e($edit['features'] ?? '') ?></textarea></label>
      <div style="display:flex;gap:12px;flex-wrap:wrap">
        <label style="display:grid;gap:5px;flex:1;min-width:200px"><span>Order button URL <span style="color:var(--muted);font-size:12px">(blank uses the store URL above)</span></span><input type="text" name="cta_url" value="<?= e($edit['cta_url'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
        <label style="display:grid;gap:5px;width:160px"><span>Button label</span><input type="text" name="cta_label" value="<?= e($edit['cta_label'] ?? 'Order now') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      </div>
      <div style="display:flex;gap:22px;flex-wrap:wrap;align-items:center">
        <label style="display:inline-flex;gap:8px;align-items:center"><input type="checkbox" name="featured" value="1" <?= (!empty($edit['featured']))?'checked':'' ?>> Highlight as "Most popular"</label>
        <label style="display:inline-flex;gap:8px;align-items:center">Sort <input type="number" name="sort" value="<?= (int)($edit['sort'] ?? (count($rows)+1)) ?>" style="width:80px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
        <label style="display:inline-flex;gap:8px;align-items:center"><input type="checkbox" name="visible" value="1" <?= (!$edit || $edit['visible']) ? 'checked' : '' ?>> Show on site</label>
      </div>
      <div style="display:flex;gap:12px">
        <button class="btn btn-acc" type="submit"><?= $edit ? 'Save changes' : 'Add plan' ?></button>
        <?php if ($edit): ?><a class="btn" href="<?= admin_url('?p=hosting') ?>">Cancel</a><?php endif; ?>
      </div>
    </form>
  </div>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'partners') {
  require_cap('settings'); partners_table();
  layout_top('partners', 'Associate partners', 'Add, edit and reorder the partner logos shown on the Companies page');
  $rows = partners_all(false);
  $edit = null;
  if (($eid = (int) ($_GET['edit'] ?? 0)) > 0) { foreach ($rows as $r) if ((int)$r['id'] === $eid) $edit = $r; }
  ?>
  <div class="page-head"><div><h1>Associate partners</h1><p>These appear in the slider on the Companies page. Drag/arrows show automatically when there are more than two.</p></div></div>

  <div class="card" style="margin-bottom:18px">
    <div class="card-hd"><b>Partners</b><span class="chip neu"><?= count($rows) ?> total</span></div>
    <div class="card-p" style="overflow-x:auto">
      <?php if (!$rows): ?><p style="color:var(--muted);margin:0">No partners yet — add one below.</p><?php else: ?>
      <table class="tbl"><thead><tr><th>Logo</th><th>Name</th><th>Link</th><th>Plate</th><th>Sort</th><th>Shown</th><th></th></tr></thead><tbody>
        <?php foreach ($rows as $r): ?>
        <tr>
          <td><?php if (!empty($r['logo'])): ?><span style="display:inline-flex;background:<?= $r['plate']==='dark'?'#161112':'#fff' ?>;padding:6px 10px;border-radius:8px"><img src="<?= e(base_url($r['logo'])) ?>" alt="" style="height:26px;width:auto"></span><?php else: ?><span style="color:var(--muted)">—</span><?php endif; ?></td>
          <td><b><?= e($r['name']) ?></b></td>
          <td style="font-size:12px"><?php if (!empty($r['url'])): ?><a href="<?= e($r['url']) ?>" target="_blank" rel="noopener" style="color:var(--acc-ink)"><?= e(preg_replace('~^https?://~','',$r['url'])) ?></a><?php else: ?><span style="color:var(--muted)">no link</span><?php endif; ?></td>
          <td><span class="chip neu"><?= e($r['plate']) ?></span></td>
          <td><?= (int)$r['sort'] ?></td>
          <td><?= $r['visible'] ? '<span class="chip up">yes</span>' : '<span class="chip neu">hidden</span>' ?></td>
          <td style="text-align:right;white-space:nowrap">
            <a class="btn" href="<?= admin_url('?p=partners&edit='.(int)$r['id']) ?>" style="padding:5px 12px;font-size:12px">Edit</a>
            <form method="post" action="<?= admin_url('?p=partners') ?>" style="display:inline" onsubmit="return confirm('Remove this partner?')"><?= csrf_field() ?><input type="hidden" name="action" value="partner_delete"><input type="hidden" name="id" value="<?= (int)$r['id'] ?>"><button class="btn" type="submit" style="padding:5px 12px;font-size:12px">Delete</button></form>
          </td>
        </tr>
        <?php endforeach; ?>
      </tbody></table>
      <?php endif; ?>
    </div>
  </div>

  <div class="card card-p" style="max-width:760px">
    <div class="sec-title" style="margin-bottom:12px"><?= $edit ? 'Edit partner' : 'Add a partner' ?></div>
    <form method="post" action="<?= admin_url('?p=partners') ?>" enctype="multipart/form-data" style="display:grid;gap:14px"><?= csrf_field() ?>
      <input type="hidden" name="action" value="partner_save">
      <input type="hidden" name="id" value="<?= $edit ? (int)$edit['id'] : 0 ?>">
      <label style="display:grid;gap:5px"><span>Name</span><input type="text" name="name" required value="<?= e($edit['name'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <label style="display:grid;gap:5px"><span>Website URL</span><input type="text" name="url" placeholder="https://example.com" value="<?= e($edit['url'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <label style="display:grid;gap:5px"><span>Short description</span><textarea name="blurb" rows="3" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"><?= e($edit['blurb'] ?? '') ?></textarea></label>
      <label style="display:grid;gap:5px"><span>Logo <?php if (!empty($edit['logo'])): ?><span style="color:var(--muted);font-size:12px">(current: <?= e($edit['logo']) ?> — upload to replace)</span><?php endif; ?></span><input type="file" name="logofile" accept=".png,.svg,.jpg,.jpeg,.webp,.gif"></label>
      <label style="display:grid;gap:5px"><span>…or logo path</span><input type="text" name="logo" placeholder="uploads/… or assets/brand/…" value="<?= e($edit['logo'] ?? '') ?>" style="padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
      <div style="display:flex;gap:22px;flex-wrap:wrap;align-items:center">
        <span>Logo plate:</span>
        <label style="display:inline-flex;gap:6px;align-items:center"><input type="radio" name="plate" value="light" <?= (($edit['plate'] ?? 'light')==='light')?'checked':'' ?>> Light (dark logos)</label>
        <label style="display:inline-flex;gap:6px;align-items:center"><input type="radio" name="plate" value="dark" <?= (($edit['plate'] ?? '')==='dark')?'checked':'' ?>> Dark (white logos)</label>
      </div>
      <div style="display:flex;gap:22px;flex-wrap:wrap;align-items:center">
        <label style="display:inline-flex;gap:8px;align-items:center">Sort <input type="number" name="sort" value="<?= (int)($edit['sort'] ?? (count($rows)+1)) ?>" style="width:80px;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)"></label>
        <label style="display:inline-flex;gap:8px;align-items:center"><input type="checkbox" name="visible" value="1" <?= (!$edit || $edit['visible']) ? 'checked' : '' ?>> Show on site</label>
      </div>
      <div style="display:flex;gap:12px">
        <button class="btn btn-acc" type="submit"><?= $edit ? 'Save changes' : 'Add partner' ?></button>
        <?php if ($edit): ?><a class="btn" href="<?= admin_url('?p=partners') ?>">Cancel</a><?php endif; ?>
      </div>
    </form>
  </div>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'security') {
  require_cap('settings');
  layout_top('security', 'Security', 'Intrusion monitor — events, source IPs, and blocking');
  $curEmail = setting('security_alert_email', '');
  $ips      = security_ip_summary(7);
  $blocked  = security_blocked_ips();
  $events   = security_recent_events(80);
  $sevChip = function($s){ $s=(int)$s;
    if ($s>=3) return '<span class="chip dn">HIGH</span>';
    if ($s>=2) return '<span class="chip" style="background:#fde8c8;color:#9a5b00">MEDIUM</span>';
    return '<span class="chip neu">low</span>'; };
  $ago = function($ts){ $t=strtotime((string)$ts); if(!$t) return ''; $d=time()-$t;
    if($d<60) return $d.'s ago'; if($d<3600) return floor($d/60).'m ago';
    if($d<86400) return floor($d/3600).'h ago'; return floor($d/86400).'d ago'; };
  ?>
  <div class="page-head"><div><h1>Security</h1><p>Suspicious activity is logged with the source IP. Review and block/release IPs after verifying — logged-in admins are never blocked.</p></div></div>

  <div class="card card-p" style="max-width:820px;margin-bottom:18px">
    <div class="sec-title" style="margin-bottom:4px">Email alerts</div>
    <p style="color:var(--muted);font-size:13px;margin:2px 0 12px">Medium/high-severity events (malicious URL probes, login brute-force) email these recipients — throttled to about one per IP per hour. Comma-separate several. Leave blank to turn alerts off.</p>
    <form method="post" action="<?= admin_url('?p=security') ?>" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="sec_save">
      <input type="text" name="security_alert_email" value="<?= e($curEmail) ?>" placeholder="you@company.com, ops@company.com" style="flex:1;min-width:280px;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)">
      <button class="btn btn-acc" type="submit">Save</button>
      <span class="chip <?= $curEmail!==''?'up':'neu' ?>"><?= $curEmail!=='' ? 'Alerts ON' : 'Alerts OFF' ?></span>
    </form>
  </div>

  <div class="card" style="margin-bottom:18px">
    <div class="card-hd"><b>Blocked IPs</b><span class="chip neu"><?= count(array_filter($blocked, function($b){return $b['status']==='active';})) ?> active</span></div>
    <div class="card-p">
      <form method="post" action="<?= admin_url('?p=security') ?>" style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin-bottom:14px"><?= csrf_field() ?><input type="hidden" name="action" value="sec_block">
        <input type="text" name="ip" placeholder="Block an IP manually, e.g. 203.0.113.9" style="min-width:240px;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)">
        <input type="text" name="reason" placeholder="Reason (optional)" style="flex:1;min-width:180px;padding:9px 12px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--text)">
        <button class="btn btn-acc" type="submit">Block</button>
      </form>
      <?php if (!$blocked): ?><p style="color:var(--muted);margin:0">No IPs have been blocked.</p><?php else: ?>
      <table class="tbl"><thead><tr><th>IP</th><th>Status</th><th>Reason</th><th>Hits</th><th>By</th><th></th></tr></thead><tbody>
        <?php foreach ($blocked as $b): ?>
        <tr>
          <td><code><?= e($b['ip']) ?></code></td>
          <td><?= $b['status']==='active' ? '<span class="chip dn">blocked</span>' : '<span class="chip neu">released</span>' ?></td>
          <td style="color:var(--text-2)"><?= e($b['reason'] ?? '') ?></td>
          <td><?= (int)($b['hits'] ?? 0) ?></td>
          <td style="color:var(--muted);font-size:12px"><?= e($b['blocked_by'] ?? '') ?></td>
          <td style="text-align:right">
            <?php if ($b['status']==='active'): ?>
            <form method="post" action="<?= admin_url('?p=security') ?>" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="sec_release"><input type="hidden" name="ip" value="<?= e($b['ip']) ?>"><button class="btn" type="submit" style="padding:5px 12px;font-size:12px">Release</button></form>
            <?php else: ?>
            <form method="post" action="<?= admin_url('?p=security') ?>" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="sec_block"><input type="hidden" name="ip" value="<?= e($b['ip']) ?>"><input type="hidden" name="reason" value="Re-blocked"><button class="btn" type="submit" style="padding:5px 12px;font-size:12px">Re-block</button></form>
            <?php endif; ?>
          </td>
        </tr>
        <?php endforeach; ?>
      </tbody></table>
      <?php endif; ?>
    </div>
  </div>

  <div class="card" style="margin-bottom:18px">
    <div class="card-hd"><b>Source IPs — last 7 days</b><span class="chip neu"><?= count($ips) ?> IPs</span></div>
    <div class="card-p">
      <?php if (!$ips): ?><p style="color:var(--muted);margin:0">No security events recorded yet.</p><?php else: ?>
      <table class="tbl"><thead><tr><th>IP</th><th>Events</th><th>Worst</th><th>Probes</th><th>Login fails</th><th>Last seen</th><th></th></tr></thead><tbody>
        <?php foreach ($ips as $r): ?>
        <tr>
          <td><code><?= e($r['ip']) ?></code></td>
          <td><?= (int)$r['events'] ?></td>
          <td><?= $sevChip($r['max_sev']) ?></td>
          <td><?= (int)$r['probes'] ?></td>
          <td><?= (int)$r['logins'] ?></td>
          <td style="color:var(--muted);font-size:12px"><?= e($ago($r['last_seen'])) ?></td>
          <td style="text-align:right">
            <?php if (($r['block_status'] ?? '') === 'active'): ?><span class="chip dn">blocked</span>
            <?php else: ?><form method="post" action="<?= admin_url('?p=security') ?>" style="margin:0"><?= csrf_field() ?><input type="hidden" name="action" value="sec_block"><input type="hidden" name="ip" value="<?= e($r['ip']) ?>"><input type="hidden" name="reason" value="Blocked from IP activity review"><button class="btn" type="submit" style="padding:5px 12px;font-size:12px">Block</button></form><?php endif; ?>
          </td>
        </tr>
        <?php endforeach; ?>
      </tbody></table>
      <?php endif; ?>
    </div>
  </div>

  <div class="card">
    <div class="card-hd"><b>Recent events</b><span class="chip neu">latest <?= count($events) ?></span></div>
    <div class="card-p" style="overflow-x:auto">
      <?php if (!$events): ?><p style="color:var(--muted);margin:0">Nothing logged yet.</p><?php else: ?>
      <table class="tbl"><thead><tr><th>When</th><th>Severity</th><th>Type</th><th>IP</th><th>Path</th><th>Detail</th></tr></thead><tbody>
        <?php foreach ($events as $e2): ?>
        <tr>
          <td style="white-space:nowrap;color:var(--muted);font-size:12px"><?= e($ago($e2['created_at'])) ?></td>
          <td><?= $sevChip($e2['severity']) ?></td>
          <td><code><?= e($e2['type']) ?></code></td>
          <td><code><?= e($e2['ip']) ?></code></td>
          <td style="color:var(--text-2);font-size:12px;max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?= e($e2['path'] ?? '') ?></td>
          <td style="color:var(--text-2);font-size:12px"><?= e($e2['detail'] ?? '') ?></td>
        </tr>
        <?php endforeach; ?>
      </tbody></table>
      <?php endif; ?>
    </div>
  </div>
  <?php
  layout_bottom();
  exit;
}

if ($p === 'backup') {
  require_cap('settings');
  layout_top('backup', 'Backup & Restore', 'Download a full copy of your website content');
  $stats = [];
  foreach (['pages'=>'Pages','sections'=>'Content sections','posts'=>'Blog posts','submissions'=>'Enquiries','applications'=>'Applications','media'=>'Media items','settings'=>'Settings'] as $tb=>$lbl){
    try { $stats[$lbl] = count_rows($tb); } catch (\Throwable $e) { $stats[$lbl] = '—'; }
  }
  ?>
  <div class="page-head"><div><h1>Backup &amp; Restore</h1><p>Keep a safe copy of everything you’ve edited. Download regularly — before big changes and after adding content.</p></div></div>

  <div class="ed-layout">
    <div class="card card-p" style="max-width:640px">
      <div class="sec-title" style="margin-bottom:6px">Download a backup</div>
      <p style="color:var(--muted);font-size:13.5px;margin:0 0 16px">Creates one <code>.sql</code> file with your <b>complete database</b> — all pages, sections, blog posts, enquiries, media records, users and settings. Store it somewhere safe (your computer / Google Drive).</p>
      <form method="post" action="<?= admin_url('?p=backup') ?>"><?= csrf_field() ?><input type="hidden" name="action" value="backup_db">
        <button class="btn btn-primary" name="do" value="download"><svg class="ic ic-sm" viewBox="0 0 24 24"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg> Download database backup (.sql)</button>
      </form>
      <div class="badge b-neu" style="display:block;padding:10px 14px;margin-top:16px;line-height:1.55;font-size:12.5px">
        <b>Note:</b> this backs up the <b>database</b> only. Your uploaded images live in the <code>/uploads</code> folder — back those up from <b>cPanel → File Manager</b> (or cPanel’s Backup Wizard) as well.
      </div>
    </div>

    <div class="grid" style="align-content:start;gap:16px">
      <div class="card card-p">
        <div class="sec-title" style="font-size:12.5px;margin-bottom:12px">What’s inside</div>
        <?php foreach ($stats as $lbl=>$n): ?>
          <div style="display:flex;justify-content:space-between;padding:7px 0;border-bottom:1px solid var(--line);font-size:13px"><span style="color:var(--muted)"><?= e($lbl) ?></span><b><?= e((string)$n) ?></b></div>
        <?php endforeach; ?>
      </div>
      <div class="card card-p">
        <div class="sec-title" style="font-size:12.5px;margin-bottom:10px">How to restore</div>
        <ol style="margin:0;padding-left:18px;color:var(--muted);font-size:12.5px;line-height:1.7">
          <li>Open <b>cPanel → phpMyAdmin</b>.</li>
          <li>Select your NTPPA database.</li>
          <li>Click <b>Import</b>, choose your backup <code>.sql</code>, click <b>Go</b>.</li>
          <li>It safely drops &amp; recreates every table, then restores your content.</li>
        </ol>
      </div>
    </div>
  </div>
  <?php
  layout_bottom();
  exit;
}

redirect(admin_url('?p=dashboard'));

function find_post_by_id($id){ $st = db()->prepare('SELECT * FROM posts WHERE id=?'); $st->execute([$id]); return $st->fetch(); }
