<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="robots" content="noindex, nofollow, noarchive">
  <title>{{ asset_name|default('Asset') }}{% if asset_tag %} — {{ asset_tag }}{% endif %}</title>
  <link rel="icon" href="data:,">

<style>
  /* v409: Public asset landing page styles. Standalone — no theme
     header/footer, no inherited storefront CSS. */
  * { box-sizing: border-box; }
  html, body {
    margin: 0; padding: 0;
    background: #f4f6f8;
    color: #111;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
    -webkit-font-smoothing: antialiased;
    min-height: 100vh;
  }
  .ops-public-wrap {
    max-width: 560px;
    margin: 24px auto;
    padding: 0 16px;
    font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  }
  .ops-card {
    background: #fff;
    border-radius: 12px;
    overflow: hidden;
    box-shadow: 0 4px 20px rgba(0,0,0,0.08);
    margin-bottom: 16px;
  }
  .ops-banner {
    padding: 16px 20px;
    color: #fff;
    text-align: center;
  }
  .ops-banner h2 {
    margin: 0 0 4px 0;
    font-size: 18px;
    font-weight: 700;
    color: #fff;
  }
  .ops-banner p {
    margin: 0;
    font-size: 13px;
    opacity: 0.95;
    color: #fff;
  }
  .ops-banner.banner-amber { background: linear-gradient(135deg, #f59e0b, #d97706); }
  .ops-banner.banner-red   { background: linear-gradient(135deg, #dc2626, #991b1b); }
  .ops-banner.banner-grey  { background: linear-gradient(135deg, #6b7280, #4b5563); }
  .ops-banner-reason {
    margin-top: 10px !important;
    padding: 8px 12px;
    background: rgba(0,0,0,0.18);
    border-radius: 6px;
    font-size: 14px;
    line-height: 1.4;
  }
  .ops-banner-reason span {
    font-weight: 700;
    text-transform: uppercase;
    font-size: 11px;
    letter-spacing: 0.04em;
    opacity: 0.9;
    margin-right: 4px;
  }
  .ops-banner-meta {
    margin-top: 6px !important;
    font-size: 12px;
    opacity: 0.92;
  }

  .ops-brand-strip {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 16px;
    padding: 16px 20px;
    background: #f9fafb;
    border-bottom: 1px solid #e5e7eb;
  }
  /* v417.9: logo gets up to ~50% of the header, larger than before */
  .ops-brand-left {
    flex: 1 1 50%;
    min-width: 0;
    display: flex;
    align-items: center;
  }
  .ops-brand-strip img {
    max-height: 64px;
    max-width: 100%;
    object-fit: contain;
  }
  .ops-brand-name {
    font-weight: 700;
    color: #111;
    font-size: 14px;
  }

  .ops-asset {
    padding: 24px 20px;
  }
  /* v417.9: title row — name (flex, wraps) + AST tag (holds its column) */
  .ops-asset-titlerow {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: 10px;
    margin-bottom: 6px;
  }
  /* v417.18: left column (name/make-model/vin) flexes; right column
     (tag + Asset Details link) holds its own width, right-aligned. */
  .ops-asset-titlemain { flex: 1 1 auto; min-width: 0; }
  .ops-asset-titleside {
    flex: 0 0 auto;
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 6px;
    text-align: right;
  }
  .ops-asset-titlerow .ops-asset-name { margin: 0; flex: 1 1 auto; min-width: 0; }
  .ops-asset-titlerow .ops-asset-tag { margin: 4px 0 0 0; flex: 0 0 auto; white-space: nowrap; }
  .ops-asset-tag {
    display: inline-block;
    font-family: "SF Mono", Menlo, Consolas, "Courier New", monospace;
    font-size: 12px;
    font-weight: 700;
    color: #6b7280;
    background: #fff8dc;
    border: 1px solid #d6d3a8;
    border-radius: 4px;
    padding: 2px 8px;
    letter-spacing: 0.04em;
    margin-bottom: 8px;
  }
  .ops-asset-name {
    font-size: 22px;
    font-weight: 700;
    color: #111;
    margin: 0 0 6px 0;
    line-height: 1.25;
  }
  .ops-asset-mmy {
    font-size: 14px;
    color: #6b7280;
    margin: 0;
  }
  /* v417.5: VIN/serial under year/make/model */
  .ops-asset-vin {
    font-size: 13px;
    color: #6b7280;
    margin: 3px 0 0 0;
    font-family: "SF Mono", Menlo, Consolas, "Courier New", monospace;
    letter-spacing: 0.02em;
  }
  /* v417.9: location notes — now a right-hand header column (up to ~50%),
     staff only. (Was an absolute top-right chip prior to v417.9.) */
  .ops-loc-note {
    flex: 0 1 50%;
    max-width: 50%;
    text-align: right;
    background: #eff6ff;
    border: 1px solid #bfdbfe;
    border-radius: 6px;
    padding: 6px 10px;
    font-size: 12px;
    line-height: 1.35;
  }
  .ops-loc-note-label {
    display: block;
    font-weight: 700;
    color: #1d4ed8;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    font-size: 9px;
  }
  .ops-loc-note-text { color: #1e3a8a; word-break: break-word; }

  /* v417.8/v417.9: checkout/checkin button, centered, below the images */
  .ops-asset-checkout {
    text-align: center;
    padding: 16px 20px 20px;
  }
  .ops-checkout-btn {
    display: inline-block;
    min-width: 200px;
    padding: 11px 22px;
    border-radius: 999px;
    border: 2px solid #2563eb;
    background: #eff6ff;
    color: #1d4ed8;
    font-size: 14px;
    font-weight: 700;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .ops-checkout-btn:active { background: #dbeafe; }
  .ops-checkout-btn.checkin {
    border-color: #f59e0b;
    background: #fef3c7;
    color: #92400e;
  }

  /* v415: Asset image gallery */
  .ops-asset-gallery {
    margin: 16px 20px 0;
  }
  .ops-asset-gallery-main {
    width: 100%;
    max-height: 320px;
    overflow: hidden;
    border-radius: 8px;
    background: #f3f4f6;
    border: 1px solid #e5e7eb;
    cursor: zoom-in;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  .ops-asset-gallery-main img {
    width: 100%;
    max-height: 320px;
    object-fit: cover;
    display: block;
  }
  .ops-asset-gallery-thumbs {
    display: flex;
    gap: 6px;
    margin-top: 8px;
    overflow-x: auto;
    padding-bottom: 4px;
    /* Smooth horizontal scroll on mobile */
    -webkit-overflow-scrolling: touch;
  }
  .ops-gallery-thumb {
    flex: 0 0 64px;
    width: 64px;
    height: 64px;
    object-fit: cover;
    border-radius: 6px;
    border: 2px solid #e5e7eb;
    cursor: pointer;
    background: #f3f4f6;
    transition: border-color 0.15s, transform 0.1s;
  }
  .ops-gallery-thumb:hover { border-color: #9ca3af; }
  .ops-gallery-thumb.active {
    border-color: #2f6f9f;
  }

  /* v417: Inline staff uploader attached to the gallery area */
  .ops-asset-gallery-empty {
    width: 100%;
    min-height: 120px;
    border-radius: 8px;
    background: #f9fafb;
    border: 1px dashed #d1d5db;
    color: #9ca3af;
    font-size: 13px;
    display: flex;
    align-items: center;
    justify-content: center;
    text-align: center;
    padding: 0 16px;
  }
  .ops-asset-uploader {
    margin-top: 10px;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 10px;
  }
  .ops-asset-upload-btn {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    padding: 9px 14px;
    background: #eff6ff;
    border: 1px solid #2563eb;
    color: #1d4ed8;
    border-radius: 8px;
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .ops-asset-upload-btn:active { background: #dbeafe; }
  .ops-asset-primary-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: #374151;
    cursor: pointer;
    user-select: none;
  }
  .ops-asset-primary-toggle input {
    width: 16px;
    height: 16px;
    margin: 0;
    accent-color: #2563eb;
  }
  .ops-asset-upload-status {
    flex: 1 1 100%;
    font-size: 12px;
    color: #6b7280;
    min-height: 14px;
  }

  /* v415: Lightbox — full-screen overlay for tap-to-zoom */
  .ops-lightbox {
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.92);
    display: none;
    align-items: center;
    justify-content: center;
    z-index: 9999;
    cursor: zoom-out;
  }
  .ops-lightbox.active { display: flex; }
  .ops-lightbox img {
    max-width: 96vw;
    max-height: 92vh;
    object-fit: contain;
  }
  .ops-lightbox-close {
    position: absolute;
    top: 12px;
    right: 16px;
    color: #fff;
    font-size: 32px;
    font-weight: 300;
    line-height: 1;
    background: none;
    border: none;
    cursor: pointer;
    padding: 4px 10px;
  }

  /* v417.16: Asset Details link + popup */
  .ops-detail-link {
    display: inline-block;
    margin: 0;
    font-size: 13px;
    font-weight: 600;
    color: #2563eb;
    text-decoration: none;
    white-space: nowrap;
  }
  .ops-detail-link:active { color: #1d4ed8; }
  .ops-detail-modal {
    display: none;
    position: fixed;
    inset: 0;
    background: rgba(0,0,0,0.55);
    z-index: 1000;
    align-items: flex-end;
    justify-content: center;
  }
  .ops-detail-modal.active { display: flex; }
  .ops-detail-box {
    background: #fff;
    width: 100%;
    max-width: 480px;
    max-height: 80vh;
    border-radius: 14px 14px 0 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
  }
  @media (min-width: 520px) {
    .ops-detail-modal { align-items: center; }
    .ops-detail-box { border-radius: 14px; }
  }
  .ops-detail-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: 14px 18px;
    border-bottom: 1px solid #eee;
    font-size: 16px;
    font-weight: 700;
    color: #111;
  }
  .ops-detail-x {
    background: none;
    border: none;
    font-size: 18px;
    color: #6b7280;
    cursor: pointer;
    padding: 4px 8px;
  }
  .ops-detail-body {
    padding: 6px 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .ops-detail-row {
    display: flex;
    justify-content: space-between;
    gap: 16px;
    padding: 11px 18px;
    border-bottom: 1px solid #f3f4f6;
    font-size: 14px;
  }
  .ops-detail-row:last-child { border-bottom: none; }
  .ops-detail-label { color: #6b7280; flex: 0 0 auto; }
  .ops-detail-value { color: #111; font-weight: 600; text-align: right; word-break: break-word; }
  /* v417.17: notes block at the top of the popup */
  .ops-detail-notes {
    padding: 12px 18px;
    border-bottom: 1px solid #f3f4f6;
    background: #fbfbfd;
  }
  .ops-detail-notes-label {
    font-size: 11px;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.04em;
    color: #6b7280;
    margin-bottom: 4px;
  }
  .ops-detail-notes-text {
    font-size: 14px;
    color: #111;
    line-height: 1.45;
    word-break: break-word;
  }

  .ops-actions {
    padding: 18px 20px 20px 20px;
  }
  .ops-actions h3 {
    font-size: 15px;
    font-weight: 600;
    color: #111;
    margin: 0 0 12px 0;
  }
  .ops-action-btn {
    display: block;
    width: 100%;
    padding: 14px 18px;
    background: #fff;
    border: 2px solid #e5e7eb;
    border-radius: 10px;
    text-align: left;
    font-size: 15px;
    font-weight: 600;
    color: #111;
    margin-bottom: 10px;
    cursor: pointer;
    transition: all 0.15s;
  }
  .ops-action-btn:hover, .ops-action-btn:focus {
    border-color: #2563eb;
    background: #eff6ff;
  }
  .ops-action-btn.urgent {
    background: #dc2626;
    border-color: #dc2626;
    color: #fff;
  }
  .ops-action-btn.urgent:hover {
    background: #991b1b;
    border-color: #991b1b;
  }
  .ops-action-btn .arrow {
    float: right;
    opacity: 0.5;
  }
  .ops-action-btn .sub {
    display: block;
    font-size: 12px;
    font-weight: 400;
    color: #6b7280;
    margin-top: 2px;
  }
  .ops-action-btn.urgent .sub {
    color: rgba(255,255,255,0.85);
  }

  .ops-form {
    padding: 0 20px 20px 20px;
  }
  .ops-form-group {
    margin-bottom: 14px;
  }
  .ops-form-group label {
    display: block;
    font-size: 13px;
    font-weight: 600;
    color: #374151;
    margin-bottom: 4px;
  }
  .ops-form-group label .req {
    color: #dc2626;
  }
  .ops-form-group input, .ops-form-group textarea {
    width: 100%;
    padding: 10px 12px;
    border: 1px solid #d1d5db;
    border-radius: 8px;
    font-size: 15px;
    font-family: inherit;
    color: #111;
    box-sizing: border-box;
  }
  .ops-form-group input:focus, .ops-form-group textarea:focus {
    outline: none;
    border-color: #2563eb;
    box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
  }
  .ops-form-group textarea {
    min-height: 90px;
    resize: vertical;
  }
  .ops-form-help {
    font-size: 12px;
    color: #6b7280;
    margin: 4px 0 0 0;
  }
  .ops-submit {
    width: 100%;
    padding: 14px;
    background: #2563eb;
    color: #fff;
    border: none;
    border-radius: 10px;
    font-size: 15px;
    font-weight: 700;
    cursor: pointer;
  }
  .ops-submit:hover { background: #1d4ed8; }
  .ops-submit.urgent { background: #dc2626; }
  .ops-submit.urgent:hover { background: #991b1b; }

  .ops-cancel {
    display: block;
    text-align: center;
    margin-top: 12px;
    font-size: 13px;
    color: #6b7280;
    text-decoration: none;
  }
  .ops-cancel:hover { color: #111; }

  .ops-flash {
    padding: 14px 18px;
    border-radius: 10px;
    margin-bottom: 16px;
    font-size: 14px;
  }
  .ops-flash.success {
    background: #d1fae5;
    color: #065f46;
    border: 1px solid #a7f3d0;
  }
  .ops-flash.error {
    background: #fee2e2;
    color: #991b1b;
    border: 1px solid #fecaca;
  }
  .ops-flash ul {
    margin: 4px 0 0 0;
    padding-left: 20px;
  }

  .ops-contact {
    background: #f9fafb;
    border-radius: 10px;
    padding: 14px 16px;
    margin-top: 16px;
    text-align: center;
    font-size: 13px;
    color: #6b7280;
  }
  .ops-contact a {
    color: #2563eb;
    text-decoration: none;
    font-weight: 600;
  }

  .ops-meta {
    text-align: center;
    color: #9ca3af;
    font-size: 11px;
    margin-top: 16px;
  }
  .ops-meta .tag {
    font-family: "SF Mono", Menlo, Consolas, "Courier New", monospace;
  }
  .ops-privacy {
    margin-top: 4px;
    font-size: 10px;
    color: #b5bcc4;
  }
  .ops-privacy a {
    color: #9ca3af;
    text-decoration: underline;
  }
  .ops-privacy a:hover { color: #6b7280; }

  /* v410: Honeypot — hidden from sighted users + screen readers via
     aria-hidden in the markup. Bots that fill all fields trigger
     the silent-discard logic on submit. */
  .ops-hp {
    position: absolute !important;
    left: -9999px !important;
    top: -9999px !important;
    width: 1px !important;
    height: 1px !important;
    overflow: hidden !important;
    opacity: 0 !important;
    pointer-events: none !important;
  }

  /* v412: Staff-only UI — distinct visual style so staff can tell
     at a glance they're seeing the privileged view. */
  .ops-staff-banner {
    background: linear-gradient(135deg, #1e3a8a, #2563eb);
    color: #fff;
    padding: 10px 14px;
    border-radius: 10px;
    margin-bottom: 12px;
    font-size: 13px;
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 10px;
    flex-wrap: wrap;
  }
  .ops-staff-banner .who { font-weight: 600; }
  .ops-staff-banner a {
    color: #fff;
    text-decoration: underline;
    font-size: 12px;
    opacity: 0.95;
  }
  .ops-staff-section {
    background: #fff;
    border-radius: 10px;
    border: 1px solid #e5e7eb;
    margin-bottom: 12px;
    overflow: hidden;
  }
  .ops-staff-section h3 {
    margin: 0;
    padding: 10px 14px;
    background: #f9fafb;
    font-size: 13px;
    color: #6b7280;
    font-weight: 600;
    border-bottom: 1px solid #e5e7eb;
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  .ops-staff-section .body { padding: 12px 14px; }
  .ops-ctx-row {
    display: flex;
    justify-content: space-between;
    padding: 8px 14px;
    font-size: 13px;
    border-bottom: 1px solid #f3f4f6;
  }
  .ops-ctx-row:last-child { border-bottom: 0; }
  .ops-ctx-row .label { color: #6b7280; }
  .ops-ctx-row .val { color: #111; font-weight: 500; }
  .ops-ctx-row.overdue .val { color: #dc2626; font-weight: 700; }
  .ops-empty {
    padding: 10px 14px;
    color: #9ca3af;
    font-size: 12px;
    font-style: italic;
  }
  .ops-staff-actions {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
    padding: 12px 14px;
  }
  /* v417.8: balanced 3-up row for Status / Task / Pulse */
  .ops-staff-actions-3 { grid-template-columns: 1fr 1fr 1fr; }
  .ops-staff-actions-3 .ops-staff-btn { padding: 12px 6px; font-size: 12px; }

  /* v417.10: Active / Out-of-Service status toggle (segmented control) */
  .ops-status-toggle {
    display: flex;
    gap: 0;
    border: 2px solid #d1d5db;
    border-radius: 10px;
    overflow: hidden;
  }
  .ops-status-opt {
    flex: 1;
    padding: 13px 10px;
    background: #fff;
    border: none;
    font-size: 14px;
    font-weight: 700;
    color: #6b7280;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 0.12s, color 0.12s;
  }
  .ops-status-opt + .ops-status-opt { border-left: 2px solid #d1d5db; }
  /* v417.15: scope under .ops-status-toggle so these beat the generic
     ".ops-staff-form button { background:#2563eb }" rule — otherwise the
     UNSELECTED toggle option inherits a blue fill. Selected stays
     green/red; Update + Cancel keep their blue. */
  .ops-status-toggle .ops-status-opt { background: #fff; color: #6b7280; }
  .ops-status-toggle .ops-status-opt.sel { background: #dcfce7; color: #166534; }
  .ops-status-toggle .ops-status-opt.danger.sel { background: #fee2e2; color: #991b1b; }
  .ops-staff-btn {
    padding: 12px 10px;
    background: #fff;
    border: 2px solid #d1d5db;
    border-radius: 8px;
    font-size: 13px;
    font-weight: 600;
    color: #111;
    cursor: pointer;
    text-align: center;
    text-decoration: none;
    transition: all 0.15s;
  }
  .ops-staff-btn:hover, .ops-staff-btn:focus {
    border-color: #2563eb;
    background: #eff6ff;
  }
  .ops-staff-btn.checkout-active {
    background: #fef3c7;
    border-color: #f59e0b;
  }
  .ops-staff-form { padding: 12px 14px; }
  .ops-staff-form select, .ops-staff-form input, .ops-staff-form textarea {
    width: 100%;
    padding: 9px 11px;
    border: 1px solid #d1d5db;
    border-radius: 6px;
    font-size: 14px;
    font-family: inherit;
    box-sizing: border-box;
    margin-bottom: 8px;
  }
  .ops-staff-form .row-2 {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }
  .ops-staff-form textarea { min-height: 70px; resize: vertical; }
  .ops-staff-form button {
    padding: 10px 14px;
    background: #2563eb;
    color: #fff;
    border: none;
    border-radius: 6px;
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
  }
  .ops-tag-mini {
    display: inline-block;
    font-size: 10px;
    padding: 1px 6px;
    border-radius: 3px;
    margin-left: 4px;
    text-transform: uppercase;
    letter-spacing: 0.04em;
  }
  .ops-tag-mini.urgent { background: #dc2626; color: #fff; }
  .ops-tag-mini.high   { background: #f59e0b; color: #fff; }
  .ops-tag-mini.overdue{ background: #991b1b; color: #fff; }

  /* v417.7: filterable multi-select pick lists for Create Task */
  .ops-assign-label {
    font-size: 11px;
    font-weight: 700;
    color: #6b7280;
    text-transform: uppercase;
    letter-spacing: 0.03em;
    margin: 8px 0 4px;
    display: flex;
    align-items: center;
    justify-content: space-between;
  }
  .ops-pick-count {
    font-size: 11px;
    font-weight: 600;
    color: #2563eb;
    text-transform: none;
    letter-spacing: 0;
  }
  .ops-pick-filter {
    width: 100%;
    padding: 9px 12px;
    border: 1px solid #d1d5db;
    border-radius: 8px;
    font-size: 14px;
    margin-bottom: 6px;
    -webkit-appearance: none;
  }
  .ops-pick-list {
    border: 1px solid #e5e7eb;
    border-radius: 8px;
    max-height: 200px;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    background: #fff;
  }
  .ops-pick-row {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 10px 12px;
    border-bottom: 1px solid #f3f4f6;
    cursor: pointer;
    font-size: 14px;
    color: #374151;
    margin: 0;
  }
  .ops-pick-row:last-child { border-bottom: none; }
  .ops-pick-row input {
    width: 18px;
    height: 18px;
    margin: 0;
    flex: 0 0 auto;
    accent-color: #2563eb;
  }
  .ops-pick-row:has(input:checked) {
    background: #eff6ff;
    color: #1d4ed8;
    font-weight: 600;
  }
  .ops-pick-list-team .ops-pick-row:has(input:checked) {
    background: #f5f3ff;
    color: #6d28d9;
  }
  .ops-pick-list-team .ops-pick-row input { accent-color: #7c3aed; }
  .ops-pick-row.ops-hidden { display: none; }
  .ops-pick-empty {
    padding: 12px;
    text-align: center;
    color: #9ca3af;
    font-size: 12px;
  }

  /* v412.1: Pulse form type-toggle and photo thumbnails */
  .ops-pulse-type {
    display: block;
    padding: 10px 8px;
    border: 2px solid #d1d5db;
    border-radius: 8px;
    text-align: center;
    cursor: pointer;
    font-size: 14px;
    font-weight: 600;
    background: #fff;
    transition: all 0.15s;
  }
  .ops-pulse-type input { display: none; }
  .ops-pulse-type input:checked + span { color: #1d4ed8; }
  .ops-pulse-type:has(input:checked) {
    border-color: #2563eb;
    background: #eff6ff;
  }
  .ops-pulse-photo {
    position: relative;
    width: 64px;
    height: 64px;
    border-radius: 6px;
    overflow: hidden;
    border: 1px solid #d1d5db;
    background: #f4f6f8;
  }
  .ops-pulse-photo img {
    width: 100%;
    height: 100%;
    object-fit: cover;
  }
  .ops-pulse-photo .x {
    position: absolute;
    top: 2px;
    right: 2px;
    background: rgba(0,0,0,0.7);
    color: #fff;
    width: 18px;
    height: 18px;
    border-radius: 50%;
    line-height: 18px;
    text-align: center;
    font-size: 12px;
    cursor: pointer;
    user-select: none;
  }
  .ops-pulse-photo.uploading {
    display: flex;
    align-items: center;
    justify-content: center;
    color: #6b7280;
    font-size: 10px;
  }
</style>
</head>
<body>

<div class="ops-public-wrap">

  {# Status banner (above the card) — shown for any status with banner config #}
  {% if banner_title %}
    <div class="ops-card">
      <div class="ops-banner {{ banner_class }}">
        <h2>{{ banner_title }}</h2>
        {% if banner_text %}<p>{{ banner_text }}</p>{% endif %}
        {# v417: show the captured out-of-service reason to everyone #}
        {% if status == 'in_repair' and status_reason %}
          <p class="ops-banner-reason"><span>Reason:</span> {{ status_reason }}</p>
        {% endif %}
        {# v417.3: who put it out of service + when #}
        {% if status == 'in_repair' and (status_by_name or status_at_fmt) %}
          <p class="ops-banner-meta">
            {% if status_by_name %}By {{ status_by_name }}{% endif %}
            {% if status_by_name and status_at_fmt %} · {% endif %}
            {% if status_at_fmt %}{{ status_at_fmt }}{% if status_at_rel %} ({{ status_at_rel }}){% endif %}{% endif %}
          </p>
        {% endif %}
      </div>
    </div>
  {% endif %}

  {# Flash messages #}
  {% if flash %}
    {% if flash.type == 'success' %}
      <div class="ops-flash success">
        <strong>✓ {{ flash.message }}</strong>
      </div>
    {% elseif flash.type == 'error' %}
      <div class="ops-flash error">
        <strong>Please fix the following:</strong>
        <ul>
          {% for e in flash.errors %}<li>{{ e }}</li>{% endfor %}
        </ul>
      </div>
    {% endif %}
  {% endif %}

  {# Main asset card #}
  <div class="ops-card">
    {# v417.9: header row — brand/logo on the left (up to ~50%, larger)
       and the staff location box on the right (up to ~50%). Gap between
       so they never touch. Location is staff-only. #}
    {% if logo_url or brand_name or (is_staff and location_notes) %}
      <div class="ops-brand-strip">
        <div class="ops-brand-left">
          {% if logo_url %}<img src="{{ logo_url }}" alt="">{% endif %}
          {% if brand_name and not logo_url %}<span class="ops-brand-name">{{ brand_name }}</span>{% endif %}
        </div>
        {% if is_staff and location_notes %}
          <div class="ops-loc-note" title="Location notes (staff only)">
            <span class="ops-loc-note-label">📍 Location</span>
            <span class="ops-loc-note-text">{{ location_notes }}</span>
          </div>
        {% endif %}
      </div>
    {% endif %}
    <div class="ops-asset">
      {# v417.18: name + make/model + VIN on the left; the AST tag and the
         "Asset Details" link stacked on the right (link just under tag).
         Checkout moved out of the header into the staff actions row. #}
      <div class="ops-asset-titlerow">
        <div class="ops-asset-titlemain">
          <h1 class="ops-asset-name">{{ asset_name }}</h1>
          {% if mmy_line %}<p class="ops-asset-mmy">{{ mmy_line }}</p>{% endif %}
          {% if vin %}<p class="ops-asset-vin">VIN/SN: {{ vin }}</p>{% endif %}
        </div>
        <div class="ops-asset-titleside">
          {% if asset_tag %}<span class="ops-asset-tag">{{ asset_tag }}</span>{% endif %}
          {% if asset_details or asset_notes or asset_capabilities %}
            <a href="#" class="ops-detail-link" onclick="opsDetailsOpen(event)">📋 Asset Details</a>
          {% endif %}
        </div>
      </div>
    </div>

    {# v415: Asset image gallery — primary image on top, additional
       thumbs below. All images are is_public=1 (the catalog model
       filters); internal-only images aren't returned here. Tap any
       thumb to swap into the primary slot. Tap the primary itself
       to open the lightbox at full resolution.
       v417: Inline staff uploader now lives directly under the gallery
       (was a separate collapsed section). Shows for staff even when the
       asset has no photos yet (empty state). #}
    {% if asset_images or is_staff %}
      <div class="ops-asset-gallery" id="ops-asset-gallery">
        {% if asset_images %}
          <div class="ops-asset-gallery-main">
            <img id="ops-gallery-primary"
                 src="{{ image_url_base }}&attachment_id={{ asset_images[0].attachment_id }}&size=thumb"
                 data-full="{{ image_url_base }}&attachment_id={{ asset_images[0].attachment_id }}"
                 alt="{{ asset_name }}"
                 onclick="opsGalleryLightbox(this.dataset.full)">
          </div>
          {% if asset_images|length > 1 %}
            <div class="ops-asset-gallery-thumbs">
              {% for img in asset_images %}
                <img class="ops-gallery-thumb{% if loop.first %} active{% endif %}"
                     src="{{ image_url_base }}&attachment_id={{ img.attachment_id }}&size=thumb"
                     data-full="{{ image_url_base }}&attachment_id={{ img.attachment_id }}"
                     alt=""
                     onclick="opsGallerySwap(this)">
              {% endfor %}
            </div>
          {% endif %}
        {% elseif is_staff %}
          <div class="ops-asset-gallery-empty">No photos yet — add the first one below.</div>
        {% endif %}

        {% if is_staff and not asset_images %}
          {# v417.15: the inline uploader only shows here when the asset has
             NO photos yet (add the first one). Once images exist, the
             uploader moves to the bottom of the page (below "open in
             admin") to keep the main area uncluttered. #}
          <div class="ops-asset-uploader">
            {# v417.18: separate camera + gallery inputs so staff can EITHER
               take a photo (capture=environment) OR pick from the gallery
               (no capture). A single input doesn't reliably offer both on
               every phone. #}
            <input id="ops-asset-photos" type="file" accept="image/*" multiple style="display:none;" onchange="opsUploadAssetPhotos(this)">
            <input id="ops-asset-camera" type="file" accept="image/*" capture="environment" style="display:none;" onchange="opsUploadAssetPhotos(this)">
            <label for="ops-asset-camera" class="ops-asset-upload-btn">📷 Take photo</label>
            <label for="ops-asset-photos" class="ops-asset-upload-btn">🖼 Gallery</label>
            <label class="ops-asset-primary-toggle">
              <input type="checkbox" id="ops-asset-set-primary" checked>
              Set as main photo
            </label>
            <div id="ops-asset-photo-status" class="ops-asset-upload-status"></div>
          </div>
        {% endif %}
      </div>
    {% endif %}
  </div>

  {# v412: Staff-only UI section — visible only when an admin user
     with ops/asset permission is logged in. Shows above the public
     actions, providing context + privileged action buttons.
     v417.9: the "Open in admin" banner moved to the bottom of the page
     (just above the contact section). #}
  {% if is_staff %}
    {# v417.14: Routine tasks moved ABOVE the staff actions section. #}
    {% if staff_ctx and staff_ctx.routine_tasks|length > 0 %}
      <div class="ops-staff-section">
        <h3>Routine tasks for this asset</h3>
        <div style="padding:0 14px 12px 14px;">
          {% for rt in staff_ctx.routine_tasks %}
            <a href="{{ admin_url|split('&asset_id=')[0]|replace({'ops/asset/detail':'ops/task/startRoutine'}) }}&routine_task_id={{ rt.routine_task_id }}" class="ops-staff-btn" style="display:block;text-align:left;margin-bottom:4px;font-size:12px;padding:8px 10px;">
              ▶ {{ rt.title }}{% if rt.is_ongoing %} <span class="ops-tag-mini high">ongoing</span>{% endif %}
            </a>
          {% endfor %}
        </div>
      </div>
    {% endif %}

    {# v417.18: Staff actions — checkout/checkin + Create task + Pulse. #}
    <div class="ops-staff-section">
      <h3>Staff actions</h3>
      <div class="ops-staff-actions ops-staff-actions-3">
        <form method="post" action="{{ form_url }}" style="margin:0;">
          {% if staff_ctx and staff_ctx.checkout and staff_ctx.checkout.current_checkout_user_id %}
            <input type="hidden" name="action" value="checkin">
            <button type="submit" class="ops-staff-btn" style="width:100%;">✅ I'm done using this</button>
          {% else %}
            <input type="hidden" name="action" value="checkout">
            <button type="submit" class="ops-staff-btn" style="width:100%;">🙋 I'm using this</button>
          {% endif %}
        </form>
        <button type="button" class="ops-staff-btn" onclick="opsToggleStaff('task')">✅ Create task</button>
        <button type="button" class="ops-staff-btn" onclick="opsToggleStaff('pulse')">💙 Pulse 💔</button>
      </div>
    </div>

    {# v417.14: Status — always visible directly under Staff actions.
       Toggling to the status that ISN'T current reveals Update + Cancel;
       choosing Out of Service also reveals the required reason box. This
       whole section is hidden by JS while the Task or Pulse form is open. #}
    <div class="ops-staff-section" id="ops-staff-status">
      <h3>Status</h3>
      <form method="post" action="{{ form_url }}" class="ops-staff-form">
        <input type="hidden" name="action" value="status_change">
        <input type="hidden" name="new_status" id="ops-status-value" value="{{ staff_ctx.current_status }}">
        <div class="ops-status-toggle">
          <button type="button" class="ops-status-opt{% if staff_ctx.current_status == 'active' %} sel{% endif %}"
                  data-status="active" onclick="opsStatusPick('active')">🟢 Ready to use</button>
          <button type="button" class="ops-status-opt danger{% if staff_ctx.current_status == 'in_repair' %} sel{% endif %}"
                  data-status="in_repair" onclick="opsStatusPick('in_repair')">🔴 Out of Service</button>
        </div>
        <div id="ops-status-reason-wrap" style="display:none;margin-top:8px;">
          <textarea name="status_reason" id="ops-status-reason"
                    placeholder="Why is this asset out of service? (required — shown to anyone who scans it)"
                    maxlength="500">{% if flash and flash.sticky and flash.sticky.action == 'status_change' %}{{ flash.sticky.status_reason }}{% endif %}</textarea>
        </div>
        {# commit row — hidden until the status is actually changed #}
        <div id="ops-status-commit" style="display:none;gap:8px;margin-top:8px;">
          <button type="submit" style="flex:1;">Update status</button>
          <button type="button" class="ops-staff-btn" onclick="opsStatusCancel()" style="flex:0 0 auto;padding:10px 14px;">Cancel</button>
        </div>
      </form>
    </div>

    {# Context: who has it, recent activity, open issues.
       v417.13: wrapped so it can be auto-hidden while a staff action
       form is open (keeps the form right under the buttons + simplifies
       the page). #}
    <div id="ops-staff-context">
    {% if staff_ctx %}
      {% set has_open_reports = staff_ctx.open_reports|length > 0 %}
      {% set has_open_tasks = staff_ctx.open_tasks|length > 0 %}
      {% set has_checkout = staff_ctx.checkout and staff_ctx.checkout.current_checkout_user_id %}
      {% set has_assigned = staff_ctx.checkout and staff_ctx.checkout.assigned_user_id %}

      {# Assignment / checkout state #}
      {% if has_assigned or has_checkout %}
        <div class="ops-staff-section">
          <h3>Status</h3>
          <div>
            {% if has_assigned %}
              <div class="ops-ctx-row">
                <span class="label">Assigned to</span>
                <span class="val">{{ staff_ctx.checkout.assigned_name }}</span>
              </div>
            {% endif %}
            {% if has_checkout %}
              <div class="ops-ctx-row">
                <span class="label">Checked out to</span>
                <span class="val">{{ staff_ctx.checkout.checkout_name }} · {{ staff_ctx.checkout.checkout_relative }}</span>
              </div>
            {% endif %}
          </div>
        </div>
      {% endif %}

      {# Open reports submitted via public form #}
      {% if has_open_reports %}
        <div class="ops-staff-section">
          <h3>Open reports ({{ staff_ctx.open_reports|length }})</h3>
          <div>
            {% for r in staff_ctx.open_reports %}
              <div class="ops-ctx-row" style="flex-direction:column;align-items:flex-start;gap:4px;">
                <div style="display:flex;justify-content:space-between;width:100%;font-size:12px;">
                  <span class="label">
                    {% if r.action == 'found' %}<span class="ops-tag-mini urgent">found</span>
                    {% else %}<span class="ops-tag-mini high">problem</span>{% endif %}
                    {{ r.reporter_name|default('(anonymous)') }}
                  </span>
                  <span class="label">{{ r.relative }}</span>
                </div>
                {% if r.text_short %}
                  <div style="font-size:13px;color:#374151;">{{ r.text_short }}{% if r.report_text|length > 140 %}…{% endif %}</div>
                {% endif %}
              </div>
            {% endfor %}
          </div>
        </div>
      {% endif %}

      {# Open tasks linked to this asset #}
      {% if has_open_tasks %}
        <div class="ops-staff-section">
          <h3>Open tasks ({{ staff_ctx.open_tasks|length }})</h3>
          <div>
            {% for t in staff_ctx.open_tasks %}
              <div class="ops-ctx-row {% if t.is_overdue %}overdue{% endif %}" style="align-items:flex-start;flex-wrap:wrap;gap:6px;">
                <span class="label" style="flex:1;min-width:0;">
                  {{ t.title }}
                  {% if t.priority == 'urgent' %}<span class="ops-tag-mini urgent">urgent</span>
                  {% elseif t.priority == 'high' %}<span class="ops-tag-mini high">high</span>{% endif %}
                  {% if t.is_overdue %}<span class="ops-tag-mini overdue">overdue</span>{% endif %}
                  <div style="font-size:11px;color:#9ca3af;font-weight:400;margin-top:2px;">
                    {% if t.assigned_names %}{{ t.assigned_names }}{% endif %}
                    {% if t.due_disp %} · due {{ t.due_disp }}{% endif %}
                  </div>
                </span>
                {# v412.1: Quick-start link to existing admin task/start endpoint #}
                <a href="{{ admin_base }}index.php?route=ops/task/start&user_token={{ user_token }}&task_id={{ t.task_id }}"
                   class="ops-staff-btn"
                   style="flex:0 0 auto;padding:6px 10px;font-size:12px;background:#2563eb;color:#fff;border-color:#2563eb;">
                  ▶ Start
                </a>
              </div>
            {% endfor %}
          </div>
        </div>
      {% endif %}

      {# Recent staff scans (excludes current user) #}
      {% if staff_ctx.recent_scans|length > 0 %}
        <div class="ops-staff-section">
          <h3>Recent staff activity</h3>
          <div>
            {% for s in staff_ctx.recent_scans %}
              <div class="ops-ctx-row">
                <span class="label">{{ s.staff_name|default('staff') }} {{ s.action_label }}</span>
                <span class="val">{{ s.relative }}</span>
              </div>
            {% endfor %}
          </div>
        </div>
      {% endif %}
    {% endif %}
    </div>{# /ops-staff-context #}

    {# v417.10: "Staff actions" section relocated to directly below the
       image section (top of the staff block). See above. #}

    {# v417: "Add note" removed from the scan page. Notes are managed
       from the admin asset detail page where the full history is
       visible. Task + Pulse cover in-the-field logging. The catalog
       'note' action handler is left intact (harmless) in case of a
       stale link. #}

    {# v417.14: status section moved up to directly below Staff actions
       and made always-visible (see above). #}

    {# Create task form (hidden by default) #}
    <div class="ops-staff-section" id="ops-staff-task" style="display:none;">
      <h3>Create a task</h3>
      {# v417.6: file input lives OUTSIDE the form (same pattern as the
         Pulse uploader) so the browser never includes the raw file in
         the form POST — photos are staged via fetch and attached by
         temp_id. capture omitted so the OS picker offers camera + gallery. #}
      <input id="ops-task-photos" type="file" accept="image/*" multiple style="display:none;" onchange="opsUploadTaskPhotos(this)">
      <form method="post" action="{{ form_url }}" class="ops-staff-form" id="ops-task-form">
        <input type="hidden" name="action" value="create_task">
        <input type="text" name="task_title" placeholder="Task title (required)" required maxlength="255" value="{% if flash and flash.sticky and flash.sticky.action == 'create_task' %}{{ flash.sticky.task_title }}{% endif %}">
        {# v417.5: optional task description #}
        <textarea name="task_description" placeholder="Description (optional)" maxlength="2000" rows="2" style="margin-top:8px;">{% if flash and flash.sticky and flash.sticky.action == 'create_task' %}{{ flash.sticky.task_description }}{% endif %}</textarea>
        {# v417.7: filterable multi-select lists for people + teams.
           Type to filter; check as many as you want. Checked rows stay
           selected even when filtered out of view. #}
        <div class="ops-assign-label">
          Assign to people <span class="ops-pick-count" id="ops-people-count">0 selected</span>
        </div>
        <input type="text" class="ops-pick-filter" placeholder="Search people…"
               oninput="opsFilterPick(this, 'ops-people-list')">
        <div class="ops-pick-list" id="ops-people-list" onchange="opsPickCount('ops-people-list','ops-people-count')">
          <label class="ops-pick-row" data-name="me {{ staff.display_name|lower }}">
            <input type="checkbox" name="task_assignee_ids[]" value="{{ staff.user_id }}" checked>
            <span>Me ({{ staff.display_name }})</span>
          </label>
          {% for u in staff_ctx.users %}
            {% if u.user_id != staff.user_id %}
              <label class="ops-pick-row" data-name="{{ u.display_name|lower }}">
                <input type="checkbox" name="task_assignee_ids[]" value="{{ u.user_id }}">
                <span>{{ u.display_name }}</span>
              </label>
            {% endif %}
          {% endfor %}
        </div>
        {% if staff_ctx.teams %}
          <div class="ops-assign-label" style="margin-top:12px;">
            Assign to teams <span class="ops-pick-count" id="ops-teams-count">0 selected</span>
          </div>
          <input type="text" class="ops-pick-filter" placeholder="Search teams…"
                 oninput="opsFilterPick(this, 'ops-teams-list')">
          <div class="ops-pick-list ops-pick-list-team" id="ops-teams-list" onchange="opsPickCount('ops-teams-list','ops-teams-count')">
            {% for t in staff_ctx.teams %}
              <label class="ops-pick-row" data-name="{{ t.name|lower }}">
                <input type="checkbox" name="task_team_ids[]" value="{{ t.team_id }}">
                <span>👥 {{ t.name }}</span>
              </label>
            {% endfor %}
          </div>
        {% endif %}
        <div class="row-2">
          <input type="date" name="task_due" value="{{ 'now'|date_modify('+7 days')|date('Y-m-d') }}">
          <select name="task_priority">
            <option value="normal" selected>Normal priority</option>
            <option value="high">High</option>
            <option value="urgent">Urgent</option>
            <option value="low">Low</option>
          </select>
        </div>
        {# v417.6: optional task photos. Staged via fetch; the Create
           button is disabled while any upload is in flight. #}
        <div style="margin-top:8px;">
          <label for="ops-task-photos" class="ops-staff-btn" style="display:block;text-align:center;cursor:pointer;background:#eff6ff;border-color:#2563eb;color:#1d4ed8;">
            📸 Add photo(s) (optional)
          </label>
          <div id="ops-task-photo-list" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;"></div>
          <div id="ops-task-upload-status" style="font-size:11px;color:#6b7280;margin-top:4px;"></div>
        </div>
        <div style="display:flex;gap:8px;">
          <button type="submit" id="ops-task-submit" style="flex:1;">Create task</button>
          <button type="button" class="ops-staff-btn" onclick="opsToggleStaff('task', false)" style="flex:0 0 auto;padding:10px 14px;">Cancel</button>
        </div>
      </form>
    </div>

    {# v412.1: Pulse form — concern/cheer with photos. Always silent,
       anchored to this asset. Photos upload via the admin pulse
       uploader (staff already has admin session). #}
    <div class="ops-staff-section" id="ops-staff-pulse" style="display:none;">
      <h3>Post to The Pulse</h3>
      {# v412.3: File input must be OUTSIDE the form to prevent the
         browser from including the selected file in the form POST
         (causing Apache mod_proxy_fcgi to fail on body-read because
         the form lacks enctype=multipart/form-data).
         v412.10: capture="environment" hint adds "Take Photo" option
         alongside the photo library picker on mobile browsers. #}
      <input id="ops-pulse-photos" type="file" accept="image/*" capture="environment" multiple style="display:none;" onchange="opsUploadPhotos(this)">
      <form method="post" action="{{ form_url }}" class="ops-staff-form" id="ops-pulse-form">
        <input type="hidden" name="action" value="pulse">

        <div style="display:flex;gap:8px;margin-bottom:10px;">
          <label class="ops-pulse-type" style="flex:1;">
            <input type="radio" name="pulse_type" value="concern" {% if not flash or not flash.sticky or flash.sticky.pulse_type != 'cheer' %}checked{% endif %} onchange="opsTogglePulseSeverity()">
            <span>💔 Concern</span>
          </label>
          <label class="ops-pulse-type" style="flex:1;">
            <input type="radio" name="pulse_type" value="cheer" {% if flash and flash.sticky and flash.sticky.pulse_type == 'cheer' %}checked{% endif %} onchange="opsTogglePulseSeverity()">
            <span>💙 Cheer</span>
          </label>
        </div>

        <input type="text" name="pulse_subject"
               placeholder="Subject (required) — e.g. 'Hydraulic leak left side'"
               required maxlength="150"
               value="{% if flash and flash.sticky and flash.sticky.action == 'pulse' %}{{ flash.sticky.pulse_subject }}{% endif %}">
        <p style="font-size:11px;color:#6b7280;margin:-4px 0 8px 0;">Will be tagged with <code>[{{ asset_tag }}]</code></p>

        <textarea name="pulse_description"
                  placeholder="Details (optional if photos attached)">{% if flash and flash.sticky and flash.sticky.action == 'pulse' %}{{ flash.sticky.pulse_description }}{% endif %}</textarea>

        <div id="ops-pulse-severity-wrap">
          <select name="pulse_severity">
            <option value="low" selected>Low severity</option>
            <option value="medium">Medium severity</option>
            <option value="high">High severity</option>
            <option value="critical">Critical severity</option>
          </select>
        </div>

        <div style="margin:6px 0 10px 0;">
          <label for="ops-pulse-photos" class="ops-staff-btn" style="display:block;text-align:center;cursor:pointer;background:#eff6ff;border-color:#2563eb;color:#1d4ed8;">
            📸 Add photos (up to 5)
          </label>
          <div id="ops-pulse-photo-list" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;"></div>
          <div id="ops-pulse-upload-status" style="font-size:11px;color:#6b7280;margin-top:4px;"></div>
        </div>

        <div style="display:flex;gap:8px;">
          <button type="submit" style="flex:1;" id="ops-pulse-submit">Post pulse</button>
          <button type="button" class="ops-staff-btn" onclick="opsToggleStaff('pulse', false)" style="flex:0 0 auto;padding:10px 14px;">Cancel</button>
        </div>
      </form>
    </div>

    {# v415: Staff-side asset photo upload. Uses the existing admin
       upload endpoint (staff are already logged in to admin via
       user_token). On success, the page reloads to show the new
       photo in the gallery. Uploads are public-by-default; if the
       worker needs to mark something internal-only, they do it from
       the admin asset detail page after uploading. #}
    <div class="ops-staff-section" id="ops-staff-photos" style="display:none;">
      {# v417: This collapsed uploader was replaced by the inline
         uploader attached to the gallery (see top of page). Kept as an
         empty stub so any lingering opsToggleStaff('photos') call is
         harmless; no longer reachable from the action grid. #}
    </div>

    {# v419: Notes & Activity — chronological staff comments with optional
       photos. Writes go to the admin addNote endpoint (staff already have
       an admin session, same as the photo uploader); it redirects back
       here via return_token. Images are served by the catalog noteImage
       route. #}
    <div class="ops-card" id="ops-notes-card" style="margin-top:14px;">
      <h3 style="margin:0 0 10px;font-size:16px;">📝 Notes &amp; Activity{% if asset_notes_log %} <span style="font-size:12px;color:#6b7280;font-weight:normal;">({{ asset_notes_log|length }})</span>{% endif %}</h3>

      <form method="post" action="{{ note_add_url }}" enctype="multipart/form-data" style="margin-bottom:8px;">
        <input type="hidden" name="return_token" value="{{ note_return_token }}">
        <textarea name="comment" required maxlength="5000" rows="2" placeholder="Add a quick note — condition, what you did, an issue to flag…" style="width:100%;box-sizing:border-box;padding:10px;border:1px solid #d1d5db;border-radius:8px;font-size:14px;"></textarea>
        <div style="display:flex;align-items:center;gap:10px;flex-wrap:wrap;margin-top:8px;">
          <input type="file" name="note_file[]" accept="image/*" capture="environment" multiple
                 style="flex:1 1 200px;font-size:13px;">
          <button type="submit" style="flex:0 0 auto;padding:10px 18px;">Add note</button>
        </div>
        <p style="font-size:11px;color:#6b7280;margin:6px 0 0;">Comment required. Photos optional — snap one or attach from your gallery.</p>
      </form>

      {% if asset_notes_log %}
        <div style="margin-top:6px;">
          {% for note in asset_notes_log %}
            <div style="border-top:1px solid #eee;padding:11px 0;">
              <div style="font-size:13px;">
                <strong>{{ note.display_name }}</strong>
                <span style="font-size:11px;color:#9ca3af;margin-left:6px;">{{ note.created_at|slice(0,16) }}</span>
              </div>
              <div style="font-size:14px;color:#333;margin-top:3px;white-space:pre-wrap;">{{ note.comment }}</div>
              {% if note.files %}
                <div style="display:flex;flex-wrap:wrap;gap:8px;margin-top:8px;">
                  {% for f in note.files %}
                    {% if f.mime_type starts with 'image/' %}
                      <a href="{{ note_image_base }}&file_id={{ f.file_id }}" target="_blank">
                        <img src="{{ note_image_base }}&file_id={{ f.file_id }}&size=thumb" alt="{{ f.original_name }}"
                             style="height:78px;width:78px;object-fit:cover;border:1px solid #ddd;border-radius:6px;display:block;">
                      </a>
                    {% endif %}
                  {% endfor %}
                </div>
              {% endif %}
            </div>
          {% endfor %}
        </div>
      {% else %}
        <p style="font-size:12px;color:#9ca3af;margin:8px 0 0;">No notes yet — add the first one above.</p>
      {% endif %}
    </div>
  {% endif %}

  {# Action selector — only when no form is open and no success flash.
     v417.18: "Report a problem" is hidden for logged-in staff (they have
     Pulse + Create task for this); the card only shows if something in
     it is still visible. #}
  {% set show_problem_public = show_problem and not is_staff %}
  {% if (show_problem_public or show_found) and not flash %}
    <div class="ops-card">
      <div class="ops-actions">
        <h3>How can we help?</h3>
        {% if show_found %}
          <button type="button" class="ops-action-btn urgent" onclick="opsShowForm('found')">
            I have this asset
            <span class="arrow">→</span>
            <span class="sub">Help return it to the owner</span>
          </button>
        {% endif %}
        {% if show_problem_public %}
          <button type="button" class="ops-action-btn" onclick="opsShowForm('problem')">
            Report a problem
            <span class="arrow">→</span>
            <span class="sub">Damage, missing parts, needs attention</span>
          </button>
        {% endif %}
      </div>
    </div>
  {% endif %}

  {# Forms — hidden by default unless flash.sticky is set #}
  {% set sticky = flash and flash.sticky ? flash.sticky : null %}
  {% set show_found_form = sticky and sticky.action == 'found' %}
  {% set show_problem_form = sticky and sticky.action == 'report_problem' %}

  {% if show_found %}
    <div class="ops-card" id="ops-form-found" style="display:{% if show_found_form %}block{% else %}none{% endif %};">
      <div class="ops-banner banner-red">
        <h2>I have this asset</h2>
        <p>We'll contact you to arrange return.</p>
      </div>
      <form method="post" action="{{ form_url }}" class="ops-form">
        <input type="hidden" name="action" value="found">
        <div class="ops-hp" aria-hidden="true"><label>Website (leave blank)</label><input type="text" name="website" value="" tabindex="-1" autocomplete="off"></div>
        <div class="ops-form-group">
          <label>Your name <span class="req">*</span></label>
          <input type="text" name="name" value="{{ sticky.name|default('') }}" required maxlength="255">
        </div>
        <div class="ops-form-group">
          <label>Phone <span class="req">*</span></label>
          <input type="tel" name="phone" value="{{ sticky.phone|default('') }}" required maxlength="64">
          <p class="ops-form-help">So we can reach you to arrange pickup.</p>
        </div>
        <div class="ops-form-group">
          <label>Email (optional)</label>
          <input type="email" name="email" value="{{ sticky.email|default('') }}" maxlength="255">
        </div>
        <div class="ops-form-group">
          <label>Where did you find it? <span class="req">*</span></label>
          <textarea name="text" required maxlength="2000" placeholder="Address, intersection, business name, etc.">{{ sticky.text|default('') }}</textarea>
        </div>
        <button type="submit" class="ops-submit urgent">Submit report</button>
        <a href="{{ form_url }}" class="ops-cancel">Cancel</a>
      </form>
    </div>
  {% endif %}

  {% if show_problem %}
    <div class="ops-card" id="ops-form-problem" style="display:{% if show_problem_form %}block{% else %}none{% endif %};">
      <div class="ops-brand-strip"><span class="ops-brand-name">Report a problem</span></div>
      <form method="post" action="{{ form_url }}" class="ops-form">
        <input type="hidden" name="action" value="report_problem">
        <div class="ops-hp" aria-hidden="true"><label>Website (leave blank)</label><input type="text" name="website" value="" tabindex="-1" autocomplete="off"></div>
        <div class="ops-form-group">
          <label>What's the problem? <span class="req">*</span></label>
          <textarea name="text" required maxlength="2000" placeholder="Describe what you're seeing — damage, missing parts, needs service, etc.">{{ sticky.text|default('') }}</textarea>
        </div>
        <div class="ops-form-group">
          <label>Your name (optional)</label>
          <input type="text" name="name" value="{{ sticky.name|default('') }}" maxlength="255">
        </div>
        <div class="ops-form-group">
          <label>Phone (optional)</label>
          <input type="tel" name="phone" value="{{ sticky.phone|default('') }}" maxlength="64">
          <p class="ops-form-help">Only if you'd like us to follow up with you.</p>
        </div>
        <button type="submit" class="ops-submit">Submit report</button>
        <a href="{{ form_url }}" class="ops-cancel">Cancel</a>
      </form>
    </div>
  {% endif %}

  {# v417.9: staff "Open in admin" banner — moved here, near the bottom,
     just above the contact section. #}
  {% if is_staff %}
    <div class="ops-staff-banner">
      <span><span class="who">👤 {{ staff.display_name }}</span> · staff view</span>
      {% if admin_url %}<a href="{{ admin_url }}" target="_blank">Open in admin →</a>{% endif %}
    </div>
  {% endif %}

  {# v417.15: when the asset already HAS photos, the uploader lives down
     here (below "open in admin") instead of in the gallery, keeping the
     main area button-free since adding more photos is usually not needed.
     Same input id as the gallery uploader — only one ever renders. #}
  {% if is_staff and asset_images %}
    <div class="ops-card" style="padding:14px 20px;">
      <input id="ops-asset-photos" type="file" accept="image/*" multiple style="display:none;" onchange="opsUploadAssetPhotos(this)">
      <input id="ops-asset-camera" type="file" accept="image/*" capture="environment" style="display:none;" onchange="opsUploadAssetPhotos(this)">
      <div class="ops-asset-uploader" style="margin-top:0;">
        <label for="ops-asset-camera" class="ops-asset-upload-btn">📷 Take photo</label>
        <label for="ops-asset-photos" class="ops-asset-upload-btn">🖼 Gallery</label>
        <label class="ops-asset-primary-toggle">
          <input type="checkbox" id="ops-asset-set-primary">
          Set as main photo
        </label>
        <div id="ops-asset-photo-status" class="ops-asset-upload-status"></div>
      </div>
    </div>
  {% endif %}

  {# Contact info card — show when there's a brand URL or phone #}
  {% if brand_phone or brand_url %}
    <div class="ops-contact">
      Need to reach us directly?
      {% if brand_phone %} <br><a href="tel:{{ brand_phone }}">{{ brand_phone }}</a>{% endif %}
      {% if brand_url %} <br><a href="{{ brand_url }}" target="_blank" rel="noopener">{{ brand_url|replace({'https://':'','http://':''})|trim('/') }}</a>{% endif %}
    </div>
  {% endif %}

  <p class="ops-meta">
    {% if asset_tag %}<span class="tag">{{ asset_tag }}</span> · {% endif %}Powered by RockOn Asset Management
  </p>

  {# v410: Required disclosure for scan logging. Single small line —
     not modal, not blocking — just clear notice. #}
  <p class="ops-meta ops-privacy">
    Scan activity is logged for asset security and recovery.
    <a href="https://rockon.com/privacy-policy" target="_blank" rel="noopener">Privacy policy</a>
  </p>

</div>

{# v417.16/17: Asset Details popup — asset notes at the top, then the
   saved label/value spec rows. Public; opened by the "Asset Details"
   link under the asset tag. #}
{% if asset_details or asset_notes or asset_capabilities %}
  <div class="ops-detail-modal" id="ops-detail-modal" onclick="opsDetailsClose(event)">
    <div class="ops-detail-box" onclick="event.stopPropagation()">
      <div class="ops-detail-head">
        <span>Asset Details</span>
        <button type="button" class="ops-detail-x" onclick="opsDetailsClose(event)" aria-label="Close">✕</button>
      </div>
      <div class="ops-detail-body">
        {% if asset_notes %}
          <div class="ops-detail-notes">
            <div class="ops-detail-notes-label">Notes</div>
            <div class="ops-detail-notes-text">{{ asset_notes|nl2br }}</div>
          </div>
        {% endif %}
        {# v418: capability values (typed, with units) right after notes #}
        {% for c in asset_capabilities %}
          <div class="ops-detail-row">
            <span class="ops-detail-label">{{ c.label }}</span>
            <span class="ops-detail-value">{{ c.value }}</span>
          </div>
        {% endfor %}
        {% for d in asset_details %}
          <div class="ops-detail-row">
            <span class="ops-detail-label">{{ d.label }}</span>
            <span class="ops-detail-value">{{ d.value }}</span>
          </div>
        {% endfor %}
      </div>
    </div>
  </div>
{% endif %}

<script>
{% if is_staff %}
// v412.1: Built once in template to use for AJAX photo uploads.
// v412.6: |raw to prevent Twig from HTML-escaping & to &amp; — fetch
// requests against URLs containing &amp; result in malformed query
// strings ("amp;t=..." instead of "t=...") which causes 404s.
var OPS_PULSE_UPLOAD_URL = "{{ admin_base|raw }}index.php?route=ops/pulse/uploadAttachment&user_token={{ user_token|raw }}";
// v412.5/.6: explicit URL for pulse form submit
var OPS_PULSE_FORM_URL = "{{ form_url|raw }}";
{% endif %}

function opsShowForm(which) {
  var formId = which === 'found' ? 'ops-form-found' : 'ops-form-problem';
  var f = document.getElementById(formId);
  if (!f) return;
  // Hide all actions + other form
  var actions = document.querySelector('.ops-actions');
  if (actions) actions.parentNode.style.display = 'none';
  var other = document.getElementById(which === 'found' ? 'ops-form-problem' : 'ops-form-found');
  if (other) other.style.display = 'none';
  f.style.display = 'block';
  // Smooth-scroll to the form
  f.scrollIntoView({behavior:'smooth', block:'start'});
  // Focus first input for instant typing
  var firstInput = f.querySelector('input[type=text], input[type=tel], textarea');
  if (firstInput) setTimeout(function(){ firstInput.focus(); }, 300);
}

// v412: toggle staff action sub-forms (note, status, task, pulse)
function opsToggleStaff(which, show) {
  // v417.14: 'status' is no longer a toggled form — it's always visible.
  var sections = ['note', 'task', 'pulse', 'photos'];
  var opened = false;
  for (var i = 0; i < sections.length; i++) {
    var el = document.getElementById('ops-staff-' + sections[i]);
    if (!el) continue;
    if (sections[i] === which) {
      if (show === false) {
        el.style.display = 'none';
      } else {
        el.style.display = 'block';
        opened = true;
        el.scrollIntoView({behavior: 'smooth', block: 'start'});
        var input = el.querySelector('input[type=text], textarea, select');
        if (input) setTimeout(function(){ input.focus(); }, 250);
      }
    } else {
      el.style.display = 'none';
    }
  }
  // v417.13/14: while the Task or Pulse form is open, hide the context
  // panels AND the always-visible status section so the open form sits
  // right under the action buttons and the page stays focused.
  var ctx = document.getElementById('ops-staff-context');
  if (ctx) ctx.style.display = opened ? 'none' : '';
  var statusSec = document.getElementById('ops-staff-status');
  if (statusSec) statusSec.style.display = opened ? 'none' : 'block';
}

// v417.14: current status (the one in effect right now). Used to decide
// when a toggle actually represents a change.
var opsCurrentStatus = {{ staff_ctx.current_status|default('active')|json_encode|raw }};

// v417.14: toggle pick. Highlights the chosen option, and only when the
// chosen status differs from the current one does it reveal the Update +
// Cancel buttons. Choosing Out of Service also reveals the required
// reason box. Picking the current status hides both again.
function opsStatusPick(status) {
  var val    = document.getElementById('ops-status-value');
  var wrap   = document.getElementById('ops-status-reason-wrap');
  var ta     = document.getElementById('ops-status-reason');
  var commit = document.getElementById('ops-status-commit');
  if (val) val.value = status;
  var opts = document.querySelectorAll('.ops-status-opt');
  Array.prototype.forEach.call(opts, function(b) {
    if (b.getAttribute('data-status') === status) b.classList.add('sel');
    else b.classList.remove('sel');
  });
  var changed = (status !== opsCurrentStatus);
  // reason box only when switching TO out of service
  if (wrap) wrap.style.display = (status === 'in_repair' && changed) ? 'block' : 'none';
  if (ta) ta.required = (status === 'in_repair' && changed);
  // Update/Cancel only when this is an actual change
  if (commit) commit.style.display = changed ? 'flex' : 'none';
}

// v417.14: Cancel — revert the toggle to the current status, which hides
// the Update/Cancel buttons and the reason box.
function opsStatusCancel() {
  opsStatusPick(opsCurrentStatus);
}

{% if flash and flash.sticky and flash.sticky.action == 'status_change' %}
// v417.14: A status change was rejected (missing reason). The status
// section is always visible now — restore the attempted status + typed
// reason and scroll to it so the user can fix and resubmit.
document.addEventListener('DOMContentLoaded', function() {
  opsStatusPick({{ flash.sticky.new_status|json_encode|raw }});
  var s = document.getElementById('ops-staff-status');
  if (s) s.scrollIntoView({behavior: 'smooth', block: 'start'});
});
{% endif %}

// v412.1: Toggle severity dropdown visibility based on pulse type
function opsTogglePulseSeverity() {
  var wrap = document.getElementById('ops-pulse-severity-wrap');
  if (!wrap) return;
  var type = document.querySelector('input[name="pulse_type"]:checked');
  if (type && type.value === 'cheer') {
    wrap.style.display = 'none';
  } else {
    wrap.style.display = 'block';
  }
}

// v412.6: Plain form submit for pulse. Previous AJAX approaches
// (v412.4/v412.5) ran into 404 issues. Since the file input is
// outside the form (v412.3) and there are no enctype concerns,
// regular form POST should work — same path as the Report a Problem
// form that already works fine on this page.
//
// We do still need to ensure the file input is cleared before submit
// to be safe.
document.addEventListener('DOMContentLoaded', function() {
  var pulseForm = document.getElementById('ops-pulse-form');
  if (!pulseForm) return;

  pulseForm.addEventListener('submit', function() {
    var fileInput = document.getElementById('ops-pulse-photos');
    if (fileInput) {
      fileInput.value = '';
      fileInput.disabled = true;
    }
    var submitBtn = document.getElementById('ops-pulse-submit');
    if (submitBtn) {
      submitBtn.disabled = true;
      submitBtn.textContent = 'Posting…';
    }
  });
});

// v412.1: Photo upload — POSTs files to the admin pulse uploader,
// which staffs the existing admin session. Returns temp_ids that
// get attached to the pulse form as hidden inputs on submit.
function opsUploadPhotos(input) {
  var files = input.files;
  if (!files || !files.length) return;
  var list = document.getElementById('ops-pulse-photo-list');
  var status = document.getElementById('ops-pulse-upload-status');
  var form = document.getElementById('ops-pulse-form');
  if (!list || !form) return;

  var uploadUrl = OPS_PULSE_UPLOAD_URL;
  var current = list.querySelectorAll('input[name="attachment_temp_ids[]"]').length;
  var max = 5;
  var remaining = max - current;
  if (remaining <= 0) {
    status.textContent = 'Maximum 5 photos. Remove one to add another.';
    input.value = '';
    return;
  }

  // v412.8: Pre-flight client-side size check. Apache's post_max_size
  // limit kills uploads silently — Apache returns 413 or just disconnects,
  // no PHP error logged. Better to catch oversized files in JS first.
  // v412.9: Bumped to 30 MB to match post_max_size 32M in .htaccess
  // (small headroom for form overhead under the server limit).
  var POST_MAX = 30 * 1024 * 1024;
  for (var fi = 0; fi < files.length; fi++) {
    if (files[fi].size > POST_MAX) {
      status.style.color = '#dc2626';
      status.textContent = 'Photo "' + files[fi].name + '" is too large (' +
        Math.round(files[fi].size / 1024 / 1024) + ' MB). Maximum 7.5 MB per photo. ' +
        'Try a smaller image, or scale down before uploading.';
      input.value = '';
      return;
    }
  }

  var toUpload = Math.min(files.length, remaining);
  status.style.color = '#6b7280';
  status.textContent = 'Uploading ' + toUpload + ' photo' + (toUpload > 1 ? 's' : '') + '…';

  var doneCount = 0;
  var attemptedCount = 0;
  for (var i = 0; i < toUpload; i++) {
    (function(file) {
      attemptedCount++;
      var thumb = document.createElement('div');
      thumb.className = 'ops-pulse-photo uploading';
      thumb.textContent = '...';
      list.appendChild(thumb);

      var fd = new FormData();
      fd.append('file', file);
      var xhr = new XMLHttpRequest();
      xhr.open('POST', uploadUrl, true);
      xhr.onload = function() {
        // v412.8: surface HTTP status + response snippet so the error is
        // visible on screen. Previously parse errors silently failed
        // with no info — making debugging impossible from the field.
        try {
          var json = JSON.parse(xhr.responseText);
          if (json.ok && json.temp_id) {
            thumb.className = 'ops-pulse-photo';
            thumb.innerHTML = '';
            if (json.preview_url) {
              var img = document.createElement('img');
              img.src = json.preview_url;
              thumb.appendChild(img);
            } else {
              thumb.style.background = '#dbeafe';
              thumb.style.display = 'flex';
              thumb.style.alignItems = 'center';
              thumb.style.justifyContent = 'center';
              thumb.textContent = '📎';
            }
            var hidden = document.createElement('input');
            hidden.type = 'hidden';
            hidden.name = 'attachment_temp_ids[]';
            hidden.value = json.temp_id;
            thumb.appendChild(hidden);
            var x = document.createElement('span');
            x.className = 'x';
            x.textContent = '×';
            x.onclick = function() { thumb.parentNode.removeChild(thumb); };
            thumb.appendChild(x);
          } else {
            thumb.parentNode.removeChild(thumb);
            status.style.color = '#dc2626';
            status.textContent = 'Upload failed [' + xhr.status + ']: ' + (json.error || 'unknown error');
          }
        } catch (e) {
          thumb.parentNode.removeChild(thumb);
          status.style.color = '#dc2626';
          var snippet = (xhr.responseText || '').substring(0, 100);
          status.textContent = 'Upload failed [HTTP ' + xhr.status + ']. ' +
            (snippet ? 'Response: ' + snippet : 'Empty response.');
        }
        doneCount++;
        if (doneCount === attemptedCount && status.style.color !== 'rgb(220, 38, 38)') {
          status.textContent = doneCount + ' photo' + (doneCount > 1 ? 's' : '') + ' ready.';
          setTimeout(function() { if (status.style.color !== 'rgb(220, 38, 38)') status.textContent = ''; }, 2500);
        }
      };
      xhr.onerror = function() {
        thumb.parentNode.removeChild(thumb);
        status.style.color = '#dc2626';
        status.textContent = 'Upload failed (network or CORS error). Status: ' + (xhr.status || 'none');
        doneCount++;
      };
      xhr.send(fd);
    })(files[i]);
  }
  // Reset input so picking the same file twice still works
  input.value = '';
}

// v415: Asset image gallery — swap primary on thumb tap, lightbox on
// primary tap. Plain JS, no external dependencies.
function opsGallerySwap(thumb) {
  var primary = document.getElementById('ops-gallery-primary');
  if (!primary) return;
  primary.src = thumb.src;
  primary.dataset.full = thumb.dataset.full;
  var thumbs = document.querySelectorAll('.ops-gallery-thumb');
  for (var i = 0; i < thumbs.length; i++) thumbs[i].classList.remove('active');
  thumb.classList.add('active');
}
function opsGalleryLightbox(fullUrl) {
  var lb = document.getElementById('ops-lightbox');
  if (!lb) {
    lb = document.createElement('div');
    lb.id = 'ops-lightbox';
    lb.className = 'ops-lightbox';
    lb.innerHTML = '<button type="button" class="ops-lightbox-close" onclick="opsGalleryCloseLightbox()">&times;</button><img alt="">';
    lb.addEventListener('click', function(e) {
      if (e.target === lb) opsGalleryCloseLightbox();
    });
    document.body.appendChild(lb);
  }
  lb.querySelector('img').src = fullUrl;
  lb.classList.add('active');
  document.body.style.overflow = 'hidden';
}
function opsGalleryCloseLightbox() {
  var lb = document.getElementById('ops-lightbox');
  if (lb) lb.classList.remove('active');
  document.body.style.overflow = '';
}
// Close on Escape key
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape') { opsGalleryCloseLightbox(); opsDetailsClose(); }
});

// v417.16: Asset Details popup (public).
function opsDetailsOpen(e) {
  if (e) e.preventDefault();
  var m = document.getElementById('ops-detail-modal');
  if (m) { m.classList.add('active'); document.body.style.overflow = 'hidden'; }
}
function opsDetailsClose(e) {
  if (e && e.preventDefault) e.preventDefault();
  var m = document.getElementById('ops-detail-modal');
  if (m) { m.classList.remove('active'); document.body.style.overflow = ''; }
}

{% if is_staff %}
// v415: Staff-side asset photo upload. Posts each file individually
// to the admin upload endpoint (which the staff is authenticated to
// via the same OC session). On the last file, reloads the page so the
// new photo(s) appear in the gallery.
function opsUploadAssetPhotos(input) {
  var files = input.files;
  if (!files || !files.length) return;
  var statusEl = document.getElementById('ops-asset-photo-status');
  var url = {{ admin_base|json_encode|raw }} + 'index.php?route=ops/asset/upload&user_token=' + {{ user_token|json_encode|raw }} + '&asset_id={{ asset_id }}';
  var done = 0;
  var failed = 0;
  var total = files.length;

  // v417: "Set as main photo" — apply to the FIRST file only so multiple
  // uploads don't fight over which becomes primary.
  var primaryEl = document.getElementById('ops-asset-set-primary');
  var setPrimary = primaryEl && primaryEl.checked;

  statusEl.style.color = '#6b7280';
  statusEl.textContent = 'Uploading ' + total + ' photo' + (total > 1 ? 's' : '') + '...';

  // Upload sequentially to avoid hammering the server with parallel
  // multipart POSTs (and to keep progress text simple).
  function uploadNext(idx) {
    if (idx >= total) {
      if (failed > 0) {
        statusEl.style.color = '#dc2626';
        statusEl.textContent = done + ' uploaded, ' + failed + ' failed. Reloading...';
      } else {
        statusEl.style.color = '#16a34a';
        statusEl.textContent = '✓ Uploaded ' + done + ' photo' + (done > 1 ? 's' : '') + '. Reloading...';
      }
      setTimeout(function() { window.location.reload(); }, 900);
      return;
    }
    var fd = new FormData();
    fd.append('asset_file[]', files[idx]);
    fd.append('is_public', '1');
    // Only the first file carries the set_primary flag.
    if (setPrimary && idx === 0) fd.append('set_primary', '1');
    fetch(url, { method: 'POST', body: fd, credentials: 'same-origin' })
      .then(function(r) { return r.json(); })
      .then(function(j) {
        if (j && j.success) done++; else failed++;
        statusEl.textContent = 'Uploaded ' + (done + failed) + ' / ' + total + '...';
        uploadNext(idx + 1);
      })
      .catch(function() {
        failed++;
        uploadNext(idx + 1);
      });
  }
  uploadNext(0);
  // Reset input so picking the same file again works
  input.value = '';
}

// v417.6: Task photo uploader. Stages each image via fetch and disables
// the Create button while uploads are in flight (Tyler's "prevent create
// until pre-uploaded"). Photos are optional — a task with none is fine.
var opsTaskUploadsPending = 0;
function opsTaskSetSubmitState() {
  var btn = document.getElementById('ops-task-submit');
  if (!btn) return;
  if (opsTaskUploadsPending > 0) {
    btn.disabled = true;
    btn.dataset.label = btn.dataset.label || btn.textContent;
    btn.textContent = 'Uploading photo…';
  } else {
    btn.disabled = false;
    if (btn.dataset.label) btn.textContent = btn.dataset.label;
  }
}
function opsUploadTaskPhotos(input) {
  var files = input.files;
  if (!files || !files.length) return;
  var form   = document.getElementById('ops-task-form');
  var list   = document.getElementById('ops-task-photo-list');
  var status = document.getElementById('ops-task-upload-status');
  if (!form || !list) return;

  var url = {{ admin_base|json_encode|raw }} + 'index.php?route=ops/asset/stageTaskPhoto&user_token=' + {{ user_token|json_encode|raw }};
  var existing = form.querySelectorAll('input[name="task_attachment_temp_ids[]"]').length;
  var max = 5;

  Array.prototype.forEach.call(files, function(file) {
    if (existing >= max) {
      if (status) { status.style.color = '#dc2626'; status.textContent = 'Max ' + max + ' photos.'; }
      return;
    }
    existing++;
    if (!/^image\//.test(file.type)) return;

    // Local preview tile with a spinner state
    var tile = document.createElement('div');
    tile.className = 'ops-pulse-photo uploading';
    var img = document.createElement('img');
    img.src = URL.createObjectURL(file);
    tile.appendChild(img);
    list.appendChild(tile);

    opsTaskUploadsPending++;
    opsTaskSetSubmitState();
    if (status) { status.style.color = '#6b7280'; status.textContent = 'Uploading…'; }

    var fd = new FormData();
    fd.append('file', file);
    fetch(url, { method: 'POST', body: fd, credentials: 'same-origin' })
      .then(function(r) { return r.json(); })
      .then(function(j) {
        opsTaskUploadsPending--;
        if (j && j.ok && j.temp_id) {
          tile.classList.remove('uploading');
          var hidden = document.createElement('input');
          hidden.type = 'hidden';
          hidden.name = 'task_attachment_temp_ids[]';
          hidden.value = j.temp_id;
          form.appendChild(hidden);
          // tap tile to remove
          var x = document.createElement('span');
          x.className = 'x';
          x.textContent = '✕';
          x.onclick = function() { tile.remove(); hidden.remove(); };
          tile.appendChild(x);
          if (status) { status.style.color = '#16a34a'; status.textContent = '✓ Photo ready'; }
        } else {
          tile.remove();
          existing--;
          if (status) { status.style.color = '#dc2626'; status.textContent = (j && j.error) ? j.error : 'Upload failed.'; }
        }
        opsTaskSetSubmitState();
      })
      .catch(function() {
        opsTaskUploadsPending--;
        tile.remove();
        existing--;
        if (status) { status.style.color = '#dc2626'; status.textContent = 'Upload failed.'; }
        opsTaskSetSubmitState();
      });
  });
  input.value = '';
}

// v417.7: filter a pick-list by typed text. Checked rows keep their
// state even while hidden, so filtering never loses a selection.
function opsFilterPick(input, listId) {
  var list = document.getElementById(listId);
  if (!list) return;
  var q = (input.value || '').toLowerCase().trim();
  var rows = list.querySelectorAll('.ops-pick-row');
  Array.prototype.forEach.call(rows, function(row) {
    var name = row.getAttribute('data-name') || '';
    if (q === '' || name.indexOf(q) !== -1) row.classList.remove('ops-hidden');
    else row.classList.add('ops-hidden');
  });
}
// v417.7: update the "N selected" counter for a pick-list.
function opsPickCount(listId, countId) {
  var list = document.getElementById(listId);
  var out = document.getElementById(countId);
  if (!list || !out) return;
  var n = list.querySelectorAll('input[type=checkbox]:checked').length;
  out.textContent = n + ' selected';
}
document.addEventListener('DOMContentLoaded', function() {
  opsPickCount('ops-people-list', 'ops-people-count');
  opsPickCount('ops-teams-list', 'ops-teams-count');
});
{% endif %}
</script>

</body>
</html>
