{"id":3135,"date":"2026-09-16T14:44:01","date_gmt":"2026-09-16T14:44:01","guid":{"rendered":"https:\/\/services.smartd.co\/?page_id=3135"},"modified":"2026-09-16T14:53:35","modified_gmt":"2026-09-16T14:53:35","slug":"troubleshooting-tool","status":"publish","type":"page","link":"https:\/\/services.smartd.co\/fr\/troubleshooting-tool\/","title":{"rendered":"Troubleshooting tool"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"3135\" class=\"elementor elementor-3135\">\n\t\t\t\t<div class=\"elementor-element elementor-element-0960276 e-flex e-con-boxed e-con e-parent\" data-id=\"0960276\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-3deb133 elementor-widget elementor-widget-html\" data-id=\"3deb133\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<title>SD Tools Suite 1.0.10<\/title>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/pako\/2.1.0\/pako.min.js\"><\/script>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jszip\/3.10.1\/jszip.min.js\"><\/script>\n<style>\n  .smartd-support-package-viewer {\n    \/* === SmartD brand palette \u2014 shared with Commissioning Report Generator === *\/\n    --g:           #02AF4C;   \/* SmartD green *\/\n    --g-dim:       #019940;   \/* darker green for hover *\/\n    --blk:         #000000;\n    --dk:          #030303;\n    --gr:          #747474;\n    --lg:          #A6A6A6;\n    --gl:          #E8F7EE;   \/* light-green tint *\/\n    --gm:          #D4EDDA;   \/* medium-green tint *\/\n    --g-border:    #c8ebd8;   \/* soft green border *\/\n\n    \/* === Shell palette (light) === *\/\n    --bg:          #ffffff;\n    --bg-elev:     #f8f9fb;\n    --bg-elev-2:   #f1f3f6;\n    --bg-hover:    #e8ebf0;\n    --border:      #d1d7e0;\n    --border-soft: #e8ebf0;\n    --text:        #1f2937;\n    --text-dim:    #6b7280;\n    --text-mute:   #9ca3af;\n    --accent:      var(--g);\n    --accent-dim:  var(--g-dim);\n    --ok:          #16a34a;\n    --warn:        #d97706;\n    --err:         #dc2626;\n    \/* JSON syntax highlighting \u2014 VS Code \/ GitHub light-ish palette (kept for readability) *\/\n    --key:         #0550ae;\n    --str:         #a31515;\n    --num:         #098658;\n    --bool:        #1d4ed8;\n    --null:        #8250df;\n    --hl:          rgba(255, 214, 0, 0.55);\n    --hl-active:   rgba(255, 145, 0, 0.75);\n    --font-mono:   \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, monospace;\n    --font-ui:     Arial, sans-serif;\n\n    \/* === Audit\/Config viewer compat vars (light from uploaded palette) === *\/\n    --surface:     #f8f9fb;\n    --surface2:    #f1f3f6;\n    --surface3:    #e8ebf0;\n    --border2:     #b8c0cc;\n    --text-bright: #111827;\n    --accent-glow: rgba(79,218,110,0.15);\n    --info:        var(--g);\n    --error:       #dc2626;\n    --success:     #16a34a;\n    --alarm:       #ea580c;\n    --mono:        \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, monospace;\n    --sans:        Arial, sans-serif;\n  }\n\n  .smartd-support-package-viewer, .smartd-support-package-viewer * { box-sizing: border-box; }\n\n  .smartd-support-package-viewer {\n    margin: 0;\n    padding: 0;\n    width: 100%;\n    background: var(--bg);\n    color: var(--text);\n    font-family: var(--font-ui);\n    font-size: 10px;\n    overflow: visible;\n  }\n\n  .smartd-support-package-viewer button { font-family: inherit; }\n\n  .smartd-support-package-viewer .app { display: flex; flex-direction: column; }\n\n  \/* ---------- Top bar ---------- *\/\n  .smartd-support-package-viewer .topbar {\n    display: flex; align-items: center; gap: 10px;\n    padding: 12px 20px;\n    background: var(--blk);\n    border-bottom: 2px solid var(--g);\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer .brand {\n    font-weight: 700; font-size: 14px; letter-spacing: 0.2px;\n    display: flex; align-items: center; gap: 10px;\n    color: #fff;\n  }\n  .smartd-support-package-viewer .brand-icon {\n    width: 24px; height: 24px; border-radius: 5px;\n    background: var(--g);\n    display: flex; align-items: center; justify-content: center;\n    color: #fff; font-size: 12px; font-weight: 700;\n  }\n  .smartd-support-package-viewer .topbar-spacer { flex: 1; }\n  .smartd-support-package-viewer .pkg-info {\n    display: flex; align-items: center; gap: 14px;\n    font-size: 12.5px; color: var(--lg);\n    max-width: 60%; overflow: hidden;\n  }\n  .smartd-support-package-viewer .pkg-info .item { display: flex; align-items: center; gap: 6px; white-space: nowrap; }\n  .smartd-support-package-viewer .pkg-info .item strong { color: #fff; font-weight: 500; }\n  .smartd-support-package-viewer .pkg-info .sep { width: 1px; height: 14px; background: rgba(255,255,255,0.2); }\n\n  .smartd-support-package-viewer .shell-btn {\n    background: var(--g); color: #fff; border: none;\n    padding: 7px 13px; border-radius: 6px; font-size: 13px; font-weight: 600;\n    cursor: pointer; display: inline-flex; align-items: center; gap: 6px;\n    transition: background 0.12s;\n    font-family: var(--font-ui);\n    text-transform: none; letter-spacing: normal;\n  }\n  .smartd-support-package-viewer .shell-btn:hover { background: var(--g-dim); }\n  .smartd-support-package-viewer .shell-btn.ghost {\n    background: transparent; color: var(--lg);\n    border: 1px solid rgba(255,255,255,0.25);\n  }\n  .smartd-support-package-viewer .shell-btn.ghost:hover { color: #fff; border-color: rgba(255,255,255,0.6); background: rgba(255,255,255,0.08); }\n  .smartd-support-package-viewer .shell-btn svg { width: 14px; height: 14px; }\n\n  \/* ---------- Tab strip ---------- *\/\n  .smartd-support-package-viewer .tabstrip-row {\n    display: none; align-items: stretch;\n    background: var(--bg-elev);\n    border-bottom: 1px solid var(--border);\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer .tabstrip {\n    display: flex; align-items: stretch;\n    overflow-x: auto; overflow-y: hidden;\n    flex: 1; min-width: 0;\n    scrollbar-width: thin;\n  }\n  .smartd-support-package-viewer .tabstrip::-webkit-scrollbar { height: 6px; }\n  .smartd-support-package-viewer .tabstrip::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }\n\n  .smartd-support-package-viewer .shell-tab {\n    display: flex; align-items: center; gap: 8px;\n    padding: 9px 14px;\n    font-size: 10px; color: #ffffff;\n    cursor: pointer; border: none; background: var(--g);\n    border-right: 1px solid rgba(255,255,255,0.18);\n    border-bottom: 2px solid transparent;\n    white-space: nowrap; flex-shrink: 0;\n    transition: color 0.12s, background 0.12s;\n    position: relative;\n    font-family: var(--font-ui);\n  }\n  .smartd-support-package-viewer .shell-tab:hover { color: #ffffff; background: var(--g-dim); }\n  .smartd-support-package-viewer .shell-tab.active {\n    color: #ffffff; background: #000000;\n    border-bottom-color: var(--g);\n  }\n  .smartd-support-package-viewer .shell-tab.active:hover { color: #ffffff; background: #000000; }\n  .smartd-support-package-viewer .shell-tab.active .tab-size { color: rgba(255,255,255,0.85); }\n  .smartd-support-package-viewer .shell-tab.active .tab-dot { box-shadow: 0 0 0 2px rgba(255,255,255,0.3); }\n  .smartd-support-package-viewer .shell-tab.missing { opacity: 0.5; cursor: not-allowed; }\n  .smartd-support-package-viewer .shell-tab.missing:hover { background: var(--g); color: #ffffff; }\n  .smartd-support-package-viewer .tab-dot { width: 7px; height: 7px; border-radius: 50%; background: #ffffff; flex-shrink: 0; }\n  .smartd-support-package-viewer .shell-tab.missing .tab-dot { background: rgba(0,0,0,0.35); }\n  .smartd-support-package-viewer .tab-size {\n    font-size: 10px; color: rgba(255,255,255,0.72);\n    font-family: var(--font-mono);\n  }\n  .smartd-support-package-viewer .shell-tab.enhanced::after {\n    content: \"\"; width: 5px; height: 5px; border-radius: 50%;\n    background: #ffffff; box-shadow: 0 0 6px rgba(255,255,255,0.6);\n    margin-left: 2px;\n  }\n\n  \/* ---------- Content area ---------- *\/\n  .smartd-support-package-viewer .content { flex: 1; position: relative; overflow: hidden; display: flex; flex-direction: column; min-height: 0; }\n\n  .smartd-support-package-viewer .empty-state-shell {\n    position: absolute; inset: 0;\n    display: flex; align-items: center; justify-content: center;\n    padding: 40px;\n  }\n  .smartd-support-package-viewer .drop-zone-shell {\n    width: 100%; max-width: 520px;\n    border: 2px dashed var(--border);\n    border-radius: 12px;\n    padding: 56px 40px;\n    text-align: center;\n    color: var(--text-dim);\n    transition: border-color 0.15s, background 0.15s;\n  }\n  .smartd-support-package-viewer .drop-zone-shell.dragging {\n    border-color: var(--accent);\n    background: rgba(2,175,76, 0.06);\n  }\n  .smartd-support-package-viewer .drop-zone-shell h2 { margin: 0 0 8px; color: var(--text); font-weight: 600; font-size: 18px; }\n  .smartd-support-package-viewer .drop-zone-shell p { margin: 0 0 24px; font-size: 13.5px; line-height: 1.55; }\n  .smartd-support-package-viewer .drop-zone-shell .formats {\n    margin-top: 18px; font-size: 10px; color: var(--text-mute);\n    font-family: var(--font-mono);\n  }\n  .smartd-support-package-viewer .drop-icon-shell {\n    width: 48px; height: 48px; margin: 0 auto 18px;\n    opacity: 0.5;\n  }\n\n  .smartd-support-package-viewer .file-view {\n    flex: 1; display: flex; flex-direction: column;\n    min-height: 0;\n  }\n  .smartd-support-package-viewer .file-toolbar {\n    display: flex; align-items: center; gap: 10px;\n    padding: 8px 14px;\n    background: var(--bg-elev);\n    border-bottom: 1px solid var(--border);\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer .file-toolbar .filename {\n    font-family: var(--font-mono); font-size: 12px;\n    color: var(--text); font-weight: 500;\n  }\n  .smartd-support-package-viewer .file-toolbar .fileinfo {\n    font-size: 10px; color: var(--text-mute); font-family: var(--font-mono);\n  }\n  .smartd-support-package-viewer .file-toolbar .spacer { flex: 1; }\n  .smartd-support-package-viewer .search-wrap-shell { position: relative; display: flex; align-items: center; }\n  .smartd-support-package-viewer .search-input-shell {\n    background: var(--bg); color: var(--text);\n    border: 1px solid var(--border); border-radius: 5px;\n    padding: 5px 28px 5px 28px;\n    font-size: 10px; font-family: var(--font-mono);\n    width: 220px;\n    outline: none;\n  }\n  .smartd-support-package-viewer .search-input-shell:focus { border-color: var(--accent); }\n  .smartd-support-package-viewer .search-wrap-shell .search-icon {\n    position: absolute; left: 8px; width: 13px; height: 13px;\n    color: var(--text-mute); pointer-events: none;\n  }\n  .smartd-support-package-viewer .search-count {\n    position: absolute; right: 6px;\n    font-size: 10px; color: var(--text-mute);\n    font-family: var(--font-mono);\n    pointer-events: none;\n  }\n  .smartd-support-package-viewer .icon-btn {\n    background: transparent; border: 1px solid var(--border);\n    color: var(--text-dim); width: 28px; height: 26px;\n    border-radius: 5px; cursor: pointer;\n    display: inline-flex; align-items: center; justify-content: center;\n    transition: all 0.12s;\n  }\n  .smartd-support-package-viewer .icon-btn:hover { color: var(--text); border-color: #484f58; background: var(--bg-hover); }\n  .smartd-support-package-viewer .icon-btn svg { width: 13px; height: 13px; }\n  .smartd-support-package-viewer .icon-btn.toggled { background: var(--accent-dim); color: #fff; border-color: var(--accent-dim); }\n\n  \/* ---------- Code viewer (for log files) ---------- *\/\n  .smartd-support-package-viewer .code-viewer {\n    flex: 1; overflow: auto;\n    font-family: var(--font-mono);\n    font-size: 10px; line-height: 1.55;\n    background: var(--bg);\n  }\n  .smartd-support-package-viewer .code-viewer::-webkit-scrollbar { width: 12px; height: 12px; }\n  .smartd-support-package-viewer .code-viewer::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer .code-viewer::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 3px solid var(--bg); }\n  .smartd-support-package-viewer .code-viewer::-webkit-scrollbar-thumb:hover { background: #484f58; }\n\n  .smartd-support-package-viewer .code-table { display: grid; grid-template-columns: auto 1fr; min-width: 100%; }\n  .smartd-support-package-viewer .ln {\n    text-align: right; padding: 0 14px; color: var(--text-mute);\n    user-select: none; border-right: 1px solid var(--border-soft);\n    background: var(--bg); position: sticky; left: 0; white-space: nowrap;\n  }\n  .smartd-support-package-viewer .lc { padding: 0 14px; white-space: pre; color: var(--text); }\n  .smartd-support-package-viewer .lc:hover { background: rgba(0,0,0,0.03); }\n\n  .smartd-support-package-viewer .tok-key   { color: var(--key); }\n  .smartd-support-package-viewer .tok-str   { color: var(--str); }\n  .smartd-support-package-viewer .tok-num   { color: var(--num); }\n  .smartd-support-package-viewer .tok-bool  { color: var(--bool); }\n  .smartd-support-package-viewer .tok-null  { color: var(--null); }\n  .smartd-support-package-viewer .tok-punct { color: var(--text-dim); }\n\n  .smartd-support-package-viewer .log-header { color: var(--accent); font-weight: 600; }\n  .smartd-support-package-viewer .log-time   { color: var(--text-mute); }\n  .smartd-support-package-viewer .log-err    { color: var(--err); }\n  .smartd-support-package-viewer .log-warn   { color: var(--warn); }\n  .smartd-support-package-viewer .log-ok     { color: var(--ok); }\n\n  .smartd-support-package-viewer .code-viewer mark.hl { background: var(--hl); color: inherit; border-radius: 2px; padding: 0 1px; }\n  .smartd-support-package-viewer .code-viewer mark.hl.active { background: var(--hl-active); }\n\n  .smartd-support-package-viewer .missing-file-notice { padding: 40px; text-align: center; color: var(--text-dim); }\n  .smartd-support-package-viewer .missing-file-notice h3 { color: var(--text); margin: 0 0 8px; font-weight: 500; font-size: 14px; }\n  .smartd-support-package-viewer .missing-file-notice code {\n    font-family: var(--font-mono); font-size: 12px;\n    background: var(--bg-elev); padding: 2px 6px; border-radius: 4px;\n    color: var(--warn);\n  }\n\n  \/* ---------- View mode toggle (Structured\/Raw) ---------- *\/\n  .smartd-support-package-viewer .view-toggle {\n    display: inline-flex;\n    border: 1px solid var(--border);\n    border-radius: 5px;\n    overflow: hidden;\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer .view-toggle button {\n    background: transparent;\n    border: none;\n    color: var(--text-dim);\n    padding: 5px 10px;\n    font-size: 10px;\n    font-family: var(--font-ui);\n    font-weight: 500;\n    cursor: pointer;\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    transition: background 0.12s, color 0.12s;\n  }\n  .smartd-support-package-viewer .view-toggle button + button { border-left: 1px solid var(--border); }\n  .smartd-support-package-viewer .view-toggle button:hover { background: var(--bg-hover); color: var(--text); }\n  .smartd-support-package-viewer .view-toggle button.active {\n    background: var(--accent);\n    color: #fff;\n  }\n  .smartd-support-package-viewer .view-toggle button.active:hover { background: var(--accent); color: #fff; }\n  .smartd-support-package-viewer .view-toggle svg { width: 12px; height: 12px; }\n\n  \/* ---------- Structured file viewer ---------- *\/\n  .smartd-support-package-viewer .structured-view {\n    flex: 1; overflow: auto;\n    padding: 18px 22px;\n    background: var(--bg);\n    font-family: var(--font-ui);\n    font-size: 10px;\n    color: var(--text);\n  }\n  .smartd-support-package-viewer .structured-view::-webkit-scrollbar { width: 12px; height: 12px; }\n  .smartd-support-package-viewer .structured-view::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer .structured-view::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 3px solid var(--bg); }\n  .smartd-support-package-viewer .structured-view::-webkit-scrollbar-thumb:hover { background: #a0a8b5; }\n\n  \/* File-level summary card (optional) *\/\n  .smartd-support-package-viewer .sf-summary {\n    background: linear-gradient(180deg, var(--surface) 0%, var(--bg) 100%);\n    border: 1px solid var(--border);\n    border-radius: 8px;\n    padding: 16px 18px;\n    margin-bottom: 16px;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 18px 28px;\n  }\n  .smartd-support-package-viewer .sf-summary-item { display: flex; flex-direction: column; gap: 3px; }\n  .smartd-support-package-viewer .sf-summary-label {\n    font-family: var(--mono);\n    font-size: 10px; font-weight: 600;\n    letter-spacing: 0.12em; text-transform: uppercase;\n    color: var(--text-dim);\n  }\n  .smartd-support-package-viewer .sf-summary-value {\n    font-family: var(--mono); font-size: 10px;\n    color: var(--text-bright); font-weight: 500;\n  }\n  .smartd-support-package-viewer .sf-summary-value.accent { color: var(--accent); }\n  .smartd-support-package-viewer .sf-summary-value.ok     { color: var(--ok); }\n  .smartd-support-package-viewer .sf-summary-value.warn   { color: var(--warn); }\n  .smartd-support-package-viewer .sf-summary-value.err    { color: var(--err); }\n\n  \/* Section card (for each ***command***  block) *\/\n  .smartd-support-package-viewer .sf-section {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    margin-bottom: 14px;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer .sf-section-header {\n    background: var(--surface2);\n    padding: 8px 14px;\n    font-family: var(--mono);\n    font-size: 10px;\n    font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    border-bottom: 1px solid var(--border);\n    display: flex;\n    align-items: center;\n    gap: 10px;\n  }\n  .smartd-support-package-viewer .sf-section-cmd { color: var(--accent); }\n  .smartd-support-package-viewer .sf-section-args {\n    color: var(--text-dim);\n    font-weight: 400;\n    font-size: 10px;\n    text-transform: none;\n    letter-spacing: 0.02em;\n  }\n  .smartd-support-package-viewer .sf-section-body { padding: 0; }\n\n  \/* KV grid (same look as config viewer fields, unscoped) *\/\n  .smartd-support-package-viewer .sf-kv {\n    display: grid;\n    grid-template-columns: minmax(180px, 0.35fr) minmax(200px, 1fr);\n    font-family: var(--mono);\n    font-size: 10px;\n  }\n  .smartd-support-package-viewer .sf-kv-label {\n    padding: 7px 14px;\n    font-size: 10px;\n    color: var(--text-dim);\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    border-right: 1px solid var(--border);\n    word-break: break-word;\n  }\n  .smartd-support-package-viewer .sf-kv-value {\n    padding: 7px 14px;\n    font-size: 10px;\n    color: var(--text-bright);\n    border-bottom: 1px solid var(--border);\n    word-break: break-all;\n    display: flex;\n    align-items: center;\n    min-height: 30px;\n    gap: 6px;\n  }\n  .smartd-support-package-viewer .sf-kv > :nth-last-child(-n+2) { border-bottom: none; }\n\n  \/* Prose text block inside a section *\/\n  .smartd-support-package-viewer .sf-prose {\n    font-family: var(--mono);\n    font-size: 10px;\n    padding: 10px 14px;\n    white-space: pre-wrap;\n    word-break: break-word;\n    color: var(--text);\n    line-height: 1.55;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer .sf-prose:last-child { border-bottom: none; }\n\n  \/* Labeled JSON block *\/\n  .smartd-support-package-viewer .sf-labeled-json {\n    padding: 10px 14px;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer .sf-labeled-json:last-child { border-bottom: none; }\n  .smartd-support-package-viewer .sf-labeled-json-label {\n    font-family: var(--mono);\n    font-size: 10px;\n    color: var(--accent);\n    text-transform: uppercase;\n    letter-spacing: 0.08em;\n    font-weight: 600;\n    margin-bottom: 8px;\n  }\n  .smartd-support-package-viewer .sf-json-block {\n    padding: 10px 14px;\n  }\n  .smartd-support-package-viewer .sf-json-block > .cfg-group:last-child, .smartd-support-package-viewer .sf-json-block > .cfg-array:last-child, .smartd-support-package-viewer .sf-json-block > .cfg-fields:last-child { margin-bottom: 0; }\n  .smartd-support-package-viewer .cfg-render { font-family: var(--mono); }\n  .smartd-support-package-viewer .cfg-render > .cfg-group:last-child, .smartd-support-package-viewer .cfg-render > .cfg-array:last-child { margin-bottom: 0; }\n\n  \/* Log table (dmesg-style [timestamp] lines) *\/\n  .smartd-support-package-viewer .sf-log {\n    font-family: var(--mono);\n    font-size: 10px;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer .sf-log:last-child { border-bottom: none; }\n  .smartd-support-package-viewer .sf-log-row {\n    display: grid;\n    grid-template-columns: 120px 1fr;\n    border-bottom: 1px solid var(--border-soft);\n  }\n  .smartd-support-package-viewer .sf-log-row:last-child { border-bottom: none; }\n  .smartd-support-package-viewer .sf-log-row:hover { background: rgba(0,0,0,0.02); }\n  .smartd-support-package-viewer .sf-log-ts {\n    padding: 4px 12px;\n    color: var(--text-mute);\n    background: var(--surface);\n    border-right: 1px solid var(--border-soft);\n    white-space: nowrap;\n    text-align: right;\n  }\n  .smartd-support-package-viewer .sf-log-msg {\n    padding: 4px 14px;\n    color: var(--text);\n    white-space: pre-wrap;\n    word-break: break-word;\n    line-height: 1.5;\n  }\n  .smartd-support-package-viewer .sf-log-msg .log-err  { color: var(--err); font-weight: 600; }\n  .smartd-support-package-viewer .sf-log-msg .log-warn { color: var(--warn); font-weight: 600; }\n  .smartd-support-package-viewer .sf-log-msg .log-ok   { color: var(--ok); }\n\n  \/* Inline JSON preview chip *\/\n  .smartd-support-package-viewer .sf-inline-json-chip {\n    font-family: var(--mono);\n    color: var(--accent);\n    background: var(--surface2);\n    border: 1px solid var(--border);\n    padding: 1px 7px;\n    border-radius: 3px;\n    cursor: default;\n  }\n\n  \/* Value pill types used inside KV values *\/\n  .smartd-support-package-viewer .sf-kv-value .val-empty, .smartd-support-package-viewer .sf-kv-value .val-null { font-style: italic; }\n  .smartd-support-package-viewer .sf-kv-value code {\n    font-family: var(--mono); font-size: 10px;\n    background: var(--surface2); padding: 1px 5px;\n    border-radius: 3px; color: var(--text-bright);\n  }\n  .smartd-support-package-viewer .sf-value-mono { font-family: var(--mono); }\n  .smartd-support-package-viewer .sf-value-hex  { color: #b45309; font-family: var(--mono); }\n  .smartd-support-package-viewer .sf-value-mac  { color: #6366f1; font-family: var(--mono); }\n  .smartd-support-package-viewer .sf-value-ip   { color: #0891b2; font-family: var(--mono); }\n  .smartd-support-package-viewer .sf-value-num  { color: #098658; font-family: var(--mono); font-weight: 600; }\n  .smartd-support-package-viewer .sf-value-pct  { color: #098658; font-family: var(--mono); font-weight: 600; }\n  .smartd-support-package-viewer .sf-value-ver  { color: #8b5cf6; font-family: var(--mono); font-weight: 600; }\n  .smartd-support-package-viewer .sf-value-path { color: var(--text-dim); font-family: var(--mono); }\n\n  \/* Progress-style bar for percentages *\/\n  .smartd-support-package-viewer .sf-pct-bar {\n    display: inline-grid;\n    grid-template-columns: 50px 80px;\n    align-items: center;\n    gap: 8px;\n  }\n  .smartd-support-package-viewer .sf-pct-bar-track {\n    height: 6px;\n    background: var(--surface3);\n    border-radius: 3px;\n    overflow: hidden;\n    position: relative;\n  }\n  .smartd-support-package-viewer .sf-pct-bar-fill {\n    position: absolute;\n    left: 0; top: 0; bottom: 0;\n    background: var(--accent);\n    transition: width 0.3s;\n  }\n  .smartd-support-package-viewer .sf-pct-bar-fill.high { background: var(--warn); }\n  .smartd-support-package-viewer .sf-pct-bar-fill.critical { background: var(--err); }\n\n  \/* \"Empty section\" placeholder *\/\n  .smartd-support-package-viewer .sf-empty {\n    padding: 16px 14px;\n    color: var(--text-dim);\n    font-style: italic;\n    text-align: center;\n  }\n\n  .smartd-support-package-viewer .error-banner {\n    padding: 16px 20px; margin: 14px;\n    background: rgba(248, 81, 73, 0.1);\n    border: 1px solid rgba(248, 81, 73, 0.35);\n    border-radius: 6px;\n    color: var(--err);\n    font-size: 12px;\n  }\n  .smartd-support-package-viewer .error-banner code {\n    background: rgba(0,0,0,0.06); padding: 1px 4px; border-radius: 3px;\n    font-family: var(--font-mono); font-size: 12px;\n  }\n\n  .smartd-support-package-viewer .future-note {\n    padding: 6px 14px; font-size: 10px; color: var(--text-mute);\n    background: var(--bg-elev-2); border-bottom: 1px solid var(--border-soft);\n    font-family: var(--font-mono);\n  }\n  .smartd-support-package-viewer .future-note strong { color: var(--text-dim); font-weight: 500; }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     AUDIT LOG VIEWER (adapted from uploaded, dark-themed)\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer #auditViewer {\n    display: none; flex-direction: column; flex: 1;\n    min-height: 0; overflow: hidden;\n  }\n  .smartd-support-package-viewer #auditViewer.active { display: flex; }\n\n  .smartd-support-package-viewer #auditViewer .audit-header {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 0 18px;\n    display: flex; align-items: center; gap: 14px;\n    height: 46px; flex-shrink: 0;\n    font-family: var(--mono);\n  }\n  .smartd-support-package-viewer #auditViewer .audit-header-title {\n    font-size: 12px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.08em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #auditViewer .audit-header-sub {\n    font-size: 11px; color: var(--text-dim);\n    letter-spacing: 0.04em;\n  }\n  .smartd-support-package-viewer #auditViewer .audit-hdr-divider { width: 1px; height: 24px; background: var(--border); }\n  .smartd-support-package-viewer #auditViewer .audit-hdr-stat { font-size: 11px; color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .audit-hdr-stat strong { color: var(--accent); font-weight: 600; }\n  .smartd-support-package-viewer #auditViewer .audit-hdr-spacer { flex: 1; }\n  .smartd-support-package-viewer #auditViewer .audit-hdr-time { font-size: 11px; color: var(--text-dim); }\n\n  \/* Toolbar *\/\n  .smartd-support-package-viewer #auditViewer .audit-toolbar {\n    background: var(--surface2);\n    border-bottom: 1px solid var(--border);\n    padding: 10px 18px;\n    display: flex; flex-wrap: wrap; gap: 10px;\n    align-items: flex-end; flex-shrink: 0;\n    overflow-x: auto;\n  }\n  .smartd-support-package-viewer #auditViewer .filter-group { display: flex; flex-direction: column; gap: 4px; }\n  .smartd-support-package-viewer #auditViewer .filter-label {\n    font-family: var(--mono);\n    font-size: 10px; font-weight: 600;\n    color: var(--text-dim);\n    letter-spacing: 0.1em; text-transform: uppercase;\n  }\n\n  .smartd-support-package-viewer #auditViewer input[type=\"text\"], .smartd-support-package-viewer #auditViewer select {\n    background: var(--surface3);\n    border: 1px solid var(--border);\n    color: var(--text);\n    font-family: var(--mono);\n    font-size: 12px;\n    padding: 6px 10px;\n    border-radius: 4px;\n    outline: none;\n    transition: border-color 0.15s, box-shadow 0.15s;\n  }\n  .smartd-support-package-viewer #auditViewer input[type=\"text\"]:focus, .smartd-support-package-viewer #auditViewer select:focus {\n    border-color: var(--accent);\n    box-shadow: 0 0 0 2px var(--accent-glow);\n  }\n  .smartd-support-package-viewer #auditViewer input::placeholder { color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer select option { background: var(--surface2); color: var(--text); }\n\n  .smartd-support-package-viewer #auditViewer .audit-search { width: 260px; }\n  .smartd-support-package-viewer #auditViewer .severity-select { width: 150px; }\n  .smartd-support-package-viewer #auditViewer .origin-select { width: 150px; }\n\n  .smartd-support-package-viewer #auditViewer .audit-btn {\n    background: var(--surface3);\n    border: 1px solid var(--border2);\n    color: var(--text);\n    font-family: var(--mono);\n    font-size: 11px; font-weight: 600;\n    letter-spacing: 0.06em;\n    padding: 7px 14px;\n    border-radius: 4px;\n    cursor: pointer;\n    transition: background 0.15s, border-color 0.15s, color 0.15s;\n    text-transform: uppercase;\n    white-space: nowrap;\n    align-self: flex-end;\n  }\n  .smartd-support-package-viewer #auditViewer .audit-btn:hover { background: var(--bg-hover); color: var(--text-bright); border-color: #5d6673; }\n  .smartd-support-package-viewer #auditViewer .audit-btn:disabled { opacity: 0.35; cursor: default; pointer-events: none; }\n  .smartd-support-package-viewer #auditViewer .audit-btn-danger { border-color: var(--error); color: var(--error); }\n  .smartd-support-package-viewer #auditViewer .audit-btn-danger:hover { background: rgba(248,81,73,0.12); color: var(--error); }\n  .smartd-support-package-viewer #auditViewer .audit-btn-toggle { border-color: var(--border2); color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .audit-btn-toggle.active { background: rgba(63,185,80,0.14); border-color: var(--ok); color: var(--ok); }\n  .smartd-support-package-viewer #auditViewer .audit-btn-toggle.active:hover { background: rgba(63,185,80,0.22); color: var(--ok); }\n\n  \/* Multi-select trigger *\/\n  .smartd-support-package-viewer #auditViewer .multiselect-wrapper { position: relative; }\n  .smartd-support-package-viewer #auditViewer .multiselect-trigger {\n    display: flex; align-items: center; justify-content: space-between;\n    gap: 8px; width: 220px;\n    background: var(--surface3); border: 1px solid var(--border);\n    color: var(--text);\n    font-family: var(--mono); font-size: 12px;\n    padding: 6px 10px; border-radius: 4px;\n    cursor: pointer; outline: none; text-align: left;\n    transition: border-color .15s, box-shadow .15s;\n    white-space: nowrap; overflow: hidden;\n  }\n  .smartd-support-package-viewer #auditViewer .multiselect-trigger:focus, .smartd-support-package-viewer #auditViewer .multiselect-trigger.open {\n    border-color: var(--accent);\n    box-shadow: 0 0 0 2px var(--accent-glow);\n  }\n  .smartd-support-package-viewer #auditViewer .multiselect-trigger span {\n    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n  }\n\n  \/* Stats bar *\/\n  .smartd-support-package-viewer #auditViewer .audit-stats-bar {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 7px 18px;\n    display: flex; gap: 20px;\n    align-items: center; flex-shrink: 0; flex-wrap: wrap;\n  }\n  .smartd-support-package-viewer #auditViewer .stat-chip {\n    display: flex; align-items: center; gap: 6px;\n    font-family: var(--mono); font-size: 11px;\n  }\n  .smartd-support-package-viewer #auditViewer .stat-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }\n  .smartd-support-package-viewer #auditViewer .stat-count { font-weight: 600; }\n  .smartd-support-package-viewer #auditViewer .results-info {\n    font-family: var(--mono); font-size: 11px;\n    color: var(--text-dim); margin-left: auto;\n  }\n  .smartd-support-package-viewer #auditViewer .results-info strong { color: var(--text-bright); }\n\n  \/* Table *\/\n  .smartd-support-package-viewer #auditViewer tr.row-jump-flash td { animation: jumpFlash 2.4s ease-out; }\n  @keyframes jumpFlash {\n    0%, 55% { background-color: rgba(2,175,76,0.30); }\n    100%    { background-color: transparent; }\n  }\n\n  .smartd-support-package-viewer #auditViewer .table-container {\n    flex: 1; overflow: auto; position: relative; min-height: 0;\n  }\n  .smartd-support-package-viewer #auditViewer .table-container::-webkit-scrollbar { width: 12px; height: 12px; }\n  .smartd-support-package-viewer #auditViewer .table-container::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer #auditViewer .table-container::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 3px solid var(--bg); }\n  .smartd-support-package-viewer #auditViewer .table-container::-webkit-scrollbar-thumb:hover { background: #484f58; }\n\n  .smartd-support-package-viewer #auditViewer table {\n    width: 100%; border-collapse: collapse; table-layout: fixed;\n  }\n  .smartd-support-package-viewer #auditViewer thead {\n    position: sticky; top: 0; z-index: 10;\n    background: var(--surface2);\n  }\n  .smartd-support-package-viewer #auditViewer th {\n    padding: 9px 14px;\n    text-align: left;\n    font-family: var(--mono);\n    font-size: 10px; font-weight: 600;\n    letter-spacing: 0.12em; text-transform: uppercase;\n    color: var(--text-dim);\n    border-bottom: 1px solid var(--border);\n    border-right: 1px solid var(--border);\n    white-space: nowrap;\n    cursor: pointer; user-select: none;\n    transition: color 0.15s;\n  }\n  .smartd-support-package-viewer #auditViewer th:hover { color: var(--text-bright); }\n  .smartd-support-package-viewer #auditViewer th.sorted { color: var(--accent); }\n  .smartd-support-package-viewer #auditViewer th:last-child { border-right: none; }\n  .smartd-support-package-viewer #auditViewer th.col-ts   { width: 190px; }\n  .smartd-support-package-viewer #auditViewer th.col-sev  { width: 110px; }\n  .smartd-support-package-viewer #auditViewer th.col-evt  { width: 200px; }\n  .smartd-support-package-viewer #auditViewer th.col-orig { width: 130px; }\n  .smartd-support-package-viewer #auditViewer th.col-user { width: 120px; }\n  .smartd-support-package-viewer #auditViewer th.col-tid  { width: 130px; }\n\n  .smartd-support-package-viewer #auditViewer tr { border-bottom: 1px solid var(--border); transition: background 0.1s; }\n  .smartd-support-package-viewer #auditViewer tr:hover td { background: var(--surface2); }\n  .smartd-support-package-viewer #auditViewer td {\n    padding: 9px 14px;\n    vertical-align: top;\n    font-family: var(--mono);\n    font-size: 11.5px;\n    color: var(--text);\n    border-right: 1px solid var(--border);\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n  }\n  .smartd-support-package-viewer #auditViewer td:last-child { border-right: none; }\n  .smartd-support-package-viewer #auditViewer td.col-msg {\n    white-space: normal;\n    word-break: break-word;\n    line-height: 1.5;\n    font-family: var(--sans);\n    font-size: 12px;\n  }\n  .smartd-support-package-viewer #auditViewer .ts-cell { color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .ts-date { color: var(--text-dim); font-size: 10.5px; }\n  .smartd-support-package-viewer #auditViewer .ts-time { color: var(--text-bright); font-size: 12px; font-weight: 500; }\n\n  \/* Badges *\/\n  .smartd-support-package-viewer #auditViewer .badge {\n    display: inline-flex; align-items: center; gap: 5px;\n    padding: 2px 8px; border-radius: 3px;\n    font-size: 10px; font-weight: 600; letter-spacing: 0.08em;\n    text-transform: uppercase; font-family: var(--mono);\n    border: 1px solid transparent;\n  }\n  .smartd-support-package-viewer #auditViewer .badge-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }\n  .smartd-support-package-viewer #auditViewer .badge-INFO       { background:rgba(2,175,76,.12); border-color:rgba(2,175,76,.3); color:var(--info); }\n  .smartd-support-package-viewer #auditViewer .badge-WARNING    { background:rgba(210,153,34,.14); border-color:rgba(210,153,34,.35); color:var(--warn); }\n  .smartd-support-package-viewer #auditViewer .badge-ERROR      { background:rgba(248,81,73,.12); border-color:rgba(248,81,73,.3); color:var(--error); }\n  .smartd-support-package-viewer #auditViewer .badge-EMERGENCY  { background:rgba(248,81,73,.22); border-color:rgba(248,81,73,.55); color:#ff8a85; }\n  .smartd-support-package-viewer #auditViewer .badge-dot-INFO      { background:var(--info); }\n  .smartd-support-package-viewer #auditViewer .badge-dot-WARNING   { background:var(--warn); }\n  .smartd-support-package-viewer #auditViewer .badge-dot-ERROR     { background:var(--error); }\n  .smartd-support-package-viewer #auditViewer .badge-dot-EMERGENCY { background:#ff8a85; }\n\n  .smartd-support-package-viewer #auditViewer .evt-chip { font-family:var(--mono); font-size:10.5px; color:var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .evt-ALARM         { color:var(--alarm); font-weight:600; }\n  .smartd-support-package-viewer #auditViewer .evt-LOGIN_ATTEMPT { color:#7c3aed; }\n  .smartd-support-package-viewer #auditViewer .evt-CONFIG_CHANGE { color:#059669; }\n  .smartd-support-package-viewer #auditViewer .evt-SETPOINTS_CHANGE { color:#0891b2; }\n  .smartd-support-package-viewer #auditViewer .evt-FACTORY_RESET { color:#dc2626; font-weight:600; }\n  .smartd-support-package-viewer #auditViewer .evt-BACKUP_COMPLETE { color:var(--success); }\n  .smartd-support-package-viewer #auditViewer .evt-BACKUP_START  { color:#16a34a; }\n  .smartd-support-package-viewer #auditViewer .evt-METRICS_RESET { color:#ca8a04; }\n  .smartd-support-package-viewer #auditViewer .evt-LOCKOUT_RESET { color:#ea580c; }\n\n  .smartd-support-package-viewer #auditViewer mark { background:rgba(251,191,36,.45); color:var(--text-bright); border-radius:2px; padding:0 2px; }\n  .smartd-support-package-viewer #auditViewer .null-cell { color:var(--border2); font-style:italic; font-size:10.5px; }\n  .smartd-support-package-viewer #auditViewer .origin-chip { font-family:var(--mono); font-size:10.5px; }\n  \/* Tooltips on chips and SystemState tokens \u2014 subtle help cursor *\/\n  .smartd-support-package-viewer #auditViewer .badge, .smartd-support-package-viewer #auditViewer .evt-chip, .smartd-support-package-viewer #auditViewer .origin-chip { cursor: help; }\n  .smartd-support-package-viewer #auditViewer .st-tip {\n    border-bottom: 1px dotted var(--text-dim);\n    cursor: help;\n  }\n  .smartd-support-package-viewer #auditViewer .origin-SYSTEM { color:var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .origin-HMI { color:#02AF4C; }\n  .smartd-support-package-viewer #auditViewer .origin-MOBILE { color:#7c3aed; }\n  .smartd-support-package-viewer #auditViewer .origin-UNSPECIFIED { color:var(--border2); }\n\n  \/* Row highlights *\/\n  .smartd-support-package-viewer #auditViewer tr.row-alarm td { background:rgba(211,47,47,.12); }\n  .smartd-support-package-viewer #auditViewer tr.row-emergency td { background:rgba(248,81,73,.08); }\n  .smartd-support-package-viewer #auditViewer tr.row-error td { background:rgba(248,81,73,.04); }\n  .smartd-support-package-viewer #auditViewer tr.row-fault td { background:rgba(194,65,12,.14); border-bottom-color:rgba(194,65,12,.24); }\n  .smartd-support-package-viewer #auditViewer tr.row-fault:hover td { background:rgba(194,65,12,.22); }\n  .smartd-support-package-viewer #auditViewer tr.row-ready td { background:rgba(63,185,80,.09); border-bottom-color: rgba(63,185,80,.18); }\n  .smartd-support-package-viewer #auditViewer tr.row-ready:hover td { background:rgba(63,185,80,.16); }\n  .smartd-support-package-viewer #auditViewer tr.row-motor-on td { background:#BBF7D0; border-bottom-color:#86EFAC; }\n  .smartd-support-package-viewer #auditViewer tr.row-motor-off td { background:#FECACA; border-bottom-color:#FCA5A5; }\n  .smartd-support-package-viewer #auditViewer tr.row-state-change td { background:#BAE6FD; border-bottom-color:#7DD3FC; }\n  .smartd-support-package-viewer #auditViewer tr.row-control-mode td { background:#E9D5FF; border-bottom-color:#D8B4FE; }\n  .smartd-support-package-viewer #auditViewer tr.row-login td { background:#B4FFE7; border-bottom-color:#8CEFCF; }\n  \/* Event-type row emphasis *\/\n  .smartd-support-package-viewer #auditViewer tr.row-lockout td { background:#fff7cc; color:#ea580c; font-weight:700; }\n  .smartd-support-package-viewer #auditViewer tr.row-lockout td .svc-concise, .smartd-support-package-viewer #auditViewer tr.row-lockout td .alarm-concise, .smartd-support-package-viewer #auditViewer tr.row-lockout td .st-tip { color:#ea580c; font-weight:700; }\n  .smartd-support-package-viewer #auditViewer tr.row-config td { background:#dbeafe; color:#1d4ed8; font-weight:700; }\n  .smartd-support-package-viewer #auditViewer tr.row-config td .svc-concise, .smartd-support-package-viewer #auditViewer tr.row-config td .alarm-concise, .smartd-support-package-viewer #auditViewer tr.row-config td .cc-cell-sec { color:#1d4ed8; font-weight:700; }\n  \/* Focus \/ context mode *\/\n  .smartd-support-package-viewer #auditViewer tbody tr { cursor: pointer; }\n  .smartd-support-package-viewer #auditViewer tr.row-focus td { background: #f0dfc4; border-top: 3px double #5c3a1a; border-bottom: 3px double #5c3a1a; }\n  .smartd-support-package-viewer #auditViewer tr.row-focus td:first-child { border-left: 3px double #5c3a1a; }\n  .smartd-support-package-viewer #auditViewer tr.row-focus td:last-child { border-right: 3px double #5c3a1a; }\n  .smartd-support-package-viewer .focus-star { color: #5c3a1a; font-weight: 800; margin-right: 5px; }\n  .smartd-support-package-viewer #auditViewer .audit-focus-banner {\n    display: flex; align-items: center; gap: 9px;\n    background: rgba(2,175,76,.10); border-bottom: 1px solid var(--border);\n    padding: 7px 16px; font-size: 12px; color: var(--text); flex-shrink: 0;\n  }\n  .smartd-support-package-viewer #auditViewer .audit-focus-banner .focus-star { font-size: 15px; margin-right: 0; }\n  .smartd-support-package-viewer #auditViewer .audit-focus-banner #btn-goto-marked { margin-left: auto; }\n  .smartd-support-package-viewer #auditViewer tr.row-motor-on:hover td { background:#A7F0C2; }\n  .smartd-support-package-viewer #auditViewer tr.row-motor-off:hover td { background:#FDBCBC; }\n  .smartd-support-package-viewer #auditViewer tr.row-state-change:hover td { background:#A5DBFB; }\n  .smartd-support-package-viewer #auditViewer tr.row-control-mode:hover td { background:#DDC5FB; }\n  .smartd-support-package-viewer #auditViewer tr.row-login:hover td { background:#A0F5D9; }\n  \/* Highlight-color filter chips (toolbar) *\/\n  .smartd-support-package-viewer #auditViewer .color-trigger { display:flex; align-items:center; gap:6px; min-width:150px; background:var(--surface3); border:1px solid var(--border); color:var(--text); font-family:var(--mono); font-size:12px; padding:6px 10px; border-radius:4px; cursor:pointer; }\n  .smartd-support-package-viewer #auditViewer .color-trigger:hover, .smartd-support-package-viewer #auditViewer .color-trigger.open { border-color:var(--accent); }\n  .smartd-support-package-viewer #auditViewer .color-trigger.cf-active { color:var(--accent); border-color:var(--accent); }\n  .smartd-support-package-viewer #auditViewer .color-trigger.cf-active .cf-title { font-weight:700; }\n  .smartd-support-package-viewer #auditViewer .color-swatch { width:11px; height:11px; border-radius:3px; border:1px solid rgba(0,0,0,0.18); flex-shrink:0; }\n  \/* Custom colored checkboxes: the box itself carries the category background color *\/\n  .smartd-support-package-viewer #cf-color-list input[type=checkbox] { position:absolute; width:1px; height:1px; opacity:0; margin:0; pointer-events:none; }\n  .smartd-support-package-viewer #cf-color-list .cc-box { width:16px; height:16px; border-radius:3px; border:1px solid rgba(0,0,0,0.28); flex-shrink:0; position:relative; box-shadow: inset 0 0 0 1px rgba(255,255,255,0.30); transition:opacity .12s; }\n  .smartd-support-package-viewer #cf-color-list .cc-box.cc-none { border-color:var(--border2); box-shadow:none; }\n  .smartd-support-package-viewer #cf-color-list input:not(:checked) + .cc-box { opacity:0.30; }\n  .smartd-support-package-viewer #cf-color-list input:checked + .cc-box::after { content:''; position:absolute; left:5px; top:2px; width:4px; height:8px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg); filter:drop-shadow(0 0 1px rgba(0,0,0,0.9)); }\n\n  \/* Power metrics card *\/\n  .smartd-support-package-viewer #auditViewer .metrics-card {\n    font-family: var(--sans); font-size: 12px; line-height: 1.4;\n  }\n  .smartd-support-package-viewer #auditViewer .metrics-title {\n    font-family: var(--mono); font-size: 11px;\n    font-weight: 600; color: var(--text-dim);\n    letter-spacing: 0.06em; text-transform: uppercase;\n    margin-bottom: 8px;\n  }\n  .smartd-support-package-viewer #auditViewer .metrics-sections {\n    display: flex; flex-wrap: wrap; gap: 10px;\n  }\n  .smartd-support-package-viewer #auditViewer .metrics-section {\n    background: var(--surface2); border: 1px solid var(--border);\n    border-radius: 6px; padding: 8px 10px;\n    min-width: 180px; flex: 1;\n  }\n  .smartd-support-package-viewer #auditViewer .metrics-section-title {\n    font-family: var(--mono); font-size: 10px;\n    font-weight: 600; letter-spacing: 0.1em;\n    text-transform: uppercase; color: var(--accent);\n    border-bottom: 1px solid var(--border);\n    padding-bottom: 5px; margin-bottom: 6px;\n  }\n  .smartd-support-package-viewer #auditViewer .metrics-grid {\n    display: grid; grid-template-columns: auto 1fr 1fr;\n    gap: 2px 10px; align-items: baseline;\n  }\n  .smartd-support-package-viewer #auditViewer .m-label {\n    font-family: var(--mono); font-size: 10.5px;\n    color: var(--text-dim); white-space: nowrap;\n  }\n  .smartd-support-package-viewer #auditViewer .m-val {\n    font-family: var(--mono); font-size: 11px;\n    font-weight: 600; color: var(--text-bright);\n    text-align: right; white-space: nowrap;\n  }\n  .smartd-support-package-viewer #auditViewer .m-val-min { color: var(--accent); }\n  .smartd-support-package-viewer #auditViewer .m-val-max { color: var(--warn); }\n  .smartd-support-package-viewer #auditViewer .m-header {\n    font-family: var(--mono); font-size: 9px;\n    font-weight: 600; color: var(--text-dim);\n    text-align: right; text-transform: uppercase;\n    letter-spacing: 0.06em; padding-bottom: 2px;\n  }\n\n  \/* Debug info ALARM messages \u2014 collapsed by default with summary chips *\/\n  .smartd-support-package-viewer #auditViewer .dbginfo-wrap { font-family: var(--sans); font-size: 12px; }\n  .smartd-support-package-viewer #auditViewer .dbginfo-details { }\n  .smartd-support-package-viewer #auditViewer .dbginfo-summary-row {\n    list-style: none;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    flex-wrap: wrap;\n    gap: 8px;\n    padding: 2px 0;\n    user-select: none;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-summary-row::-webkit-details-marker { display: none; }\n  .smartd-support-package-viewer #auditViewer .dbginfo-chev {\n    flex-shrink: 0;\n    color: var(--text-dim);\n    transition: transform 0.15s;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-details[open] .dbginfo-chev { transform: rotate(90deg); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-label {\n    font-family: var(--mono); font-size: 11px;\n    font-weight: 600; letter-spacing: 0.06em;\n    text-transform: uppercase;\n    color: var(--accent);\n    background: var(--accent-glow);\n    border: 1px solid rgba(2,175,76,0.3);\n    padding: 2px 8px;\n    border-radius: 3px;\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-summary {\n    display: flex;\n    flex-wrap: wrap;\n    gap: 4px;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-chip {\n    display: inline-flex; align-items: center; gap: 5px;\n    font-family: var(--mono); font-size: 10.5px;\n    padding: 1px 7px; border-radius: 3px;\n    border: 1px solid var(--border);\n    background: var(--surface2);\n    color: var(--text);\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-chip-label {\n    font-size: 9.5px;\n    color: var(--text-dim);\n    text-transform: uppercase;\n    letter-spacing: 0.06em;\n    font-weight: 600;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-chip-value { font-weight: 600; color: var(--text-bright); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-accent { border-color: rgba(79,218,110,0.25); background: var(--accent-glow); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-accent .dbginfo-chip-value { color: var(--accent); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-ok { border-color: rgba(22,163,74,0.3); background: rgba(22,163,74,0.06); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-ok .dbginfo-chip-value { color: #15803d; }\n  .smartd-support-package-viewer #auditViewer .dbginfo-warn { border-color: rgba(234,179,8,0.3); background: rgba(234,179,8,0.08); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-warn .dbginfo-chip-value { color: #b45309; }\n  .smartd-support-package-viewer #auditViewer .dbginfo-err { border-color: rgba(239,68,68,0.3); background: rgba(239,68,68,0.08); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-err .dbginfo-chip-value { color: #b91c1c; }\n\n  .smartd-support-package-viewer #auditViewer .dbginfo-tree {\n    margin-top: 10px;\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 5px;\n    padding: 10px;\n    max-height: 500px;\n    overflow-y: auto;\n  }\n  .smartd-support-package-viewer #auditViewer .dbginfo-tree::-webkit-scrollbar { width: 10px; }\n  .smartd-support-package-viewer #auditViewer .dbginfo-tree::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-tree::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid var(--bg); }\n  .smartd-support-package-viewer #auditViewer .dbginfo-tree > .cfg-group:last-child, .smartd-support-package-viewer #auditViewer .dbginfo-tree > .cfg-array:last-child { margin-bottom: 0; }\n\n  \/* Empty state (scoped) *\/\n  .smartd-support-package-viewer #auditViewer .empty-state {\n    padding: 60px 20px; text-align: center;\n    color: var(--text-dim); font-family: var(--mono); font-size: 12px;\n  }\n  .smartd-support-package-viewer #auditViewer .empty-icon { font-size: 32px; margin-bottom: 12px; }\n  .smartd-support-package-viewer #auditViewer .sort-indicator { margin-left: 4px; opacity: 0.6; }\n  \/* Column-header filter triggers *\/\n  .smartd-support-package-viewer #auditViewer th.filt-col { padding: 0; }\n  .smartd-support-package-viewer #auditViewer .col-filter-trigger {\n    display: flex; align-items: center; gap: 6px; width: 100%;\n    background: none; border: none; cursor: pointer;\n    font: inherit; color: inherit; letter-spacing: inherit; text-transform: inherit;\n    padding: 9px 14px;\n  }\n  .smartd-support-package-viewer #auditViewer .col-filter-trigger:hover, .smartd-support-package-viewer #auditViewer .col-filter-trigger.open { background: var(--surface2); color: var(--text); }\n  .smartd-support-package-viewer #auditViewer .cf-title { white-space: nowrap; }\n  .smartd-support-package-viewer #auditViewer .cf-chevron { flex-shrink: 0; opacity: 0.55; transition: transform .15s; margin-left: auto; }\n  .smartd-support-package-viewer #auditViewer .cf-badge {\n    font-size: 9px; font-weight: 700; line-height: 1; font-family: var(--mono);\n    background: var(--accent); color: #fff; border-radius: 8px;\n    padding: 2px 5px; min-width: 8px; text-align: center; letter-spacing: 0;\n  }\n  .smartd-support-package-viewer #auditViewer .cf-badge:empty { display: none; }\n  .smartd-support-package-viewer #auditViewer .col-filter-trigger.cf-active { color: var(--accent); }\n  .smartd-support-package-viewer #auditViewer .col-filter-trigger.cf-active .cf-title { font-weight: 700; }\n\n  \/* Pagination *\/\n  .smartd-support-package-viewer #auditViewer .pagination {\n    background: var(--surface);\n    border-top: 1px solid var(--border);\n    padding: 9px 18px;\n    display: flex; align-items: center; gap: 8px;\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer #auditViewer .pagination .audit-btn { padding: 5px 12px; font-size: 10px; }\n  .smartd-support-package-viewer #auditViewer .page-info {\n    font-family: var(--mono); font-size: 11px;\n    color: var(--text-dim);\n    flex: 1; text-align: center;\n  }\n  .smartd-support-package-viewer #auditViewer .page-size-label { font-family: var(--mono); font-size: 11px; color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .page-size-select { width: 80px; padding: 4px 8px; }\n  .smartd-support-package-viewer #auditViewer .viewtype-label { font-size: 11.5px; color: var(--text-dim); margin-left: 6px; align-self: center; }\n  .smartd-support-package-viewer #auditViewer .viewtype-toggle { display: flex; gap: 6px; }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     EXPORT PDF button + dropdown menu (shared: audit & timeline)\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer .export-wrap { position: relative; display: inline-block; }\n  #ceRoot .ce-toolbar #ceSaveBtn { margin-left: auto; }\n  #audit-export-wrap { margin-left: auto; }\n  #configViewer .cfg-header #commissioning-report-btn { margin-left: auto; }\n  \/* Commissioning Report + Export PDF -> same pill style as editor buttons *\/\n  #commissioning-report-btn, #audit-export-btn { display:inline-flex !important; align-items:center; gap:6px; padding:8px 14px !important; border-radius:999px !important; border:1px solid var(--b) !important; background:#02AF4C !important; color:var(--t) !important; font-family:inherit !important; font-size:13px !important; font-weight:600 !important; text-transform:none !important; letter-spacing:normal !important; }\n  #commissioning-report-btn:hover, #audit-export-btn:hover { background:#00FF00 !important; }\n  \/* Remove the live date\/time clocks on every screen *\/\n  #mv-header-time, #hd-header-time, #audit-current-time, #config-current-time { display:none !important; }\n  .smartd-support-package-viewer .audit-btn-export {\n    border-color: rgba(2,175,76,0.3) !important;\n    color: var(--accent) !important;\n  }\n  .smartd-support-package-viewer .audit-btn-export:hover {\n    background: var(--accent-glow) !important;\n    border-color: var(--accent) !important;\n  }\n  .smartd-support-package-viewer .audit-btn-export[aria-expanded=\"true\"] {\n    background: var(--accent) !important;\n    color: #fff !important;\n    border-color: var(--accent) !important;\n  }\n  \/* Menu is portaled to <body> with inline position: fixed; the JS handles top\/left *\/\n  .smartd-support-package-viewer .export-menu {\n    display: none;\n    min-width: 220px;\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    box-shadow: 0 8px 24px rgba(0,0,0,0.12);\n    padding: 4px;\n    font-family: var(--sans);\n  }\n  .smartd-support-package-viewer .export-menu.open { display: block; }\n  .smartd-support-package-viewer .export-menu-item {\n    display: flex; align-items: center; gap: 10px;\n    width: 100%; padding: 8px 10px;\n    background: transparent; border: none;\n    border-radius: 4px; cursor: pointer;\n    text-align: left;\n    color: var(--text);\n    font-family: inherit;\n    transition: background 0.12s;\n  }\n  .smartd-support-package-viewer .export-menu-item:hover { background: var(--bg-hover); }\n  .smartd-support-package-viewer .export-menu-item svg { flex-shrink: 0; color: var(--accent); }\n  .smartd-support-package-viewer .export-menu-title {\n    font-size: 12px; font-weight: 500;\n    color: var(--text-bright);\n    margin-bottom: 1px;\n  }\n  .smartd-support-package-viewer .export-menu-sub {\n    font-size: 10.5px;\n    color: var(--text-dim);\n  }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     TIME RANGE FILTER (shared: audit & timeline)\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer .daterange-wrap {\n    display: flex; align-items: center; gap: 4px;\n  }\n  .smartd-support-package-viewer .daterange-input {\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 4px;\n    padding: 5px 8px;\n    font-family: var(--mono);\n    font-size: 10.5px;\n    color: var(--text);\n    outline: none;\n    width: 180px;\n    line-height: 1.2;\n    transition: border-color 0.12s;\n    cursor: pointer;\n  }\n  .smartd-support-package-viewer .daterange-input:focus { border-color: var(--accent); }\n  .smartd-support-package-viewer .daterange-input:hover { border-color: var(--text-dim); }\n  .smartd-support-package-viewer .daterange-input:invalid { border-color: rgba(239,68,68,0.4); }\n  \/* Subtle visual cue when a value is set \u2014 warmer tint.\n     Attribute is present (regardless of value string) when the filter has a value *\/\n  .smartd-support-package-viewer .daterange-input[data-has-value] {\n    background: var(--accent-glow);\n    border-color: rgba(2,175,76,0.35);\n    color: var(--text-bright);\n  }\n  .smartd-support-package-viewer .daterange-input::-webkit-calendar-picker-indicator {\n    cursor: pointer;\n    opacity: 0.55;\n    filter: invert(0);\n  }\n  .smartd-support-package-viewer .daterange-input::-webkit-calendar-picker-indicator:hover { opacity: 1; }\n  .smartd-support-package-viewer .daterange-sep {\n    color: var(--text-dim);\n    font-family: var(--mono);\n    font-size: 12px;\n    font-weight: 600;\n    padding: 0 2px;\n  }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     VFD HEALTH DASHBOARD \u2014 first tab, aggregates config + metrics + logs\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer #healthViewer {\n    display: none; flex-direction: column; flex: 1;\n    min-height: 0; overflow: hidden;\n  }\n  .smartd-support-package-viewer #healthViewer.active { display: flex; }\n\n  .smartd-support-package-viewer #healthViewer .hd-header {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 0 18px;\n    display: flex; align-items: center; gap: 14px;\n    height: 46px; flex-shrink: 0;\n    font-family: var(--mono);\n  }\n  .smartd-support-package-viewer #healthViewer .hd-header-title {\n    font-size: 12px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.08em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-header-sub { font-size: 11px; color: var(--text-dim); letter-spacing: 0.04em; }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-divider { width: 1px; height: 24px; background: var(--border); }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-stat { font-size: 11px; color: var(--text-dim); }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-stat strong { color: var(--g); font-weight: 600; }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-stat.neutral strong { color: var(--text-bright); font-family: var(--mono); font-size: 11.5px; }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-stat strong.warn { color: var(--warn); }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-stat strong.crit { color: var(--error); }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-spacer { flex: 1; }\n  .smartd-support-package-viewer #healthViewer .hd-hdr-time { font-size: 11px; color: var(--text-dim); font-family: var(--mono); }\n\n  .smartd-support-package-viewer #healthViewer .hd-body {\n    flex: 1; overflow-y: auto;\n    padding: 18px 20px 24px;\n    background: var(--bg);\n    display: flex; flex-direction: column; gap: 16px;\n  }\n\n  \/* \u2500\u2500 Info sections (Device \/ Network \/ Mobile) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n  .smartd-support-package-viewer #healthViewer .hd-sections {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 14px;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-section {\n    background: #fff;\n    border: 1px solid var(--border);\n    border-radius: 8px;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-section-head {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 10px 14px;\n    display: flex; align-items: center; gap: 10px;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-section-icon {\n    width: 22px; height: 22px; border-radius: 5px;\n    background: var(--gl);\n    display: flex; align-items: center; justify-content: center;\n    color: var(--g);\n  }\n  .smartd-support-package-viewer #healthViewer .hd-section-title {\n    font-family: var(--mono);\n    font-size: 12px; font-weight: 700;\n    color: var(--text-bright);\n    letter-spacing: 0.06em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-info-grid {\n    padding: 12px 14px;\n    display: grid; grid-template-columns: minmax(140px, 1fr) 2fr;\n    gap: 8px 14px;\n    align-items: baseline;\n    font-family: var(--sans); font-size: 12.5px;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-info-key {\n    color: var(--text-dim);\n    font-family: var(--mono); font-size: 10.5px;\n    letter-spacing: 0.05em; text-transform: uppercase;\n    font-weight: 600;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-info-val {\n    color: var(--text-bright);\n    font-weight: 500;\n    word-break: break-all;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-info-val.mono { font-family: var(--mono); font-size: 12px; }\n  .smartd-support-package-viewer #healthViewer .hd-info-val.na   { color: var(--text-dim); font-style: italic; font-weight: 400; }\n  .smartd-support-package-viewer #healthViewer #hd-metrics-body {\n    margin-top: 14px;\n    display: grid;\n    grid-template-columns: 67fr 33fr;   \/* Operational Data 67% \/ Fan 33% *\/\n    gap: 14px;\n    align-items: start;\n  }\n  @media (max-width: 1100px) {\n    .smartd-support-package-viewer #healthViewer #hd-metrics-body { grid-template-columns: 1fr; }\n  }\n  .smartd-support-package-viewer #mv-body { display: grid; grid-template-columns: 67fr 33fr; gap: 14px; align-items: start; }\n  @media (max-width: 1100px) { .smartd-support-package-viewer #mv-body { grid-template-columns: 1fr; } }\n  .smartd-support-package-viewer #healthViewer .hd-section.collapsible .hd-section-head { cursor: pointer; user-select: none; }\n  .smartd-support-package-viewer #healthViewer .hd-section-chevron { color: var(--text-dim); flex-shrink: 0; margin-left: auto; transition: transform 0.15s; }\n  .smartd-support-package-viewer #healthViewer .hd-section.collapsible.collapsed .hd-section-chevron { transform: rotate(-90deg); }\n  .smartd-support-package-viewer #healthViewer .hd-section.collapsible.collapsed .hd-info-grid { display: none; }\n\n  \/* \u2500\u2500 Alarms section (two panels side-by-side) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\n  .smartd-support-package-viewer #healthViewer .hd-alarms {\n    display: grid;\n    grid-template-columns: 1fr 1fr;\n    gap: 14px;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-section .hd-section-subtitle {\n    font-family: var(--mono); font-size: 10.5px;\n    color: var(--text-dim); margin-left: auto;\n    letter-spacing: 0.04em;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-list {\n    display: flex; flex-direction: column;\n    padding: 0;\n    max-height: 440px; overflow-y: auto;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-row {\n    display: grid;\n    grid-template-columns: 1fr auto auto auto;\n    gap: 12px;\n    align-items: center;\n    padding: 8px 14px;\n    border-bottom: 1px solid var(--border);\n    font-family: var(--sans); font-size: 12px;\n    position: relative;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-help-btn {\n    width: 20px; height: 20px; border-radius: 50%;\n    border: 1px solid var(--border2); background: transparent;\n    color: var(--text-dim); font-size: 11px; font-weight: 700;\n    font-family: var(--sans); line-height: 1;\n    display: inline-flex; align-items: center; justify-content: center;\n    cursor: pointer; flex-shrink: 0; align-self: start; justify-self: end;\n    padding: 0; transition: color 0.12s, border-color 0.12s, background 0.12s;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-help-btn:hover {\n    color: var(--g); border-color: var(--g); background: var(--gl);\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-row:last-child { border-bottom: none; }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-row:hover { background: var(--surface); cursor: pointer; }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-code {\n    color: var(--text-bright); font-weight: 500;\n    overflow: hidden; text-overflow: ellipsis; white-space: nowrap;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-code .code-raw {\n    font-family: var(--mono); font-size: 10px; color: var(--text-dim);\n    display: block; letter-spacing: 0.02em;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip {\n    font-family: var(--sans); font-size: 10px; font-weight: 700;\n    padding: 2px 8px; border-radius: 10px;\n    text-transform: uppercase; letter-spacing: 0.06em;\n    background: var(--surface2); color: var(--text-dim);\n    white-space: nowrap;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.crit    { background: rgba(239,68,68,0.14); color: var(--error); }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.major   { background: rgba(239,68,68,0.10); color: var(--error); }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.medium  { background: rgba(245,158,11,0.18); color: var(--warn); }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.minor   { background: rgba(245,158,11,0.16); color: var(--warn); }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.warning { background: rgba(245,158,11,0.16); color: var(--warn); }\n  .smartd-support-package-viewer #healthViewer .hd-sev-chip.info    { background: var(--gl); color: var(--g); }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-meta {\n    font-family: var(--mono); font-size: 10.5px; color: var(--text-dim);\n    white-space: nowrap;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-meta strong {\n    color: var(--text-bright); font-weight: 600;\n  }\n  .smartd-support-package-viewer #healthViewer .hd-alarm-empty {\n    padding: 20px 14px; text-align: center;\n    color: var(--text-dim); font-style: italic; font-size: 12px;\n  }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     METRICS VIEWER \u2014 SystemMetrics \/ CoolingManagementMetrics \/ MotorControllerMetrics\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer #metricsViewer {\n    display: none; flex-direction: column; flex: 1;\n    min-height: 0; overflow: hidden;\n  }\n  .smartd-support-package-viewer #metricsViewer.active { display: flex; }\n\n  .smartd-support-package-viewer #metricsViewer .mv-header {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 0 18px;\n    display: flex; align-items: center; gap: 14px;\n    height: 46px; flex-shrink: 0;\n    font-family: var(--mono);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-header-title {\n    font-size: 12px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.08em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-header-sub { font-size: 11px; color: var(--text-dim); letter-spacing: 0.04em; }\n  .smartd-support-package-viewer #metricsViewer .mv-hdr-divider { width: 1px; height: 24px; background: var(--border); }\n  .smartd-support-package-viewer #metricsViewer .mv-hdr-stat { font-size: 11px; color: var(--text-dim); }\n  .smartd-support-package-viewer #metricsViewer .mv-hdr-stat strong { color: var(--g); font-weight: 600; }\n  .smartd-support-package-viewer #metricsViewer .mv-hdr-spacer { flex: 1; }\n  .smartd-support-package-viewer #metricsViewer .mv-hdr-time { font-size: 11px; color: var(--text-dim); font-family: var(--mono); }\n\n  .smartd-support-package-viewer #metricsViewer .mv-body {\n    flex: 1; overflow-y: auto;\n    padding: 18px 20px 24px;\n    background: var(--bg);\n    display: flex; flex-direction: column; gap: 16px;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-empty, .smartd-support-package-viewer #healthViewer .mv-empty {\n    color: var(--text-dim); font-style: italic; text-align: center;\n    padding: 40px 20px;\n  }\n\n  .smartd-support-package-viewer #metricsViewer .mv-section, .smartd-support-package-viewer #healthViewer .mv-section {\n    background: #fff;\n    border: 1px solid var(--border);\n    border-radius: 8px;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-section-head, .smartd-support-package-viewer #healthViewer .mv-section-head {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 10px 14px;\n    display: flex; align-items: center; gap: 10px;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-section-icon, .smartd-support-package-viewer #healthViewer .mv-section-icon {\n    width: 22px; height: 22px; border-radius: 5px;\n    background: var(--gl);\n    display: flex; align-items: center; justify-content: center;\n    color: var(--g);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-section-title, .smartd-support-package-viewer #healthViewer .mv-section-title {\n    font-family: var(--mono);\n    font-size: 12px; font-weight: 700;\n    color: var(--text-bright);\n    letter-spacing: 0.06em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-section-subtitle, .smartd-support-package-viewer #healthViewer .mv-section-subtitle {\n    font-family: var(--mono);\n    font-size: 11px; color: var(--text-dim);\n    margin-left: auto;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-section.collapsible .mv-section-head, .smartd-support-package-viewer #healthViewer .mv-section.collapsible .mv-section-head { cursor: pointer; user-select: none; }\n  .smartd-support-package-viewer #metricsViewer .mv-section-chevron, .smartd-support-package-viewer #healthViewer .mv-section-chevron { color: var(--text-dim); flex-shrink: 0; margin-left: auto; transition: transform 0.15s; }\n  .smartd-support-package-viewer #metricsViewer .mv-section.collapsed .mv-section-chevron, .smartd-support-package-viewer #healthViewer .mv-section.collapsed .mv-section-chevron { transform: rotate(-90deg); }\n  .smartd-support-package-viewer #metricsViewer .mv-section.collapsed .mv-tiles, .smartd-support-package-viewer #healthViewer .mv-section.collapsed .mv-tiles, .smartd-support-package-viewer #metricsViewer .mv-section.collapsed .mv-fans, .smartd-support-package-viewer #healthViewer .mv-section.collapsed .mv-fans { display: none; }\n  .smartd-support-package-viewer #metricsViewer .mv-tiles, .smartd-support-package-viewer #healthViewer .mv-tiles {\n    padding: 12px 14px 14px;\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n    gap: 10px;\n  }\n  .smartd-support-package-viewer .mv-body-sections { padding: 12px 14px 14px; display: block; }\n  .smartd-support-package-viewer .mv-body-sections .mv-tiles { padding: 0; }\n  .smartd-support-package-viewer .mv-subhead { font-family: var(--mono); font-size: 11px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; color: var(--text-bright, #163f2d); margin: 0 0 10px; }\n  .smartd-support-package-viewer .mv-subsep { height: 1px; background: var(--border, rgba(22,63,45,0.14)); margin: 16px 0 14px; }\n  .smartd-support-package-viewer #metricsViewer .mv-tile, .smartd-support-package-viewer #healthViewer .mv-tile {\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    padding: 10px 12px;\n    display: flex; flex-direction: column; gap: 4px;\n    background: #fafbfc;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-tile-label, .smartd-support-package-viewer #healthViewer .mv-tile-label {\n    font-family: var(--mono);\n    font-size: 10.5px; color: var(--text-dim);\n    letter-spacing: 0.05em; text-transform: uppercase;\n    font-weight: 600;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-tile-value, .smartd-support-package-viewer #healthViewer .mv-tile-value {\n    font-family: var(--sans);\n    font-size: 20px; font-weight: 600;\n    color: var(--text-bright);\n    line-height: 1.15;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-tile-sub, .smartd-support-package-viewer #healthViewer .mv-tile-sub {\n    font-family: var(--mono);\n    font-size: 10.5px; color: var(--text-dim);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-tile-value .mv-unit, .smartd-support-package-viewer #healthViewer .mv-tile-value .mv-unit {\n    font-size: 13px; font-weight: 400;\n    color: var(--text-dim);\n    margin-left: 3px;\n  }\n\n  \/* Fan cards: full-width row of side-by-side fans with a health bar *\/\n  .smartd-support-package-viewer #metricsViewer .mv-fans, .smartd-support-package-viewer #healthViewer .mv-fans {\n    padding: 12px 14px 14px;\n    display: grid;\n    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n    gap: 12px;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan, .smartd-support-package-viewer #healthViewer .mv-fan {\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    padding: 12px 14px;\n    background: #fafbfc;\n    display: flex; flex-direction: column; gap: 8px;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-head, .smartd-support-package-viewer #healthViewer .mv-fan-head {\n    display: flex; align-items: center; justify-content: space-between;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-title, .smartd-support-package-viewer #healthViewer .mv-fan-title {\n    font-family: var(--mono); font-size: 12px; font-weight: 700;\n    color: var(--text-bright);\n    letter-spacing: 0.06em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-pct, .smartd-support-package-viewer #healthViewer .mv-fan-pct {\n    font-family: var(--sans); font-size: 14px; font-weight: 600;\n    color: var(--g);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-pct.warn, .smartd-support-package-viewer #healthViewer .mv-fan-pct.warn { color: var(--warn); }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-pct.crit, .smartd-support-package-viewer #healthViewer .mv-fan-pct.crit { color: var(--error); }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-bar, .smartd-support-package-viewer #healthViewer .mv-fan-bar {\n    height: 8px; border-radius: 4px;\n    background: var(--surface2);\n    overflow: hidden; position: relative;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-bar-fill, .smartd-support-package-viewer #healthViewer .mv-fan-bar-fill {\n    height: 100%; background: var(--g);\n    border-radius: 4px;\n    transition: width 0.3s;\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-bar-fill.warn, .smartd-support-package-viewer #healthViewer .mv-fan-bar-fill.warn { background: var(--warn); }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-bar-fill.crit, .smartd-support-package-viewer #healthViewer .mv-fan-bar-fill.crit { background: var(--error); }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-stats, .smartd-support-package-viewer #healthViewer .mv-fan-stats {\n    display: flex; justify-content: space-between;\n    font-family: var(--mono); font-size: 10.5px; color: var(--text-dim);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-fan-stats span strong, .smartd-support-package-viewer #healthViewer .mv-fan-stats span strong {\n    color: var(--text-bright); font-weight: 600;\n  }\n\n  \/* Raw JSON view \u2014 monospace, syntax-highlighted, matches code-viewer style *\/\n  .smartd-support-package-viewer #metricsViewer .mv-raw {\n    flex: 1; overflow: auto;\n    font-family: var(--font-mono);\n    font-size: 12.5px; line-height: 1.55;\n    background: var(--bg);\n    padding: 14px 18px;\n    white-space: pre;\n    color: var(--text);\n  }\n  .smartd-support-package-viewer #metricsViewer .mv-raw::-webkit-scrollbar { width: 12px; height: 12px; }\n  .smartd-support-package-viewer #metricsViewer .mv-raw::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer #metricsViewer .mv-raw::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 3px solid var(--bg); }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     CONFIG VIEWER (adapted from uploaded, dark-themed)\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer #configViewer {\n    display: none; flex-direction: column; flex: 1;\n    min-height: 0; overflow: hidden;\n  }\n  .smartd-support-package-viewer #configViewer.active { display: flex; }\n\n  .smartd-support-package-viewer #configViewer .cfg-header {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 0 18px;\n    display: flex; align-items: center; gap: 14px;\n    height: 46px; flex-shrink: 0;\n    font-family: var(--mono);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-header-title {\n    font-size: 12px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.08em; text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-header-sub {\n    font-size: 11px; color: var(--text-dim);\n    letter-spacing: 0.04em;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-hdr-divider { width: 1px; height: 24px; background: var(--border); }\n  .smartd-support-package-viewer #configViewer .cfg-hdr-stat { font-size: 11px; color: var(--text-dim); }\n  .smartd-support-package-viewer #configViewer .cfg-hdr-stat strong { color: var(--accent); font-weight: 600; }\n  .smartd-support-package-viewer #configViewer .cfg-hdr-spacer { flex: 1; }\n  .smartd-support-package-viewer #configViewer .cfg-hdr-time { font-size: 11px; color: var(--text-dim); }\n\n  .smartd-support-package-viewer #configViewer .cfg-metadata {\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 12px 18px;\n    display: flex; flex-wrap: wrap;\n    gap: 18px 28px; flex-shrink: 0;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-meta-item {\n    display: flex; flex-direction: column; gap: 3px;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-meta-label {\n    font-family: var(--mono);\n    font-size: 9.5px; font-weight: 600;\n    letter-spacing: 0.12em; text-transform: uppercase;\n    color: var(--text-dim);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-meta-value {\n    font-family: var(--mono); font-size: 12px;\n    color: var(--text-bright); font-weight: 500;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-body {\n    flex: 1; display: flex; min-height: 0; overflow: hidden;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-sidebar {\n    width: 240px;\n    border-right: 1px solid var(--border);\n    background: var(--surface);\n    display: flex; flex-direction: column; flex-shrink: 0;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-sidebar-head {\n    padding: 10px 12px;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-search {\n    width: 100%;\n    background: var(--surface3);\n    border: 1px solid var(--border);\n    border-radius: 4px;\n    padding: 6px 10px;\n    font-family: var(--mono);\n    font-size: 11px;\n    color: var(--text);\n    outline: none;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-search:focus {\n    border-color: var(--accent);\n    box-shadow: 0 0 0 2px var(--accent-glow);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-search::placeholder { color: var(--text-dim); }\n\n  .smartd-support-package-viewer #configViewer .cfg-nav {\n    flex: 1; overflow-y: auto; padding: 6px 0;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-nav::-webkit-scrollbar { width: 8px; }\n  .smartd-support-package-viewer #configViewer .cfg-nav::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }\n\n  .smartd-support-package-viewer #configViewer .cfg-nav-item {\n    display: flex; align-items: center; gap: 8px;\n    padding: 8px 14px;\n    cursor: pointer;\n    font-family: var(--mono);\n    font-size: 11.5px;\n    color: var(--text);\n    border-left: 2px solid transparent;\n    transition: background .12s, border-color .12s, color .12s;\n    text-transform: uppercase;\n    letter-spacing: 0.04em;\n    font-weight: 500;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-nav-item:hover { background: var(--surface2); }\n  .smartd-support-package-viewer #configViewer .cfg-nav-item.active {\n    background: var(--accent-glow);\n    border-left-color: var(--accent);\n    color: var(--accent);\n    font-weight: 600;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-nav-count {\n    margin-left: auto;\n    font-size: 10px;\n    color: var(--text-dim);\n    background: var(--surface3);\n    padding: 1px 6px;\n    border-radius: 3px;\n    font-weight: 400;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-nav-item.active .cfg-nav-count {\n    background: var(--bg);\n    color: var(--accent);\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-main {\n    flex: 1;\n    overflow-y: auto;\n    padding: 20px 28px;\n    background: var(--bg);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-main::-webkit-scrollbar { width: 12px; }\n  .smartd-support-package-viewer #configViewer .cfg-main::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer #configViewer .cfg-main::-webkit-scrollbar-thumb { background: var(--border); border-radius: 6px; border: 3px solid var(--bg); }\n  .smartd-support-package-viewer #configViewer .cfg-main::-webkit-scrollbar-thumb:hover { background: #484f58; }\n\n  .smartd-support-package-viewer #configViewer .cfg-section-title {\n    font-family: var(--mono);\n    font-size: 16px; font-weight: 600;\n    letter-spacing: 0.02em;\n    color: var(--text-bright);\n    margin-bottom: 4px;\n    text-transform: uppercase;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-section-path {\n    font-family: var(--mono);\n    font-size: 10px;\n    color: var(--text-dim);\n    margin-bottom: 18px;\n    letter-spacing: 0.06em;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-group, .smartd-support-package-viewer .cfg-render .cfg-group {\n    background: var(--surface);\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    margin-bottom: 14px;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-group-title, .smartd-support-package-viewer .cfg-render .cfg-group-title {\n    background: var(--surface2);\n    padding: 8px 14px;\n    font-family: var(--mono);\n    font-size: 11px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-fields, .smartd-support-package-viewer .cfg-render .cfg-fields {\n    display: grid;\n    grid-template-columns: minmax(200px, 0.4fr) minmax(200px, 1fr);\n    font-family: var(--mono);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-field-row, .smartd-support-package-viewer .cfg-render .cfg-field-row { display: contents; }\n  .smartd-support-package-viewer #configViewer .cfg-field-label, .smartd-support-package-viewer .cfg-render .cfg-field-label {\n    padding: 7px 14px;\n    font-size: 11.5px;\n    color: var(--text-dim);\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    border-right: 1px solid var(--border);\n    word-break: break-word;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-field-value, .smartd-support-package-viewer .cfg-render .cfg-field-value {\n    padding: 7px 14px;\n    font-size: 11.5px;\n    color: var(--text-bright);\n    border-bottom: 1px solid var(--border);\n    word-break: break-word;\n    display: flex;\n    align-items: center;\n    min-height: 30px;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-field-row, .smartd-support-package-viewer .cfg-render .cfg-field-row:last-child .cfg-field-label, .smartd-support-package-viewer .cfg-render .cfg-field-row:last-child .cfg-field-value { border-bottom: none; }\n\n  .smartd-support-package-viewer #configViewer .val-string, .smartd-support-package-viewer .cfg-render .val-string { color: var(--text-bright); }\n  .smartd-support-package-viewer #configViewer .val-number, .smartd-support-package-viewer .cfg-render .val-number { color: var(--g); font-weight: 600; }\n  .smartd-support-package-viewer #configViewer .val-bool-true, .smartd-support-package-viewer .cfg-render .val-bool-true {\n    display: inline-flex; align-items: center; gap: 4px;\n    padding: 2px 8px; border-radius: 3px;\n    background: rgba(63,185,80,.14);\n    border: 1px solid rgba(63,185,80,.35);\n    color: var(--ok);\n    font-size: 10.5px; font-weight: 600;\n    text-transform: uppercase; letter-spacing: 0.06em;\n  }\n  .smartd-support-package-viewer #configViewer .val-bool-false, .smartd-support-package-viewer .cfg-render .val-bool-false {\n    display: inline-flex; align-items: center; gap: 4px;\n    padding: 2px 8px; border-radius: 3px;\n    background: rgba(148,163,184,.08);\n    border: 1px solid rgba(148,163,184,.22);\n    color: var(--text-dim);\n    font-size: 10.5px; font-weight: 600;\n    text-transform: uppercase; letter-spacing: 0.06em;\n  }\n  .smartd-support-package-viewer #configViewer .val-null, .smartd-support-package-viewer .cfg-render .val-null {\n    color: var(--text-mute);\n    font-style: italic;\n    font-size: 11px;\n  }\n  .smartd-support-package-viewer #configViewer .val-enum, .smartd-support-package-viewer .cfg-render .val-enum {\n    display: inline-block;\n    padding: 2px 8px;\n    border-radius: 3px;\n    background: rgba(2,175,76,.12);\n    border: 1px solid rgba(2,175,76,.3);\n    color: var(--accent);\n    font-size: 10.5px;\n    font-weight: 500;\n    letter-spacing: 0.04em;\n  }\n  .smartd-support-package-viewer #configViewer .val-enum-disabled, .smartd-support-package-viewer .cfg-render .val-enum-disabled {\n    background: rgba(148,163,184,.08);\n    border-color: rgba(148,163,184,.22);\n    color: var(--text-dim);\n  }\n  .smartd-support-package-viewer #configViewer .val-enum-known, .smartd-support-package-viewer .cfg-render .val-enum-known {\n    border-bottom-style: dotted;\n    border-bottom-width: 2px;\n    cursor: help;\n  }\n  .smartd-support-package-viewer .cc-enum-known {\n    border-bottom: 1px dotted currentColor;\n    cursor: help;\n  }\n  .smartd-support-package-viewer #configViewer .val-empty, .smartd-support-package-viewer .cfg-render .val-empty {\n    color: var(--text-mute);\n    font-style: italic;\n    font-size: 11px;\n  }\n  .smartd-support-package-viewer #configViewer .val-date, .smartd-support-package-viewer .cfg-render .val-date { color: var(--text); }\n  .smartd-support-package-viewer #configViewer .val-duration, .smartd-support-package-viewer .cfg-render .val-duration {\n    color: #8b5cf6;\n    font-weight: 600;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-array, .smartd-support-package-viewer .cfg-render .cfg-array {\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    margin-bottom: 14px;\n    overflow: hidden;\n    background: var(--surface);\n  }\n  .smartd-support-package-viewer #configViewer .cfg-array-title, .smartd-support-package-viewer .cfg-render .cfg-array-title {\n    background: var(--surface2);\n    padding: 8px 14px;\n    font-family: var(--mono);\n    font-size: 11px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    border-bottom: 1px solid var(--border);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-array-count, .smartd-support-package-viewer .cfg-render .cfg-array-count {\n    font-size: 10px;\n    color: var(--text-dim);\n    background: var(--surface3);\n    padding: 1px 7px;\n    border-radius: 3px;\n    font-weight: 500;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-array-item, .smartd-support-package-viewer .cfg-render .cfg-array-item {\n    border-bottom: 1px solid var(--border);\n    padding: 10px 14px;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-array-item, .smartd-support-package-viewer .cfg-render .cfg-array-item:last-child { border-bottom: none; }\n  .smartd-support-package-viewer #configViewer .cfg-array-item-title, .smartd-support-package-viewer .cfg-render .cfg-array-item-title {\n    font-family: var(--mono);\n    font-size: 10px; font-weight: 600;\n    color: var(--accent);\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n    margin-bottom: 6px;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-inline-fields, .smartd-support-package-viewer .cfg-render .cfg-inline-fields {\n    display: grid;\n    grid-template-columns: minmax(160px, 0.4fr) minmax(200px, 1fr);\n    gap: 4px 12px;\n    font-family: var(--mono);\n    font-size: 11px;\n  }\n  .smartd-support-package-viewer #configViewer .cfg-inline-label, .smartd-support-package-viewer .cfg-render .cfg-inline-label { color: var(--text-dim); }\n  .smartd-support-package-viewer #configViewer .cfg-inline-value, .smartd-support-package-viewer .cfg-render .cfg-inline-value { color: var(--text-bright); }\n\n  .smartd-support-package-viewer #configViewer .cfg-no-data, .smartd-support-package-viewer .cfg-render .cfg-no-data {\n    padding: 40px 20px;\n    text-align: center;\n    color: var(--text-dim);\n    font-family: var(--mono);\n    font-size: 12px;\n  }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     SHARED: toast, multi-select dropdown (portalled to body)\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer .multiselect-dropdown {\n    display: none;\n    position: fixed;\n    z-index: 9999;\n    background: #ffffff;\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    box-shadow: 0 8px 24px rgba(0,0,0,0.12);\n    min-width: 240px;\n    max-width: 320px;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer .multiselect-dropdown.open { display: block; }\n  .smartd-support-package-viewer .ms-actions {\n    display: flex; gap: 0;\n    border-bottom: 1px solid var(--border);\n  }\n  .smartd-support-package-viewer .ms-action-btn {\n    flex: 1;\n    background: var(--surface2);\n    border: none;\n    border-right: 1px solid var(--border);\n    color: var(--accent);\n    font-family: var(--mono);\n    font-size: 10px;\n    font-weight: 600;\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    padding: 7px 10px;\n    cursor: pointer;\n    transition: background .15s;\n  }\n  .smartd-support-package-viewer .ms-action-btn:last-child { border-right: none; }\n  .smartd-support-package-viewer .ms-action-btn:hover { background: var(--surface3); }\n  .smartd-support-package-viewer .ms-list {\n    max-height: 300px;\n    overflow-y: auto;\n    padding: 4px 0;\n  }\n  .smartd-support-package-viewer .ms-list::-webkit-scrollbar { width: 8px; }\n  .smartd-support-package-viewer .ms-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }\n  .smartd-support-package-viewer .ms-item {\n    display: flex; align-items: center; gap: 9px;\n    padding: 6px 12px;\n    cursor: pointer;\n    font-family: var(--mono);\n    font-size: 11px;\n    color: var(--text);\n    transition: background .1s;\n    user-select: none;\n  }\n  .smartd-support-package-viewer .ms-item:hover { background: var(--surface2); }\n  .smartd-support-package-viewer .ms-item input[type=checkbox] {\n    width: 13px; height: 13px;\n    margin: 0; padding: 0;\n    accent-color: var(--accent);\n    cursor: pointer;\n    flex-shrink: 0;\n  }\n  .smartd-support-package-viewer .ms-item-label { line-height: 1.3; }\n\n  \/* Toast *\/\n  .smartd-support-package-viewer .toast {\n    position: fixed;\n    top: 14px; right: 14px;\n    z-index: 10000;\n    padding: 10px 16px;\n    border-radius: 6px;\n    font-family: var(--font-ui);\n    font-size: 12.5px;\n    font-weight: 500;\n    box-shadow: 0 6px 20px rgba(0,0,0,0.12);\n    opacity: 0;\n    pointer-events: none;\n    transform: translateY(-8px);\n    transition: opacity .18s, transform .18s;\n    max-width: 420px;\n  }\n  .smartd-support-package-viewer .toast.visible {\n    opacity: 1;\n    transform: translateY(0);\n  }\n  .smartd-support-package-viewer .toast-info {\n    background: var(--gl);\n    border: 1px solid var(--g-border);\n    color: var(--g-dim);\n  }\n  .smartd-support-package-viewer .toast-error {\n    background: #fef2f2;\n    border: 1px solid #fecaca;\n    color: #b91c1c;\n  }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     SmartD API Reference modal\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  \/* \u2500\u2500 CONFIG_CHANGE diff modal & compact cell \u2500\u2500 *\/\n  .smartd-support-package-viewer .cc-toolbar {\n    display: flex; align-items: center; justify-content: space-between;\n    gap: 12px; padding: 10px 18px;\n    border-bottom: 1px solid var(--border); background: var(--surface);\n  }\n  .smartd-support-package-viewer .cc-modes { display: flex; gap: 6px; }\n  .smartd-support-package-viewer .cc-mode-btn {\n    font: inherit; font-size: 12px; padding: 5px 12px;\n    border: 1px solid var(--border); background: var(--bg); color: var(--text-dim);\n    border-radius: 6px; cursor: pointer;\n  }\n  .smartd-support-package-viewer .cc-mode-btn.active { background: var(--g); border-color: var(--g); color: #fff; }\n  .smartd-support-package-viewer .cc-counts { display: flex; gap: 6px; }\n  .smartd-support-package-viewer .cc-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-family: var(--mono); }\n  .smartd-support-package-viewer .cc-badge.cc-add { background: rgba(2,175,76,0.12); color: #02893d; }\n  .smartd-support-package-viewer .cc-badge.cc-chg { background: rgba(217,119,6,0.14); color: #b45309; }\n  .smartd-support-package-viewer .cc-badge.cc-rem { background: rgba(220,38,38,0.12); color: #dc2626; }\n  .smartd-support-package-viewer .cc-previnfo {\n    padding: 8px 18px; font-size: 11.5px; color: var(--text-dim);\n    border-bottom: 1px solid var(--border-soft); background: var(--surface2);\n  }\n  .smartd-support-package-viewer .cc-body { overflow: auto; padding: 6px 0; }\n  .smartd-support-package-viewer .cc-row {\n    display: grid; grid-template-columns: 18px 1fr auto; gap: 10px; align-items: baseline;\n    padding: 3px 18px; font-family: var(--mono); font-size: 11.5px;\n    border-left: 3px solid transparent;\n  }\n  .smartd-support-package-viewer .cc-row.cc-add { background: rgba(2,175,76,0.07); border-left-color: #02af4c; }\n  .smartd-support-package-viewer .cc-row.cc-chg { background: rgba(217,119,6,0.08); border-left-color: #d97706; }\n  .smartd-support-package-viewer .cc-row.cc-rem { background: rgba(220,38,38,0.07); border-left-color: #dc2626; }\n  .smartd-support-package-viewer .cc-sign { font-weight: 700; text-align: center; color: var(--text-dim); }\n  .smartd-support-package-viewer .cc-row.cc-add .cc-sign { color: #02893d; }\n  .smartd-support-package-viewer .cc-row.cc-chg .cc-sign { color: #b45309; }\n  .smartd-support-package-viewer .cc-row.cc-rem .cc-sign { color: #dc2626; }\n  .smartd-support-package-viewer .cc-path { color: var(--text); word-break: break-all; }\n  .smartd-support-package-viewer .cc-val { color: var(--text-bright); text-align: right; word-break: break-all; }\n  .smartd-support-package-viewer .cc-old { color: var(--text-mute); text-decoration: line-through; }\n  .smartd-support-package-viewer .cc-empty { padding: 22px 18px; color: var(--text-dim); font-size: 12.5px; text-align: center; }\n  .smartd-support-package-viewer .cc-raw {\n    white-space: pre-wrap; word-break: break-word; margin: 0;\n    font-family: var(--mono); font-size: 11.5px; line-height: 1.5; color: var(--text);\n    padding: 12px 18px;\n  }\n  .smartd-support-package-viewer .cc-cell-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n  .smartd-support-package-viewer .cc-tag {\n    font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;\n    padding: 2px 7px; border-radius: 4px; background: rgba(5,150,105,0.12); color: #059669;\n  }\n  .smartd-support-package-viewer .cc-btn {\n    font: inherit; font-size: 11px; padding: 3px 9px;\n    border: 1px solid var(--border); background: var(--bg); color: var(--text);\n    border-radius: 5px; cursor: pointer; white-space: nowrap;\n  }\n  .smartd-support-package-viewer .cc-btn:hover { background: var(--g); border-color: var(--g); color: #fff; }\n  .smartd-support-package-viewer .cc-cell-sec { margin-top: 3px; font-family: var(--mono); font-size: 10.5px; color: var(--text-dim); }\n  .smartd-support-package-viewer .cc-cell-content { margin-top: 6px; max-height: 360px; overflow: auto; border: 1px solid var(--border); border-radius: 6px; background: var(--surface); padding: 4px 0; }\n  .smartd-support-package-viewer .cc-resp-err { margin-top: 5px; font-family: var(--mono); font-size: 11px; color: #b91c1c; background: rgba(220,38,38,0.08); border: 1px solid rgba(220,38,38,0.25); border-left: 3px solid #dc2626; border-radius: 4px; padding: 5px 9px; white-space: pre-wrap; word-break: break-word; }\n  .smartd-support-package-viewer #auditViewer .sp-req { font-family: var(--mono); font-size: 11.5px; }\n  .smartd-support-package-viewer #auditViewer .sp-req-head { border-bottom: 1px solid var(--border); padding-bottom: 5px; margin-bottom: 5px; }\n  .smartd-support-package-viewer #auditViewer .sp-req-head .sp-req-k { font-weight: 700; color: var(--text-bright); }\n  .smartd-support-package-viewer #auditViewer .sp-req-row { display: grid; grid-template-columns: minmax(120px, 0.32fr) 1fr; gap: 2px 12px; padding: 2px 0; align-items: baseline; }\n  .smartd-support-package-viewer #auditViewer .sp-req-k { color: var(--text-dim); }\n  .smartd-support-package-viewer #auditViewer .sp-req-v { color: var(--text-bright); word-break: break-word; }\n  .smartd-support-package-viewer #auditViewer .sp-req-title { font-family: var(--mono); font-size: 10.5px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; color: var(--accent); margin-bottom: 5px; }\n  .smartd-support-package-viewer .api-modal-panel.cc-panel { width: 96vw; max-width: 96vw; }\n  \/* \u2500\u2500 STARTUP structured\/raw viewer \u2500\u2500 *\/\n  .smartd-support-package-viewer .at-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }\n  .smartd-support-package-viewer .at-tag {\n    font-size: 10.5px; font-weight: 600; letter-spacing: 0.03em;\n    padding: 2px 7px; border-radius: 4px; background: rgba(37,99,235,0.12); color: #2563eb;\n  }\n  .smartd-support-package-viewer .at-tag-startup { background: rgba(2,175,76,0.12); color: #02893d; }\n  .smartd-support-package-viewer .at-tag-alarm { background: rgba(220,38,38,0.12); color: #dc2626; }\n  .smartd-support-package-viewer .alarm-concise { font-weight: 600; }\n  .smartd-support-package-viewer .alarm-concise.al-active  { color: #dc2626; }\n  .smartd-support-package-viewer .alarm-concise.al-cleared { color: #02893d; }\n  .smartd-support-package-viewer .alarm-concise.al-enable  { color: #111827; font-weight: 400; }\n  .smartd-support-package-viewer .alarm-concise.al-disable { color: #111827; font-weight: 400; }\n  .smartd-support-package-viewer .alarm-concise.al-oos     { color: var(--text-dim); }\n  .smartd-support-package-viewer .alarm-concise.al-ack     { color: #2563eb; }\n  .smartd-support-package-viewer .alarm-metrics-wrap { margin-top: 8px; }\n  .smartd-support-package-viewer .svc-concise { font-weight: 600; }\n  .smartd-support-package-viewer .svc-concise.svc-ok  { color: #02893d; }\n  .smartd-support-package-viewer .svc-concise.svc-err { color: #dc2626; }\n  .smartd-support-package-viewer .svc-concise.svc-req { color: var(--text); font-weight: 500; }\n  .smartd-support-package-viewer .at-btn {\n    font: inherit; font-size: 11px; padding: 3px 9px;\n    border: 1px solid var(--border); background: var(--bg); color: var(--text-dim);\n    border-radius: 5px; cursor: pointer; white-space: nowrap;\n  }\n  .smartd-support-package-viewer .at-btn.active { background: var(--g); border-color: var(--g); color: #fff; }\n  .smartd-support-package-viewer .at-btn:disabled { opacity: 0.45; cursor: default; }\n  .smartd-support-package-viewer .at-view { margin-top: 6px; }\n  .smartd-support-package-viewer .at-prefix { font-size: 11px; color: var(--text-dim); margin-bottom: 5px; }\n  .smartd-support-package-viewer .at-rows { display: flex; flex-direction: column; }\n  .smartd-support-package-viewer .at-row {\n    display: grid; grid-template-columns: 1fr auto; gap: 12px;\n    font-family: var(--mono); font-size: 11px; padding: 2px 0;\n    border-bottom: 1px solid var(--border-soft);\n  }\n  .smartd-support-package-viewer .at-path { color: var(--text); word-break: break-all; }\n  .smartd-support-package-viewer .at-val { color: var(--text-bright); text-align: right; word-break: break-all; }\n  .smartd-support-package-viewer .at-raw {\n    white-space: pre-wrap; word-break: break-word; margin: 0;\n    font-family: var(--mono); font-size: 11px; color: var(--text);\n    background: var(--surface2); padding: 8px 10px; border-radius: 6px;\n    max-height: 360px; overflow: auto;\n  }\n  .smartd-support-package-viewer .at-empty { font-size: 11.5px; color: var(--text-dim); padding: 6px 0; }\n\n  .smartd-support-package-viewer .api-modal {\n    position: fixed; inset: 0;\n    z-index: 9999;\n    display: flex; align-items: center; justify-content: center;\n    font-family: var(--font-ui);\n  }\n  .smartd-support-package-viewer .api-modal-backdrop {\n    position: absolute; inset: 0;\n    background: rgba(15, 23, 42, 0.55);\n    backdrop-filter: blur(2px);\n  }\n  .smartd-support-package-viewer .api-modal-panel {\n    position: relative;\n    width: 90vw; max-width: 920px;\n    max-height: 86vh;\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 8px;\n    box-shadow: 0 18px 50px rgba(0,0,0,0.25);\n    display: flex; flex-direction: column;\n    overflow: hidden;\n  }\n  .smartd-support-package-viewer .api-modal-header {\n    display: flex; align-items: center; justify-content: space-between;\n    padding: 14px 18px;\n    border-bottom: 1px solid var(--border);\n    background: var(--surface);\n  }\n  .smartd-support-package-viewer .api-modal-title {\n    font-size: 14px; font-weight: 600;\n    color: var(--text-bright);\n    letter-spacing: 0.04em;\n  }\n  .smartd-support-package-viewer .api-modal-sub {\n    font-family: var(--mono);\n    font-size: 10.5px;\n    color: var(--text-dim);\n    margin-top: 2px;\n  }\n  .smartd-support-package-viewer .api-modal-close {\n    background: transparent; border: none;\n    font-size: 22px; line-height: 1;\n    color: var(--text-dim);\n    width: 28px; height: 28px; border-radius: 4px;\n    cursor: pointer;\n    display: flex; align-items: center; justify-content: center;\n    transition: background 0.12s, color 0.12s;\n  }\n  .smartd-support-package-viewer .api-modal-close:hover { background: var(--surface2); color: var(--text-bright); }\n  .smartd-support-package-viewer .api-modal-toolbar {\n    display: flex; gap: 8px; align-items: center;\n    padding: 10px 18px;\n    border-bottom: 1px solid var(--border);\n    background: var(--surface2);\n  }\n  .smartd-support-package-viewer .api-modal-search {\n    flex: 1;\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 4px;\n    padding: 6px 10px;\n    font-family: var(--mono);\n    font-size: 11.5px;\n    color: var(--text);\n    outline: none;\n  }\n  .smartd-support-package-viewer .api-modal-search:focus { border-color: var(--accent); }\n  .smartd-support-package-viewer .api-modal-filter {\n    background: var(--bg);\n    border: 1px solid var(--border);\n    border-radius: 4px;\n    padding: 6px 10px;\n    font-family: var(--mono);\n    font-size: 11.5px;\n    color: var(--text);\n    outline: none;\n    cursor: pointer;\n    width: 180px;\n  }\n  .smartd-support-package-viewer .api-modal-filter:focus { border-color: var(--accent); }\n  .smartd-support-package-viewer .api-modal-count {\n    font-family: var(--mono);\n    font-size: 10.5px;\n    color: var(--text-dim);\n    white-space: nowrap;\n  }\n  .smartd-support-package-viewer .api-modal-body {\n    flex: 1;\n    overflow-y: auto;\n    padding: 4px 0;\n    background: var(--bg);\n  }\n  .smartd-support-package-viewer .api-modal-body::-webkit-scrollbar { width: 10px; }\n  .smartd-support-package-viewer .api-modal-body::-webkit-scrollbar-track { background: var(--bg); }\n  .smartd-support-package-viewer .api-modal-body::-webkit-scrollbar-thumb { background: var(--border); border-radius: 5px; border: 2px solid var(--bg); }\n  .smartd-support-package-viewer .api-cat-group { margin-bottom: 4px; }\n  .smartd-support-package-viewer .api-cat-header {\n    position: sticky; top: 0;\n    background: var(--surface);\n    border-bottom: 1px solid var(--border);\n    padding: 7px 18px;\n    font-family: var(--mono);\n    font-size: 10.5px;\n    font-weight: 700;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n    color: var(--accent);\n    z-index: 1;\n  }\n  .smartd-support-package-viewer .api-cat-count {\n    color: var(--text-dim);\n    font-weight: 500;\n    letter-spacing: 0.04em;\n    margin-left: 6px;\n  }\n  .smartd-support-package-viewer .api-row {\n    display: grid;\n    grid-template-columns: 60px 280px 1fr;\n    gap: 12px;\n    padding: 6px 18px;\n    border-bottom: 1px solid var(--border-soft);\n    align-items: baseline;\n  }\n  .smartd-support-package-viewer .api-row:hover { background: rgba(2,175,76,0.03); }\n  .smartd-support-package-viewer .api-row-code {\n    font-family: var(--mono);\n    font-size: 11px;\n    color: var(--text-dim);\n    text-align: right;\n  }\n  .smartd-support-package-viewer .api-row-name {\n    font-family: var(--mono);\n    font-size: 11.5px;\n    font-weight: 600;\n    color: var(--text-bright);\n    word-break: break-all;\n  }\n  .smartd-support-package-viewer .api-row-desc {\n    font-size: 11.5px;\n    color: var(--text);\n    line-height: 1.45;\n  }\n  .smartd-support-package-viewer .api-empty {\n    padding: 32px 24px;\n    text-align: center;\n    color: var(--text-dim);\n    font-size: 12px;\n  }\n  .smartd-support-package-viewer .api-modal mark {\n    background: rgba(234,179,8,0.35); color: inherit;\n    padding: 0 1px; border-radius: 2px;\n  }\n  .smartd-support-package-viewer .alarm-help-panel { width: 90vw; max-width: 480px; max-height: none; }\n  .smartd-support-package-viewer .alarm-help-panel .api-modal-body { padding: 18px; }\n  .smartd-support-package-viewer .alarm-help-desc {\n    font-size: 13px; line-height: 1.55;\n    color: var(--text-bright);\n    margin: 0 0 16px 0;\n  }\n  .smartd-support-package-viewer .alarm-help-link {\n    display: inline-flex; align-items: center; gap: 6px;\n    font-family: var(--font-ui); font-size: 12px; font-weight: 600;\n    color: var(--accent); text-decoration: none;\n    padding: 8px 12px;\n    border: 1px solid var(--border);\n    border-radius: 6px;\n    transition: background 0.12s, border-color 0.12s;\n  }\n  .smartd-support-package-viewer .alarm-help-link:hover { background: var(--gl); border-color: var(--accent); }\n\n  \/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n     COMMISSIONING REPORT MODAL \u2014 full workflow form\n     \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n  .smartd-support-package-viewer .cr-modal { position: fixed; inset: 0; z-index: 5000; display: flex; align-items: center; justify-content: center; padding: 20px; }\n  .smartd-support-package-viewer .cr-modal-backdrop { position: absolute; inset: 0; background: rgba(0,0,0,0.5); backdrop-filter: blur(2px); }\n  .smartd-support-package-viewer .cr-modal-panel {\n    position: relative;\n    width: min(760px, 100%);\n    max-height: 90vh;\n    background: var(--bg);\n    border-radius: 10px;\n    box-shadow: 0 24px 60px rgba(0,0,0,0.35);\n    display: flex; flex-direction: column;\n    overflow: hidden;\n    font-family: var(--font-ui);\n  }\n  .smartd-support-package-viewer .cr-modal-header {\n    background: var(--blk); color: #fff;\n    padding: 14px 20px;\n    display: flex; align-items: center; justify-content: space-between;\n    border-bottom: 2px solid var(--g);\n  }\n  .smartd-support-package-viewer .cr-modal-title { font-size: 15px; font-weight: 700; letter-spacing: 0.2px; }\n  .smartd-support-package-viewer .cr-modal-sub { font-size: 11px; color: var(--lg); margin-top: 2px; font-weight: 400; letter-spacing: 0.4px; text-transform: uppercase; }\n  .smartd-support-package-viewer .cr-modal-close {\n    background: transparent; border: none; color: var(--lg);\n    font-size: 22px; line-height: 1; width: 28px; height: 28px;\n    border-radius: 4px; cursor: pointer;\n    display: flex; align-items: center; justify-content: center;\n    transition: background 0.12s, color 0.12s;\n  }\n  .smartd-support-package-viewer .cr-modal-close:hover { background: rgba(255,255,255,0.1); color: #fff; }\n  .smartd-support-package-viewer .cr-modal-body { padding: 18px 22px 20px; overflow-y: auto; flex: 1; }\n  .smartd-support-package-viewer .cr-modal-footer {\n    padding: 12px 22px; border-top: 1px solid var(--border);\n    background: var(--bg-elev);\n    display: flex; align-items: center; gap: 12px;\n    flex-wrap: wrap;\n  }\n\n  .smartd-support-package-viewer .cr-sec-label { font-size: 11px; font-weight: 700; color: var(--g); text-transform: uppercase; letter-spacing: 0.8px; margin: 14px 0 8px; }\n  .smartd-support-package-viewer .cr-sec-label:first-child { margin-top: 0; }\n\n  \/* Configuration preview *\/\n  .smartd-support-package-viewer .cr-preview-grid { border-radius: 6px; overflow: hidden; border: 1px solid var(--border); }\n  .smartd-support-package-viewer .cr-preview-row { display: flex; justify-content: space-between; padding: 6px 12px; font-size: 12.5px; }\n  .smartd-support-package-viewer .cr-preview-row:nth-child(odd) { background: var(--gl); }\n  .smartd-support-package-viewer .cr-preview-k { font-weight: 600; color: var(--dk); }\n  .smartd-support-package-viewer .cr-preview-v { color: var(--gr); text-align: right; max-width: 62%; }\n\n  \/* Section toggles *\/\n  .smartd-support-package-viewer .cr-sec-toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; gap: 10px; }\n  .smartd-support-package-viewer .cr-sec-count { font-size: 11px; color: var(--gr); font-weight: 600; }\n  .smartd-support-package-viewer .cr-sec-mini-btns { display: flex; gap: 6px; }\n  .smartd-support-package-viewer .cr-mini-btn {\n    background: transparent; border: 1px solid var(--g-border); color: var(--g);\n    font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px;\n    padding: 3px 9px; border-radius: 4px; cursor: pointer; transition: all 0.15s;\n  }\n  .smartd-support-package-viewer .cr-mini-btn:hover { background: var(--gl); border-color: var(--g); }\n  .smartd-support-package-viewer .cr-sec-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 6px; }\n  .smartd-support-package-viewer .cr-sec-toggle {\n    display: flex; align-items: center; gap: 8px;\n    background: var(--gl); color: var(--dk);\n    border-radius: 6px; padding: 7px 11px; font-size: 12px; font-weight: 600;\n    border: 1px solid var(--g-border); cursor: pointer; user-select: none;\n    transition: all 0.15s;\n  }\n  .smartd-support-package-viewer .cr-sec-toggle:hover { background: var(--gm); border-color: var(--g); }\n  .smartd-support-package-viewer .cr-sec-toggle input[type=\"checkbox\"] {\n    appearance: none; -webkit-appearance: none;\n    width: 15px; height: 15px; border: 1.5px solid var(--g); border-radius: 3px;\n    background: #fff; cursor: pointer;\n    display: flex; align-items: center; justify-content: center; flex-shrink: 0;\n    transition: background 0.12s;\n  }\n  .smartd-support-package-viewer .cr-sec-toggle input[type=\"checkbox\"]:checked { background: var(--g); border-color: var(--g); }\n  .smartd-support-package-viewer .cr-sec-toggle input[type=\"checkbox\"]:checked::after {\n    content: \"\u2713\"; color: #fff; font-size: 11px; font-weight: 900; line-height: 1;\n  }\n  .smartd-support-package-viewer .cr-sec-toggle.unchecked { background: #f7f7f7; color: var(--gr); border-color: #e0e0e0; }\n  .smartd-support-package-viewer .cr-sec-toggle.unchecked input[type=\"checkbox\"] { border-color: var(--lg); }\n\n  \/* Fields *\/\n  .smartd-support-package-viewer .cr-fields-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 6px; }\n  .smartd-support-package-viewer .cr-field-wrap { display: flex; flex-direction: column; gap: 4px; }\n  .smartd-support-package-viewer .cr-field-wrap.full { grid-column: 1 \/ -1; }\n  .smartd-support-package-viewer .cr-field-label { font-size: 11px; font-weight: 700; color: var(--gr); text-transform: uppercase; letter-spacing: 0.6px; }\n  .smartd-support-package-viewer .cr-field-input {\n    border: 1.5px solid #ddd; border-radius: 6px; padding: 8px 12px;\n    font-size: 13px; font-family: var(--font-ui); color: var(--dk);\n    transition: border-color 0.2s;\n  }\n  .smartd-support-package-viewer .cr-field-input:focus { outline: none; border-color: var(--g); }\n  .smartd-support-package-viewer .cr-field-input::placeholder { color: var(--lg); }\n\n  \/* Format toggle *\/\n  .smartd-support-package-viewer .cr-fmt-row { display: flex; gap: 10px; flex: 1; min-width: 220px; }\n  .smartd-support-package-viewer .cr-fmt-btn {\n    flex: 1; border: 2px solid #e0e0e0; border-radius: 6px;\n    padding: 8px 12px; cursor: pointer; background: #fff;\n    transition: all 0.15s;\n    display: flex; align-items: center; justify-content: center; gap: 6px;\n    font-size: 12.5px; font-weight: 700; color: var(--dk);\n  }\n  .smartd-support-package-viewer .cr-fmt-btn:hover { border-color: var(--g); background: var(--gl); }\n  .smartd-support-package-viewer .cr-fmt-btn.active { border-color: var(--g); background: var(--gl); color: var(--g); }\n\n  .smartd-support-package-viewer .cr-generate-btn {\n    background: var(--g); color: #fff; border: none; border-radius: 6px;\n    padding: 10px 20px; font-size: 13.5px; font-weight: 700; cursor: pointer;\n    transition: background 0.15s;\n    display: flex; align-items: center; gap: 8px;\n    font-family: var(--font-ui);\n  }\n  .smartd-support-package-viewer .cr-generate-btn:hover:not(:disabled) { background: var(--g-dim); }\n  .smartd-support-package-viewer .cr-generate-btn:disabled { background: #a0d4b4; cursor: not-allowed; }\n  .smartd-support-package-viewer .cr-status { font-size: 12px; color: var(--gr); flex-basis: 100%; margin-top: 4px; }\n  .smartd-support-package-viewer .cr-status.ok { color: var(--g); font-weight: 700; }\n  .smartd-support-package-viewer .cr-status.err { color: #c00; font-weight: 600; }\n\n\/* ===== services.smartd.co integration and SmartD 2026 branding ===== *\/\n.smartd-support-package-viewer {\n  --g: #4FDA6E;\n  --g-dim: #163F2D;\n  --blk: #101010;\n  --dk: #1B1B1B;\n  --rich: #101010;\n  --charcoal: #1B1B1B;\n  --dark-green: #163F2D;\n  --mint: #ACFFC9;\n  --gr: #747474;\n  --lg: #A6A6A6;\n  --gl: #F0FFF5;\n  --gm: #DDFBE7;\n  --g-border: rgba(79, 218, 110, 0.28);\n  --bg: #ffffff;\n  --bg-elev: #f8faf9;\n  --bg-elev-2: #f1f7f3;\n  --bg-hover: #eef8f1;\n  --border: #d8e2dc;\n  --border-soft: #e8f0eb;\n  --text: #1B1B1B;\n  --text-dim: #5f6b63;\n  --text-mute: #8a948e;\n  --accent: var(--g);\n  --accent-dim: var(--dark-green);\n  --font-ui: \"Plus Jakarta Sans\", Arial, sans-serif;\n  --font-mono: \"SFMono-Regular\", Consolas, \"Liberation Mono\", Menlo, monospace;\n  --sans: \"Plus Jakarta Sans\", Arial, sans-serif;\n  width: 100%;\n  max-width: 100%;\n  margin: 0 auto;\n  color: var(--text);\n  font-family: var(--font-ui);\n}\n\n.smartd-support-package-viewer .smartd-spv-intro {\n  margin: 0 0 4px;\n  padding: clamp(12px, 1vw, 0.1px);\n  border-radius: 18px;\n  background:\n    radial-gradient(circle at top right, rgba(172,255,201,0.35), transparent 36%),\n    linear-gradient(135deg, #101010 0%, #163F2D 100%);\n  color: #ffffff;\n}\n\n.smartd-support-package-viewer .smartd-spv-eyebrow {\n  margin: 0 0 4px;\n  color: var(--mint);\n  font-family: \"Sora\", var(--font-ui);\n  font-size: 0.10rem;\n  font-weight: 800\n  text-transform: uppercase;\n}\n\n.smartd-support-package-viewer .smartd-spv-intro h2 {\n  margin: 0 0 4px;\n  color: var(--mint);\n  font-family: \"Sora\", var(--font-ui);\n  font-size: 1rem;\n  font-weight: 800;\n  line-height: 1.40;\n}\n\n.smartd-support-package-viewer .smartd-spv-intro p {\n  max-width: 860px;\n  margin: 0;\n  color: rgba(255,255,255,0.82);\n  font-size: 1rem;\n  line-height: 1.0;\n}\n\n.smartd-support-package-viewer .app {\n  height: min(900px, 100vh);\n  min-height: 720px;\n  max-height: 100%;\n  overflow: hidden;\n  border: 1px solid rgba(22, 63, 45, 0.15);\n  border-radius: 24px;\n  background: var(--bg);\n  box-shadow: 0 22px 60px rgba(16, 16, 16, 0.10);\n}\n\n.smartd-support-package-viewer .topbar {\n  background:\n    linear-gradient(135deg, #101010 0%, #1B1B1B 55%, #163F2D 100%);\n  border-bottom-color: var(--mint);\n}\n\n.smartd-support-package-viewer .brand {\n  font-family: \"Sora\", var(--font-ui);\n  font-size: 13px;\n  letter-spacing: -0.01em;\n}\n\n.smartd-support-package-viewer .brand-icon {\n  background: var(--mint);\n  color: var(--rich);\n}\n\n.smartd-support-package-viewer .brand-text small {\n  display: block;\n  margin-top: 2px;\n  color: rgba(255,255,255,0.58);\n  font-family: var(--font-ui);\n  font-size: 11px;\n  font-weight: 500;\n  letter-spacing: 0;\n}\n\n.smartd-support-package-viewer .shell-btn {\n  background: var(--mint);\n  color: var(--rich);\n  border: 1px solid rgba(172, 255, 201, 0.35);\n  border-radius: 999px;\n}\n\n.smartd-support-package-viewer .shell-btn:hover {\n  background: var(--g);\n  color: #000000;\n}\n\n.smartd-support-package-viewer .shell-btn.ghost {\n  background: transparent;\n  color: rgba(255,255,255,0.76);\n  border-color: rgba(255,255,255,0.26);\n}\n\n.smartd-support-package-viewer .shell-btn.ghost:hover {\n  color: #ffffff;\n  border-color: var(--mint);\n  background: rgba(172,255,201,0.10);\n}\n\n.smartd-support-package-viewer .shell-tab.active {\n  border-bottom-color: var(--g);\n}\n\n.smartd-support-package-viewer .tab-dot,\n.smartd-support-package-viewer .hd-card-bar-fill,\n.smartd-support-package-viewer .sf-pct-bar-fill,\n.smartd-support-package-viewer #metricsViewer .mv-fan-bar-fill {\n  background: var(--g);\n}\n\n.smartd-support-package-viewer .drop-zone-shell {\n  border-color: rgba(22,63,45,0.22);\n  background: linear-gradient(180deg, #ffffff 0%, #f8faf9 100%);\n}\n\n.smartd-support-package-viewer .drop-zone-shell h2,\n.smartd-support-package-viewer .hd-overall-status,\n.smartd-support-package-viewer .hd-overall-score,\n.smartd-support-package-viewer #healthViewer .hd-section-title,\n.smartd-support-package-viewer #metricsViewer .mv-section-title {\n  font-family: \"Sora\", var(--font-ui);\n}\n\n.smartd-support-package-viewer a,\n.smartd-support-package-viewer .sf-summary-value.accent,\n.smartd-support-package-viewer .sf-section-cmd,\n.smartd-support-package-viewer .sf-labeled-json-label,\n.smartd-support-package-viewer #auditViewer .audit-header-title,\n.smartd-support-package-viewer #configViewer .cfg-header-title,\n.smartd-support-package-viewer #debugLogViewer .dl-header-title,\n.smartd-support-package-viewer #timelineViewer .tl-header-title,\n.smartd-support-package-viewer #metricsViewer .mv-header-title,\n.smartd-support-package-viewer #healthViewer .hd-header-title {\n  color: var(--accent);\n}\n\n.smartd-support-package-viewer .toast {\n  right: 24px;\n  top: 24px;\n}\n\n@media (max-width: 1024px) {\n  .smartd-support-package-viewer .app {\n    min-height: 720px;\n    height: 84vh;\n  }\n\n  .smartd-support-package-viewer #healthViewer .hd-cards {\n    grid-template-columns: repeat(2, 1fr);\n  }\n\n  .smartd-support-package-viewer #healthViewer .hd-sections,\n  .smartd-support-package-viewer #healthViewer .hd-alarms {\n    grid-template-columns: 1fr;\n  }\n}\n\n@media (max-width: 767px) {\n  .smartd-support-package-viewer .smartd-spv-intro {\n    padding: 28px 20px;\n    border-radius: 20px;\n  }\n\n  .smartd-support-package-viewer .app {\n    min-height: 700px;\n    height: 85vh;\n    border-radius: 18px;\n  }\n\n  .smartd-support-package-viewer .topbar {\n    align-items: flex-start;\n    flex-wrap: wrap;\n    gap: 10px;\n    padding: 12px;\n  }\n\n  .smartd-support-package-viewer .brand {\n    width: 100%;\n  }\n\n  .smartd-support-package-viewer .topbar-spacer {\n    display: none;\n  }\n\n  .smartd-support-package-viewer .pkg-info {\n    order: 10;\n    width: 100%;\n    max-width: 100%;\n    overflow-x: auto;\n  }\n\n  .smartd-support-package-viewer .file-toolbar,\n  .smartd-support-package-viewer #auditViewer .audit-toolbar,\n  .smartd-support-package-viewer #debugLogViewer .dl-toolbar,\n  .smartd-support-package-viewer #timelineViewer .tl-toolbar {\n    overflow-x: auto;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-body {\n    flex-direction: column;\n  }\n\n  .smartd-support-package-viewer #configViewer .cfg-sidebar {\n    width: 100%;\n    max-height: 180px;\n    border-right: 0;\n    border-bottom: 1px solid var(--border);\n  }\n\n  .smartd-support-package-viewer #healthViewer .hd-cards,\n  .smartd-support-package-viewer #healthViewer .hd-sections,\n  .smartd-support-package-viewer #healthViewer .hd-alarms,\n  .smartd-support-package-viewer #metricsViewer .mv-fans {\n    grid-template-columns: 1fr;\n  }\n\n  .smartd-support-package-viewer #auditViewer .audit-search,\n  .smartd-support-package-viewer #debugLogViewer .dl-search,\n  .smartd-support-package-viewer #timelineViewer .tl-search {\n    width: 100%;\n    min-width: 220px;\n  }\n}\n\n\/* ===== SD Tools Suite: Config Editor + Report tab panels ===== *\/\n.smartd-support-package-viewer #editorViewer,\n.smartd-support-package-viewer #reportViewer { display:none; flex-direction:column; flex:1; min-height:0; overflow:auto; }\n.smartd-support-package-viewer #editorViewer.active,\n.smartd-support-package-viewer #reportViewer.active { display:flex; }\n.smartd-support-package-viewer #reportViewer .rv-inner { padding:28px; overflow:auto; }\n.smartd-support-package-viewer #reportViewer .rv-card { max-width:600px; margin:0 auto; background:var(--surface2); border:1px solid var(--border); border-radius:16px; padding:26px 28px; }\n.smartd-support-package-viewer #reportViewer .rv-title { font-size:20px; margin:0 0 6px; color:var(--text); font-weight:700; }\n.smartd-support-package-viewer #reportViewer .rv-desc { font-size:13px; color:var(--text-dim); margin:0 0 18px; line-height:1.55; }\n.smartd-support-package-viewer #reportViewer .rv-summary { display:flex; flex-direction:column; gap:6px; margin-bottom:18px; }\n.smartd-support-package-viewer #reportViewer .rv-row { display:flex; justify-content:space-between; gap:12px; font-size:13px; padding:8px 12px; background:var(--surface); border:1px solid var(--border); border-radius:8px; }\n.smartd-support-package-viewer #reportViewer .rv-row span { color:var(--text-dim); }\n.smartd-support-package-viewer #reportViewer .rv-row b { color:var(--text); font-weight:600; }\n\/* ===== SD Tools Suite: scoped Config Editor (from Configurator 1.1) ===== *\/\n#ceRoot{\n  \/* SmartD 2026 brand palette \u2014 matched to the Support Package Viewer *\/\n  --g:#4FDA6E;          \/* mint-green accent *\/\n  --g-dim:#163F2D;      \/* deep green (hover \/ dim) *\/\n  --mint:#ACFFC9;       \/* light mint (buttons, titles) *\/\n  --rich:#101010;       \/* near-black *\/\n  --charcoal:#1B1B1B;\n  --dark-green:#163F2D; \/* readable green on light backgrounds *\/\n  --w:#fff;\n  --bg:#f4f7f5;         \/* soft green-neutral page *\/\n  --b:#d8e2dc;          \/* border *\/\n  --t:#1B1B1B;          \/* text *\/\n  --m:#5f6b63;          \/* muted text *\/\n  --blk:#101010;\n  --dk:#1B1B1B;\n  --gr:#747474;\n  --lg:#A6A6A6;\n  --gl:#F0FFF5;\n  --gm:#DDFBE7;\n  --font-ui:\"Plus Jakarta Sans\",Arial,sans-serif;\n  --font-heading:\"Sora\",\"Plus Jakarta Sans\",Arial,sans-serif;\n  --font-mono:\"SFMono-Regular\",Consolas,\"Liberation Mono\",Menlo,monospace;\n}#ceRoot \/* Header-scoped button overrides: outlined ghost look that reads correctly on black *\/\nheader button{background:transparent;color:rgba(255,255,255,.76);border:1px solid rgba(255,255,255,.26);padding:7px 15px;border-radius:999px;font-weight:600;font-size:13px;cursor:pointer;transition:background .15s,border-color .15s,color .15s}#ceRoot button{padding:8px 14px;border-radius:999px;border:1px solid var(--b);background:var(--w);color:var(--t);cursor:pointer;font-weight:600;transition:background .2s,border-color .2s}#ceRoot button.primary{background:var(--g);border:none;color:var(--rich)}#ceRoot button:hover{background:#00ff00}#ceRoot button.primary:hover{filter:brightness(1.05)}#ceRoot button.danger{border-color:#dc2626;color:#dc2626}#ceRoot button.danger:hover{background:#fee2e2}#ceRoot button.accent{border-color:var(--dark-green);color:var(--dark-green)}#ceRoot button.accent:hover{background:#f0fff5}#ceRoot .disclaimer{background:#f2f2f2;border-radius:10px;padding:12px;border:1px solid var(--b);color:var(--m)}#ceRoot .editor{flex:1;padding:16px;background:var(--bg);display:grid;gap:14px;border:1px solid var(--b);border-radius:12px;box-shadow:0 1px 2px rgba(0,0,0,.05)}#ceRoot .group{border:1px solid var(--b);border-radius:12px;background:#fff;overflow:hidden}#ceRoot .group>.head{display:flex;align-items:center;gap:12px;padding:12px 14px;border-bottom:1px solid var(--b);background:#fff;cursor:pointer;user-select:none}#ceRoot .group>.head h3{margin:0;font-size:15px;color:var(--dark-green);font-family:var(--font-heading);font-weight:700}#ceRoot .chev{width:18px;height:18px;transition:transform .18s;flex-shrink:0}#ceRoot .group.closed .chev{transform:rotate(-90deg)}#ceRoot .group>.body{padding:12px 14px;display:grid;gap:8px}#ceRoot .group.closed>.body{display:none}#ceRoot .subhead{margin:6px 0 2px;font-size:13px;color:var(--dark-green);font-weight:700;text-transform:uppercase;letter-spacing:.06em}#ceRoot .indent .field, #ceRoot .indent .subhead{margin-left:3ch}#ceRoot .field{display:grid;grid-template-columns:25fr 76fr;gap:12px;align-items:center}#ceRoot .field label{color:var(--t);font-weight:600;word-break:break-word;display:flex;align-items:center;flex-wrap:wrap;gap:2px}#ceRoot input, #ceRoot select, #ceRoot textarea{width:100%;max-width:100%;box-sizing:border-box;padding:8px 10px;border-radius:8px;border:1px solid var(--b);background:#fff;color:var(--t);font:inherit}#ceRoot textarea{min-height:80px;resize:vertical}#ceRoot .readonly{background:#f5f5f5;color:#555}#ceRoot .help-icon{display:inline-flex;width:18px;height:18px;border-radius:999px;background:var(--g);color:var(--rich);align-items:center;justify-content:center;font-size:11px;font-weight:700;cursor:pointer;flex-shrink:0;margin-left:5px}#ceRoot .help-icon:hover{opacity:.85}#ceRoot .card{border:1px solid var(--b);border-radius:8px;padding:10px 12px;background:#fafafa;display:grid;gap:8px;margin-bottom:4px}#ceRoot .card-header{display:flex;align-items:center;gap:8px}#ceRoot .card-header .card-title{font-weight:600;flex:1;font-size:13px}#ceRoot .card-body{display:grid;gap:8px;padding-top:8px;border-top:1px solid var(--b);margin-top:4px}#ceRoot .card.closed .card-body{display:none}#ceRoot .badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.04em;margin-left:4px}#ceRoot .badge-critical{background:#fee2e2;color:#991b1b}#ceRoot .badge-major{background:#ffedd5;color:#9a3412}#ceRoot .badge-medium{background:#fef9c3;color:#854d0e}#ceRoot .badge-minor{background:#dbeafe;color:#1e40af}#ceRoot .badge-oos{background:#f3f4f6;color:#6b7280}#ceRoot .list-builder{display:grid;gap:6px}#ceRoot .list-item{display:flex;align-items:center;gap:8px}#ceRoot .list-item input{flex:1}#ceRoot .help-drawer-overlay{position:fixed;inset:0;background:rgba(0,0,0,.25);opacity:0;pointer-events:none;transition:opacity .2s;z-index:9998}#ceRoot .help-drawer{position:fixed;top:0;right:0;height:100vh;width:min(520px,45vw);background:#fff;box-shadow:-8px 0 24px rgba(0,0,0,.2);transform:translateX(100%);transition:transform .25s;z-index:9999;display:flex;flex-direction:column;border-left:1px solid #e5e7eb}#ceRoot .help-drawer.open{transform:translateX(0)}#ceRoot .help-drawer-overlay.open{opacity:1;pointer-events:auto}#ceRoot .help-drawer-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.5rem .75rem;border-bottom:1px solid #e5e7eb;background:#f9fafb}#ceRoot .help-drawer-title{font-weight:600;font-size:.95rem;color:#111827}#ceRoot .help-drawer-close{border:1px solid #e5e7eb;background:#fff;border-radius:.5rem;padding:.25rem .5rem;cursor:pointer;font-size:1.1rem;line-height:1}#ceRoot .help-drawer-body{flex:1;overflow-y:auto}#ceRoot #ceHdContent{padding:.75rem .9rem;line-height:1.5;font-size:.9rem;color:#1f2937}#ceRoot #ceHdContent a{color:var(--g)}#ceRoot .overlay{position:fixed;inset:0;background:rgba(0,0,0,.45);display:none;align-items:center;justify-content:center;z-index:1000}#ceRoot .overlay.open{display:flex}#ceRoot .modal{background:#fff;border-radius:12px;display:flex;flex-direction:column;box-shadow:0 10px 40px rgba(0,0,0,.3);overflow:hidden;max-height:90vh}#ceRoot .modal-hd{background:var(--g);color:var(--rich);padding:12px 18px;display:flex;align-items:center;justify-content:space-between;flex:0 0 auto}#ceRoot .modal-hd-title{font-weight:800;font-size:1rem;font-family:var(--font-heading)}#ceRoot .modal-hd button{border:1px solid rgba(255,255,255,.5);background:rgba(255,255,255,.15);color:#fff;border-radius:8px;padding:4px 12px;cursor:pointer;font-weight:600}#ceRoot .modal-hd button:hover{background:rgba(255,255,255,.28)}#ceRoot .modal-hd button.apply{background:#fff;color:var(--dark-green)}#ceRoot .modal-bd{flex:1;overflow-y:auto;padding:18px 20px;background:var(--bg)}#ceRoot .app-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px}#ceRoot .app-card{border:2px solid var(--b);border-radius:10px;padding:14px 12px;cursor:pointer;background:#fff;transition:border-color .15s,background .15s,box-shadow .15s;display:flex;flex-direction:column;gap:5px}#ceRoot .app-card:hover{border-color:var(--g);box-shadow:0 2px 10px rgba(79,218,110,.18)}#ceRoot .app-card.selected{border-color:var(--g);background:#f0fdf4}#ceRoot .app-card .icon{font-size:26px}#ceRoot .app-card .name{font-weight:700;font-size:13px}#ceRoot .app-card.selected .name{color:var(--g)}#ceRoot .app-card .desc{font-size:11px;color:var(--m);line-height:1.4}#ceRoot .app-card .tag{font-size:10px;font-weight:700;margin-top:2px}#ceRoot .field.hl select, #ceRoot .field.hl input{border-color:var(--g);background:#f0fdf4;box-shadow:0 0 0 2px rgba(79,218,110,.20)}#ceRoot .field.hl label{color:var(--dark-green)}#ceRoot .hl-dot{width:8px;height:8px;border-radius:50%;background:var(--g);display:inline-block;margin-left:5px;flex-shrink:0}#ceRoot .preset-banner{display:flex;align-items:center;gap:10px;background:#f0fdf4;border:1px solid #86efac;border-radius:8px;padding:10px 14px;font-size:12px;color:#166534}#ceRoot .preset-banner .clr{margin-left:auto;font-size:11px;color:#dc2626;cursor:pointer;text-decoration:underline;white-space:nowrap}@media(max-width:900px){#ceRoot .field{grid-template-columns:1fr}}@media(max-width:600px){#ceRoot .group>.head, #ceRoot .group>.body{padding:8px 12px}#ceRoot .editor{padding:10px}}\n#ceRoot{font-family:var(--font-ui);color:var(--t);font-size:14px;padding:16px 18px}\n#ceRoot .ce-toolbar{display:flex;gap:8px;flex-wrap:wrap;justify-content:flex-start;margin:0 0 12px}\n#ceRoot .editor{margin-top:4px}\n\n\/* ===== SD Tools Suite: landing \/ home screen ===== *\/\n.smartd-support-package-viewer .empty-state-shell { align-items: flex-start; justify-content: center; overflow: auto; padding: 28px 24px; }\n.smartd-support-package-viewer .landing { width: 100%; max-width: none; margin: 0; }\n.smartd-support-package-viewer .landing .drop-zone-shell { max-width: none; width: 100%; display: flex; align-items: center; gap: 30px; padding: 26px 32px; text-align: left; }\n.smartd-support-package-viewer .dz-main { flex: 1; text-align: center; }\n.smartd-support-package-viewer .dz-main .drop-icon-shell { width: 52px; height: 52px; margin: 0 auto 10px; color: #2f78d6; opacity: 1; }\n.smartd-support-package-viewer .dz-main h2 { font-family: \"Sora\", var(--font-ui); font-size: 20px; margin: 0 0 6px; color: var(--text); font-weight: 600; }\n.smartd-support-package-viewer .dz-supported { margin: 0 0 10px; font-size: 12.5px; color: var(--text-dim); }\n.smartd-support-package-viewer .dz-browse { color: var(--accent); font-weight: 600; font-size: 13.5px; cursor: pointer; text-decoration: none; }\n.smartd-support-package-viewer .dz-browse:hover { text-decoration: underline; }\n.smartd-support-package-viewer .dz-legend { display: flex; flex-direction: column; gap: 15px; padding-left: 30px; border-left: 1px solid var(--border); min-width: 224px; }\n.smartd-support-package-viewer .dz-leg-item { display: flex; align-items: center; gap: 11px; }\n.smartd-support-package-viewer .dz-leg-ico { width: 30px; height: 30px; display: inline-flex; align-items: center; justify-content: center; flex: none; }\n.smartd-support-package-viewer .dz-leg-ico svg { width: 26px; height: 26px; }\n.smartd-support-package-viewer .dz-leg-ico.cfg { color: #16a34a; }\n.smartd-support-package-viewer .dz-leg-ico.aud { color: #2563eb; }\n.smartd-support-package-viewer .dz-leg-ico.pkg { color: #7c3aed; }\n.smartd-support-package-viewer .dz-leg-txt b { display: block; font-size: 12.5px; color: var(--text); font-weight: 600; }\n.smartd-support-package-viewer .dz-leg-txt small { font-size: 11px; color: var(--text-dim); }\n.smartd-support-package-viewer .landing-cards { margin-top: 22px; display: grid; gap: 14px; grid-template-columns: repeat(5, minmax(0,1fr)); }\n@media (max-width: 1080px){ .smartd-support-package-viewer .landing-cards { grid-template-columns: repeat(3, minmax(0,1fr)); } }\n@media (max-width: 720px) { .smartd-support-package-viewer .landing-cards { grid-template-columns: repeat(2, minmax(0,1fr)); } }\n@media (max-width: 480px) { .smartd-support-package-viewer .landing-cards { grid-template-columns: 1fr; } }\n\/* Full-width responsive landing \u2014 drop zone + cards adapt to device (PC \/ laptop \/ tablet \/ mobile) *\/\n@media (max-width: 900px) {\n  .smartd-support-package-viewer .empty-state-shell { padding: 20px 16px; }\n  .smartd-support-package-viewer .landing .drop-zone-shell { flex-direction: column; align-items: stretch; gap: 18px; padding: 22px 20px; }\n  .smartd-support-package-viewer .dz-legend { flex-direction: row; flex-wrap: wrap; justify-content: center; gap: 14px 22px; padding-left: 0; padding-top: 16px; border-left: none; border-top: 1px solid var(--border); min-width: 0; }\n}\n@media (max-width: 480px) {\n  .smartd-support-package-viewer .empty-state-shell { padding: 14px 12px; }\n  .smartd-support-package-viewer .landing .drop-zone-shell { padding: 20px 16px; }\n  .smartd-support-package-viewer .dz-legend { flex-direction: column; align-items: flex-start; gap: 12px; }\n  .smartd-support-package-viewer .landing-title { font-size: 15px; }\n  .smartd-support-package-viewer .dz-main h2 { font-size: 18px; }\n}\n.smartd-support-package-viewer .lc-card { --lc:#16a34a; --lc-soft:rgba(22,163,74,0.10); display:flex; flex-direction:column; align-items:center; text-align:center; background:#fff; border:1px solid var(--border); border-top:3px solid var(--lc); border-radius:12px; padding:20px 15px 18px; transition:box-shadow .15s, transform .15s; }\n.smartd-support-package-viewer .lc-card:hover { box-shadow:0 8px 22px rgba(20,40,70,0.10); transform:translateY(-2px); }\n.smartd-support-package-viewer .lc-ico { width:56px; height:56px; border-radius:14px; background:var(--lc-soft); color:var(--lc); display:flex; align-items:center; justify-content:center; margin-bottom:14px; }\n.smartd-support-package-viewer .lc-ico svg { width:30px; height:30px; }\n.smartd-support-package-viewer .lc-card h3 { font-family:\"Sora\", var(--font-ui); font-size:14.5px; line-height:1.28; margin:0 0 8px; color:var(--text); font-weight:600; }\n.smartd-support-package-viewer .lc-card p { font-size:12px; line-height:1.5; color:var(--text-dim); margin:0 0 16px; flex:1; }\n.smartd-support-package-viewer .lc-btn { width:100%; border:none; border-radius:8px; padding:9px 12px; font-size:12.5px; font-weight:600; color:#fff; background:var(--lc); cursor:pointer; transition:filter .15s; }\n.smartd-support-package-viewer .lc-btn:hover { filter:brightness(1.07); }\n.smartd-support-package-viewer .lc-green  { --lc:#22a34a; --lc-soft:rgba(34,163,74,0.10); }\n.smartd-support-package-viewer .lc-blue   { --lc:#2563eb; --lc-soft:rgba(37,99,235,0.10); }\n.smartd-support-package-viewer .lc-teal   { --lc:#0d9488; --lc-soft:rgba(13,148,136,0.10); }\n.smartd-support-package-viewer .lc-orange { --lc:#ea580c; --lc-soft:rgba(234,88,12,0.10); }\n.smartd-support-package-viewer .lc-purple { --lc:#7c3aed; --lc-soft:rgba(124,58,237,0.10); }\n.smartd-support-package-viewer .lc-violet { --lc:#8b5cf6; --lc-soft:rgba(139,92,246,0.10); }\n\n\/* ============================================================================\n   SmartD Industrial Core \u2014 DESIGN.md theme layer  (SD Tools Suite 1.0.9)\n   Appended last so it wins the cascade without touching upstream rules.\n   Strategy per brief: \"Dark Core\" hybrid \u2014 charcoal shell frames light,\n   high-legibility diagnostic workspaces. Light-workspace tokens (borders\n   #D8E2DC, row shading #F1F7F3\/#EEF8F1, status + code-key\/code-num) already\n   match the brief and are intentionally left untouched.\n   ========================================================================== *\/\n.smartd-support-package-viewer {\n  \/* Primary ramp (M3 tonal) *\/\n  --primary:            #6ef787;\n  --on-primary:         #003912;\n  --primary-container:  #4FDA6E;\n  --on-primary-container:#005c22;\n  --inverse-primary:    #006e2a;\n  --surface-tint:       #56e073;\n  --secondary:          #86d8a4;\n  --tertiary:           #bae6cd;\n  --mint:               #ACFFC9;\n\n  \/* Dark-core shell surfaces *\/\n  --background:         #131313;\n  --surface:            #f8f9fb;   \/* light diagnostic-workspace surface (dark shell uses --surface-charcoal\/--background) *\/\n  --surface-charcoal:   #1B1B1B;\n  --surface-container:  #201f1f;\n  --surface-container-high:#2a2a2a;\n  --rich:               #101010;\n  --charcoal:           #1B1B1B;\n  --on-surface-dark:    #e5e2e1;\n  --on-surface-variant-dark:#bccbb9;\n  --outline:            #879584;\n  --outline-variant:    #3d4a3c;\n\n  \/* Keep --g as the actionable Primary Mint Green fill (#4FDA6E, black text) *\/\n  --g:                  #4FDA6E;\n\n  \/* Focus glow \u2014 Primary Mint Green @ 0.15 per brief *\/\n  --accent-glow:        rgba(79,218,110,0.15);\n\n  \/* Syntax tokens pinned to the brief *\/\n  --code-key:           #0550AE;\n  --code-num:           #098658;\n  --key:                #0550AE;\n  --num:                #098658;\n\n  \/* Status ramp pinned to the brief *\/\n  --status-ok:          #16A34A;\n  --status-warn:        #D97706;\n  --status-error:       #DC2626;\n\n  \/* Charcoal backdrop so the 24px shell reads as a framed \"portal\" instrument *\/\n  background: var(--background);\n  padding: clamp(0px, 1.4vw, 16px);\n}\n\n\/* Portal shell: float the app on charcoal; guard height against the new padding\n   so internal scroll regions keep working. Depth via tonal layering + border,\n   not heavy shadow (per brief). *\/\n.smartd-support-package-viewer .app {\n  max-height: calc(100vh - 2 * clamp(0px, 1.4vw, 16px));\n  border: 1px solid var(--outline-variant);\n  box-shadow: 0 18px 50px rgba(0,0,0,0.45);\n}\n\n\/* Top navigation: 2px Primary Mint Green accent border (was pale #ACFFC9) *\/\n.smartd-support-package-viewer .topbar {\n  border-bottom: 2px solid var(--g);\n}\n\n\/* Primary shell buttons: solid mint fill, black text; brighten toward --primary\n   on hover rather than darkening. *\/\n.smartd-support-package-viewer .shell-btn {\n  background: var(--g);\n  color: #000000;\n  border: 1px solid transparent;\n}\n.smartd-support-package-viewer .shell-btn:hover {\n  background: var(--primary);\n  color: #000000;\n}\n\n\/* Ghost buttons: no fill, 1px mint outline (brief spec) *\/\n.smartd-support-package-viewer .shell-btn.ghost {\n  background: transparent;\n  color: rgba(255,255,255,0.82);\n  border: 1px solid rgba(172,255,201,0.55);\n}\n.smartd-support-package-viewer .shell-btn.ghost:hover {\n  color: #ffffff;\n  border-color: var(--mint);\n  background: rgba(172,255,201,0.10);\n}\n\n\/* Active tab: rich-black seat with a Primary Mint Green underline *\/\n.smartd-support-package-viewer .shell-tab.active {\n  background: var(--rich);\n  border-bottom-color: var(--g);\n}\n\n\/* Toasts: deep charcoal with a primary accent left-border (brief spec) *\/\n.smartd-support-package-viewer .toast {\n  background: var(--surface-charcoal);\n  color: var(--on-surface-dark);\n  border-left: 3px solid var(--g);\n  box-shadow: 0 18px 50px rgba(0,0,0,0.35);\n}\n.smartd-support-package-viewer .toast-error { border-left-color: var(--status-error); }\n.smartd-support-package-viewer .toast-info  { border-left-color: var(--g); }\n\n\/* Reduced-motion floor *\/\n@media (prefers-reduced-motion: reduce) {\n  .smartd-support-package-viewer .shell-tab,\n  .smartd-support-package-viewer .shell-btn,\n  .smartd-support-package-viewer .lc-card,\n  .smartd-support-package-viewer .toast { transition: none !important; }\n}\n<\/style>\n\n<style id=\"sd-v107-nav\">\n\/* ===== SD Tools Suite 1.0.9 \u2014 top nav bar, support-package sub-menu, home screen ===== *\/\n.smartd-support-package-viewer .app { background: var(--surface-light, #fff); }\n\/* Home replaces the old Close button; keep #unloadBtn in DOM but hidden. *\/\n.smartd-support-package-viewer #unloadBtn { display: none !important; }\n\n\/* ---- Top navigation bar (dark) ---- *\/\n.smartd-support-package-viewer .topnav {\n  display: flex; align-items: center; gap: 4px; flex-shrink: 0;\n  height: 60px; padding: 0 18px;\n  background: var(--rich, #101010);\n  border-bottom: 2px solid var(--g);\n}\n.smartd-support-package-viewer .tn-brand {\n  font-family: \"Sora\", var(--font-ui); font-weight: 800; font-size: 20px;\n  letter-spacing: .04em; color: var(--primary, #6ef787);\n  padding-right: 16px; white-space: nowrap;\n}\n.smartd-support-package-viewer .tn-menu {\n  display: flex; align-items: stretch; gap: 0; height: 100%;\n  overflow-x: auto; scrollbar-width: none;\n}\n.smartd-support-package-viewer .tn-menu::-webkit-scrollbar { display: none; }\n.smartd-support-package-viewer .tn-item {\n  display: inline-flex; align-items: center; gap: 8px; height: 100%;\n  padding: 0 15px; border: none; background: transparent; cursor: pointer;\n  color: rgba(255,255,255,0.68);\n  font-family: \"Sora\", var(--font-ui); font-weight: 700; font-size: 12px;\n  letter-spacing: .05em; text-transform: uppercase; white-space: nowrap;\n  border-bottom: 2px solid transparent; margin-bottom: -2px;\n  transition: color .12s, border-color .12s;\n}\n.smartd-support-package-viewer .tn-item:hover { color: #fff; }\n.smartd-support-package-viewer .tn-item.active { color: var(--primary,#6ef787); border-bottom-color: var(--primary,#6ef787); }\n.smartd-support-package-viewer .tn-item:focus-visible { outline: 2px solid var(--g); outline-offset: -4px; }\n.smartd-support-package-viewer .tn-ico { width: 17px; height: 17px; flex-shrink: 0; }\n.smartd-support-package-viewer .tn-spacer { flex: 1 1 auto; min-width: 8px; }\n.smartd-support-package-viewer .topnav .pkg-info { color: rgba(255,255,255,0.68); max-width: 32vw; }\n.smartd-support-package-viewer .topnav .pkg-info .item strong { color: #fff; }\n.smartd-support-package-viewer .tn-avatar {\n  width: 38px; height: 38px; border-radius: 50%; flex-shrink: 0; margin-left: 10px;\n  background: var(--g); color: var(--rich,#101010);\n  display: flex; align-items: center; justify-content: center;\n  font-family: \"Sora\", var(--font-ui); font-weight: 800; font-size: 13px;\n}\n\n\/* ---- Support-package sub navigation ---- *\/\n.smartd-support-package-viewer .subnav {\n  display: none; align-items: stretch; gap: 0; flex-shrink: 0;\n  height: 44px; padding: 0 18px;\n  background: var(--surface-charcoal,#1B1B1B);\n  border-bottom: 1px solid var(--outline-variant,#3d4a3c);\n}\n.smartd-support-package-viewer .sn-item {\n  display: inline-flex; align-items: center; height: 100%; padding: 0 16px;\n  border: none; background: transparent; cursor: pointer;\n  color: rgba(255,255,255,0.62);\n  font-family: \"Sora\", var(--font-ui); font-weight: 700; font-size: 11.5px;\n  letter-spacing: .06em; text-transform: uppercase; white-space: nowrap;\n  border-bottom: 2px solid transparent; margin-bottom: -1px;\n  transition: color .12s, border-color .12s;\n}\n.smartd-support-package-viewer .sn-item:hover { color: #fff; }\n.smartd-support-package-viewer .sn-item.active { color: var(--primary,#6ef787); border-bottom-color: var(--primary,#6ef787); }\n.smartd-support-package-viewer .sn-item.sn-missing { opacity: .35; cursor: not-allowed; }\n\n\/* ---- Content: white workspace ---- *\/\n.smartd-support-package-viewer .content { background: var(--surface-light,#fff); }\n\n\/* ---- Home \/ empty state (dark instrument panel) ---- *\/\n.smartd-support-package-viewer #emptyState {\n  flex: 1 1 auto; display: flex; flex-direction: column;\n  align-items: stretch; justify-content: flex-start; gap: 0;\n  padding: 40px 44px 46px; overflow: auto;\n  background-color: var(--background,#131313);\n  background-image:\n    radial-gradient(120% 62% at 16% -12%, rgba(79,218,110,0.12), transparent 60%),\n    linear-gradient(rgba(255,255,255,0.028) 1px, transparent 1px),\n    linear-gradient(90deg, rgba(255,255,255,0.028) 1px, transparent 1px);\n  background-size: auto, 26px 26px, 26px 26px;\n}\n.smartd-support-package-viewer .home-hero { max-width: 1100px; margin: 2px 0 26px; }\n.smartd-support-package-viewer .home-title {\n  font-family: \"Sora\", var(--font-ui); font-weight: 800;\n  font-size: clamp(26px, 3.2vw, 40px); letter-spacing: -0.01em; line-height: 1.08;\n  margin: 0 0 12px; color: #f4f6f4;\n}\n.smartd-support-package-viewer .home-sub {\n  margin: 0; max-width: 720px; color: var(--on-surface-variant-dark, #bccbb9);\n  font-family: var(--font-ui); font-size: 14.5px; line-height: 1.5;\n}\n.smartd-support-package-viewer #emptyState .drop-zone-shell {\n  flex: 1 1 auto; width: 100%; max-width: none; align-self: stretch; min-height: 300px; display: flex; align-items: center; justify-content: center;\n  border: 1.5px dashed rgba(172,255,201,0.30); border-radius: 16px;\n  background: rgba(0,0,0,0.18); padding: 32px;\n  transition: border-color .15s, background .15s;\n}\n.smartd-support-package-viewer #emptyState .drop-zone-shell.dragging {\n  border-color: var(--g); background: rgba(79,218,110,0.09);\n}\n.smartd-support-package-viewer #emptyState .dz-main {\n  display: flex; flex-direction: column; align-items: center; text-align: center;\n  gap: 4px; width: 100%; cursor: pointer; border: none; background: transparent;\n}\n.smartd-support-package-viewer #emptyState .dz-main:focus-visible { outline: 2px solid var(--g); outline-offset: 8px; border-radius: 12px; }\n.smartd-support-package-viewer #emptyState .dz-iconwrap {\n  width: 74px; height: 74px; border-radius: 16px; margin-bottom: 18px;\n  display: flex; align-items: center; justify-content: center;\n  background: var(--surface-container,#201f1f);\n  border: 1px solid var(--outline-variant,#3d4a3c); color: #e5e2e1;\n}\n.smartd-support-package-viewer #emptyState .dz-iconwrap svg { width: 30px; height: 30px; }\n.smartd-support-package-viewer #emptyState .dz-main h2 {\n  font-family: \"Sora\", var(--font-ui); font-weight: 700; font-size: 20px; color: #f4f6f4; margin: 0 0 6px;\n}\n.smartd-support-package-viewer #emptyState .dz-supported {\n  margin: 0; color: var(--on-surface-variant-dark,#bccbb9); font-size: 13px;\n}\n\n@media (max-width: 900px) {\n  .smartd-support-package-viewer .topnav { height: auto; flex-wrap: wrap; padding: 8px 12px; }\n  .smartd-support-package-viewer .tn-brand { padding-right: 10px; font-size: 18px; }\n  .smartd-support-package-viewer .tn-menu { order: 3; width: 100%; height: 46px; }\n  .smartd-support-package-viewer .tn-item { padding: 0 11px; font-size: 11px; }\n  .smartd-support-package-viewer #emptyState { padding: 24px 18px 30px; }\n}\n\n\/* ---- Edit Config: thin drag & drop \/ upload strip ---- *\/\n.smartd-support-package-viewer #editorViewer .ce-dropbar {\n  display: flex; align-items: center; justify-content: center; gap: 10px;\n  margin: 0 0 12px; padding: 9px 14px;\n  border: 1.5px dashed var(--border); border-radius: 8px;\n  background: var(--bg-elev); color: var(--text-dim);\n  font-family: var(--font-ui); font-size: 12.5px; line-height: 1.2;\n  cursor: pointer; text-align: center;\n  transition: border-color .15s, background .15s, color .15s;\n}\n.smartd-support-package-viewer #editorViewer .ce-dropbar:hover,\n.smartd-support-package-viewer #editorViewer .ce-dropbar:focus-visible { border-color: var(--g); background: var(--gl); color: var(--text); outline: none; }\n.smartd-support-package-viewer #editorViewer .ce-dropbar.dragging { border-color: var(--g); background: var(--gl); color: var(--text); }\n.smartd-support-package-viewer #editorViewer .ce-dropbar svg { width: 17px; height: 17px; flex-shrink: 0; color: var(--g); }\n.smartd-support-package-viewer #editorViewer .ce-dropbar b { color: var(--text); font-family: var(--font-mono); font-weight: 700; }\n\".smartd-support-package-viewer #editorViewer .ce-dropbar-link { color: var(--accent); font-weight: 700; text-decoration: underline; }\n\n\/* ---- Brand logo + language toggle ---- *\/\n.smartd-support-package-viewer .tn-brand { display: flex; align-items: center; }\n.smartd-support-package-viewer .tn-logo { height: 30px; width: auto; display: block; }\n.smartd-support-package-viewer .tn-avatar { cursor: pointer; user-select: none; transition: filter .12s, box-shadow .12s; }\n.smartd-support-package-viewer .tn-avatar:hover { filter: brightness(1.08); }\n.smartd-support-package-viewer .tn-avatar:focus-visible { outline: none; box-shadow: 0 0 0 3px rgba(110,247,135,0.45); }\n@media (max-width: 900px) { .smartd-support-package-viewer .tn-logo { height: 26px; } }\n\n\/* ---- Support-package second menu: SmartD green band ---- *\/\n.smartd-support-package-viewer .subnav {\n  background: linear-gradient(180deg, #58e07a 0%, #4FDA6E 100%);\n  border-top: 1px solid rgba(255,255,255,0.30);\n  border-bottom: 1px solid #2fae55;\n  box-shadow: inset 0 1px 0 rgba(255,255,255,0.35), 0 1px 3px rgba(16,40,25,0.12);\n}\n.smartd-support-package-viewer .sn-item { color: rgba(16,16,16,0.66); }\n.smartd-support-package-viewer .sn-item:hover { color: #0a0a0a; background: rgba(255,255,255,0.20); }\n.smartd-support-package-viewer .sn-item.active {\n  color: #0a1f10; background: rgba(255,255,255,0.32);\n  border-bottom-color: #0e3d22;\n}\n.smartd-support-package-viewer .sn-item.sn-missing { color: rgba(16,16,16,0.38); opacity: .7; }\n\n\/* \u22ef raw-files button in the support-package sub-menu *\/\n.smartd-support-package-viewer .sn-more{ margin-left:auto; align-self:center; border:0; background:transparent; cursor:pointer; font-size:20px; line-height:1; font-weight:700; color:#0a1f10; padding:0 14px; height:100%; opacity:.75; transition:opacity .12s, background .12s; }\n.smartd-support-package-viewer .sn-more:hover{ opacity:1; background:rgba(255,255,255,0.22); }\n\n\/* Raw-files popup *\/\n.smartd-support-package-viewer .rawfx-overlay{ position:fixed; inset:0; z-index:4000; background:rgba(10,20,14,0.55); display:flex; align-items:center; justify-content:center; padding:24px; }\n.smartd-support-package-viewer .rawfx-modal{ width:min(940px,96vw); height:min(80vh,760px); background:#ffffff; border-radius:12px; overflow:hidden; display:flex; flex-direction:column; box-shadow:0 20px 60px rgba(0,0,0,0.45); font-family:var(--font-ui,Arial,sans-serif); }\n.smartd-support-package-viewer .rawfx-head{ display:flex; align-items:center; gap:12px; padding:12px 16px; background:#101010; color:#fff; flex-shrink:0; }\n.smartd-support-package-viewer .rawfx-title{ font-weight:700; font-size:14px; flex:1; }\n.smartd-support-package-viewer .rawfx-close{ border:0; background:transparent; color:#fff; font-size:22px; line-height:1; cursor:pointer; padding:0 6px; opacity:.8; }\n.smartd-support-package-viewer .rawfx-close:hover{ opacity:1; }\n.smartd-support-package-viewer .rawfx-body{ display:flex; flex:1; min-height:0; }\n.smartd-support-package-viewer .rawfx-list{ width:240px; flex-shrink:0; overflow:auto; border-right:1px solid #e2e8e6; background:#f6f8f7; padding:6px; }\n.smartd-support-package-viewer .rawfx-item{ display:block; width:100%; text-align:left; border:0; background:transparent; padding:7px 9px; border-radius:6px; cursor:pointer; font-family:var(--font-mono,monospace); font-size:12px; color:#1b1b1b; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }\n.smartd-support-package-viewer .rawfx-item:hover{ background:#e8f7ee; }\n.smartd-support-package-viewer .rawfx-item.active{ background:#02AF4C; color:#fff; }\n.smartd-support-package-viewer .rawfx-empty{ padding:12px; color:#5f6b63; font-size:13px; }\n.smartd-support-package-viewer .rawfx-view{ flex:1; min-width:0; display:flex; flex-direction:column; }\n.smartd-support-package-viewer .rawfx-view-head{ display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid #e2e8e6; flex-shrink:0; }\n.smartd-support-package-viewer .rawfx-fname{ font-family:var(--font-mono,monospace); font-weight:700; font-size:12.5px; color:#111827; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }\n.smartd-support-package-viewer .rawfx-fsize{ font-size:11px; color:#5f6b63; white-space:nowrap; }\n.smartd-support-package-viewer .rawfx-pre{ flex:1; margin:0; overflow:auto; padding:12px 14px; font-family:var(--font-mono,monospace); font-size:12px; line-height:1.5; color:#1b1b1b; background:#fbfdfc; white-space:pre; tab-size:2; }\n.smartd-support-package-viewer .rawfx-item.rawfx-nodisp{ color:#8a978f; }\n.smartd-support-package-viewer .rawfx-item.rawfx-nodisp.active{ color:#ffffff; }\n.smartd-support-package-viewer .rawfx-pre.rawfx-pre-msg{ color:#5f6b63; font-style:italic; font-family:var(--font-ui,Arial,sans-serif); white-space:normal; }\n\n\/* ---- Full-bleed layout: inner app fills the entire viewport (no card framing) ---- *\/\nhtml, body { margin: 0; padding: 0; height: 100%; }\n.smartd-support-package-viewer { padding: 0; width: 100%; height: 100vh; min-height: 100vh; display: block; }\n.smartd-support-package-viewer .app { width: 100%; height: 100vh; max-height: none; min-height: 0; border: 0; border-radius: 0; box-shadow: none; overflow: hidden; }\n<\/style>\n\n<div class=\"smartd-support-package-viewer\" id=\"smartd-support-package-analyzer\">\n\n<div class=\"app\">\n  <header class=\"topnav\" id=\"topNav\">\n    <div class=\"tn-brand\"><img decoding=\"async\" class=\"tn-logo\" src=\"data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAABBCAYAAABrYJlFAAABSWlDQ1BJQ0MgUHJvZmlsZQAAeJx9kM9KAlEYxY9mKSEU0bJg2rQyKZOgpRpY5GKaCqrdONoYjHa7MxFBtOoBegF3LXuAapX7NlEEQdC+bSBByXSuU2l\/6MLH+XHuN3MPBwjHTCGcCIBqzZNGPqutrW9o0SfE0IcBjCFsWq7I6HqBK\/jU76d5h5DSmwn1r9\/3\/57+Utm1qG+cpCWkB4QSZH3PE4oPyMOSocjHiu2ATxQXAz5r76wYOfIVedCqmCXyAzlR7PLtLq46u9ZHBpU+Xq6tLlOHOCMoIA8N85iDQU1jkf38vZ9u7+ewDYF9SGzBRgUev8rQEXBQJi+gBgtJJMgpTHJmVM8\/++t4h7fA7CPhsuMtXQPnR4x+2vHGpxilDjQawpTmV6uhZsTdnE4FHJdA74vvP48C0QugJX3\/te77LXbYc88ndt4Bi5lb52hZSaYAACOaSURBVHja7X17fJTFuf\/3mXl3NxcSyIVAuEiBCiEKgsEq\/Kwk1h+kotRad9EerPSgRK3Uy8H+bG3drLXWlqLH1oKg9qi1IknVVmpb6xESflqFQrUoCqGWm4JyJyEh2X1nnvPH++5mr8nmCp7P+\/18AnubeWeeeea5zTMzgAMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHpw7U2\/XV1NSIwYMHEwCUl5f3d3+YiFSyL\/x+vygvLxenitDl5eVcXV3NgUBA90Z9fr9fVFdXU11dHZ1ubT6VtE63zcxMdXV18nSdmOXl5YqIOExPlEPUAejNGVWHurR\/W3SwiGt9tRoAd\/S7mpoaOXjwYKqrq0N5eTnq6up0b\/F8r6KmpkYSERx0jHXr1hl+v7\/bk9nv9wtmlp+lNn8W6ewguXnjrfHKDpSA6GvDqDcqImaGrQ0GPv744xcCuOW8886joqIiDB06tM\/pqLVmIQRprT+WUi5gZgprJ6\/XK2tra9XcuXNnzp079w4ppVashIBFWw2gS1wdVyBVeQ1AEqGpqQm79uw6efnMygfnzpv36bZt2xpswWMEAgGzS5PQ7zcq7DLz58+f+O1vf3vkq6++ukgpRQSCIAHTDGJI8TAUFQ2GqU2IZK1L2QfrlVIKOz\/c2Xz+tPMfXbJkyZ6XXnppe1gp+Xw+1YEwNQKBgHnLLbd8Y+bMmV8HYGqtje7SNt1CRBKNjU3Yt+\/j1iuuuOJBr9f7yZYtWxrCgquioiJC5zA\/zJs3b\/rll19+T3ZmpgqapkRXOaJbbe2Uj2EYhmo92Sr\/+Kc\/\/ujJJ5\/8\/wBwwYLpt0+9vWyWUqygtOxec7h9uhOgTUbz\/mawqRMLRlemgWP\/PNroynU99Lsbf\/cBgGP+dX4jUBFQ0daW3+8XgUBAP\/HEym+cffbE66Qw+NMDB44\/+vNH71nzypqt4e97SiOjJ4WZmQCAiHjJkiW3z549+6Zx48adKWX\/WtpCiPD\/++O\/y8vLEwBUQUHBrK9+9auzTqF+uuLFF19s2bJly6urV6++IxAI\/IuZZSoXNgmtJRGZCxcunPb1r3+9etSoUV\/63Oc+J6dMmdKXbb5y+PDhJ++6665nHnrooaU+n297R20eNmwYAcBZZ008+\/LLLz9VtP7K6tU1LTt3\/uu\/V69e7a+oqHiHmQURaQAoLS0lABg7duwIn893KvmhQ2zbvu3pCH8D52XkZMwKtoVARAARwGzJH44zPzjRJCFuF1nWZ1b5jEEZMXIs8oZjCxVOHIxQY9B79W+u+fCj9Xt\/EKgIrCIiMHPEsiIi\/bvf\/e6qkSNHPsXMT5w4ceKoy2V8adF\/LPo9eWhKdXX1iUAgQJ25lH0psMiSVcQbNmx4eurUqdfagkOlpZV0+2gk\/EiL1GYLor7WdoUiUvOxDtp70m5bSGvtCgu5\/jSoS0pKskpKSr5y8cUXf6GsrOwRIro\/DaEVtmDVkiVLfvLNb37zpoKCghx74FVfN3r06NGZo0ePviE\/P\/8bXq\/XR0QvrVixwlVVVRVKyVQGWsK0BuDqfzqPzyopGT9n0jnnzLz44ovvI6IfxVtaUspg04kmlTMgx9RaG6eAH5JaWEIIs7m52QAQbGcAam5tbFNKKROajVjpRMmtqIhcsH8XEUKU+F1UHWy\/p7j6SBKKvjB0bN74\/GdHfmlk8SrfquULVyw0V1atNKWUGoBRUFj4vfXr1788ffr0q0tLS7F3795nGxubym5ffPtXiOiZrijo3hZYZEtV1dDQ8OyZZ555jS0IJAkhKRn9bOEU+Sglf0QJq4TyDAa1FxUULTwFgI5Mu\/D3mohkmsZzYhuSf5RWZbbrqgoLC4tvvfW2H2VkZGUQ0T0duYd+v18Skbj33nuXLVq0aIHH44HWWgGQQgjZ1Tak+ijV+3Cbx48f7y4uLn7hL3\/5i2\/mzJkvdOQeat1Oa4ClNVk6en7Uu4S2hrV++uMQbvPwYcMyrr322vuGDB6SUVFR8YN169YZdXV1Ee+AQBLWQo3slCa9yAftooFiaGDHgJmIpBDtzM3EggjSqoVku5yhlM+JrZtso4qiLCy0W1HgGOFFYfkW3TpmtB1v1UaGSxVNHrr0yieuKl65YOWdtntoVlZWypzcAcahQwdez8rKnN3U1ASlQvT225uV2+25HsAzvbFA1C2BVVNTI4hIPf\/8iyvCwoqZbaslFYNFS+70B7i9mmSSn7rINun9luL8\/XiNRZ1WmZwGQhABMLRSOjs7y7zxxoU\/2Lfvo08CgcCyeAsg2g30+\/1XLV68eIHH4wkqpVxSSpnasuZEuiSZdfF9iOf9MA3siWMopXRubi6dc845q++6666vzZ0796VoVys1xSmu7mR8ENVeSh5mbZd5KfqARDorpVhKaX7p\/17y\/QcffLC5oqLigffee88dCASSki0VDbrHy4lCODkvR\/dTp6BBBxHniOChmLIxyoAJTNwuqNolYdj8iLwnSt0EIYVQrSaBOTTs\/GGLK++r\/DhQEfhPmw9Cd3\/vblVQMPgbGzf+rSkUCtWXlk4omjRp0qC2tuBSADh48CD3VGB12Q72er3S6\/XqW2+9dVZ5+UUL7cCqy9IOySZJN+P7CdVQirq6UC9T1x6e6AHHCa9Uj++YBkJKoZUysrKy9I033nT37AsvzDt48CAzc3zH9MCBAwd5vd6fZWZmKqWUIaWkjvtNnfShk485uRSWUgqtNRcVFRmzZ89+ND8\/PycdDUBJBFh31n9i+IDTGz8pJSmlDMOQ6oorrggsWLBg0llnnRVK7\/E95WVKk5cTyxAn\/45SDDMlBK\/iAlsULZziv45ScBQ1\/pyMHgBJItVmGiTAhWcPfmj89PHj7WHVH+\/7+CeXzb5sQlNT078mnH32k21tbXMGDy76dOfOnS\/bFrnud4FVU1MDIuLKysrv5ufnAwBF\/H+OoQbbKqPf\/jQ0d1kOxQ0JMyf9S6eiVGU5hbYCwJ\/\/\/NhhNyxe\/E2fz6ei84JsrcX333\/\/wrPOOmtUWGik\/cy4P53m7xgM1kjos+WBkAQQnDZt2tDvfve7c4mI161bJ1PRIln7kG47Omtjp2MUEbQEgEePHu2+9NJLbw+vHkdP6+TP6Zi2aS6UpPGnE+rVSeu32pSKb2OUMccL9ShhFO2cUBIFR1HmrDUh2svZfyQEqaAKFZQW6nFXjbuWiHjF31a4rr766ufq6uuunDBhQgtp\/SPT5EcefvjhS2+55ZYTXq+30xyuXncJ7bwWXVlZWXreeeeNB6C01iIisGIIEGOS9DWE9Y\/wpOcSUip5ppE6KGh00h8molRpCjJRORCU0iSl0EOGDLkdwDPl5eWHolMyAND0adO9Hcpgq726C\/o+XePAiB9MECFs5ZWUlPw7gMfLy8tVoksmNBGFAIT6OTdPJl3GsXiUJ0+eMmPGjBkj6uvrP5JSwuVyhQCYQoiuTiRKZ+7Y\/MDpjoxNK9OT4UHSlXbqxDjjVAZ2orWX1Da2hDQD0EIKTcISXCQsAciaAc1QSpM2WRjEyBmVe2k+8n+6b82+EzbvvgjgxcSpQdwbA9wlgVVdXU1EpB9bvvxzOTk5QwGYlMiRYVIcAdCCFIt9ov2fJN\/aMkjb74WwP9UQENDQEFpEl7deaOxJjzmSezLMLJi1IBIJmtIK5XQsr5gTV8S01rAMI0rC0BAAeNiwYSNuumlBAREdYGbh9XoFEamLLrrovMLBhWdZ1WgZu5Jl9aG1tVUGg8Gk33U3oqe1QkZGJtxud2Kb7Yk0adKkYbZwSmDExqbGHACu9lBBdyKKqb\/lJCNpr6Qi2fPIGlAeOnTI6KVLl3qmTp2K5ubmzFAo6PK43S5mjfgxT0tqddA3pRROnmwxOutDTF+YQSRczS3NaGlp0fHRDLINnnY3Lk3hlSC34gJ2DGawFlLAcBtSuoVkE9I8aUIFFXRIgyRBeiSkW8Kd44YKaYABI9M458wFZ2YGAoHGQHWA\/H6\/UV1drYQQrLUWtrTi3tJI3Qq6e6+5Juh2uzlh0CwimQBcSqk7X3jhhWfGjBljZGRkxFge7wMoTVl73LdRb8MvDx48qMvLyzUQN1lEhJG4ixNEAxBr1qzZsH37+7\/OyMiWpmkqAHC73dTS0sI3VlXdnpObO9bKpWiXDuGk1dbWtkPLly+vjq7U7XbL48ePq3+75prZo0aP\/rLWWkWv7IUnSXFxsQ4EfmwuX\/5EOE6I2tpaTP\/iFwsGDMjOtBWDaO8DQ2tWQpBcv379b5YtW7YpLy9PhEKhHscI3FLSsaYmnj9\/\/qI5c+aM0dBaRPU3\/GLkyJGhaNcPAPbt26cAYNPfNq1+5JFH\/qW11kopEWV5ESBAxKS1llrbSYsRHWUpKUslwV4wFjbFtZXFEm6B0JE0GbfhFk1NTfrC6dO\/\/MUZM2YCUGDIiJdj8ajOysoSdXWvXQbg4V27dv3t0eWP3uLxeGLa2KH5JiWUUmhpac392lVX3lcyfrywUxFiFJQQAtu2beMf\/vCHP9BaHxNCkCZiCQWlAAlp56MoyyiUClAW3wphUDDYGjpw4MBab41X1vpqFWLCStxxLJLT8W3CiwEMMJRwSenKMmTbsTY07mvad\/zDxoas4uzfHtpygJr2NPGJj07Ak+tB7thcGjgmjz35nmFtR1qvK5w0WGYVZA0tW1hGG57YYK17BsgML2p0tiDTbwJr4MCByUlCMYN7xOfzBRGVT9LbCMd50vWNOxhDDUA0Hm\/c8p3vfPeXyX5w5513XglgLLTgaKPQtjAoIyPjyB133JG07Pe+970cAF+GEBpRqRdhWe9yuSL7L8MCCwAunDbNzMnJSRaWhRCkAMiZM2c+OWvWrP\/ubdrOnz+\/EsAYEdffjsgZzmRetWrVxlWrVm1EP2Prlq0agCWwKCHFhZmZDh48Ug7g4eeee27HqlWrdnTzUZ7Kyln3AhCcIjgdDAaxevXqh1J5GemgrLTMZUs1UJLQekToRC\/rUXqKWmvWhkuwkeWSJw+dbD7y\/uGXP337kzVrf7J2Naz8uc5wz6hRo1wl80vm5ZbnnujIUDjlAuuDDz7AhAkTks9823V55513qpYvX35hVlaWYMUaYa1p62kR9uRsRWl9oGMTSnXsS0nEipl27tz55vPPP7+OiI7YCwHhFYhuEKx9OA2Xkblu3Trj4MGDrsGDB4cAYPjw4fLjjz9WANwdLVNorY1169bF0HPs2LGuDz\/8MKS1HiCE6PIKR8aADIpNtWpvazjZ8e23375z2bJll2VkZAhm1rEiGJ2I6KR+jggFg7owv3Ci9TPd5ZZHb37esmWLvP3229u0ZR4VbdiwQdTX15994MCBy2yLUKQgaLQhm7y99mdSStl4olH9c9eH00onlqbia0lEeurUqY8AwNq1a7vM+zk5OdTU1MQ\/\/vGPi8LrH5RiBZGIMGvWrKILLrjgo1huTg+BQEB1bB2FLako55iT5yPELAJrZhKkPQPcMng8iKPbPl11YMtx\/3\/f93JEeJMg3PPaPZGcNdShfdd1OfCt8m+xT\/jU7t271e7A7scR6D+l1KVB8\/l8AIBfPvpLBO4JoKCgAPEmcTgCP3ny5MrJkydX9kWjTdPE1ddcfWzre1ufuu66637m8\/k+irK2uhE\/jahgXVFRYa5YsYJ8Pp8ZVsvjxo1T3MnSkBCCk+RR0RlnnGHGCJKuRJpaU7c1TOcpU6bMnDJlysw+W82ISmDswmTTgUBAM7OoqKgwBw7EoKef+f2i\/EH53y6dUDrg3HPPzTAMoy\/5WsTHIIkIBw8eFPv27dsFAOXl5bqrLkt4QaSsrMxEGiuFra2tZiAQMHtjH53FQ8TxKQftVhd1EmIFmLWWHkMIKeSR7Ye37H11z8\/qf1H\/awDws1\/UVdeJ+kC9yZoRqIhLZK4PDy5QH3kDmuGfIesD9eZpKbBqa2u1PWh\/W3Tzot0FBQUjAU6lJdNavepWow3DKJ1QOqh0QumtU6dO\/dquXbuuIqINyZIvP9NIb3T6is5GXLp0l7BixQoXEYV++uBPL6m8ZNZzEydOKoj7SQjJd7\/1BMlXCVlrSZIOHzq09e677z5unyzSN0eecJ\/USe4BbtHa3KpZsQmGJAGyEkI7UXxsrdgIQ8CT7RHN+5tbP\/37gaVrbvv9fQBa\/ewXgeoAAhTQSfiIkDyjJ5IR0p\/CCuh6HhbbZY5t3frBry0FLHUHzOOK+eO49139s8szQFprBmCOHj16xKRJk9668847l1RUVJg1NTXd33ndz6fj9M7jWHaPjpx8XBgutl53u3k1NTWyqqoq9NRTT10y7+p5L9vCKqSUslPdGPY4GjFtieMPTvHa+m3S9otkHCuFZAC05d13nz1x4sQhO17IfTJ6vRjGuaz4MgU\/hGpWS3b\/ZecboWMhIyPHY7iyDGtjoECINStoZtbMbCdzsWa2wjAwSRIyBmYIKIh99R+\/07Bm+6Q1t\/3++ySodYZ\/hhGggEYgRlBR2YoyF1mGNYPi\/gD2M4uyhWWu\/p8x3YthaWYWs2fP\/uXkyZO+OWbMmKFKKZ0sqbHXZ2hUbhsJQWAYSpm6oKAAixcvXjRq1KjfzJ07953UW0a4c9O5096j148V6Zm86kZ\/KG4w4vLrqQfjVVNTI+fOnatuuummGTNnzXp56JAhLlOZ2pCGK5xbFF5Zpvi2UGp2SQwVUVp8pVlDkJDvvvtu6IEHHvgNAKqrq+uxdcVRa3Z9pfVsN5I2YMMHG1Zv+OIX5k2rGDNn9GxhyOsHjsrN9eR4XJCACppg1Z6gLCRBuCVBQZw8dFId+eDwu4ffOfSfrz7w6ioAQS97ZS3V6jjriLzsFbVUqzZXbQ4BQFlZWdZFv7iIm93NBACbqzdj8x82I0DUErbGZqybYdRXnKYuoc1sXFNTI\/74xz9+UlJS8n2\/3\/9fubm5QTuhUHRtR2jPBZg0DKGUUkVFRZ4pU6b8lpknADCTuxvUYfAoLeV4uh0J11e07t44ktfr1T6fL+vaa699bOiQIW6ltTKkIfvwmZ3FfhgAuV2uY0OGDFHMjOrq6l6jO\/W9S8hgEEnijc+8uXbjM2+uPeOMM+4be\/W4imEXFH+BtZqqFf+fjLwMTUSCAW470kra1FuZ8epHa\/e89Ndf\/XUTgBAJAiumWqqNCejP8M8w6u+tN2upVg0bNmxk2X+UfT1zSPbsjLyMs1vcrVoKQ4CBibdN4rG+z4NcYs2hfxzY8toDrz1RX1F\/\/HS3sODz+ZR9wsCTxcXFn1+wYMHdeXl5AGAqrayd5kSJB13EMWT8fnWkTIpjMFO7Mk7YVCwkgND5558\/5le\/+pWXiJ79XxfPSjG5w3lZMdspwlsx4ugYIVuScbAWzu0R4U5le6q4lUFEoYceemjRtGnTzgQQEkSutPsQ89AUyb1J+SjsvST+3t6aY44vKRk8b968\/0dEizZt2mT0+DA5jspl6usgFoEZTF6vV5TeXEqBisDRPT\/d8wKAFwCgeFxxYemVpWwGTTLcBu99YS81NDQcsxU3iAhXrb5K1vpqdVxUimasmyFtC6lg9o9m31lwTuGNg8YMGkgeCdVqWtntNtEHDM1BzohcSLe8bvBZhRh6wbC7ju048tDfH\/z7w\/v37285bQWWba6atuv1\/aNHj+5bsGDBd8aMGTMqbkuBGTNtEkx+6thVif4lRcXG4veBWltGpJSSCgsLvwPg2UsuueR\/r7CK1fAMkIrNw6GOjbGU40DRxWUXg+60cOFCs6qqyjP1vPP+DQDbY5LM6rG2rcRYKfG5RJSWNxtdnpmNhOxzBhRrIYXgadOmfXHEiBH5ZWVlx9DDgH943ifsp++7rUhcW1urUGvJZm+tV+QdzROP3fRYaH\/D\/kP7H4g9u5IE4YblN7iK9xWrQCDAkQRUG+Gk1PqKenOmf+YVxRcM\/\/6gMYPKlFIIhVSIW4LS3joQUXIME2hjhJpJC0PowtLCwYNGD7q\/cELWfz11WW1L3Lay00tg2YJC20Jr2eOPP\/78448\/fusZZ5xxzYgRI88oKMgXPa2\/Kz5EeGJceOGFAwEr47irLgd\/FgRVYh+oz+jchfBY+JjspUuXZpeMHz\/eYg9KxTfoizYnfZ51moEAwIMGDTrH7\/fnE9ERO9Wg20PefvhBP8Q\/\/BB++COJuSBwLWoVrBViSsa4TIyVVSsTEkBn+GcY63+43qz11aozLz5z+OTrzq3OL8m\/PiM\/A6HmYEibbJAgF0kRu39IhPtKACBZsQieCDJJnMwcUdBvl0z06MRRv98vy8vL4ff73ffee++nc+bM+R6ApTU1Nbl5eXmDXnnllVvad5lT7Lk7UYa8xWzhFBNqPw2DOXKEjyBBJ0+e5BsW3nDppImThoa3xEQGSLMgQaYQYnhlZaX3z3\/+c+2MGTOMxCDVZ\/yiDLv59llPtHbd2sY9u\/es8ngyYJoha3eLFuHd4FaeHKxTWq0odNQezKhtMAICWmhIIhEMmnrChAlzzj\/\/\/OJ4OqdCVVWVASC0ffv2GwoLCyWs7URGEqGG48ePY+funauJ6ZgUgpTWvH37drS1tUVyRcN90AKREwF1zAYh62RaSUymNnn06LGeCRMmzC8sLIw8J96CzM\/P10OHDh0B4J\/V1dVIei5WusPA1H9qLgAdQAAkCBe9dpFRtKzIsrYQM4VSwuv1StQAtaJW2YF2Y6Z\/5uLiacO\/M2jsoLxgS1C3NbbBElQUeyBpB3FSEkRssoF+PBG9JwKLw6dk1tfHrBIc9vl8h+3XC3q7wbfddtsrAIaKuG0u4fwQZu3Kzs4eBFjXjO3fvz\/tCDV1\/\/jI\/rey7EY0NOz45KYbb7yxtx\/x4osvngmgOIHOKVBWVgYAmDhxYoH9+1AsQTl8eoFx\/PjxF6acM+Xq3m7zW2+9lVtYWHil9Rw22rPOLRkPwHjzrTcXAKhbuXKlfSJqzwahj\/lAANBT5kyZmjUo6+QbT7+xNbIiZwfi71H3CNRBxF7ZVY6i8vf5t\/K3KhJkt5rpmr109jX54woWDxo9aCIEoa2pzSTAIEHpTpPYOdDPi1DdEVgEgM8\/\/+IhQ4YMmEVEMQfPEbOAEGDBRNoQEoCStjJQ7ZyvIrENFflcRf9rfy2VhIKC221tcD1+\/Pho2+WjZOdwMwOtra0qDSOlBz7hKbbSoh7v8XiMFStWuBYuXIiVK1f2uOpx48bJhoYG5Xa73N0pn5WVFYqN8ySq6Y0bNx5nZrlmzRrPsGHDQgCwefPmbre5uLjYdfnllwffe\/+9twFcifZVhwQFI0Tv3JDC\/XBsTtnCMrl55WadkZ9x28Qbz\/nq8Fkj3nfneJZtfezd\/W\/T239mcIqEz\/qYYNv0f58+Z\/iMkRdkD8m6Jrt4wOeMTAOhlqDSJguSsVZwajnMiQE7AnRQcfPRE6evwKqpqRE+n09961vzJ1VWVj4VFhoJR2VQ2PWz\/OlwnylmwGM\/5\/bwaeQswMgFIUTQSiEnN9dmvNjzQOy8KyPTk7nn4MGDvwWs\/VgLFy40EklPSc\/q5n6XQz231FhrrqqqCt18881Qqud3Utjbk9Qf\/vAH7k5L405NTVo+FAoaRKRWrFgRmjNnTqinbd60aROIyNy0aZOnMwlPveTDUT9GPIUhTnoGebKGnlc8lZh+de7iqTh3cdm2I1sPC5K0PndU3rrG3UelzHApHdQia0i2Pnm4ZUroRPAr+aUFypVplGQNzoI2NUJtpmpraiMikiS7ckg9JRgG0hA49mGTa8sj\/6D+iud12yUcOXJkyN4grNJxGXq5zQnhYKU1pBD0xoY3Qhs3bmwUQsDOhk8+fcJpAJQkktoRohJHeypuesWx7CchS2m3mJIqBkoMJvZBG6Pr7WO3Pfr+hr5+FLNQpmIVUiEioow8jyFcsiS7eACkIcZpk6\/PHp5txe2YIVwChe4CKFOBNUMFlW490abBEASSJBK9idi4X4qN1LH3G2jDYwjzhLnpvbXvHfezXwQowKetwGprayPTNF2GYQittXXrc9L8GU644aPjG1uSJdlYplZ4X1B8sIkZkEIoALR3996VAEgpJWKuFNJxNVM3fcKoezbS59Hk6fFd1kgRPuJuuLHdQ3vL07axko9sP4T9Yk1CSv5o3TsLWhGXsL\/O1SUQBAQrbbAGlKl1+DQ\/IUlb90cQM5hUq+ZgS5DAECC2DiMCiZgkvHjvjiiBdpaTk8xHJJAEm62Kmg+1LAPQWldXZ8BOY+rroF73aUjtR2kQEUgQiET7e7LfCwFEfSbsyyCj38f8Pvq9oEj5cLnYuDPArDQA1\/vvv3\/4xcceW25rixjO1KJXmadrskK33ynck2nYPgc7v86g1xnEtj51F+dY\/1qF3Ln71lv3D\/afzki0Sa15IIQQggiSNbugYbDWLigY0OwiIoMECSIhmBI1NUfTJvregahkXk4hkVmzcme55dEPD+94+Y41q\/x+v+iv7Tmiv9iGusCznNR14iQDSFBKsRDSDAaD5ptvvrnopb\/+tQlJkgJFSuuKu2XxdMlgED0hNp2SuEmqZoju8kFXXe\/uaVAkD1ZRjMXYR6TpBxMLEdam8CkzRO3x4CRhjqQ3lEVlQrB16EM7b3Wy5YiV1kaWwa2HTtLuV3Z9G4xQHer6ba2w2w8yOmBO6pSLu3C5DcdZFRz2MjW00kpKqZXS7qeffnrl9ddf\/5x9l59G2m2Iv\/U2vZnYI+HB3dXMybaq9N9KAfektdyTmtK1QrhTQdJbM6sfDtdMICYnVWJJdgpEzROKIjclUc2EmITQ9t9xohXJmpWR5RK6RRu7\/rLz7teXvf5nb61XnLbnYUUjJJmZWQFQzGy5a+mEb6yradJnHIqrhBBJdCQJefjwYbVp09s333DDDb+2z2AyOwjHhNsrmETkGEgRzhjuWAHb5bUCieg+hE86VZ2XhQIgwn2I4kNKEWxlZlZEpOxtD\/F1MlHfXFdvHzqoIv3SAIm0+hvb11hVb9NZ9E1mtKWoEp9tIZ0x7gJ9rPoSrkPTbGWF96JEIyYdnmvWrajtlqp1HhZbiawUdbgftSv48PccFTQPJ3NzxF2hyL5Iji9PDMMt4cp0ycaPGo\/uemXnj1+7\/7UlMWfOn+4Cy4BhGIYhYV2Z3iUbrutaTiS8++ST\/dix45\/ra2trq3\/xi1+sE0Kgqqqqo0oyYa1mxrTXfhVe5czuoPwA63fWmcVRLQqXHZjOs7tCFiJy2Ru7E2gctUlvYN\/MfRoQ02aRXn+JOaGvUeXtz3R2HwnZ5M+Oajcz9\/jZWmvKzMj02OMQ+xD7YIqsrCxorXvF\/GVwtjvbLc2gKSmZe03J7hGKKZ\/qRPhEEy7JbhDWjKaPmnBi34lXdvxp16J3Vm3YYR9R06\/CqlsCa+vWrQwAGzZsONLa0vq6dEltr8ilPA2ge4GfxKKCiEOhEDU2Nq7ftOmdP\/385w++DgB+\/zojEKhQyUakuLiYAaCxsfH9HQ07XpeGVKZpymhrRRApaRjy4MFP\/wEA+\/btS7hGd8eOHW+53e6TwWAw1mbUYMNtkGmGPo5\/dl1dXbhsQ1ZW1uvNLc0mMRlxW7dZSkFa6+b4Z+7Zs+dQQ0PD64ZhxLaZAcWKs7Oy6cChQ3vtidirnt\/+\/fvf2r17d0J\/NTS7DTeZphnT3zDNPjl8oGHnzp2vM7OpTBXTVyJShsuQn36alM7dRlNTEwPA3r0fNxQWFr5umqaptDaiySwEKSkMeezYsb8DQF5eXrctrZMnT7Y1NDTUZ2RmyNZgKyS1Cy1WCq4MD7Zt28bcxK096deYS8bozSs3wwyarx16\/9BIrZQGk2BogEXMRvCkIis66hFzoIfddRa2USpiyjA0SAoEj7fhxEdNcA3wPLNr7Z73\/vHC5jcAe+P0KRBWn3kwMzGzcCjhwEHfw8teaV+mfMrQbZfQzmg+ZY2vra0N33qclpb2+\/2iurq6Q\/uuurqaU52TZAvGzm5+1t2lFSW5cTpNGnNfnE\/e3f72lM49VWCd0au3nl1TUyPD17Gl4k+fz9c7VogfwnuW99TtB\/MC8AGOVeXAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHDhw4cODAgQMHNv4H9pXoS3Jf+ugAAAAASUVORK5CYII=\" alt=\"SmartD\" \/><\/div>\n    <nav class=\"tn-menu\" id=\"tnMenu\">\n      <button type=\"button\" class=\"tn-item\" data-nav=\"home\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"\/><path d=\"M3 3v5h5\"\/><\/svg><span data-i18n=\"nav.home\">Reset<\/span><\/button>\n      <button type=\"button\" class=\"tn-item\" data-nav=\"create\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M6 3v5M6 14v7M12 3v3M12 12v9M18 3v9M18 18v3\"\/><circle cx=\"6\" cy=\"11\" r=\"2.2\"\/><circle cx=\"12\" cy=\"9\" r=\"2.2\"\/><circle cx=\"18\" cy=\"15\" r=\"2.2\"\/><\/svg><span data-i18n=\"nav.create\">Create Config<\/span><\/button>\n      <button type=\"button\" class=\"tn-item\" data-nav=\"edit\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2v-6\"\/><path d=\"M13 3v5h5\"\/><path d=\"m16.5 12.5 3 3-4.2 1.2 1.2-4.2z\"\/><\/svg><span data-i18n=\"nav.edit\">Edit Config<\/span><\/button>\n      <button type=\"button\" class=\"tn-item\" data-nav=\"viewreport\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 4v16h16\"\/><rect x=\"7.5\" y=\"11\" width=\"2.6\" height=\"6\" rx=\"0.4\"\/><rect x=\"12\" y=\"7.5\" width=\"2.6\" height=\"9.5\" rx=\"0.4\"\/><rect x=\"16.5\" y=\"13.5\" width=\"2.6\" height=\"3.5\" rx=\"0.4\"\/><\/svg><span data-i18n=\"nav.viewreport\">View Config &amp; Report<\/span><\/button>\n      <button type=\"button\" class=\"tn-item\" data-nav=\"audit\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 6h11M9 12h11M9 18h11\"\/><path d=\"M4 6h.01M4 12h.01M4 18h.01\"\/><\/svg><span data-i18n=\"nav.audit\">View Audit Log<\/span><\/button>\n      <button type=\"button\" class=\"tn-item\" data-nav=\"package\"><svg class=\"tn-ico\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 13a8 8 0 0 1 16 0\"\/><path d=\"M4 13v3a2 2 0 0 0 2 2h1v-5H6a2 2 0 0 0-2 2z\"\/><path d=\"M20 13v3a2 2 0 0 1-2 2h-1v-5h1a2 2 0 0 1 2 2z\"\/><path d=\"M17.5 18a3.5 3.5 0 0 1-3.5 3h-2\"\/><\/svg><span data-i18n=\"nav.package\">View Support Package<\/span><\/button>\n    <\/nav>\n    <div class=\"tn-spacer\"><\/div>\n    <div class=\"pkg-info\" id=\"pkgInfo\" style=\"display:none\"><\/div>\n    <button class=\"shell-btn ghost\" id=\"unloadBtn\" style=\"display:none\">Close<\/button>\n    <div class=\"tn-avatar\" id=\"tnAvatar\" aria-hidden=\"true\">EN<\/div>\n    <input type=\"file\" id=\"fileInput\" accept=\".tar.gz,.tgz,.zip,.gz,.json\" style=\"display:none\" \/>\n  <\/header>\n\n  <div class=\"subnav\" id=\"subNav\" style=\"display:none\">\n    <button type=\"button\" class=\"sn-item\" data-sub=\"health\"><span data-i18n=\"sub.summary\">Summary<\/span><\/button>\n    <button type=\"button\" class=\"sn-item\" data-sub=\"config\"><span data-i18n=\"sub.config\">Configuration<\/span><\/button>\n    <button type=\"button\" class=\"sn-item\" data-sub=\"audit\"><span data-i18n=\"sub.audit\">Audit Log<\/span><\/button>\n    <button type=\"button\" class=\"sn-more\" id=\"snMore\" data-i18n-title=\"rawfx.more\" title=\"View other raw files (JSON \/ LOG)\" aria-label=\"View other raw files\">\u22ef<\/button>\n  <\/div>\n\n  <div class=\"rawfx-overlay\" id=\"rawFilesOverlay\" style=\"display:none\" role=\"dialog\" aria-modal=\"true\" aria-label=\"Raw package files\">\n    <div class=\"rawfx-modal\">\n      <div class=\"rawfx-head\">\n        <div class=\"rawfx-title\" data-i18n=\"rawfx.title\">Raw package files<\/div>\n        <button type=\"button\" class=\"rawfx-close\" id=\"rawFilesClose\" aria-label=\"Close\">\u00d7<\/button>\n      <\/div>\n      <div class=\"rawfx-body\">\n        <div class=\"rawfx-list\" id=\"rawFilesList\"><\/div>\n        <div class=\"rawfx-view\">\n          <div class=\"rawfx-view-head\">\n            <span class=\"rawfx-fname\" id=\"rawFileName\">\u2014<\/span>\n            <span class=\"rawfx-fsize\" id=\"rawFileSize\"><\/span>\n          <\/div>\n          <pre class=\"rawfx-pre\" id=\"rawFilePre\"><\/pre>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <div class=\"content\" id=\"content\">\n    <div class=\"empty-state-shell\" id=\"emptyState\">\n      <div class=\"home-hero\">\n        <h1 class=\"home-title\" data-i18n=\"home.title\">SmartD Clean Power VFD Tools Suite<\/h1>\n        <p class=\"home-sub\" data-i18n=\"home.desc\">Access configuration tools, Audit logs or support package.<\/p>\n      <\/div>\n      <div class=\"drop-zone-shell\" id=\"dropZone\">\n        <div class=\"dz-main\" id=\"dropBtn\" role=\"button\" tabindex=\"0\">\n          <div class=\"dz-iconwrap\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\"\/><path d=\"M14 3v5h5\"\/><path d=\"M12 18v-6\"\/><path d=\"m9.5 14 2.5-2.5 2.5 2.5\"\/><\/svg><\/div>\n          <h2 data-i18n=\"home.drop\">Drag &amp; Drop a file here<\/h2>\n          <p class=\"dz-supported\" data-i18n=\"home.supported\">Supported files: Configuration JSON, Audit Log JSON, Support Package<\/p>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"file-view\" id=\"fileView\" style=\"display:none\">\n      <div class=\"file-toolbar\">\n        <span class=\"filename\" id=\"curFilename\"><\/span>\n        <span class=\"fileinfo\" id=\"curFileinfo\"><\/span>\n        <div class=\"spacer\"><\/div>\n        <div class=\"view-toggle\" id=\"viewToggle\">\n          <button data-mode=\"structured\" id=\"viewModeStructured\" title=\"Structured view\">\n            <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm0 5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V8zm1 4a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1H3z\"\/><\/svg>\n            Structured\n          <\/button>\n          <button data-mode=\"raw\" id=\"viewModeRaw\" title=\"Raw text view\">\n            <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M2 3h12v1.5H2V3zm0 3.25h12v1.5H2v-1.5zm0 3.25h12V11H2V9.5zm0 3.25h8v1.5H2v-1.5z\"\/><\/svg>\n            Raw\n          <\/button>\n        <\/div>\n        <button class=\"icon-btn\" id=\"wrapBtn\" title=\"Toggle word wrap\">\n          <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M1 2.75A.75.75 0 0 1 1.75 2h12.5a.75.75 0 0 1 0 1.5H1.75A.75.75 0 0 1 1 2.75zm0 5A.75.75 0 0 1 1.75 7H11a2.5 2.5 0 0 1 0 5H8.66l.72.72a.75.75 0 1 1-1.06 1.06l-2-2a.75.75 0 0 1 0-1.06l2-2a.75.75 0 1 1 1.06 1.06l-.72.72H11a1 1 0 0 0 0-2H1.75A.75.75 0 0 1 1 7.75zm0 5A.75.75 0 0 1 1.75 12h3.5a.75.75 0 0 1 0 1.5h-3.5a.75.75 0 0 1-.75-.75z\"\/><\/svg>\n        <\/button>\n        <div class=\"search-wrap-shell\">\n          <svg class=\"search-icon\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M10.68 11.74a6 6 0 0 1-7.922-8.982 6 6 0 0 1 8.982 7.922l3.04 3.04a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215zM11.5 7a4.499 4.499 0 1 0-8.997 0A4.499 4.499 0 0 0 11.5 7z\"\/><\/svg>\n          <input class=\"search-input-shell\" id=\"codeSearchInput\" data-i18n-ph=\"ft.searchfile\" placeholder=\"Search in file...\" spellcheck=\"false\" \/>\n          <span class=\"search-count\" id=\"codeSearchCount\"><\/span>\n        <\/div>\n        <button class=\"icon-btn\" id=\"copyBtn\" title=\"Copy contents\">\n          <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25z\"\/><path d=\"M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25z\"\/><\/svg>\n        <\/button>\n        <button class=\"icon-btn\" id=\"downloadBtn\" title=\"Download file\">\n          <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M2.75 14A1.75 1.75 0 0 1 1 12.25v-2.5a.75.75 0 0 1 1.5 0v2.5c0 .138.112.25.25.25h10.5a.25.25 0 0 0 .25-.25v-2.5a.75.75 0 0 1 1.5 0v2.5A1.75 1.75 0 0 1 13.25 14zM7.25 7.689V2a.75.75 0 0 1 1.5 0v5.689l1.97-1.969a.749.749 0 1 1 1.06 1.06l-3.25 3.25a.749.749 0 0 1-1.06 0L4.22 6.78a.749.749 0 1 1 1.06-1.06z\"\/><\/svg>\n        <\/button>\n      <\/div>\n      <div class=\"code-viewer\" id=\"codeViewer\"><\/div>\n      <div class=\"structured-view\" id=\"structuredView\" style=\"display:none\"><\/div>\n    <\/div>\n\n    <!-- Rich Audit Log viewer -->\n    <div id=\"auditViewer\">\n      <div class=\"audit-header\">\n        <div>\n          <div class=\"audit-header-title\" data-i18n=\"audit.title\">Audit Log<\/div>\n          <div class=\"audit-header-sub\" id=\"audit-header-filename\">\u2014<\/div>\n        <\/div>\n        <div class=\"audit-hdr-divider\"><\/div>\n        <div class=\"audit-hdr-stat\"><strong id=\"audit-total-count\">0<\/strong> total events<\/div>\n        <div class=\"audit-hdr-divider\"><\/div>\n        <div class=\"audit-hdr-stat\">Range: <strong id=\"audit-date-range\">\u2014<\/strong><\/div>\n        <div class=\"audit-hdr-spacer\"><\/div>\n        <div class=\"audit-hdr-time\" id=\"audit-current-time\"><\/div>\n      <\/div>\n\n      <div class=\"audit-toolbar\">\n        <div class=\"filter-group\">\n          <div class=\"filter-label\" data-i18n=\"audit.view\">View<\/div>\n          <div class=\"viewtype-toggle\">\n            <button class=\"audit-btn audit-btn-toggle active\" id=\"btn-view-formatted\" data-i18n=\"audit.formatted\" title=\"Show every event formatted \u2014 structured\/concise views where available\">Formatted<\/button>\n            <button class=\"audit-btn audit-btn-toggle\" id=\"btn-view-raw\" data-i18n=\"audit.raw\" title=\"Show every event as its raw verbatim message\">Raw<\/button>\n          <\/div>\n        <\/div>\n        <div class=\"filter-group\">\n          <div class=\"filter-label\" data-i18n=\"audit.timerange\">Time Range (UTC)<\/div>\n          <div class=\"daterange-wrap\">\n            <input type=\"datetime-local\" step=\"1\" class=\"daterange-input\" id=\"audit-date-from\" title=\"Start date & time (UTC, HH:MM:SS)\" placeholder=\"Start\">\n            <span class=\"daterange-sep\">\u2192<\/span>\n            <input type=\"datetime-local\" step=\"1\" class=\"daterange-input\" id=\"audit-date-to\" title=\"End date & time (UTC, HH:MM:SS)\" placeholder=\"End\">\n          <\/div>\n        <\/div>\n        <div class=\"filter-group\" style=\"min-width:0;\">\n          <div class=\"filter-label\" data-i18n=\"audit.highlight\">Highlight color<\/div>\n          <button class=\"color-trigger\" id=\"cf-color-trigger\" type=\"button\" title=\"Filter rows by highlight color\">\n            <span class=\"cf-title\">All colors<\/span><span class=\"cf-badge\" id=\"cf-color-badge\"><\/span>\n            <svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" class=\"cf-chevron\" id=\"cf-color-chevron\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n          <\/button>\n        <\/div>\n        <button class=\"audit-btn audit-btn-toggle\" id=\"btn-show-startshutdown\" data-i18n=\"audit.showunspec\" title=\"Start-up and shutdown events \u2014 all entries with a blank\/undefined Event type (systemd boot &amp; shutdown journal lines) \u2014 are hidden by default. Click to show them; click again to hide.\">Show Unspecified Events<\/button>\n        <button class=\"audit-btn audit-btn-danger\" id=\"btn-clear\" data-i18n=\"audit.clear\">Clear Filters<\/button>\n        <div class=\"filter-group\" style=\"flex:1;min-width:180px;\">\n          <div class=\"filter-label\" data-i18n=\"audit.search\">Search<\/div>\n          <input type=\"text\" class=\"audit-search\" id=\"auditSearch\" placeholder=\"Messages, transaction IDs, users\u2026\">\n        <\/div>\n        <div class=\"export-wrap\" id=\"audit-export-wrap\">\n          <button class=\"audit-btn\" id=\"audit-export-btn\" title=\"Export displayed entries to PDF\">\n            <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\"><path d=\"M6 1.5V8m0 0L3.5 5.5M6 8l2.5-2.5M2 9.5v1A1 1 0 0 0 3 11.5h6a1 1 0 0 0 1-1v-1\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n            Export PDF\n            <svg width=\"9\" height=\"9\" viewBox=\"0 0 10 10\" fill=\"none\" aria-hidden=\"true\" style=\"margin-left:2px\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n          <\/button>\n          <div class=\"export-menu\" id=\"audit-export-menu\">\n            <button class=\"export-menu-item\" data-export-order=\"desc\" type=\"button\">\n              <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\"><path d=\"M6 10V2M6 2L3 5M6 2l3 3\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n              <div><div class=\"export-menu-title\">Newest \u2192 Oldest<\/div><div class=\"export-menu-sub\">Latest events first<\/div><\/div>\n            <\/button>\n            <button class=\"export-menu-item\" data-export-order=\"asc\" type=\"button\">\n              <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\"><path d=\"M6 2v8M6 10L3 7M6 10l3-3\" stroke=\"currentColor\" stroke-width=\"1.3\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n              <div><div class=\"export-menu-title\">Oldest \u2192 Newest<\/div><div class=\"export-menu-sub\">Chronological order<\/div><\/div>\n            <\/button>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"audit-stats-bar\">\n        <div class=\"stat-chip\">\n          <div class=\"stat-dot\" style=\"background:var(--info)\"><\/div>\n          Info: <span class=\"stat-count\" id=\"s-info\" style=\"color:var(--info)\">0<\/span>\n        <\/div>\n        <div class=\"stat-chip\">\n          <div class=\"stat-dot\" style=\"background:var(--warn)\"><\/div>\n          Warning: <span class=\"stat-count\" id=\"s-warn\" style=\"color:var(--warn)\">0<\/span>\n        <\/div>\n        <div class=\"stat-chip\">\n          <div class=\"stat-dot\" style=\"background:var(--error)\"><\/div>\n          Error: <span class=\"stat-count\" id=\"s-error\" style=\"color:var(--error)\">0<\/span>\n        <\/div>\n        <div class=\"stat-chip\">\n          <div class=\"stat-dot\" style=\"background:#ff8a85\"><\/div>\n          Emergency: <span class=\"stat-count\" id=\"s-emerg\" style=\"color:#ff8a85\">0<\/span>\n        <\/div>\n        <div class=\"stat-chip\">\n          <div class=\"stat-dot\" style=\"background:var(--alarm)\"><\/div>\n          Alarms: <span class=\"stat-count\" id=\"s-alarm\" style=\"color:var(--alarm)\">0<\/span>\n        <\/div>\n        <div class=\"results-info\">Showing <strong id=\"results-count\">0<\/strong> of <strong id=\"results-total\">0<\/strong> events<\/div>\n      <\/div>\n\n      <div class=\"audit-focus-banner\" id=\"audit-focus-banner\" style=\"display:none\">\n        <span class=\"focus-star\">*<\/span>\n        <span>Focused on one entry \u2014 showing the surrounding \u00b1<strong>20<\/strong> events in context.<\/span>\n        <button class=\"audit-btn\" id=\"btn-goto-marked\" type=\"button\">Go to marked event<\/button>\n        <button class=\"audit-btn\" id=\"btn-clear-focus\" type=\"button\">Clear focus<\/button>\n      <\/div>\n\n      <div class=\"table-container\" id=\"audit-table-container\">\n        <table id=\"log-table\">\n          <thead>\n            <tr>\n              <th class=\"col-ts sorted\" data-sort=\"timestamp\" id=\"th-timestamp\"><span id=\"th-ts-label\">Order: latest first<\/span> <span class=\"sort-indicator\" id=\"si-timestamp\"><\/span><\/th>\n              <th class=\"col-evt filt-col\" id=\"th-event\">\n                <button class=\"col-filter-trigger\" id=\"cf-event-trigger\" type=\"button\" title=\"Filter by event type\">\n                  <span class=\"cf-title\" data-i18n=\"audit.evtype\">Event Type<\/span><span class=\"cf-badge\" id=\"cf-event-badge\"><\/span>\n                  <svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" class=\"cf-chevron\" id=\"cf-event-chevron\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n                <\/button>\n              <\/th>\n              <th class=\"col-sev filt-col\" id=\"th-severity\">\n                <button class=\"col-filter-trigger\" id=\"cf-severity-trigger\" type=\"button\" title=\"Filter by severity\">\n                  <span class=\"cf-title\" data-i18n=\"audit.severity\">Severity<\/span><span class=\"cf-badge\" id=\"cf-severity-badge\"><\/span>\n                  <svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" class=\"cf-chevron\" id=\"cf-severity-chevron\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n                <\/button>\n              <\/th>\n              <th class=\"col-orig filt-col\" id=\"th-origin\">\n                <button class=\"col-filter-trigger\" id=\"cf-origin-trigger\" type=\"button\" title=\"Filter by origin\">\n                  <span class=\"cf-title\" data-i18n=\"audit.origin\">Origin<\/span><span class=\"cf-badge\" id=\"cf-origin-badge\"><\/span>\n                  <svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" class=\"cf-chevron\" id=\"cf-origin-chevron\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n                <\/button>\n              <\/th>\n              <th class=\"col-user filt-col\" id=\"th-userId\">\n                <button class=\"col-filter-trigger\" id=\"cf-user-trigger\" type=\"button\" title=\"Filter by user\">\n                  <span class=\"cf-title\" data-i18n=\"audit.user\">User<\/span><span class=\"cf-badge\" id=\"cf-user-badge\"><\/span>\n                  <svg width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\" class=\"cf-chevron\" id=\"cf-user-chevron\"><path d=\"M2 3.5l3 3 3-3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n                <\/button>\n              <\/th>\n              <th class=\"col-tid\" id=\"th-transactionId\" data-i18n=\"audit.tid\">Transaction ID<\/th>\n              <th class=\"col-msg\" id=\"th-message\" data-i18n=\"audit.message\">Message<\/th>\n            <\/tr>\n          <\/thead>\n          <tbody id=\"log-body\"><\/tbody>\n        <\/table>\n        <div class=\"empty-state\" id=\"audit-empty-state\" style=\"display:none\">\n          <div class=\"empty-icon\">\u2b1b<\/div>\n          <div>No events match the current filters.<\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"pagination\" id=\"audit-footer\">\n        <button class=\"audit-btn\" id=\"btn-first-entry\" data-i18n=\"audit.first\" title=\"Jump to the first entry (scroll to top)\">\u2191 First Entry<\/button>\n        <button class=\"audit-btn\" id=\"btn-last-entry\" data-i18n=\"audit.last\" title=\"Jump to the last entry (load all &amp; scroll to bottom)\">\u2193 Last Entry<\/button>\n        <div class=\"page-info\" id=\"audit-loaded-info\"><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n         VFD HEALTH DASHBOARD (first tab) \u2014 aggregates config + metrics\n                                             + device-info.log + hmi-info.log + version.log\n    \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 -->\n    <div id=\"healthViewer\">\n      <div class=\"hd-header\">\n        <div>\n          <div class=\"hd-header-title\" data-i18n=\"sub.summary\">Summary<\/div>\n          <div class=\"hd-header-sub\" id=\"hd-header-sub\">\u2014<\/div>\n        <\/div>\n        <div class=\"hd-hdr-divider\"><\/div>\n        <div class=\"hd-hdr-stat neutral\">Drive: <strong id=\"hd-hdr-drivename\">\u2014<\/strong><\/div>\n        <div class=\"hd-hdr-divider\"><\/div>\n        <div class=\"hd-hdr-stat neutral\">S\/N: <strong id=\"hd-hdr-serial\">\u2014<\/strong><\/div>\n        <div class=\"hd-hdr-divider\"><\/div>\n        <div class=\"hd-hdr-spacer\"><\/div>\n        <div class=\"hd-hdr-time\" id=\"hd-header-time\">\u2014<\/div>\n      <\/div>\n      <div class=\"hd-body\">\n        <!-- Info sections FIRST -->\n        <div class=\"hd-sections\">\n          <div class=\"hd-section collapsible collapsed\" id=\"hd-section-device\">\n            <div class=\"hd-section-head\">\n              <div class=\"hd-section-icon\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2\" y=\"3\" width=\"12\" height=\"10\" rx=\"1.5\"\/><path d=\"M4 6.5h8M4 9h6M4 11.5h4\"\/><\/svg>\n              <\/div>\n              <div class=\"hd-section-title\" data-i18n=\"hd.device\">VFD Information<\/div>\n              <svg class=\"hd-section-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6l4 4 4-4\"\/><\/svg>\n            <\/div>\n            <div class=\"hd-info-grid\" id=\"hd-device-info\"><\/div>\n          <\/div>\n          <div class=\"hd-section collapsible collapsed\" id=\"hd-section-network\">\n            <div class=\"hd-section-head\">\n              <div class=\"hd-section-icon\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 5h12M4 8h8M6 11h4\"\/><circle cx=\"8\" cy=\"13.5\" r=\"0.6\" fill=\"currentColor\"\/><\/svg>\n              <\/div>\n              <div class=\"hd-section-title\" data-i18n=\"hd.network\">Network<\/div>\n              <svg class=\"hd-section-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6l4 4 4-4\"\/><\/svg>\n            <\/div>\n            <div class=\"hd-info-grid\" id=\"hd-network-info\"><\/div>\n          <\/div>\n          <div class=\"hd-section collapsible collapsed\" id=\"hd-section-mobile\">\n            <div class=\"hd-section-head\">\n              <div class=\"hd-section-icon\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"5\" y=\"1.5\" width=\"6\" height=\"13\" rx=\"1\"\/><path d=\"M7 12.5h2\"\/><\/svg>\n              <\/div>\n              <div class=\"hd-section-title\" data-i18n=\"hd.mobile\">Mobile Application<\/div>\n              <svg class=\"hd-section-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6l4 4 4-4\"\/><\/svg>\n            <\/div>\n            <div class=\"hd-info-grid\" id=\"hd-mobile-info\"><\/div>\n          <\/div>\n        <\/div>\n\n        <!-- Alarms \u2014 two panels: Recent + Most Frequent (moved above the health scoring windows) -->\n        <div class=\"hd-alarms\" id=\"hd-alarms\">\n          <div class=\"hd-section\">\n            <div class=\"hd-section-head\">\n              <div class=\"hd-section-icon\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 1.5v0M3 6.5a5 5 0 0 1 10 0v3.5l1.5 2H1.5l1.5-2V6.5zM6 13.5a2 2 0 0 0 4 0\"\/><\/svg>\n              <\/div>\n              <div class=\"hd-section-title\" data-i18n=\"hd.recentAlarms\">Recent Alarms \u00b7 Last 10 Triggered (Audit Log)<\/div>\n              <div class=\"hd-section-subtitle\" id=\"hd-recent-alarms-count\">\u2014<\/div>\n            <\/div>\n            <div class=\"hd-alarm-list\" id=\"hd-recent-alarms\"><\/div>\n          <\/div>\n          <div class=\"hd-section\">\n            <div class=\"hd-section-head\">\n              <div class=\"hd-section-icon\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M2 13V9M6 13V5M10 13V7M14 13V3\"\/><\/svg>\n              <\/div>\n              <div class=\"hd-section-title\" data-i18n=\"hd.alarmHistory\">Alarm History \u00b7 Occurrences<\/div>\n              <div class=\"hd-section-subtitle\" id=\"hd-frequent-alarms-count\">\u2014<\/div>\n            <\/div>\n            <div class=\"hd-alarm-list\" id=\"hd-frequent-alarms\"><\/div>\n          <\/div>\n        <\/div>\n\n        <!-- Structured Metrics \u2014 three collapsible panels in 3 columns:\n             System \/ Motor Control \/ Cooling (Fan), parsed from smartdrive-metrics.json -->\n        <div id=\"hd-metrics-body\"><\/div>\n      <\/div>\n    <\/div>\n\n    <!-- Rich Metrics viewer \u2014 SystemMetrics \/ CoolingManagementMetrics \/ MotorControllerMetrics -->\n    <div id=\"metricsViewer\">\n      <div class=\"mv-header\">\n        <div>\n          <div class=\"mv-header-title\" data-i18n=\"metrics.title\">Metrics<\/div>\n          <div class=\"mv-header-sub\" id=\"mv-header-filename\">\u2014<\/div>\n        <\/div>\n        <div class=\"mv-hdr-divider\"><\/div>\n        <div class=\"mv-hdr-stat\">Snapshot: <strong id=\"mv-header-updated\">\u2014<\/strong><\/div>\n        <div class=\"mv-hdr-spacer\"><\/div>\n        <div class=\"view-toggle\" id=\"mv-view-toggle\">\n          <button data-mode=\"structured\" id=\"mv-mode-structured\" class=\"active\" title=\"Structured view\">\n            <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M2 3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V3zm0 5a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V8zm1 4a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1H3z\"\/><\/svg>\n            Structured\n          <\/button>\n          <button data-mode=\"raw\" id=\"mv-mode-raw\" title=\"Raw JSON view\">\n            <svg viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M2 3h12v1.5H2V3zm0 3.25h12v1.5H2v-1.5zm0 3.25h12V11H2V9.5zm0 3.25h8v1.5H2v-1.5z\"\/><\/svg>\n            Raw\n          <\/button>\n        <\/div>\n        <div class=\"mv-hdr-time\" id=\"mv-header-time\">\u2014<\/div>\n      <\/div>\n      <div class=\"mv-body\" id=\"mv-body\">\n        <div class=\"mv-empty\">No metrics data.<\/div>\n      <\/div>\n      <div class=\"mv-raw\" id=\"mv-raw\" style=\"display:none\"><\/div>\n    <\/div>\n\n\n    <!-- Rich Configuration viewer -->\n    <div id=\"editorViewer\">\n<div id=\"ceRoot\" class=\"ce-scope\">\n  <div class=\"ce-dropbar\" id=\"editorDrop\" role=\"button\" tabindex=\"0\" title=\"Load a configuration file to edit\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\"\/><path d=\"M14 3v5h5\"\/><path d=\"M12 18v-6\"\/><path d=\"m9.5 14 2.5-2.5 2.5 2.5\"\/><\/svg><span id=\"editorDropText\" data-i18n-html=\"editor.drop\">Drag &amp; drop a configuration <b>.json<\/b> here, or <span class=\"ce-dropbar-link\">click to upload<\/span> a file to edit<\/span><\/div>\n  <div class=\"ce-toolbar\">\n    <button id=\"ceResetBtn\" data-i18n=\"editor.reset\">Reset<\/button>\n    <button id=\"ceWizardBtn\" data-i18n=\"editor.preset\" data-i18n-title=\"editor.presetTitle\" title=\"Apply a preset application configuration\">Preset Application Config.<\/button>\n    <button id=\"ceSaveBtn\" class=\"primary\" data-i18n=\"editor.save\">Save JSON<\/button>\n  <\/div>\n  <div class=\"disclaimer\" data-i18n-html=\"editor.disclaimer\"><b>DISCLAIMER:<\/b> This editor is provided \"as is\" without support. It does not validate parameter ranges or inter-dependencies. Always review all settings against official SmartD documentation before use. Save the configuration, copy it to a USB drive as <code>smartdrive-config.json<\/code>, and import it into the Clean Power VFD.<\/div>\n  <div class=\"help-drawer-overlay\" id=\"ceHdOverlay\"><\/div>\n  <div class=\"help-drawer\" id=\"ceHelpDrawer\">\n    <div class=\"help-drawer-header\">\n      <div class=\"help-drawer-title\" id=\"ceHdTitle\">Help<\/div>\n      <button class=\"help-drawer-close\" id=\"ceHdClose\">&#215;<\/button>\n    <\/div>\n    <div class=\"help-drawer-body\"><div id=\"ceHdContent\"><\/div><\/div>\n  <\/div>\n  <section class=\"editor\" id=\"ceEditor\"><\/section>\n  <div class=\"overlay\" id=\"ceWizardOverlay\">\n    <div class=\"modal\" style=\"width:min(1000px,96vw)\">\n      <div class=\"modal-hd\">\n        <div class=\"modal-hd-title\" data-i18n=\"editor.wizTitle\">Preset Application Configuration \u2014 Select Application<\/div>\n        <div style=\"display:flex;gap:8px\">\n          <button id=\"ceWizardCancel\" data-i18n=\"editor.wizCancel\">Cancel<\/button>\n          <button id=\"ceWizardApply\" class=\"apply\" data-i18n=\"editor.wizApply\">Apply &amp; Edit<\/button>\n        <\/div>\n      <\/div>\n      <div class=\"modal-bd\">\n        <p style=\"font-size:13px;color:var(--m);margin:0 0 14px\" data-i18n-html=\"editor.wizDesc\">Choose the closest application. Key parameters will be set and <strong style=\"color:var(--t)\">highlighted in the editor<\/strong> for review.<\/p>\n        <div class=\"app-grid\" id=\"ceAppGrid\"><\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n  <script id=\"ceDefaultConfig\" type=\"application\/json\">{\"_metadata\":{\"description\":\"\",\"label\":\"\",\"deviceModel\":\"SDB-1-2185-A\",\"version\":\"v2\",\"minorVersion\":\"115\"},\"configurations\":{\"system\":{\"driveName\":\"VFD\",\"driveNotes\":\"\",\"controlMode\":{\"switchControlModeBehavior\":\"SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_UNSPECIFIED\",\"remoteControlSource1\":\"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED\",\"remoteControlSource2\":\"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED\",\"controlSourceSelector\":\"SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_LOCAL\"},\"emergencyMode\":{\"state\":\"SYSTEM_EMERGENCY_MODE_STATE_DISABLED\",\"action\":\"SYSTEM_EMERGENCY_MODE_ACTION_UNSPECIFIED\"},\"mobilePermissions\":\"SYSTEM_MOBILE_PERMISSION_FULL_CONTROL\"},\"line\":{\"frequency\":60,\"phases\":\"LINE_PHASES_THREE\",\"nominalVoltage\":480},\"motor\":{\"motorNameplate\":{\"nominalPower\":18.5,\"nominalCurrent\":30,\"nominalVoltage\":460,\"nominalVelocity\":1755,\"nominalInputFrequency\":60,\"minInputFrequency\":1,\"maxInputFrequency\":60,\"numberOfPoles\":4,\"phaseInversion\":true,\"timeBetweenStarts\":\"10s\",\"nominalPowerUnit\":\"MOTOR_CONTROLLER_POWER_UNIT_W\",\"powerFactor\":0.85,\"brand\":\"\",\"reference\":\"\"},\"algorithm\":{\"controlMode\":\"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_FREQUENCY_V_F\",\"constantVf\":{\"startTorqueBoost\":{\"state\":\"MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_DISABLED\",\"voltagePercentage\":0,\"cutoffFrequencyPercentage\":25},\"enableOvermodulation\":false,\"start\":{\"mode\":\"MOTOR_CONTROLLER_VF_START_MODE_START_FROM_ZERO\"}},\"foc\":{\"enableOvermodulation\":false,\"speedControllerResponse\":\"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_NORMAL\",\"speedSource\":\"MOTOR_CONTROLLER_FOC_SPEED_SOURCE_ESTIMATED\",\"ecoMode\":\"MOTOR_CONTROLLER_FOC_ECO_MODE_OFF\"}},\"ramps\":{\"frequencyRampUpCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}],\"frequencyRampDownCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}],\"velocityRampUpCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}],\"velocityRampDownCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}],\"torqueRampUpCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}],\"torqueRampDownCurves\":[{\"linear\":{\"rampDuration\":\"20s\"},\"selectable\":\"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\"}]},\"limits\":{\"maxCurrent\":{\"threshold\":0},\"maxStartCurrent\":{\"threshold\":0},\"maxMotoringPower\":{\"threshold\":300},\"maxGeneratingPower\":{\"threshold\":-300},\"lowTorque\":{\"threshold\":-300},\"highTorque\":{\"threshold\":300},\"minVelocity\":{\"threshold\":0},\"maxVelocity\":{\"threshold\":0}},\"presets\":{\"frequencies\":[],\"velocities\":[]},\"advancedParameters\":{\"statorResistance\":null,\"rotorResistance\":null,\"magnetizingInductance\":null,\"leakageInductance\":null,\"momentOfInertia\":null},\"autoRestart\":{\"mode\":\"MOTOR_CONTROLLER_AUTO_RESTART_MODE_DISABLED\",\"maximumRetries\":5,\"restartDelay\":\"10s\"},\"emergencyRun\":{\"frequency\":0,\"velocity\":0,\"direction\":\"MOTOR_CONTROLLER_DIRECTION_FORWARD\"},\"thermalProtection\":{\"nominalLoad\":100,\"coolingType\":\"MOTOR_CONTROLLER_COOLING_TYPE_SELF_COOLED\",\"temperatureAtPowerUp\":\"MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_COLD_START\"},\"options\":{\"reverseEnabled\":true,\"zeroSpeedBehaviour\":\"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_STOP_RAMP_DOWN\",\"lowVoltageRideThrough\":\"MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_ENABLED\",\"normalStopBehaviour\":\"MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_RAMP_DOWN\",\"bumplessDirectionChange\":\"MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_ENABLED\",\"spinningDirectionPriority\":\"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_RAISE_ALARM\",\"voltageDropCompensations\":\"MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_ENABLED\"}},\"dateTime\":{\"protocol\":\"DATE_TIME_UPDATER_PROTOCOL_NTP\",\"timezone\":\"America\/Toronto\",\"timeServerUri\":[],\"disableBleSynchronization\":false},\"ios\":{\"digitalInputs\":[{\"index\":1,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"},{\"index\":2,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"},{\"index\":3,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"},{\"index\":4,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"},{\"index\":5,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"},{\"index\":6,\"function\":\"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\",\"triggerType\":\"IO_TRIGGER_TYPE_UNSPECIFIED\",\"debounceDelay\":\"0.200\"}],\"relays\":[{\"index\":1,\"output\":\"IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT\",\"function\":\"IO_RELAY_FUNCTION_UNSPECIFIED\"},{\"index\":2,\"output\":\"IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT\",\"function\":\"IO_RELAY_FUNCTION_UNSPECIFIED\"},{\"index\":3,\"output\":\"IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT\",\"function\":\"IO_RELAY_FUNCTION_UNSPECIFIED\"}],\"analogInputs\":[{\"index\":1,\"function\":\"IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED\",\"mode\":\"IO_ANALOG_INPUT_MODE_UNSPECIFIED\",\"curvePoints\":[{\"x\":0,\"y\":0},{\"x\":0,\"y\":0}]},{\"index\":2,\"function\":\"IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED\",\"mode\":\"IO_ANALOG_INPUT_MODE_UNSPECIFIED\",\"curvePoints\":[{\"x\":0,\"y\":0},{\"x\":0,\"y\":0}]},{\"index\":3,\"function\":\"IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED\",\"mode\":\"IO_ANALOG_INPUT_MODE_UNSPECIFIED\",\"curvePoints\":[{\"x\":0,\"y\":0},{\"x\":0,\"y\":0}]}],\"analogOutputs\":[{\"index\":1,\"function\":\"IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED\",\"mode\":\"IO_ANALOG_OUTPUT_MODE_UNSPECIFIED\",\"curvePoints\":[{\"x\":0,\"y\":0},{\"x\":0,\"y\":0}]},{\"index\":2,\"function\":\"IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED\",\"mode\":\"IO_ANALOG_OUTPUT_MODE_UNSPECIFIED\",\"curvePoints\":[{\"x\":0,\"y\":0},{\"x\":0,\"y\":0}]}],\"digitalInputsPowerDirection\":\"IO_DIGITAL_INPUTS_POWER_DIRECTION_SOURCE\"},\"modbus\":{\"active\":false},\"network\":{\"mode\":\"NETWORK_SWITCH_MODE_UNSPECIFIED\",\"interfaces\":[{\"method\":\"NETWORK_CONFIGURATION_METHOD_DHCP\",\"ipv4Address\":\"\",\"ipv4NetworkMask\":\"\",\"ipv4Gateway\":\"\"}],\"dns\":{\"method\":\"NETWORK_CONFIGURATION_METHOD_DHCP\",\"resolvers\":[\"1.1.1.1\",\"8.8.8.8\"]},\"ping\":{\"mode\":\"NETWORK_PING_MODE_DISABLED\"}},\"ble\":{\"advertisingState\":\"BLE_ADVERTISING_STATE_ENABLED\",\"pairingTimeout\":\"0s\"},\"bacnet\":{\"active\":false,\"deviceId\":100,\"deviceName\":\"VFD\",\"port\":47808},\"ethernetIp\":null,\"display\":{\"screenBrightness\":80,\"screensaverConfiguration\":{\"timeout\":\"300s\",\"mode\":\"DISPLAY_SCREENSAVER_MODE_BACKLIGHT_OFF\"}},\"softwareUpdate\":{\"selectedSource\":\"SOFTWARE_UPDATE_SOURCE_TYPE_SMARTD\",\"lanUrl\":null},\"pids\":{\"instances\":[{\"index\":\"PID_INDEX_1\",\"enable\":false,\"kP\":10,\"tI\":1,\"tD\":10000,\"engineeringUnit\":\"PID_ENGINEERING_UNIT_UNSPECIFIED\",\"engineeringUnitCustom\":\"\",\"minProcessValue\":0,\"maxProcessValue\":100,\"controlMode\":\"PID_CONTROL_MODE_REVERSE\",\"minControlOutput\":0,\"maxControlOutput\":100,\"controlOutputCompensation\":0,\"sleepThreshold\":0,\"sleepDelay\":\"0s\",\"wakeupThreshold\":0,\"wakeupDelay\":\"0s\",\"forcedOutputPreset\":1,\"forcedOutputType\":\"PID_FORCED_OUTPUT_TYPE_VALUE\",\"deadbandValue\":0,\"deadbandDelay\":\"0s\",\"processRampUpRate\":0,\"processRampDownRate\":0,\"wakeupThresholdError\":0,\"defaultSetpoint\":80,\"sensorOffset\":0}]},\"alarms\":{\"configs\":[{\"code\":\"ALARM_CODE_MOTOR_OVERCURRENT\",\"disabled\":false,\"triggerThreshold\":150,\"triggerDelay\":\"0s\",\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_FREEWHEEL\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_DC_OVERVOLTAGE\",\"disabled\":false,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_FREEWHEEL\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_DC_UNDERVOLTAGE\",\"disabled\":false,\"triggerThreshold\":null,\"triggerDelay\":\"0.5s\",\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_RAMP_DOWN\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_MOTOR_THERMAL_OVERLOAD\",\"disabled\":false,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_RAMP_DOWN\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_MOTOR_GROUND_FAULT\",\"disabled\":false,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_FREEWHEEL\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_EXTERNAL_1\",\"disabled\":true,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_FREEWHEEL\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_EXTERNAL_2\",\"disabled\":true,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_RAMP_DOWN\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_EXTERNAL_3\",\"disabled\":true,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_NO_ACTION\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_EXTERNAL_4\",\"disabled\":true,\"triggerThreshold\":null,\"triggerDelay\":null,\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_NO_ACTION\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"},{\"code\":\"ALARM_CODE_MODBUS_COMM_LOSS\",\"disabled\":true,\"triggerThreshold\":null,\"triggerDelay\":\"5s\",\"resetThreshold\":null,\"resetDelay\":null,\"triggerActions\":[{\"action\":\"ALARM_ACTION_STOP_RAMP_DOWN\"}],\"bootState\":\"ALARM_BOOT_STATE_NORMAL\"}]}}}<\/script>\n<\/div>\n    <\/div>\n\n    <div id=\"reportViewer\">\n      <div class=\"rv-inner\">\n        <div class=\"rv-card\">\n          <h2 class=\"rv-title\" data-i18n=\"report.title\">Commissioning Report<\/h2>\n          <p class=\"rv-desc\" data-i18n=\"report.desc\">Generate an IEEE 3007.2 \u00b7 IEC 61800 \u00b7 NEMA MG-1 commissioning report from the loaded configuration. Pick the sections, add commissioning details, and export as PDF or XML.<\/p>\n          <div id=\"rvSummary\" class=\"rv-summary\"><\/div>\n          <button class=\"shell-btn\" id=\"rvOpenBtn\" data-i18n=\"report.open\">Open Report Generator<\/button>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div id=\"configViewer\">\n      <div class=\"cfg-header\">\n        <div>\n          <div class=\"cfg-header-title\">Configuration<\/div>\n          <div class=\"cfg-header-sub\" id=\"config-filename\">\u2014<\/div>\n        <\/div>\n        <div class=\"cfg-hdr-divider\"><\/div>\n        <div class=\"cfg-hdr-stat\"><span data-i18n=\"cfg.device\">Device:<\/span> <strong id=\"config-device\">\u2014<\/strong><\/div>\n        <div class=\"cfg-hdr-divider\"><\/div>\n        <div class=\"cfg-hdr-stat\"><span data-i18n=\"cfg.version\">Version:<\/span> <strong id=\"config-version\">\u2014<\/strong><\/div>\n        <div class=\"cfg-hdr-spacer\"><\/div>\n        <button class=\"audit-btn\" id=\"commissioning-report-btn\" title=\"Open Commissioning Report generator\" style=\"display:inline-flex;align-items:center;gap:6px\">\n          <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\" aria-hidden=\"true\"><path d=\"M2 1.5h5.5L10 4v6.5A1 1 0 0 1 9 11.5H2A1 1 0 0 1 1 10.5v-8A1 1 0 0 1 2 1.5zM7 1.5V4h3M3.5 6h5M3.5 8h5M3.5 4h1.5\" stroke=\"currentColor\" stroke-width=\"1.1\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n          <span data-i18n=\"cfg.commReport\">Commissioning Report<\/span>\n        <\/button>\n        <div class=\"cfg-hdr-time\" id=\"config-current-time\"><\/div>\n      <\/div>\n\n      <div class=\"cfg-metadata\" id=\"cfg-metadata\"><\/div>\n\n      <div class=\"cfg-body\">\n        <aside class=\"cfg-sidebar\">\n          <div class=\"cfg-sidebar-head\">\n            <input type=\"text\" class=\"cfg-search\" id=\"cfg-search\" data-i18n-ph=\"cfg.searchFields\" placeholder=\"Search fields\u2026\">\n          <\/div>\n          <nav class=\"cfg-nav\" id=\"cfg-nav\"><\/nav>\n        <\/aside>\n        <main class=\"cfg-main\" id=\"cfg-main\"><\/main>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- Portalled column-filter dropdowns (Event Type, Severity, Origin, User) -->\n<div class=\"multiselect-dropdown\" id=\"cf-event-dropdown\">\n  <div class=\"ms-actions\">\n    <button class=\"ms-action-btn\" id=\"cf-event-all\">Select All<\/button>\n    <button class=\"ms-action-btn\" id=\"cf-event-none\">Deselect All<\/button>\n  <\/div>\n  <div class=\"ms-list\" id=\"cf-event-list\"><\/div>\n<\/div>\n<div class=\"multiselect-dropdown\" id=\"cf-severity-dropdown\">\n  <div class=\"ms-actions\">\n    <button class=\"ms-action-btn\" id=\"cf-severity-all\">Select All<\/button>\n    <button class=\"ms-action-btn\" id=\"cf-severity-none\">Deselect All<\/button>\n  <\/div>\n  <div class=\"ms-list\" id=\"cf-severity-list\"><\/div>\n<\/div>\n<div class=\"multiselect-dropdown\" id=\"cf-origin-dropdown\">\n  <div class=\"ms-actions\">\n    <button class=\"ms-action-btn\" id=\"cf-origin-all\">Select All<\/button>\n    <button class=\"ms-action-btn\" id=\"cf-origin-none\">Deselect All<\/button>\n  <\/div>\n  <div class=\"ms-list\" id=\"cf-origin-list\"><\/div>\n<\/div>\n<div class=\"multiselect-dropdown\" id=\"cf-user-dropdown\">\n  <div class=\"ms-actions\">\n    <button class=\"ms-action-btn\" id=\"cf-user-all\">Select All<\/button>\n    <button class=\"ms-action-btn\" id=\"cf-user-none\">Deselect All<\/button>\n  <\/div>\n  <div class=\"ms-list\" id=\"cf-user-list\"><\/div>\n<\/div>\n<div class=\"multiselect-dropdown\" id=\"cf-color-dropdown\">\n  <div class=\"ms-actions\">\n    <button class=\"ms-action-btn\" id=\"cf-color-all\" type=\"button\">Select All<\/button>\n    <button class=\"ms-action-btn\" id=\"cf-color-none\" type=\"button\">Deselect All<\/button>\n  <\/div>\n  <div class=\"ms-list\" id=\"cf-color-list\">\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-motor-on\" checked><span class=\"cc-box\" style=\"background:#BBF7D0\"><\/span><span class=\"ms-item-label\">Motor ON<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-motor-off\" checked><span class=\"cc-box\" style=\"background:#FECACA\"><\/span><span class=\"ms-item-label\">Motor OFF<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-state-change\" checked><span class=\"cc-box\" style=\"background:#BAE6FD\"><\/span><span class=\"ms-item-label\">State Change<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-control-mode\" checked><span class=\"cc-box\" style=\"background:#E9D5FF\"><\/span><span class=\"ms-item-label\">Control Mode<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-login\" checked><span class=\"cc-box\" style=\"background:#B4FFE7\"><\/span><span class=\"ms-item-label\">Login Attempt<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-alarm\" checked><span class=\"cc-box\" style=\"background:#D32F2F\"><\/span><span class=\"ms-item-label\">Alarm<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-ready\" checked><span class=\"cc-box\" style=\"background:#3fb950\"><\/span><span class=\"ms-item-label\">Ready<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-lockout\" checked><span class=\"cc-box\" style=\"background:#fbbf24\"><\/span><span class=\"ms-item-label\">Lockout<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"row-config\" checked><span class=\"cc-box\" style=\"background:#3b82f6\"><\/span><span class=\"ms-item-label\">Config<\/span><\/label>\n    <label class=\"ms-item\"><input type=\"checkbox\" data-color-key=\"\" checked><span class=\"cc-box cc-none\" style=\"background:#e9edf2\"><\/span><span class=\"ms-item-label\">No highlight<\/span><\/label>\n  <\/div>\n<\/div>\n\n<!-- SmartD API Reference modal -->\n<div id=\"apiRefModal\" class=\"api-modal\" style=\"display:none\">\n  <div class=\"api-modal-backdrop\" id=\"apiRefBackdrop\"><\/div>\n  <div class=\"api-modal-panel\">\n    <div class=\"api-modal-header\">\n      <div>\n        <div class=\"api-modal-title\">SmartD gRPC API Reference<\/div>\n        <div class=\"api-modal-sub\" id=\"apiRefVersion\">\u2014<\/div>\n      <\/div>\n      <button class=\"api-modal-close\" id=\"apiRefClose\" aria-label=\"Close\">\u00d7<\/button>\n    <\/div>\n    <div class=\"api-modal-toolbar\">\n      <input type=\"text\" class=\"api-modal-search\" id=\"apiRefSearch\" placeholder=\"Search enum names, codes, descriptions\u2026\" autocomplete=\"off\">\n      <select class=\"api-modal-filter\" id=\"apiRefCategory\">\n        <option value=\"\">All categories<\/option>\n        <option value=\"AuditLogEvent\">Audit Log Events<\/option>\n        <option value=\"AuditLogOrigin\">Audit Log Origins<\/option>\n        <option value=\"AuditLogSeverity\">Audit Log Severities<\/option>\n        <option value=\"AlarmCode\">Alarm Codes<\/option>\n        <option value=\"AlarmSeverity\">Alarm Severities<\/option>\n        <option value=\"AlarmState\">Alarm States<\/option>\n        <option value=\"SystemState\">System States<\/option>\n        <option value=\"MotorControllerState\">Motor Controller States<\/option>\n        <option value=\"ResponseStatus\">Response Statuses<\/option>\n      <\/select>\n      <span class=\"api-modal-count\" id=\"apiRefCount\">\u2014<\/span>\n    <\/div>\n    <div class=\"api-modal-body\" id=\"apiRefBody\"><\/div>\n  <\/div>\n<\/div>\n\n<!-- Alarm description popup (opened by clicking a row in the Health Dashboard alarm panels) -->\n<div id=\"alarmHelpModal\" class=\"api-modal\" style=\"display:none\">\n  <div class=\"api-modal-backdrop\" id=\"alarmHelpBackdrop\"><\/div>\n  <div class=\"api-modal-panel alarm-help-panel\">\n    <div class=\"api-modal-header\">\n      <div>\n        <div class=\"api-modal-title\" id=\"alarmHelpTitle\">\u2014<\/div>\n        <div class=\"api-modal-sub\" id=\"alarmHelpSub\">\u2014<\/div>\n      <\/div>\n      <button class=\"api-modal-close\" id=\"alarmHelpClose\" aria-label=\"Close\">\u00d7<\/button>\n    <\/div>\n    <div class=\"api-modal-body\" id=\"alarmHelpBody\"><\/div>\n  <\/div>\n<\/div>\n\n<!-- CONFIG_CHANGE diff modal \u2014 compares a Set Configuration payload with the previous config change -->\n<div id=\"ccModal\" class=\"api-modal\" style=\"display:none\">\n  <div class=\"api-modal-backdrop\" id=\"ccBackdrop\"><\/div>\n  <div class=\"api-modal-panel cc-panel\">\n    <div class=\"api-modal-header\">\n      <div>\n        <div class=\"api-modal-title\" id=\"ccTitle\">Configuration Change<\/div>\n        <div class=\"api-modal-sub\" id=\"ccSub\">\u2014<\/div>\n      <\/div>\n      <button class=\"api-modal-close\" id=\"ccClose\" aria-label=\"Close\">\u00d7<\/button>\n    <\/div>\n    <div class=\"cc-toolbar\">\n      <div class=\"cc-modes\">\n        <button class=\"cc-mode-btn active\" id=\"ccModeChanges\">Changes only<\/button>\n        <button class=\"cc-mode-btn\" id=\"ccModeAll\">View all<\/button>\n        <button class=\"cc-mode-btn\" id=\"ccModeRaw\">Raw<\/button>\n      <\/div>\n      <div class=\"cc-counts\" id=\"ccCounts\"><\/div>\n    <\/div>\n    <div class=\"cc-previnfo\" id=\"ccPrevInfo\"><\/div>\n    <div class=\"api-modal-body cc-body\" id=\"ccBody\"><\/div>\n  <\/div>\n<\/div>\n\n<!-- Commissioning Report modal -->\n<div id=\"crModal\" class=\"cr-modal\" style=\"display:none\">\n  <div class=\"cr-modal-backdrop\" id=\"crBackdrop\"><\/div>\n  <div class=\"cr-modal-panel\">\n    <div class=\"cr-modal-header\">\n      <div>\n        <div class=\"cr-modal-title\">Commissioning Report<\/div>\n        <div class=\"cr-modal-sub\">IEEE 3007.2 \u00b7 IEC 61800 \u00b7 NEMA MG-1<\/div>\n      <\/div>\n      <button class=\"cr-modal-close\" id=\"crClose\" aria-label=\"Close\">\u00d7<\/button>\n    <\/div>\n    <div class=\"cr-modal-body\">\n      <div class=\"cr-sec-label\">Configuration Preview<\/div>\n      <div class=\"cr-preview-grid\" id=\"crPreview\"><\/div>\n\n      <div class=\"cr-sec-label\" style=\"display:flex;justify-content:space-between;align-items:center\">\n        <span>Report Sections <span class=\"cr-sec-count\" id=\"crSecCount\">\u2014<\/span><\/span>\n        <span class=\"cr-sec-mini-btns\">\n          <button class=\"cr-mini-btn\" id=\"crAllBtn\" type=\"button\">All<\/button>\n          <button class=\"cr-mini-btn\" id=\"crNoneBtn\" type=\"button\">None<\/button>\n        <\/span>\n      <\/div>\n      <div class=\"cr-sec-grid\" id=\"crSecGrid\"><\/div>\n\n      <div class=\"cr-sec-label\">Commissioning Details<\/div>\n      <div class=\"cr-fields-grid\">\n        <div class=\"cr-field-wrap\">\n          <label class=\"cr-field-label\" for=\"crTech\">Technician Name<\/label>\n          <input class=\"cr-field-input\" id=\"crTech\" placeholder=\"e.g. John Smith\">\n        <\/div>\n        <div class=\"cr-field-wrap\">\n          <label class=\"cr-field-label\" for=\"crCompany\">Company Name<\/label>\n          <input class=\"cr-field-input\" id=\"crCompany\" placeholder=\"e.g. Acme Engineering Inc.\">\n        <\/div>\n        <div class=\"cr-field-wrap full\">\n          <label class=\"cr-field-label\" for=\"crLocation\">Site \/ Location<\/label>\n          <input class=\"cr-field-input\" id=\"crLocation\" placeholder=\"e.g. Plant A \u2014 Building 3, Montr\u00e9al, QC\">\n        <\/div>\n        <div class=\"cr-field-wrap\">\n          <label class=\"cr-field-label\" for=\"crWO\">Work Order \/ PO<\/label>\n          <input class=\"cr-field-input\" id=\"crWO\" placeholder=\"e.g. WO-2026-0042\">\n        <\/div>\n        <div class=\"cr-field-wrap\">\n          <label class=\"cr-field-label\" for=\"crDate\">Commissioning Date<\/label>\n          <input class=\"cr-field-input\" id=\"crDate\" type=\"date\">\n        <\/div>\n      <\/div>\n    <\/div>\n    <div class=\"cr-modal-footer\">\n      <div class=\"cr-fmt-row\">\n        <button class=\"cr-fmt-btn active\" id=\"crFmtPDF\" type=\"button\">\ud83d\udcc4 PDF<\/button>\n        <button class=\"cr-fmt-btn\" id=\"crFmtXML\" type=\"button\">\ud83d\uddc2\ufe0f XML<\/button>\n      <\/div>\n      <button class=\"cr-generate-btn\" id=\"crGenBtn\" type=\"button\">\u2b07 Generate<\/button>\n      <div class=\"cr-status\" id=\"crStatus\"><\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<div id=\"toast\" class=\"toast\"><\/div>\n\n<\/div>\n\n<script>\n\/* =====================================================================\n   Support Package Viewer \u2014 merged (shell + audit + config)\n   ===================================================================== *\/\n\nconst TAB_DEFS = [\n  { id: 'health', label: 'Summary',       filename: null,                     type: 'virtual', mode: 'health' },\n  { id: 'config', label: 'Config Viewer', filename: 'smartdrive-config.json', type: 'json', mode: 'config' },\n  { id: 'editor', label: 'Config Editor', filename: 'smartdrive-config.json', type: 'json', mode: 'editor' },\n  { id: 'audit',  label: 'Audit Log',     filename: 'audit-log.json',         type: 'json', mode: 'audit'  },\n  { id: 'report', label: 'Report',        filename: 'smartdrive-config.json', type: 'json', mode: 'report' },\n];\n\n\/\/ All recognized support-package files. Every file listed here is extracted into\n\/\/ state.files on load so the Health Dashboard can aggregate them, even though\n\/\/ they no longer have their own tabs.\nconst KNOWN_PACKAGE_FILES = [\n  { filename: 'smartdrive-config.json' },\n  { filename: 'audit-log.json' },\n  { filename: 'device-info.log' },\n  { filename: 'device-status.log' },\n  { filename: 'system-state-and-alarms.json', aliases: ['system-alarms.log', 'system-alarms'] },\n  { filename: 'smartdrive-metrics.json' },\n  { filename: 'smartdrive-opdata.json' },\n  { filename: 'version.log' }\n];\n\nconst state = {\n  files: {}, extras: [],\n  activeTabId: null, packageName: null, packageSize: 0,\n  wrap: false, codeSearchTerm: '',\n  auditLoaded: false, configLoaded: false,\n  configObj: null, editorInited: false, loadKind: null,\n  fileViewMode: 'structured',\n  landingIntent: null,\n  topMode: 'home', pendingTopMode: null,\n};\nconst FULL_ACCEPT = '.tar.gz,.tgz,.zip,.gz,.json';\n\nconst SMARTD_SPV_ROOT = document.getElementById('smartd-support-package-analyzer');\nconst $  = (sel) => (SMARTD_SPV_ROOT || document).querySelector(sel);\nconst $$ = (sel) => Array.from((SMARTD_SPV_ROOT || document).querySelectorAll(sel));\nconst escapeHtml = (s) => String(s == null ? '' : s).replace(\/[&<>\"']\/g, c => ({\n  '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'\n}[c]));\nconst humanSize = (bytes) => {\n  if (bytes < 1024) return bytes + ' B';\n  if (bytes < 1024*1024) return (bytes\/1024).toFixed(1) + ' KB';\n  return (bytes\/1024\/1024).toFixed(2) + ' MB';\n};\nfunction debounce(fn, ms) { let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }\n\n\/\/ ---------- TAR parser (USTAR) ----------\nfunction parseTar(buf) {\n  const out = []; let pos = 0; let longName = null; const td = new TextDecoder();\n  const readStr = (offset, len) => {\n    let end = offset; const limit = offset + len;\n    while (end < limit && buf[end] !== 0) end++;\n    return td.decode(buf.subarray(offset, end));\n  };\n  const readOctal = (offset, len) => {\n    const s = readStr(offset, len).trim();\n    return s ? parseInt(s, 8) : 0;\n  };\n  while (pos + 512 <= buf.length) {\n    let allZero = true;\n    for (let i = 0; i < 512; i++) if (buf[pos + i] !== 0) { allZero = false; break; }\n    if (allZero) break;\n    let name = readStr(pos, 100);\n    const size = readOctal(pos + 124, 12);\n    const mtime = readOctal(pos + 136, 12);\n    const typeflag = String.fromCharCode(buf[pos + 156]);\n    const prefix = readStr(pos + 345, 155);\n    if (prefix) name = prefix + '\/' + name;\n    pos += 512;\n    const dataStart = pos;\n    pos += Math.ceil(size \/ 512) * 512;\n    if (typeflag === 'L') {\n      longName = td.decode(buf.subarray(dataStart, dataStart + size)).replace(\/\\0+$\/, '');\n      continue;\n    }\n    if (longName) { name = longName; longName = null; }\n    if (typeflag === '5' || typeflag === 'L' || typeflag === 'K' || typeflag === 'x' || typeflag === 'g') continue;\n    if (!name) continue;\n    out.push({ name, size, mtime, data: buf.subarray(dataStart, dataStart + size) });\n  }\n  return out;\n}\n\n\/\/ ---------- Package loading ----------\nasync function loadPackage(file) {\n  try {\n    resetView();\n    const buf = new Uint8Array(await file.arrayBuffer());\n    state.packageName = file.name;\n    state.packageSize = file.size;\n    \/\/ Bare JSON upload: SmartD\/Toshiba\/Schneider configuration file, or an audit log.\n    if (\/\\.json$\/i.test(file.name)) {\n      const jtext = new TextDecoder('utf-8', { fatal: false }).decode(buf);\n      let parsed;\n      try { parsed = JSON.parse(jtext); }\n      catch (e) { showError('Invalid JSON file \u2014 could not parse.'); return; }\n      const isConfig = parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.configurations && typeof parsed.configurations === 'object';\n      let auditArr = Array.isArray(parsed) ? parsed : null;\n      if (!auditArr && parsed && typeof parsed === 'object') {\n        const k = ['logs','entries','events','auditLog','audit_log','records','data','items'].find(kk => Array.isArray(parsed[kk]));\n        if (k) auditArr = parsed[k];\n      }\n      const isAudit = !isConfig && Array.isArray(auditArr) && auditArr.length > 0;\n      if (isConfig)     { state.files['smartdrive-config.json'] = { content: jtext, size: buf.length, mtime: 0 }; state.loadKind = 'config'; }\n      else if (isAudit) { state.files['audit-log.json']         = { content: jtext, size: buf.length, mtime: 0 }; state.loadKind = 'audit';  }\n      else { showError('Unrecognized JSON \u2014 expected a SmartD configuration (object with a \"configurations\" property) or an audit log (array of events).'); return; }\n      preInitRichViewers();\n      renderAfterLoad();\n      return;\n    }\n    let entries;\n    if (buf[0] === 0x1f && buf[1] === 0x8b) {\n      const inflated = window.pako.inflate(buf);\n      const isTar = inflated.length > 512 &&\n                    inflated[257] === 0x75 && inflated[258] === 0x73 &&\n                    inflated[259] === 0x74 && inflated[260] === 0x61 &&\n                    inflated[261] === 0x72;\n      if (isTar) entries = parseTar(inflated);\n      else { showError('Package is a gzip file but does not contain a tar archive.'); return; }\n    } else if (buf[0] === 0x50 && buf[1] === 0x4b) {\n      const zip = await window.JSZip.loadAsync(buf);\n      entries = [];\n      for (const name of Object.keys(zip.files)) {\n        const f = zip.files[name];\n        if (f.dir) continue;\n        const data = await f.async('uint8array');\n        entries.push({ name, size: data.length, mtime: 0, data });\n      }\n    } else {\n      showError('Unrecognized file format. Expected a <code>.json<\/code> configuration\/audit-log file, or a <code>.tar.gz<\/code> \/ <code>.tgz<\/code> \/ <code>.zip<\/code> support package.');\n      return;\n    }\n    if (!entries.length) { showError('Archive appears to be empty.'); return; }\n    indexEntries(entries);\n    state.loadKind = 'package';\n    preInitRichViewers();\n    renderAfterLoad();\n  } catch (err) {\n    console.error(err);\n    showError('Failed to parse package: ' + escapeHtml(err.message || String(err)));\n  }\n}\n\nfunction indexEntries(entries) {\n  const wanted = new Set();\n  KNOWN_PACKAGE_FILES.forEach(f => {\n    wanted.add(f.filename);\n    (f.aliases || []).forEach(a => wanted.add(a));\n  });\n  const td = new TextDecoder('utf-8', { fatal: false });\n  const extras = [];\n  for (const e of entries) {\n    const base = e.name.split('\/').pop();\n    if (!base) continue;\n    if (wanted.has(base)) {\n      state.files[base] = { content: td.decode(e.data), size: e.size, mtime: e.mtime };\n    } else {\n      extras.push(base);\n    }\n  }\n  state.extras = extras;\n  state.allFiles = entries.map(e => {\n    const base = (e.name.split('\/').pop() || e.name);\n    return { name: base, path: e.name, size: e.size, content: td.decode(e.data) };\n  }).filter(f => f.name);\n}\n\nfunction preInitRichViewers() {\n  const cfg = state.files['smartdrive-config.json'];\n  if (cfg) {\n    try {\n      const parsed = JSON.parse(cfg.content);\n      if (parsed && typeof parsed === 'object' && parsed.configurations && typeof parsed.configurations === 'object') {\n        state.configObj = parsed;\n        initConfigViewer(parsed, 'smartdrive-config.json');\n        state.configLoaded = true;\n      }\n    } catch (e) { console.warn('Config parse failed:', e); }\n  }\n  const audit = state.files['audit-log.json'];\n  if (audit) {\n    try {\n      const parsed = JSON.parse(audit.content);\n      let data = parsed;\n      if (!Array.isArray(data)) {\n        if (data && typeof data === 'object') {\n          const key = ['logs','entries','events','auditLog','audit_log','records','data','items'].find(k => Array.isArray(data[k]));\n          if (key) data = data[key];\n          else {\n            const first = Object.keys(data).find(k => Array.isArray(data[k]));\n            if (first) data = data[first];\n          }\n        }\n      }\n      if (Array.isArray(data) && data.length) {\n        initAuditViewer(data, 'audit-log.json');\n        state.auditLoaded = true;\n      }\n    } catch (e) { console.warn('Audit log parse failed:', e); }\n  }\n\n  const met = state.files['smartdrive-metrics.json'];\n  if (met) {\n    try {\n      const parsed = JSON.parse(met.content);\n      if (parsed && typeof parsed === 'object') {\n        initMetricsViewer(parsed, 'smartdrive-metrics.json');\n        renderMetrics(parsed);\n        state.metricsLoaded = true;\n      }\n    } catch (e) { console.warn('Metrics parse failed:', e); }\n  }\n\n  \/\/ Health Dashboard aggregates config + metrics + device-info + hmi-info + version.log.\n  \/\/ Runs last so it can pick up whatever the other init blocks parsed.\n  try { initHealthDashboard(); }\n  catch (e) { console.warn('Health dashboard init failed:', e); }\n}\n\nfunction renderAfterLoad() {\n  renderPkgInfo();\n  renderTabs();\n  let target = null;\n  if (state.landingIntent) {\n    const wanted = TAB_DEFS.find(t => t.id === state.landingIntent);\n    if (wanted && getFileForTab(wanted)) target = wanted;\n  }\n  if (!target) target = TAB_DEFS.find(t => getFileForTab(t));\n  state.landingIntent = null;\n  if (state.loadKind === 'package') { state.topMode = 'package'; }\n  else if (target) {\n    const _m = { config:'config', report:'config', audit:'audit', editor:(state.pendingTopMode || 'edit'), health:'package' };\n    state.topMode = _m[target.id] || state.topMode;\n  }\n  state.pendingTopMode = null;\n  if (target) activateTab(target.id);\n  else { showError('No recognized data found. Please choose a configuration JSON, an audit-log JSON, or a support package.'); renderTopNav(); }\n}\n\nfunction getFileForTab(tabDef) {\n  if (tabDef.type === 'virtual') {\n    \/\/ Health dashboard is always accessible \u2014 aggregates whatever files are present.\n    if (tabDef.mode === 'health') {\n      \/\/ Summary aggregates package-level data (metrics, system state, device info,\n      \/\/ alarms), so it is only offered for a full support package.\n      return state.loadKind === 'package' ? { virtual: true, size: 0 } : null;\n    }\n    \/\/ Other virtual tabs (e.g. timeline) require multiple files to be present.\n    const reqs = tabDef.requires || [];\n    if (reqs.length && reqs.every(fn => state.files[fn])) {\n      const total = reqs.reduce((s, fn) => s + (state.files[fn].size || 0), 0);\n      return { virtual: true, size: total };\n    }\n    return null;\n  }\n  if (state.files[tabDef.filename]) return state.files[tabDef.filename];\n  for (const a of (tabDef.aliases || [])) if (state.files[a]) return state.files[a];\n  return null;\n}\nfunction getFileNameForTab(tabDef) {\n  if (tabDef.type === 'virtual') return tabDef.label;\n  if (state.files[tabDef.filename]) return tabDef.filename;\n  for (const a of (tabDef.aliases || [])) if (state.files[a]) return a;\n  return tabDef.filename;\n}\n\nfunction renderPkgInfo() {\n  const info = $('#pkgInfo');\n  info.style.display = 'flex';\n  info.innerHTML = `\n    <div class=\"item\"><strong>${escapeHtml(state.packageName)}<\/strong><\/div>\n  `;\n  $('#unloadBtn').style.display = 'inline-flex';\n}\n\nfunction renderTabs() {\n  \/\/ v1.0.9: horizontal tabstrip replaced by the dark top navigation bar +\n  \/\/ the VIEW SUPPORT PACKAGE sub-menu (Summary \/ Configuration \/ Audit Log).\n  renderTopNav();\n}\n\nfunction startFromScratch() {\n  resetView();\n  const defEl = document.getElementById('ceDefaultConfig');\n  const text = (defEl && defEl.textContent.trim()) || '{\"configurations\":{}}';\n  state.packageName = (typeof LANG!=='undefined' && LANG==='fr') ? 'Nouvelle configuration' : 'New configuration';\n  state.packageSize = text.length;\n  state.files['smartdrive-config.json'] = { content: text, size: text.length, mtime: 0 };\n  state.loadKind = 'config';\n  state.landingIntent = 'editor';\n  preInitRichViewers();\n  renderAfterLoad();\n}\n\nfunction renderReportPanel() {\n  const c = state.configObj;\n  const sum = $('#rvSummary');\n  if (sum) {\n    if (c) {\n      const sys = (c.configurations || {}).system || {}, m = c._metadata || {};\n      const ex = m.extraInformation || {};\n      const serial = m.deviceSerialNumber||m.serialNumber||m.serial_number||m.serial||c.deviceSerialNumber||c.serialNumber||ex.serial_number||ex.serialNumber||ex.deviceSerialNumber||'';\n      sum.innerHTML =\n        `<div class=\"rv-row\"><span data-i18n=\"report.driveName\">VFD Name<\/span><b>${escapeHtml(sys.driveName||'\u2014')}<\/b><\/div>` +\n        `<div class=\"rv-row\"><span data-i18n=\"report.configName\">Configuration Name<\/span><b>${escapeHtml(sys.configName||m.label||'\u2014')}<\/b><\/div>` +\n        `<div class=\"rv-row\"><span data-i18n=\"cfg.m.model\">Device Model<\/span><b>${escapeHtml(m.deviceModel||'\u2014')}<\/b><\/div>` +\n        `<div class=\"rv-row\"><span data-i18n=\"cfg.m.serial\">Serial Number<\/span><b>${escapeHtml(serial||'\u2014')}<\/b><\/div>`;\n    } else { sum.innerHTML = '<div class=\"rv-row\"><span>No configuration loaded<\/span><b>\u2014<\/b><\/div>'; }\n  }\n  const btn = $('#rvOpenBtn');\n  if (btn) btn.onclick = () => { if (state.configObj && window.openCommissioningReport) window.openCommissioningReport(state.configObj); };\n  if (typeof applyLang === 'function') applyLang(LANG);\n}\n\nfunction activateTab(tabId) {\n  const tabDef = TAB_DEFS.find(t => t.id === tabId);\n  if (!tabDef) return;\n  state.activeTabId = tabId;\n  $$('.shell-tab').forEach(el => el.classList.toggle('active', el.dataset.tabId === tabId));\n  $('#emptyState').style.display = 'none';\n  $('#fileView').style.display = 'none';\n  const auditEl = $('#auditViewer');\n  const configEl = $('#configViewer');\n  const metricsEl = $('#metricsViewer');\n  const healthEl = $('#healthViewer');\n  if (auditEl) auditEl.classList.remove('active');\n  if (configEl) configEl.classList.remove('active');\n  if (metricsEl) metricsEl.classList.remove('active');\n  if (healthEl) healthEl.classList.remove('active');\n  const editorEl = $('#editorViewer'); if (editorEl) editorEl.classList.remove('active');\n  const reportEl = $('#reportViewer'); if (reportEl) reportEl.classList.remove('active');\n\n  const file = getFileForTab(tabDef);\n\n  if (tabDef.mode === 'health') {\n    if (healthEl) healthEl.classList.add('active');\n  } else if (tabDef.mode === 'config') {\n    if (state.configLoaded && configEl) configEl.classList.add('active');\n    else fallbackToCode(tabDef, file, 'configuration');\n  } else if (tabDef.mode === 'audit') {\n    if (state.auditLoaded && auditEl) auditEl.classList.add('active');\n    else fallbackToCode(tabDef, file, 'audit log');\n  } else if (tabDef.mode === 'editor') {\n    if (editorEl) editorEl.classList.add('active');\n    if (state.configObj && window.SDEditor && !state.editorInited) {\n      window.SDEditor.init(state.configObj); state.editorInited = true;\n    }\n  } else if (tabDef.mode === 'report') {\n    if (reportEl) reportEl.classList.add('active');\n    renderReportPanel();\n  } else {\n    state.codeSearchTerm = '';\n    $('#codeSearchInput').value = '';\n    $('#codeSearchCount').textContent = '';\n    $('#fileView').style.display = 'flex';\n    $('#viewToggle').style.display = 'inline-flex';\n    renderActiveCodeFile(tabDef, file);\n    renderStructuredFile(tabDef, file);\n    applyFileViewMode();\n  }\n  renderTopNav();\n}\n\nfunction fallbackToCode(tabDef, file, kind) {\n  state.codeSearchTerm = '';\n  $('#codeSearchInput').value = '';\n  $('#codeSearchCount').textContent = '';\n  $('#fileView').style.display = 'flex';\n  $('#viewToggle').style.display = 'none';\n  renderActiveCodeFile(tabDef, file);\n  state.fileViewMode = 'raw';\n  $('#structuredView').innerHTML = '';\n  applyFileViewMode();\n  \/\/ Ensure search\/wrap are visible again when forced into raw mode\n  $('#wrapBtn').style.display = '';\n  $('#codeSearchInput').parentElement.style.display = '';\n  if (file) showToast(`Could not render ${kind} viewer \u2014 showing raw JSON.`, 'error');\n}\n\nfunction renderActiveCodeFile(tabDef, file) {\n  const actualName = getFileNameForTab(tabDef);\n  $('#curFilename').textContent = actualName;\n  const viewer = $('#codeViewer');\n  if (!file) {\n    $('#curFileinfo').textContent = '';\n    viewer.innerHTML = `\n      <div class=\"missing-file-notice\">\n        <h3>File not present in this package<\/h3>\n        <p>Expected: <code>${escapeHtml(tabDef.filename)}<\/code><\/p>\n      <\/div>`;\n    return;\n  }\n  $('#curFileinfo').textContent = `${humanSize(file.size)} \u00b7 ${countLines(file.content)} lines`;\n  const lines = file.content.split(\/\\r?\\n\/);\n  if (lines.length && lines[lines.length - 1] === '') lines.pop();\n  let htmlLines;\n  if (tabDef.type === 'json') {\n    let pretty = file.content;\n    try { pretty = JSON.stringify(JSON.parse(file.content), null, 2); } catch (e) {}\n    htmlLines = pretty.split('\\n').map(highlightJsonLine);\n  } else {\n    htmlLines = lines.map(highlightLogLine);\n  }\n  const rows = [];\n  for (let i = 0; i < htmlLines.length; i++) {\n    rows.push(`<div class=\"ln\">${i+1}<\/div><div class=\"lc\">${htmlLines[i] || '&nbsp;'}<\/div>`);\n  }\n  viewer.innerHTML = `<div class=\"code-table\" id=\"codeTable\">${rows.join('')}<\/div>`;\n  applyWrap();\n}\n\nfunction countLines(s) {\n  if (!s) return 0;\n  const n = (s.match(\/\\n\/g) || []).length;\n  return s.endsWith('\\n') ? n : n + 1;\n}\n\nfunction highlightJsonLine(line) {\n  const match = line.match(\/^(\\s*)(.*)$\/);\n  const indent = match[1];\n  const rest = match[2];\n  if (!rest) return escapeHtml(line);\n  let out = ''; let i = 0;\n  while (i < rest.length) {\n    const ch = rest[i];\n    if (ch === '\"') {\n      let j = i + 1;\n      while (j < rest.length) {\n        if (rest[j] === '\\\\' && j + 1 < rest.length) { j += 2; continue; }\n        if (rest[j] === '\"') break;\n        j++;\n      }\n      const str = rest.slice(i, j + 1);\n      let k = j + 1;\n      while (k < rest.length && \/\\s\/.test(rest[k])) k++;\n      if (rest[k] === ':') out += `<span class=\"tok-key\">${escapeHtml(str)}<\/span>`;\n      else out += `<span class=\"tok-str\">${escapeHtml(str)}<\/span>`;\n      i = j + 1; continue;\n    }\n    if (\/[-0-9]\/.test(ch)) {\n      const m = rest.slice(i).match(\/^-?(?:0|[1-9]\\d*)(?:\\.\\d+)?(?:[eE][+-]?\\d+)?\/);\n      if (m) { out += `<span class=\"tok-num\">${escapeHtml(m[0])}<\/span>`; i += m[0].length; continue; }\n    }\n    if (rest.startsWith('true', i))  { out += `<span class=\"tok-bool\">true<\/span>`;  i += 4; continue; }\n    if (rest.startsWith('false', i)) { out += `<span class=\"tok-bool\">false<\/span>`; i += 5; continue; }\n    if (rest.startsWith('null', i))  { out += `<span class=\"tok-null\">null<\/span>`;  i += 4; continue; }\n    if ('{}[],:'.includes(ch)) { out += `<span class=\"tok-punct\">${ch}<\/span>`; i++; continue; }\n    out += escapeHtml(ch); i++;\n  }\n  return escapeHtml(indent) + out;\n}\n\nfunction highlightLogLine(line) {\n  if (!line) return '';\n  if (\/^\\*\\*\\*.*\\*\\*\\*:?$\/.test(line)) return `<span class=\"log-header\">${escapeHtml(line)}<\/span>`;\n  let esc = escapeHtml(line);\n  esc = esc.replace(\/^(\\[\\s*[\\d.]+\\])\/, '<span class=\"log-time\">$1<\/span>');\n  esc = esc.replace(\/(\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?)\/g, '<span class=\"log-time\">$1<\/span>');\n  esc = esc.replace(\/\\b(ERROR|ERR|FAIL(?:ED|URE)?|FATAL|PANIC)\\b\/gi, '<span class=\"log-err\">$1<\/span>');\n  esc = esc.replace(\/\\b(WARN(?:ING)?)\\b\/gi, '<span class=\"log-warn\">$1<\/span>');\n  esc = esc.replace(\/\\b(OK|SUCCESS|PASSED|RUNNING)\\b\/g, '<span class=\"log-ok\">$1<\/span>');\n  return esc;\n}\n\nfunction runCodeSearch(term) {\n  state.codeSearchTerm = term;\n  const codeTable = $('#codeViewer').querySelector('#codeTable');\n  if (!codeTable) return;\n  codeTable.querySelectorAll('mark.hl').forEach(m => {\n    const p = m.parentNode; p.replaceChild(document.createTextNode(m.textContent), m); p.normalize();\n  });\n  if (!term) { $('#codeSearchCount').textContent = ''; return; }\n  const esc = term.replace(\/[.*+?^${}()|[\\]\\\\]\/g, '\\\\$&');\n  const re = new RegExp(esc, 'gi');\n  let total = 0, firstHit = null;\n  codeTable.querySelectorAll('.lc').forEach(cell => {\n    const nodes = []; const w = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);\n    let n; while ((n = w.nextNode())) nodes.push(n);\n    for (const tn of nodes) {\n      const txt = tn.nodeValue;\n      if (!re.test(txt)) { re.lastIndex = 0; continue; }\n      re.lastIndex = 0;\n      const frag = document.createDocumentFragment();\n      let last = 0, m;\n      while ((m = re.exec(txt)) !== null) {\n        if (m.index > last) frag.appendChild(document.createTextNode(txt.slice(last, m.index)));\n        const mark = document.createElement('mark');\n        mark.className = 'hl'; mark.textContent = m[0];\n        frag.appendChild(mark);\n        last = m.index + m[0].length;\n        total++; if (!firstHit) firstHit = mark;\n        if (m[0].length === 0) re.lastIndex++;\n      }\n      if (last < txt.length) frag.appendChild(document.createTextNode(txt.slice(last)));\n      tn.parentNode.replaceChild(frag, tn);\n    }\n  });\n  $('#codeSearchCount').textContent = total ? String(total) : '0';\n  if (firstHit) { firstHit.classList.add('active'); firstHit.scrollIntoView({ block: 'center' }); }\n}\n\nfunction applyWrap() {\n  $('#codeViewer').querySelectorAll('.lc').forEach(c => { c.style.whiteSpace = state.wrap ? 'pre-wrap' : 'pre'; });\n  $('#wrapBtn').classList.toggle('toggled', state.wrap);\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ STRUCTURED FILE VIEWER\n\/\/ Parses ***command***-style output into sections, then each\n\/\/ section body into logical blocks: KV pairs, labeled JSON,\n\/\/ log lines, and prose.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction parseStructuredFile(content) {\n  const sections = [];\n  const lines = content.split(\/\\r?\\n\/);\n  let current = null;\n  const header = \/^\\*\\*\\*\\s*(.*?)\\s*\\*\\*\\*:?\\s*$\/;\n  for (const line of lines) {\n    const m = line.match(header);\n    if (m) {\n      if (current) sections.push(current);\n      const inner = m[1].trim();\n      const argMatch = inner.match(\/^(.+?)\\s+(\\[.*\\])\\s*$\/);\n      current = argMatch\n        ? { command: argMatch[1].trim(), args: argMatch[2].trim(), body: '' }\n        : { command: inner, args: null, body: '' };\n    } else {\n      if (!current) current = { command: null, args: null, body: '' };\n      current.body += line + '\\n';\n    }\n  }\n  if (current) sections.push(current);\n  for (const s of sections) {\n    s.body = s.body.replace(\/\\s+$\/, '');\n    s.blocks = parseSectionBody(s.body);\n  }\n  return { sections };\n}\n\nfunction matchKVLine(line) {\n  \/\/ \"key : value\" with value present; permissive on whitespace\n  const m = line.match(\/^(\\s*)([A-Za-z][A-Za-z0-9 _.\/()+-]*?)\\s*:\\s*(.+)$\/);\n  if (!m) return null;\n  const key = m[2].trim();\n  const value = m[3].trim();\n  if (!key || key.length > 80 || !value) return null;\n  \/\/ Reject if value starts with { or [ \u2014 those are JSON block starts, handled elsewhere\n  if (value === '{' || value === '[' || value.startsWith('{\"') || value.startsWith('[{')) return null;\n  return { key, value };\n}\n\nfunction tryExtractJsonFromLines(lines, startIdx) {\n  \/\/ Find first { or [ at or after startIdx, allowing only blank lines in between\n  let brace = null;\n  for (let k = startIdx; k < lines.length; k++) {\n    const l = lines[k];\n    const col = l.search(\/[{[]\/);\n    if (col >= 0) { brace = { line: k, col }; break; }\n    if (l.trim() !== '') return null;\n  }\n  if (!brace) return null;\n  let text = '';\n  for (let k = brace.line; k < lines.length; k++) {\n    const start = (k === brace.line) ? brace.col : 0;\n    text += (k > brace.line ? '\\n' : '') + lines[k].slice(start);\n    \/\/ Only attempt parse when line ends with a closing brace\/bracket (perf)\n    const trimmed = lines[k].trim();\n    if (trimmed.endsWith('}') || trimmed.endsWith(']')) {\n      try { return { parsed: JSON.parse(text), endLine: k }; } catch (e) {}\n    }\n  }\n  return null;\n}\n\nfunction parseSectionBody(body) {\n  const blocks = [];\n\n  \/\/ Pattern 0: whole body is a single JSON object\/array \u2192 render as a structured tree.\n  \/\/ Handles BLE Operational Data, BLE Config, and any similar JSON-only sections.\n  const trimmed = body.trim();\n  if ((trimmed.startsWith('{') && trimmed.endsWith('}')) ||\n      (trimmed.startsWith('[') && trimmed.endsWith(']'))) {\n    try {\n      const parsed = JSON.parse(trimmed);\n      if (parsed && typeof parsed === 'object') {\n        return [{ type: 'json', json: parsed }];\n      }\n    } catch (e) { \/* fall through *\/ }\n  }\n\n  const lines = body.split(\/\\r?\\n\/);\n  let i = 0;\n  while (i < lines.length) {\n    const line = lines[i];\n    if (line.trim() === '') { i++; continue; }\n\n    \/\/ Pattern 1: \"Getting XXX...\" label followed by optional \"Decoded response:\" and JSON\n    const gettingMatch = line.trim().match(\/^Getting\\s+(.+?)\\.\\.\\.\\s*$\/);\n    if (gettingMatch) {\n      let searchStart = i + 1;\n      if (searchStart < lines.length && \/^Decoded response:?\\s*$\/.test(lines[searchStart].trim())) {\n        searchStart++;\n      }\n      const result = tryExtractJsonFromLines(lines, searchStart);\n      if (result) {\n        blocks.push({ type: 'json-labeled', label: 'Getting ' + gettingMatch[1], json: result.parsed });\n        i = result.endLine + 1;\n        continue;\n      }\n      blocks.push({ type: 'prose', text: line.trim() });\n      i++;\n      continue;\n    }\n\n    \/\/ Pattern 2: \"Some Label: {\" or \"Some Label: [\" \u2014 label + multi-line JSON\n    const labelJsonMatch = line.match(\/^([A-Za-z][\\w\\s.-]*?):\\s*(\\{|\\[)\/);\n    if (labelJsonMatch) {\n      const braceCol = line.indexOf(labelJsonMatch[2]);\n      \/\/ Build text from brace onward\n      let text = line.slice(braceCol);\n      let endLine = i;\n      let found = null;\n      const trimmedFirst = line.slice(braceCol).trim();\n      if (trimmedFirst.endsWith('}') || trimmedFirst.endsWith(']')) {\n        try { found = JSON.parse(text); } catch {}\n      }\n      if (!found) {\n        for (let k = i + 1; k < lines.length; k++) {\n          text += '\\n' + lines[k];\n          endLine = k;\n          const trimmed = lines[k].trim();\n          if (trimmed.endsWith('}') || trimmed.endsWith(']')) {\n            try { found = JSON.parse(text); break; } catch {}\n          }\n        }\n      }\n      if (found) {\n        blocks.push({ type: 'json-labeled', label: labelJsonMatch[1].trim(), json: found });\n        i = endLine + 1;\n        continue;\n      }\n      \/\/ Couldn't parse \u2014 fall through\n    }\n\n    \/\/ Pattern 3: [timestamp] message (dmesg\/kernel log)\n    if (\/^\\[\\s*[\\d.]+\\]\/.test(line)) {\n      const logLines = [];\n      let j = i;\n      while (j < lines.length) {\n        const l = lines[j];\n        if (\/^\\[\\s*[\\d.]+\\]\/.test(l)) {\n          logLines.push(l);\n        } else if (l.trim() === '') {\n          break;\n        } else {\n          \/\/ Continuation of the previous log line (rare, but safe)\n          if (logLines.length > 0) logLines[logLines.length - 1] += '\\n' + l;\n          else break;\n        }\n        j++;\n      }\n      blocks.push({ type: 'log', lines: logLines });\n      i = j;\n      continue;\n    }\n\n    \/\/ Pattern 4: 2+ consecutive KV lines (allowing blank lines between)\n    const kv = matchKVLine(line);\n    if (kv) {\n      const items = [kv];\n      let j = i + 1;\n      while (j < lines.length) {\n        if (lines[j].trim() === '') { j++; continue; }\n        const m2 = matchKVLine(lines[j]);\n        if (!m2) break;\n        items.push(m2);\n        j++;\n      }\n      if (items.length >= 2) {\n        blocks.push({ type: 'kv', items });\n        i = j;\n        continue;\n      }\n    }\n\n    \/\/ Pattern 5: fallback to prose (accumulate until next recognized pattern)\n    const proseLines = [];\n    while (i < lines.length) {\n      const l = lines[i];\n      if (l.trim() === '' && proseLines.length > 0 &&\n          (proseLines[proseLines.length - 1] || '').trim() === '') break;\n      if (\/^Getting\\s+.+\\.\\.\\.\\s*$\/.test(l.trim())) break;\n      if (\/^\\*\\*\\*\/.test(l)) break;\n      if (\/^\\[\\s*[\\d.]+\\]\/.test(l)) break;\n      if (\/^[A-Za-z][\\w\\s.-]*?:\\s*(\\{|\\[)\/.test(l)) break;\n      \/\/ Break to KV block if we see 2+ consecutive KV lines\n      const km = matchKVLine(l);\n      if (km && i + 1 < lines.length) {\n        const km2 = matchKVLine(lines[i + 1]);\n        if (km2) break;\n      }\n      proseLines.push(l);\n      i++;\n    }\n    const proseText = proseLines.join('\\n').replace(\/^\\n+|\\n+$\/g, '');\n    if (proseText) blocks.push({ type: 'prose', text: proseText });\n  }\n  return blocks;\n}\n\n\/\/ ---------- Rendering ----------\nfunction renderStructuredFile(tabDef, file) {\n  const actualName = getFileNameForTab(tabDef);\n  $('#curFilename').textContent = actualName;\n  const view = $('#structuredView');\n  if (!file) {\n    $('#curFileinfo').textContent = '';\n    view.innerHTML = `\n      <div class=\"missing-file-notice\">\n        <h3>File not present in this package<\/h3>\n        <p>Expected: <code>${escapeHtml(tabDef.filename)}<\/code><\/p>\n      <\/div>`;\n    return;\n  }\n  $('#curFileinfo').textContent = `${humanSize(file.size)} \u00b7 ${countLines(file.content)} lines`;\n\n  const parsed = parseStructuredFile(file.content);\n  let html = '';\n\n  \/\/ File-specific summary card\n  const summary = buildFileSummary(tabDef.id, parsed, file);\n  if (summary) html += summary;\n\n  for (const section of parsed.sections) {\n    html += renderStructuredSection(section);\n  }\n  view.innerHTML = html || `<div class=\"sf-empty\">(empty file)<\/div>`;\n}\n\nfunction renderStructuredSection(section) {\n  const header = (section.command || section.args)\n    ? `<div class=\"sf-section-header\">\n         ${section.command ? `<span class=\"sf-section-cmd\">${escapeHtml(section.command)}<\/span>` : ''}\n         ${section.args ? `<span class=\"sf-section-args\">${escapeHtml(section.args)}<\/span>` : ''}\n       <\/div>`\n    : '';\n  let body = '';\n  if (!section.blocks || section.blocks.length === 0) {\n    body = '<div class=\"sf-empty\">(empty section)<\/div>';\n  } else {\n    for (const block of section.blocks) body += renderStructuredBlock(block);\n  }\n  return `<div class=\"sf-section\">${header}<div class=\"sf-section-body\">${body}<\/div><\/div>`;\n}\n\nfunction renderStructuredBlock(block) {\n  if (block.type === 'kv') {\n    const rows = block.items.map(item => `\n      <div class=\"sf-kv-label\">${escapeHtml(item.key)}<\/div>\n      <div class=\"sf-kv-value\">${renderKVValue(item.value, item.key)}<\/div>\n    `).join('');\n    return `<div class=\"sf-kv\">${rows}<\/div>`;\n  }\n  if (block.type === 'json-labeled') {\n    let rendered;\n    const v = block.json;\n    if (v === null || typeof v !== 'object') {\n      rendered = `<div style=\"padding:10px 14px\">${renderConfigValue(v)}<\/div>`;\n    } else if (Array.isArray(v)) {\n      rendered = renderConfigArray(v, block.label) || `<div class=\"sf-empty\">(empty array)<\/div>`;\n    } else {\n      rendered = renderConfigGroup(v, null) || `<div class=\"sf-empty\">(empty object)<\/div>`;\n    }\n    return `<div class=\"sf-labeled-json\">\n      <div class=\"sf-labeled-json-label\">${escapeHtml(block.label)}<\/div>\n      <div class=\"cfg-render\">${rendered}<\/div>\n    <\/div>`;\n  }\n  if (block.type === 'json') {\n    let rendered;\n    const v = block.json;\n    if (v === null || typeof v !== 'object') {\n      rendered = `<div style=\"padding:10px 14px\">${renderConfigValue(v)}<\/div>`;\n    } else if (Array.isArray(v)) {\n      rendered = renderConfigArray(v, 'items') || `<div class=\"sf-empty\">(empty array)<\/div>`;\n    } else {\n      rendered = renderConfigGroup(v, null) || `<div class=\"sf-empty\">(empty object)<\/div>`;\n    }\n    return `<div class=\"sf-json-block cfg-render\">${rendered}<\/div>`;\n  }\n  if (block.type === 'log') {\n    const rows = block.lines.map(line => {\n      const m = line.match(\/^\\[\\s*([\\d.]+)\\]\\s*([\\s\\S]*)$\/);\n      if (m) {\n        const ts = m[1];\n        let msg = escapeHtml(m[2]);\n        msg = msg.replace(\/\\b(ERROR|ERR|FAIL(?:ED|URE)?|FATAL|PANIC)\\b\/gi, '<span class=\"log-err\">$1<\/span>');\n        msg = msg.replace(\/\\b(WARN(?:ING)?)\\b\/gi, '<span class=\"log-warn\">$1<\/span>');\n        msg = msg.replace(\/\\b(OK|SUCCESS|PASSED|RUNNING)\\b\/g, '<span class=\"log-ok\">$1<\/span>');\n        return `<div class=\"sf-log-row\"><div class=\"sf-log-ts\">${escapeHtml(ts)}<\/div><div class=\"sf-log-msg\">${msg}<\/div><\/div>`;\n      }\n      return `<div class=\"sf-log-row\"><div class=\"sf-log-ts\"><\/div><div class=\"sf-log-msg\">${escapeHtml(line)}<\/div><\/div>`;\n    }).join('');\n    return `<div class=\"sf-log\">${rows}<\/div>`;\n  }\n  if (block.type === 'prose') {\n    return `<div class=\"sf-prose\">${escapeHtml(block.text)}<\/div>`;\n  }\n  return '';\n}\n\nfunction renderKVValue(value, key) {\n  if (value === '' || value === '-' || value === '\u2014') return '<span class=\"val-empty\">(empty)<\/span>';\n  if (\/^(null|None|n\\\/a)$\/i.test(value)) return '<span class=\"val-null\">null<\/span>';\n  if (\/^true$\/i.test(value))  return '<span class=\"val-bool-true\">\u2713 True<\/span>';\n  if (\/^false$\/i.test(value)) return '<span class=\"val-bool-false\">\u2717 False<\/span>';\n  \/\/ Hex (0x...)\n  if (\/^0x[0-9a-fA-F]+$\/.test(value)) return `<span class=\"sf-value-hex\">${escapeHtml(value)}<\/span>`;\n  \/\/ MAC address\n  if (\/^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$\/.test(value)) return `<span class=\"sf-value-mac\">${escapeHtml(value)}<\/span>`;\n  \/\/ IPv4\n  if (\/^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(\\\/\\d{1,2})?$\/.test(value)) return `<span class=\"sf-value-ip\">${escapeHtml(value)}<\/span>`;\n  \/\/ Percent-bearing values like \"22%\" or \"30.9% used, 69.1% available\"\n  if (\/^\\d+(\\.\\d+)?%$\/.test(value)) {\n    const n = parseFloat(value);\n    const cls = n >= 90 ? 'critical' : n >= 75 ? 'high' : '';\n    return `<span class=\"sf-pct-bar\">\n      <span class=\"sf-value-pct\">${escapeHtml(value)}<\/span>\n      <span class=\"sf-pct-bar-track\"><span class=\"sf-pct-bar-fill ${cls}\" style=\"width:${Math.min(n,100)}%\"><\/span><\/span>\n    <\/span>`;\n  }\n  \/\/ Version strings\n  if (\/^v?\\d+\\.\\d+\\.\\d+([-+.][\\w+.-]+)?$\/.test(value)) return `<span class=\"sf-value-ver\">${escapeHtml(value)}<\/span>`;\n  \/\/ Pure number\n  if (\/^-?\\d+(\\.\\d+)?$\/.test(value) && value.length < 20) return `<span class=\"sf-value-num\">${escapeHtml(value)}<\/span>`;\n  \/\/ Duration like \"0 days 0 hours 1 minutes\" or \"58s\"\n  if (\/^\\d+\\s*days?\\s+\\d+\\s*hours?\\s+\\d+\\s*minutes?$\/i.test(value) || \/^-?\\d+(\\.\\d+)?s$\/.test(value)) {\n    return `<span class=\"sf-value-ver\">${escapeHtml(value)}<\/span>`;\n  }\n  \/\/ File path\n  if (\/^\\\/[\\w.\/-]+\/.test(value) && !value.includes(' ')) return `<span class=\"sf-value-path\">${escapeHtml(value)}<\/span>`;\n  \/\/ Default\n  return `<span class=\"sf-value-mono\">${escapeHtml(value)}<\/span>`;\n}\n\n\/\/ ---------- File-specific summary cards ----------\nfunction buildFileSummary(tabId, parsed, file) {\n  \/\/ Collect all KV items across all sections for summary lookups\n  const allKV = {};\n  for (const s of parsed.sections) {\n    for (const b of s.blocks) {\n      if (b.type === 'kv') for (const it of b.items) allKV[it.key] = it.value;\n    }\n  }\n\n  \/\/ ALARMS tab \u2014 handles both the legacy system-alarms.log (labeled JSON blocks for\n  \/\/ \"System opdata\" \/ \"alarms\") and the newer system-state-and-alarms.json\n  \/\/ (whole body is one JSON object with Alarms_Metrics \/ Alarms_OpData \/ System_OpData).\n  if (tabId === 'alarms') {\n    \/\/ Find the top-level JSON across blocks (it's either a whole-body JSON block or a labeled one)\n    let rootJson = null;\n    for (const s of parsed.sections) {\n      for (const b of s.blocks) {\n        if (b.type === 'json' && b.json && typeof b.json === 'object') { rootJson = b.json; break; }\n      }\n      if (rootJson) break;\n    }\n    \/\/ Extract fields (both formats)\n    let sysOp = null;\n    let alarmOp = null;\n    if (rootJson) {\n      if (rootJson.System_OpData) sysOp = rootJson.System_OpData.operationalData || rootJson.System_OpData;\n      else if (rootJson.operationalData) sysOp = rootJson.operationalData;\n      if (rootJson.Alarms_OpData) alarmOp = rootJson.Alarms_OpData.operationalData || rootJson.Alarms_OpData;\n    }\n    \/\/ Also look in labeled-JSON blocks (old format)\n    if (!sysOp) {\n      for (const s of parsed.sections) {\n        for (const b of s.blocks) {\n          if (b.type === 'json-labeled' && \/System.*opdata\/i.test(b.label) && b.json) {\n            sysOp = b.json.operationalData || b.json;\n          }\n        }\n      }\n    }\n\n    const items = [];\n    if (sysOp && sysOp.systemState) {\n      items.push(['System State', shortenEnum(sysOp.systemState),\n        \/FAULT|ERROR\/i.test(sysOp.systemState) ? 'err' :\n        \/READY|RUNNING|NORMAL\/i.test(sysOp.systemState) ? 'ok' : 'accent']);\n    }\n    if (sysOp && sysOp.controlMode) {\n      items.push(['Control Mode', shortenEnum(sysOp.controlMode), 'accent']);\n    }\n    if (alarmOp) {\n      const total = alarmOp.totalActiveAlarmCount;\n      if (total !== undefined) {\n        items.push(['Active Alarms', String(total), total > 0 ? 'err' : 'ok']);\n      }\n      const sevs = [\n        ['Critical', alarmOp.activeCriticalAlarmCount, 'err'],\n        ['Major',    alarmOp.activeMajorAlarmCount,    'err'],\n        ['Medium',   alarmOp.activeMediumAlarmCount,   'warn'],\n        ['Minor',    alarmOp.activeMinorAlarmCount,    'warn'],\n      ];\n      for (const [lbl, val, activeClass] of sevs) {\n        if (val !== undefined && val !== null) {\n          items.push([lbl, String(val), val > 0 ? activeClass : 'ok']);\n        }\n      }\n      if (alarmOp.totalNeedAcknowledgeAlarmCount !== undefined && alarmOp.totalNeedAcknowledgeAlarmCount > 0) {\n        items.push(['Unack', String(alarmOp.totalNeedAcknowledgeAlarmCount), 'warn']);\n      }\n    }\n    if (items.length) {\n      return `<div class=\"sf-summary\">\n        ${items.map(([k, v, cls]) => `\n          <div class=\"sf-summary-item\">\n            <div class=\"sf-summary-label\">${escapeHtml(k)}<\/div>\n            <div class=\"sf-summary-value ${cls || 'accent'}\" style=\"font-size:12.5px\">${escapeHtml(v)}<\/div>\n          <\/div>`).join('')}\n      <\/div>`;\n    }\n  }\n\n  if (tabId === 'version') {\n    const items = [\n      ['Current',    allKV['Current']],\n      ['Backup',     allKV['Backup']],\n      ['Motor CoPro',allKV['Motor-CoPro']],\n      ['AFE CoPro',  allKV['AFE-CoPro']],\n      ['BLE Image',  allKV['BLE-Image']],\n      ['BLE CoPro',  allKV['BLE-CoPro']],\n    ].filter(([, v]) => v);\n    if (items.length) {\n      return `<div class=\"sf-summary\">\n        ${items.map(([k, v]) => `\n          <div class=\"sf-summary-item\">\n            <div class=\"sf-summary-label\">${escapeHtml(k)}<\/div>\n            <div class=\"sf-summary-value accent\" style=\"font-size:11.5px\">${escapeHtml(v)}<\/div>\n          <\/div>`).join('')}\n      <\/div>`;\n    }\n  }\n  if (tabId === 'device-info') {\n    const items = [\n      ['Model',         allKV['model']],\n      ['Serial',        allKV['serial_number']],\n      ['HW Revision',   allKV['hw_revision']],\n      ['Control Board', allKV['ctrl_board']],\n      ['eth0 MAC',      allKV['eth0_addr']],\n      ['eth1 MAC',      allKV['eth1_addr']],\n    ].filter(([, v]) => v);\n    if (items.length) {\n      return `<div class=\"sf-summary\">\n        ${items.map(([k, v]) => `\n          <div class=\"sf-summary-item\">\n            <div class=\"sf-summary-label\">${escapeHtml(k)}<\/div>\n            <div class=\"sf-summary-value accent\" style=\"font-size:12.5px\">${escapeHtml(v)}<\/div>\n          <\/div>`).join('')}\n      <\/div>`;\n    }\n  }\n  if (tabId === 'hmi') {\n    const items = [\n      ['Model',          allKV['model']],\n      ['Serial',         allKV['serial_number']],\n      ['Revision',       allKV['revision']],\n      ['MAC Address',    allKV['mac_address']],\n      ['App Firmware',   allKV['app_firmware_version']],\n      ['BLE Protocol',   allKV['ble_protocol_version']],\n    ].filter(([, v]) => v);\n    if (items.length) {\n      return `<div class=\"sf-summary\">\n        ${items.map(([k, v]) => `\n          <div class=\"sf-summary-item\">\n            <div class=\"sf-summary-label\">${escapeHtml(k)}<\/div>\n            <div class=\"sf-summary-value accent\" style=\"font-size:12.5px\">${escapeHtml(v)}<\/div>\n          <\/div>`).join('')}\n      <\/div>`;\n    }\n  }\n  if (tabId === 'device-status') {\n    const items = [];\n    if (allKV['CPU']) items.push(['CPU', allKV['CPU']]);\n    if (allKV['Memory']) items.push(['Memory', allKV['Memory']]);\n    if (allKV['Uptime']) items.push(['Uptime', allKV['Uptime']]);\n    if (allKV['IP']) items.push(['IP', allKV['IP']]);\n    if (allKV['Bank']) items.push(['Bank', allKV['Bank']]);\n    if (allKV['M4 Copro']) items.push(['M4 CoPro', allKV['M4 Copro']]);\n    if (items.length) {\n      return `<div class=\"sf-summary\">\n        ${items.map(([k, v]) => `\n          <div class=\"sf-summary-item\">\n            <div class=\"sf-summary-label\">${escapeHtml(k)}<\/div>\n            <div class=\"sf-summary-value\" style=\"font-size:12.5px\">${escapeHtml(v)}<\/div>\n          <\/div>`).join('')}\n      <\/div>`;\n    }\n  }\n  if (tabId === 'dmesg') {\n    \/\/ Count log lines\n    let total = 0;\n    for (const s of parsed.sections) for (const b of s.blocks) if (b.type === 'log') total += b.lines.length;\n    const errors = (file.content.match(\/\\b(error|fail|fatal|panic)\\b\/gi) || []).length;\n    const warnings = (file.content.match(\/\\bwarn(ing)?\\b\/gi) || []).length;\n    return `<div class=\"sf-summary\">\n      <div class=\"sf-summary-item\">\n        <div class=\"sf-summary-label\">Kernel lines<\/div>\n        <div class=\"sf-summary-value accent\">${total.toLocaleString()}<\/div>\n      <\/div>\n      <div class=\"sf-summary-item\">\n        <div class=\"sf-summary-label\">Error keywords<\/div>\n        <div class=\"sf-summary-value ${errors > 0 ? 'err' : 'ok'}\">${errors}<\/div>\n      <\/div>\n      <div class=\"sf-summary-item\">\n        <div class=\"sf-summary-label\">Warn keywords<\/div>\n        <div class=\"sf-summary-value ${warnings > 0 ? 'warn' : 'ok'}\">${warnings}<\/div>\n      <\/div>\n    <\/div>`;\n  }\n  return null;\n}\n\n\/\/ ---------- View mode switching ----------\nfunction applyFileViewMode() {\n  const mode = state.fileViewMode || 'structured';\n  const codeVis  = mode === 'raw' ? '' : 'none';\n  const structVis = mode === 'raw' ? 'none' : '';\n  $('#codeViewer').style.display = codeVis;\n  $('#structuredView').style.display = structVis;\n  $('#viewModeStructured').classList.toggle('active', mode === 'structured');\n  $('#viewModeRaw').classList.toggle('active', mode === 'raw');\n  \/\/ Raw-only controls\n  $('#wrapBtn').style.display = mode === 'raw' ? '' : 'none';\n  $('#codeSearchInput').parentElement.style.display = mode === 'raw' ? '' : 'none';\n}\n\nfunction copyActiveFile() {\n  const tabDef = TAB_DEFS.find(t => t.id === state.activeTabId);\n  const file = tabDef && getFileForTab(tabDef);\n  if (!file) return;\n  navigator.clipboard.writeText(file.content).then(() => {\n    const btn = $('#copyBtn');\n    btn.classList.add('toggled');\n    setTimeout(() => btn.classList.remove('toggled'), 700);\n  });\n}\nfunction downloadActiveFile() {\n  const tabDef = TAB_DEFS.find(t => t.id === state.activeTabId);\n  const file = tabDef && getFileForTab(tabDef);\n  if (!file) return;\n  const name = getFileNameForTab(tabDef);\n  const blob = new Blob([file.content], { type: 'text\/plain' });\n  const url = URL.createObjectURL(blob);\n  const a = document.createElement('a');\n  a.href = url; a.download = name;\n  document.body.appendChild(a); a.click(); a.remove();\n  setTimeout(() => URL.revokeObjectURL(url), 1000);\n}\n\nfunction showError(msg) {\n  $('#emptyState').style.display = 'flex';\n  $('#dropZone').innerHTML = `<div class=\"error-banner\">${msg}<\/div>`;\n}\n\nfunction resetView() {\n  state.files = {}; state.extras = [];\n  state.activeTabId = null; state.packageName = null; state.packageSize = 0;\n  state.codeSearchTerm = ''; state.auditLoaded = false; state.configLoaded = false;\n  state.metricsLoaded = false;\n  state.healthLoaded = false;\n  state.configObj = null; state.editorInited = false; state.loadKind = null;\n  state.fileViewMode = 'structured';\n  state.topMode = 'home'; state.pendingTopMode = null;\n  { const _sn = $('#subNav'); if (_sn) _sn.style.display = 'none'; }\n  $('#pkgInfo').style.display = 'none';\n  $('#unloadBtn').style.display = 'none';\n  $('#fileView').style.display = 'none';\n  const auditEl = $('#auditViewer');\n  const configEl = $('#configViewer');\n  const metricsEl = $('#metricsViewer');\n  const healthEl = $('#healthViewer');\n  if (auditEl) auditEl.classList.remove('active');\n  if (configEl) configEl.classList.remove('active');\n  if (metricsEl) metricsEl.classList.remove('active');\n  if (healthEl) healthEl.classList.remove('active');\n  const editorEl = $('#editorViewer'); if (editorEl) editorEl.classList.remove('active');\n  const reportEl = $('#reportViewer'); if (reportEl) reportEl.classList.remove('active');\n  $('#emptyState').style.display = 'flex';\n  $('#dropZone').innerHTML = `\n        <div class=\"dz-main\" id=\"dropBtn\" role=\"button\" tabindex=\"0\">\n          <div class=\"dz-iconwrap\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z\"\/><path d=\"M14 3v5h5\"\/><path d=\"M12 18v-6\"\/><path d=\"m9.5 14 2.5-2.5 2.5 2.5\"\/><\/svg><\/div>\n          <h2 data-i18n=\"home.drop\">Drag &amp; Drop a file here<\/h2>\n          <p class=\"dz-supported\" data-i18n=\"home.supported\">Supported files: Configuration JSON, Audit Log JSON, Support Package<\/p>\n        <\/div>`;\n  $('#dropBtn').addEventListener('click', () => { const fi = $('#fileInput'); fi.accept = FULL_ACCEPT; state.landingIntent = null; fi.click(); });\n  renderTopNav();\n  applyLang(LANG);\n}\n\nfunction showToast(msg, kind) {\n  const t = $('#toast');\n  t.textContent = msg;\n  t.className = 'toast toast-' + (kind || 'info') + ' visible';\n  clearTimeout(showToast._to);\n  showToast._to = setTimeout(() => { t.classList.remove('visible'); }, 4000);\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ AUDIT LOG VIEWER\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet auditAllData = [];\nlet auditTxColor = {};   \/\/ transactionId -> SETPOINTS_CHANGE row colour (request), inherited by responses\nlet auditFilteredData = [];\nlet auditSortCol = 'timestamp';\nlet auditSortDir = -1;\nconst AUDIT_CHUNK = 200;   \/\/ rows appended per infinite-scroll batch\nlet auditRendered = 0;     \/\/ number of rows currently in the DOM\nlet auditShowStartShutdown = false;   \/\/ default: start-up\/shutdown (untyped) events are HIDDEN\n\/\/ An entry is a start-up\/shutdown event when it carries no Event type\n\/\/ (undefined \/ null \/ blank). These are the systemd boot & shutdown journal lines.\nconst auditIsUntypedEvent = e => !e || e.event == null || String(e.event).trim() === '';\nlet auditViewMode = 'formatted';   \/\/ 'formatted' | 'raw' \u2014 global audit view mode\nlet auditFocusEntry = null;        \/\/ pinned entry (focus\/context mode), or null\nconst AUDIT_FOCUS_WINDOW = 50;     \/\/ entries shown before & after the pinned entry (100 total)\n\nconst shortEvent    = e => (e || '').replace('AUDIT_LOG_EVENT_', '');\nconst shortSeverity = s => (s || '').replace('AUDIT_LOG_SEVERITY_', '');\nconst shortOrigin   = o => (o || '').replace('AUDIT_LOG_ORIGIN_', '');\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ SmartD gRPC API reference data \u2014 sourced from smartd_api docs (2026-07-02).\n\/\/ Used for tooltips on chips, decoding numeric codes, and the API Ref modal.\n\/\/ Keep this list in sync with the upstream API doc.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst SMARTD_API = {\n  apiVersion: '2026-07-02',\n  AuditLogEvent: {\n    UNSPECIFIED: { n: 0, desc: 'Unspecified \/ unknown event type.' },\n    SHUTDOWN: { n: 1, desc: 'System shutdown event.' },\n    STARTUP: { n: 2, desc: 'System startup event.' },\n    NETWORK_CHANGE: { n: 3, desc: 'Network configuration change.' },\n    NETWORK_FAILURE: { n: 4, desc: 'Network failure detected.' },\n    CONFIG_CHANGE: { n: 5, desc: 'Configuration changed (Set Configuration request). Look at the message for the protobuf-text payload.' },\n    SETPOINTS_CHANGE: { n: 6, desc: 'Setpoints changed at runtime (motor command, speed, date\/time, etc.).' },\n    ALARM: { n: 7, desc: 'Alarm state transition or alarm-related event (including Debug info snapshots).' },\n    LOGIN_ATTEMPT: { n: 8, desc: 'Authentication attempt (success or failure).' },\n    ACCEPT_CURRENT_VERSION: { n: 9, desc: 'User accepted the current software version (creates a backup).' },\n    REJECT_CURRENT_VERSION: { n: 10, desc: 'User rejected the current software version (rolls back to previous).' },\n    START_SOFTWARE_UPDATE: { n: 11, desc: 'Software update process started.' },\n    CANCEL_SOFTWARE_UPDATE: { n: 12, desc: 'Software update was cancelled.' },\n    FETCH_SOFTWARE_UPDATE: { n: 13, desc: 'Software update package was fetched from source.' },\n    SOFTWARE_UPDATE_COMPLETE: { n: 14, desc: 'Software update finished successfully.' },\n    SOFTWARE_UPDATE_ERROR: { n: 15, desc: 'Software update encountered an error.' },\n    FACTORY_RESET: { n: 16, desc: 'Factory reset performed (all settings cleared).' },\n    USER_CREATION: { n: 17, desc: 'A user account was created.' },\n    USER_UPDATE: { n: 18, desc: 'A user account was updated (password \/ pin \/ permissions).' },\n    BACKUP_START: { n: 19, desc: 'Configuration backup started.' },\n    BACKUP_COMPLETE: { n: 20, desc: 'Configuration backup completed.' },\n    BACKUP_ERROR: { n: 21, desc: 'Configuration backup failed.' },\n    REBOOT_SOFTWARE_UPDATE: { n: 22, desc: 'Reboot to complete software update.' },\n    METRICS_RESET: { n: 23, desc: 'A metrics counter was reset (motor runtime, fan hours, etc.).' },\n    LOCKOUT_RESET: { n: 24, desc: 'System.ResetLockout was called \u2014 drive came out of LOCKOUT state.' },\n    AUTOTUNE_EXECUTED: { n: 25, desc: 'Motor autotune was executed (parameters re-measured).' },\n  },\n  AuditLogOrigin: {\n    UNSPECIFIED: { n: 0, desc: 'Unspecified source.' },\n    GRPC_API: { n: 1, desc: 'Generic gRPC API caller.' },\n    MOBILE: { n: 2, desc: 'SmartD mobile app.' },\n    WEB: { n: 3, desc: 'Web client.' },\n    HMI: { n: 4, desc: 'Local touchscreen \/ panel.' },\n    IO_MODULE: { n: 5, desc: 'Digital \/ analog I\/O module on the drive.' },\n    MODBUS: { n: 6, desc: 'Modbus client (TCP or RTU).' },\n    BLE: { n: 7, desc: 'Bluetooth Low Energy client.' },\n    SYSTEM: { n: 8, desc: 'Drive itself (background processes, state machine).' },\n    PID: { n: 9, desc: 'PID controller action.' },\n    BACNET: { n: 10, desc: 'BACnet building-automation client.' },\n    ETHERNET_IP: { n: 11, desc: 'EtherNet\/IP industrial client.' },\n  },\n  AuditLogSeverity: {\n    UNSPECIFIED: { n: 0, desc: 'Unspecified severity.' },\n    EMERGENCY: { n: 1, desc: 'Hard fault; usually paired with SystemStateFault.' },\n    ERROR: { n: 2, desc: 'Something failed but the system continues.' },\n    WARNING: { n: 3, desc: 'Attention-worthy but not an error.' },\n    INFO: { n: 4, desc: 'Routine event, no action needed.' },\n  },\n  AlarmCode: {\n    1:  { name: 'STO',                                  desc: 'Safe Torque Off triggered by hardware via STO1\/STO2 digital input.' },\n    2:  { name: 'STO_LOSS',                             desc: 'Defect in STO1 or STO2 circuit. Contact support.' },\n    3:  { name: 'LINE_PHASE_INVERSION',                 desc: 'Grid incorrectly wired \u2014 swap two phases.' },\n    4:  { name: 'DC_UNDERVOLTAGE',                      desc: 'DC bus voltage too low to maintain operation.' },\n    5:  { name: 'DC_OVERVOLTAGE',                       desc: 'DC bus voltage dangerously high.' },\n    6:  { name: 'LINE_VOLTAGE_SAG',                     desc: 'Line voltage is low.' },\n    7:  { name: 'LINE_VOLTAGE_BALANCE',                 desc: 'Line voltage unbalance between phases.' },\n    8:  { name: 'LINE_PHASE_LOSS',                      desc: 'NOT IMPLEMENTED.' },\n    9:  { name: 'HMI_COMM_LOSS',                        desc: 'Communication problem between control board and HMI.' },\n    10: { name: 'LINE_CURRENT_BALANCE',                 desc: 'NOT IMPLEMENTED.' },\n    11: { name: 'LINE_OVERCURRENT',                     desc: 'Hardware protection for grid-side overcurrent.' },\n    12: { name: 'MOTOR_CURRENT_BALANCE',                desc: 'Motor current unbalance between phases.' },\n    13: { name: 'MOTOR_OVERCURRENT',                    desc: 'Sustained current \u2014 higher intensity for shorter period than OVERLOAD.' },\n    14: { name: 'MOTOR_OVERTEMP',                       desc: 'Overtemperature detected on the motor.' },\n    15: { name: 'MOTOR_OVERLOAD',                       desc: 'Sustained current above motor nominal \u2014 lower intensity for longer period than OVERCURRENT.' },\n    16: { name: 'DRIVE_OVERTEMP',                       desc: 'Hardware protection for overtemperature.' },\n    17: { name: 'UNKNOWN_BOOT_STATE',                   desc: 'Something went wrong during boot; reboot required.' },\n    18: { name: 'BOOTED_BACKUP_SOFTWARE',               desc: 'Booted on backup version compatible with current config.' },\n    19: { name: 'BOOTED_BACKUP_SOFTWARE_WITH_FACTORY_DEFAULT_CONFIG', desc: 'Booted on backup version incompatible with config \u2014 caused factory reset.' },\n    20: { name: 'DRIVE_HIGH_TEMPERATURE',               desc: 'Temperature of power electronics is too high.' },\n    21: { name: 'LINE_VOLTAGE_SWELL',                   desc: 'Line voltage is high.' },\n    22: { name: 'SOFTWARE_UPDATE_FAILED',               desc: 'Software upgrade failed.' },\n    23: { name: 'DC_SINGLE_PHASED',                     desc: 'Voltage ripple on DC bus AND line voltage unbalanced (e.g. phase loss).' },\n    24: { name: 'COOLING_FAN1_FAULT',                   desc: 'Cooling fan #1 not operating as expected.' },\n    25: { name: 'COOLING_FAN2_FAULT',                   desc: 'Cooling fan #2 not operating as expected.' },\n    26: { name: 'COOLING_FAN3_FAULT',                   desc: 'Cooling fan #3 not operating as expected.' },\n    27: { name: 'COOLING_FAN4_FAULT',                   desc: 'Cooling fan #4 not operating as expected.' },\n    28: { name: 'COOLING_FAN_CAPACITY_FAULT',           desc: 'Not enough fans are operating.' },\n    29: { name: 'LINE_UNEXPECTED_VOLTAGE',              desc: 'Measured line voltage does not match the configured line voltage.' },\n    30: { name: 'LOW_LEVEL_COMM_ERRORS',                desc: 'Severe internal communication errors between drive components. Contact support.' },\n    31: { name: 'DC_UNBALANCED',                        desc: 'DC bus voltage unbalanced (DC+ vs DC-).' },\n    32: { name: 'DRIVE_OUTPUT_OVERCURRENT',             desc: 'Hardware protection for load-side overcurrent.' },\n    33: { name: 'SIMULTANEOUS_FORWARD_AND_REVERSE_COMMAND', desc: 'Forward and reverse I\/O commands detected simultaneously.' },\n    34: { name: 'SYSTEM_BOOT_TIME_EXCEEDED',            desc: 'System remained in booting state too long.' },\n    35: { name: 'SYSTEM_DEGRADED',                      desc: 'System is degraded.' },\n    36: { name: 'ABNORMAL_CPU_USAGE',                   desc: 'CPU usage is too high.' },\n    37: { name: 'ABNORMAL_MEMORY_USAGE',                desc: 'Memory usage is too high.' },\n    38: { name: 'DC_OVERVOLTAGE_STALL_PREVENTION',      desc: 'Drive limiting deceleration because DC bus voltage too high.' },\n    39: { name: 'MOTOR_OVERCURRENT_STALL_PREVENTION',   desc: 'Drive limiting acceleration because motor current too high.' },\n    40: { name: 'LINE_UNEXPECTED_FREQUENCY',            desc: 'Measured line frequency does not match configured frequency.' },\n    41: { name: 'DC_UNEXPECTED_VOLTAGE',                desc: 'DC bus voltage not high enough vs. measured grid \u2014 not safe to enable bypass relay.' },\n    42: { name: 'AFE_FAULT',                            desc: 'Active Front-End (AFE) cannot operate due to important fault.' },\n    43: { name: 'AFE_LOW_VOLTAGE_RIDE_THROUGH',         desc: 'AFE detected line voltage sag or unbalance \u2014 Low Voltage Ride Through (LVRT). Motor control adapted to keep drive operating.' },\n    44: { name: 'REMOTE_TO_LOCAL_OVERRIDE',             desc: 'User intentionally forced drive to local mode while remote control was selected.' },\n    45: { name: 'ANALOG_OUTPUT_SENSOR_LOSS',            desc: 'Analog output sensor is defective.' },\n    46: { name: 'PID_1_LOW_PROCESS_VALUE',              desc: 'PID #1 process feedback value too low.' },\n    47: { name: 'PID_1_HIGH_PROCESS_VALUE',             desc: 'PID #1 process feedback value too high.' },\n    48: { name: 'CONTROL_BOARD_POWER_LOSS',             desc: 'Control board power supply was lost.' },\n    49: { name: 'MOTOR_CONTROL_INSTABILITY',            desc: 'Unstable motor control conditions detected; drive must stop to protect itself.' },\n    50: { name: 'MOTOR_OVERSPEED',                      desc: 'Motor turning too fast.' },\n    51: { name: 'MOTOR_UNDERSPEED',                     desc: 'Motor turning too slow.' },\n    52: { name: 'MOTOR_WRONG_DIRECTION',                desc: 'Motor turning in the wrong direction.' },\n    53: { name: 'ENCODER_SPEED_ERROR',                  desc: 'Encoder not measuring motor speed correctly.' },\n    54: { name: 'ENCODER_COMMUNICATION_FAILURE',        desc: 'Encoder not wired properly or defective.' },\n    55: { name: 'ANALOG_INPUT_1_SENSOR_LOSS',           desc: 'Analog input 1 sensor defective.' },\n    56: { name: 'ANALOG_INPUT_2_SENSOR_LOSS',           desc: 'Analog input 2 sensor defective.' },\n    57: { name: 'ANALOG_INPUT_3_SENSOR_LOSS',           desc: 'Analog input 3 sensor defective.' },\n    58: { name: 'MODBUS_COMM_LOSS',                     desc: 'Modbus communication interrupted.' },\n    59: { name: 'HIGH_AMBIENT_TEMPERATURE',             desc: 'Ambient temperature too high.' },\n    60: { name: 'MOTOR_TEMP_SENSOR_SHORTED',            desc: 'Motor temperature sensor is shorted.' },\n    61: { name: 'MOTOR_PHASE_LOSS',                     desc: 'No current reading on one of the motor phases.' },\n    62: { name: 'MOTOR_STALLED',                        desc: 'Motor shaft is stalled (when using velocity setpoint).' },\n    63: { name: 'MOTOR_DISCONNECTED',                   desc: 'No current readings on any motor phases (open-circuit VFD\u2194motor).' },\n    64: { name: 'MOTOR_APPLICATION_UNDERLOAD',          desc: 'Motor underload defined by user application.' },\n    65: { name: 'MOTOR_AUTO_RESTART_FAILURE',           desc: 'Motor auto-restart attempt failed.' },\n    66: { name: 'AFE_LOW_VOLTAGE_RIDE_THROUGH_CANNOT_RECOVER', desc: 'AFE detected line voltage sag\/unbalance and could not recover \u2014 LVRT failure.' },\n    67: { name: 'DRIVE_OVERLOAD',                       desc: 'Sustained current above drive capability.' },\n    68: { name: 'PIN_INVALID',                          desc: 'User entered an invalid pin.' },\n    69: { name: 'DAMPER_FEEDBACK_LOSS',                 desc: 'Damper feedback signal lost while motor running.' },\n    70: { name: 'EXTERNAL_START_FEEDBACK_TIMEOUT',      desc: 'Max delay exceeded for external feedback while energized and waiting to drive motor.' },\n    71: { name: 'EXTERNAL_POST_STOP_FEEDBACK_TIMEOUT',  desc: 'Max delay exceeded for external feedback to be removed.' },\n    72: { name: 'MOTOR_GROUND_FAULT',                   desc: 'Motor ground fault.' },\n    73: { name: 'EMERGENCY_MODE_FORBIDDEN',             desc: 'Emergency mode forbidden (lockout or maintenance mode).' },\n    74: { name: 'EXTERNAL_1',                           desc: 'External alarm 1 (user-defined via digital input).' },\n    75: { name: 'EXTERNAL_2',                           desc: 'External alarm 2 (user-defined via digital input).' },\n    76: { name: 'EXTERNAL_3',                           desc: 'External alarm 3 (user-defined via digital input).' },\n    77: { name: 'EXTERNAL_4',                           desc: 'External alarm 4 (user-defined via digital input).' },\n    78: { name: 'MOTOR_THERMAL_OVERLOAD',               desc: 'Motor thermal overload protection.' },\n    79: { name: 'INTERNAL_MCU_COMM_LOSS',               desc: 'A7 \u2194 M4 internal communication lost.' },\n    80: { name: 'BACNET_COMM_LOSS',                     desc: 'BACnet communication interrupted.' },\n  },\n  AlarmSeverity: {\n    UNSPECIFIED: { n: 0, desc: 'Unspecified severity.' },\n    MINOR:       { n: 1, desc: 'No effect on system state or motor \u2014 purely informational.' },\n    MEDIUM:      { n: 2, desc: 'Puts system in Fault, motor ramps down. Motor cannot restart while active.' },\n    MAJOR:       { n: 3, desc: 'Puts system in Fault, motor freewheels (immediate halt). Motor cannot restart while active.' },\n    CRITICAL:    { n: 4, desc: 'Puts drive in LOCKOUT until System.ResetLockout is called. Motor freewheels.' },\n  },\n  AlarmState: {\n    UNSPECIFIED: { n: 0, desc: 'Unspecified.' },\n    NORMAL:      { n: 1, desc: 'Inactive.' },\n    OOS:         { n: 2, desc: 'Out of service (disabled).' },\n    ACTIVE:      { n: 3, desc: 'Active.' },\n  },\n  SystemState: {\n    UNSPECIFIED:              { n: 0,  desc: 'N\/A.' },\n    BOOTING:                  { n: 1,  desc: 'System is booting.' },\n    UNCONFIGURED:             { n: 2,  desc: 'System must be configured to be ready to run (default after factory reset).' },\n    READY_TO_RUN:             { n: 3,  desc: 'System configured and ready to run.' },\n    RUNNING_AUTO_TUNING:      { n: 4,  desc: 'System is running the autotuning process.' },\n    RUNNING_NORMAL:           { n: 5,  desc: 'System running normally.' },\n    EMERGENCY:                { n: 6,  desc: 'Emergency mode \u2014 ignores most alarms except hardware faults and STO.' },\n    MAINTENANCE:              { n: 7,  desc: 'Maintenance mode \u2014 certain alarms ignored.' },\n    LOCKOUT:                  { n: 8,  desc: 'Caused by CRITICAL alarm. Requires System.ResetLockout to exit.' },\n    SHUTTING_DOWN:            { n: 9,  desc: 'NOT IMPLEMENTED \u2014 for brownout handling.' },\n    FACTORY_RESET:            { n: 10, desc: 'Applying factory reset; will shut down after done.' },\n    FAULT:                    { n: 11, desc: 'System fault \u2014 motor ramp-down (MEDIUM) or halt\/freewheel (MAJOR). Motor cannot restart.' },\n    NEEDS_AUTOTUNING:         { n: 12, desc: 'Configured but requires autotuning before being ready to run.' },\n    RUNNING_DEGRADED:         { n: 13, desc: 'System running with active alarms.' },\n    HARDWARE_TEST:            { n: 14, desc: 'Manufacturing scripts running; not in a clean state.' },\n    SOFTWARE_VERSION_CHANGE:  { n: 15, desc: 'Undergoing software version change.' },\n  },\n  MotorControllerState: {\n    UNSPECIFIED:    { n: 0, desc: 'N\/A.' },\n    DISABLED:       { n: 1, desc: 'No voltage \/ not running. ON\/RESUME commands forbidden.' },\n    HALTED:         { n: 2, desc: 'No voltage \/ not running. Motor halted.' },\n    RAMPING_UP:     { n: 3, desc: 'Voltage \/ running. Motor ramping up.' },\n    RUNNING:        { n: 4, desc: 'Voltage \/ running. Steady state.' },\n    RAMPING_DOWN:   { n: 5, desc: 'Voltage \/ running. Motor ramping down.' },\n    PAUSED:         { n: 6, desc: 'No voltage \/ running. ON does nothing; only speed-change command restarts.' },\n    AUTOTUNING:     { n: 7, desc: 'Voltage \/ running. Motor running an autotuning process.' },\n    MAGNETIZING:    { n: 8, desc: 'Voltage \/ running. Motor in pre-magnetization state.' },\n    STARTING:       { n: 9, desc: 'No voltage \/ not running. Waiting for start sequence (bypass relay enabling, AFE starting).' },\n  },\n  ResponseStatus: {\n    UNSPECIFIED:            { n: 0,  desc: 'Unspecified.' },\n    OK:                     { n: 1,  desc: 'Accepted, final response (HTTP 200).' },\n    PARTIALLY_ACCEPTED:     { n: 2,  desc: 'Partially accepted; some parts failed (HTTP 207).' },\n    INVALID_ARGUMENT:       { n: 3,  desc: 'Bad argument(s) (HTTP 400).' },\n    UNAUTHORIZED:           { n: 4,  desc: 'User must be authenticated (HTTP 401).' },\n    NOT_FOUND:              { n: 5,  desc: 'Resource not found (HTTP 404).' },\n    CONFLICT:               { n: 6,  desc: 'Conflict with current state (HTTP 409).' },\n    INTERNAL_SERVER_ERROR:  { n: 7,  desc: 'Error processing the request (HTTP 500).' },\n    NOT_IMPLEMENTED:        { n: 8,  desc: 'Request not supported (HTTP 501).' },\n    TIMEOUT:                { n: 9,  desc: 'Processing led to a timeout (HTTP 504).' },\n    FORBIDDEN:              { n: 10, desc: 'User lacks permission (HTTP 403).' },\n  },\n};\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ V1.8.1 \u2014 Configuration-parameter enum reference (smartd_api 2026-07-02).\n\/\/ The audit\/system enums above are the \"event\" vocabulary; the block below adds\n\/\/ the full configuration-parameter vocabulary (Motor Controller, I\/O, PID,\n\/\/ System, etc.) so CONFIG_CHANGE payloads and the Configuration tab can show a\n\/\/ plain-language meaning for every enum value, and so the API Reference modal\n\/\/ can browse them.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nSMARTD_API.ConfigEnums = {\n  \"AlarmAction\": {\n    \"label\": \"Alarm \u00b7 Trigger Action\",\n    \"domain\": \"Alarms\",\n    \"prefix\": \"ALARM_ACTION_\",\n    \"values\": {\n      \"ALARM_ACTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"N\/A.\"\n      },\n      \"ALARM_ACTION_PID_1_FORCED_OUTPUT\": {\n        \"n\": 1,\n        \"desc\": \"Set PID #1 to FORCED_OUTPUT.\"\n      },\n      \"ALARM_ACTION_PID_1_CONSTANT_OUTPUT\": {\n        \"n\": 2,\n        \"desc\": \"Set PID #1 to CONSTANT_OUTPUT.\"\n      },\n      \"ALARM_ACTION_STOP_FREEWHEEL\": {\n        \"n\": 3,\n        \"desc\": \"Stop the motor by coasting (motor state HALTED).\"\n      },\n      \"ALARM_ACTION_STOP_RAMP_DOWN\": {\n        \"n\": 4,\n        \"desc\": \"Stop the motor by ramping down.\"\n      },\n      \"ALARM_ACTION_NO_ACTION\": {\n        \"n\": 5,\n        \"desc\": \"Raise the alarm but take no action.\"\n      }\n    }\n  },\n  \"AlarmBootState\": {\n    \"label\": \"Alarm \u00b7 Boot State\",\n    \"domain\": \"Alarms\",\n    \"prefix\": \"ALARM_BOOT_STATE_\",\n    \"values\": {\n      \"ALARM_BOOT_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"N\/A.\"\n      },\n      \"ALARM_BOOT_STATE_NORMAL\": {\n        \"n\": 1,\n        \"desc\": \"Alarm resets to normal inactive state on boot (default).\"\n      },\n      \"ALARM_BOOT_STATE_RESTORE\": {\n        \"n\": 2,\n        \"desc\": \"Alarm state is persisted through a reboot.\"\n      }\n    }\n  },\n  \"AlarmSensorSource\": {\n    \"label\": \"Alarm \u00b7 Sensor Source (deprecated)\",\n    \"domain\": \"Alarms\",\n    \"prefix\": \"ALARM_SENSOR_SOURCE_\",\n    \"values\": {\n      \"ALARM_SENSOR_SOURCE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Deprecated since v2026.04.00. Source is inferred automatically by the backend.\"\n      },\n      \"ALARM_SENSOR_SOURCE_AI1\": {\n        \"n\": 1,\n        \"desc\": \"Deprecated. Analog input 1.\"\n      },\n      \"ALARM_SENSOR_SOURCE_AI2\": {\n        \"n\": 2,\n        \"desc\": \"Deprecated. Analog input 2.\"\n      },\n      \"ALARM_SENSOR_SOURCE_AI3\": {\n        \"n\": 3,\n        \"desc\": \"Deprecated. Analog input 3.\"\n      }\n    }\n  },\n  \"AuditLogExportStatus\": {\n    \"label\": \"Audit Log \u00b7 Export Status\",\n    \"domain\": \"Audit Logs\",\n    \"prefix\": \"AUDIT_LOG_EXPORT_STATUS_\",\n    \"values\": {\n      \"AUDIT_LOG_EXPORT_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"AUDIT_LOG_EXPORT_STATUS_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"No export in progress.\"\n      },\n      \"AUDIT_LOG_EXPORT_STATUS_IN_PROGRESS\": {\n        \"n\": 2,\n        \"desc\": \"Export to USB is running.\"\n      },\n      \"AUDIT_LOG_EXPORT_STATUS_FAILED\": {\n        \"n\": 3,\n        \"desc\": \"Export failed.\"\n      },\n      \"AUDIT_LOG_EXPORT_STATUS_COMPLETED\": {\n        \"n\": 4,\n        \"desc\": \"Export completed.\"\n      }\n    }\n  },\n  \"AuditLogTimestampPresetFilter\": {\n    \"label\": \"Audit Log \u00b7 Timestamp Preset Filter\",\n    \"domain\": \"Audit Logs\",\n    \"prefix\": \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_\",\n    \"values\": {\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_ALL\": {\n        \"n\": 1,\n        \"desc\": \"All logs.\"\n      },\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_PAST_HOUR\": {\n        \"n\": 2,\n        \"desc\": \"Logs from the past hour.\"\n      },\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_PAST_DAY\": {\n        \"n\": 3,\n        \"desc\": \"Logs from the past day.\"\n      },\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_PAST_WEEK\": {\n        \"n\": 4,\n        \"desc\": \"Logs from the past week.\"\n      },\n      \"AUDIT_LOG_TIMESTAMP_PRESET_FILTER_PAST_MONTH\": {\n        \"n\": 5,\n        \"desc\": \"Logs from the past month.\"\n      }\n    }\n  },\n  \"AuthAdminPasswordStatus\": {\n    \"label\": \"Auth \u00b7 Admin Password Status\",\n    \"domain\": \"Auth\",\n    \"prefix\": \"AUTH_ADMIN_PASSWORD_STATUS_\",\n    \"values\": {\n      \"AUTH_ADMIN_PASSWORD_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"AUTH_ADMIN_PASSWORD_STATUS_DEFAULT\": {\n        \"n\": 1,\n        \"desc\": \"Admin account still uses the VFD factory default password.\"\n      },\n      \"AUTH_ADMIN_PASSWORD_STATUS_UP_TO_DATE\": {\n        \"n\": 2,\n        \"desc\": \"Admin password has been changed from the default.\"\n      }\n    }\n  },\n  \"AuthLockMode\": {\n    \"label\": \"Auth \u00b7 Lock Mode\",\n    \"domain\": \"Auth\",\n    \"prefix\": \"AUTH_LOCK_MODE_\",\n    \"values\": {\n      \"AUTH_LOCK_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"AUTH_LOCK_MODE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Default setting \u2014 authentication lock disabled.\"\n      },\n      \"AUTH_LOCK_MODE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Authentication lock enabled.\"\n      }\n    }\n  },\n  \"BLEAdvertisingState\": {\n    \"label\": \"BLE \u00b7 Advertising State\",\n    \"domain\": \"BLE\",\n    \"prefix\": \"BLE_ADVERTISING_STATE_\",\n    \"values\": {\n      \"BLE_ADVERTISING_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unknown status.\"\n      },\n      \"BLE_ADVERTISING_STATE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Advertising disabled (no advertisement).\"\n      },\n      \"BLE_ADVERTISING_STATE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Advertising enabled.\"\n      }\n    }\n  },\n  \"BLEStatus\": {\n    \"label\": \"BLE \u00b7 Status\",\n    \"domain\": \"BLE\",\n    \"prefix\": \"BLE_STATUS_\",\n    \"values\": {\n      \"BLE_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Not connected \/ unknown.\"\n      },\n      \"BLE_STATUS_OK\": {\n        \"n\": 1,\n        \"desc\": \"Connected and advertising normally.\"\n      },\n      \"BLE_STATUS_DISCONNECTED\": {\n        \"n\": 2,\n        \"desc\": \"Device is disconnected.\"\n      },\n      \"BLE_STATUS_UPDATING\": {\n        \"n\": 3,\n        \"desc\": \"Deprecated since v2026.07.00. Update only done at boot; no longer reported.\"\n      },\n      \"BLE_STATUS_PAIRING\": {\n        \"n\": 4,\n        \"desc\": \"Pairing (replaces advertising, which is now the OK state).\"\n      }\n    }\n  },\n  \"BLETestMode\": {\n    \"label\": \"BLE \u00b7 Test Mode\",\n    \"domain\": \"BLE\",\n    \"prefix\": \"BLE_TEST_MODE_\",\n    \"values\": {\n      \"BLE_TEST_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unknown status.\"\n      },\n      \"BLE_TEST_MODE_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Test mode deactivated.\"\n      },\n      \"BLE_TEST_MODE_TX_LOW_CHANNEL_0\": {\n        \"n\": 2,\n        \"desc\": \"Test transmit on low channel 0.\"\n      },\n      \"BLE_TEST_MODE_TX_MID_CHANNEL_19\": {\n        \"n\": 3,\n        \"desc\": \"Test transmit on mid channel 19.\"\n      },\n      \"BLE_TEST_MODE_TX_HIGH_CHANNEL_39\": {\n        \"n\": 4,\n        \"desc\": \"Test transmit on high channel 39.\"\n      }\n    }\n  },\n  \"ConfigurationImportExportLocation\": {\n    \"label\": \"Configuration \u00b7 Import\/Export Location\",\n    \"domain\": \"Configuration\",\n    \"prefix\": \"CONFIGURATION_IMPORT_EXPORT_LOCATION_\",\n    \"values\": {\n      \"CONFIGURATION_IMPORT_EXPORT_LOCATION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to USB.\"\n      },\n      \"CONFIGURATION_IMPORT_EXPORT_LOCATION_USB\": {\n        \"n\": 1,\n        \"desc\": \"USB thumb drive.\"\n      },\n      \"CONFIGURATION_IMPORT_EXPORT_LOCATION_SOFTWARE_UPDATE\": {\n        \"n\": 2,\n        \"desc\": \"Software-update package.\"\n      }\n    }\n  },\n  \"ConfigurationProfileID\": {\n    \"label\": \"Configuration \u00b7 Profile ID\",\n    \"domain\": \"Configuration\",\n    \"prefix\": \"CONFIGURATION_PROFILE_ID_\",\n    \"values\": {\n      \"CONFIGURATION_PROFILE_ID_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"CONFIGURATION_PROFILE_ID_1\": {\n        \"n\": 1,\n        \"desc\": \"Configuration profile 1.\"\n      },\n      \"CONFIGURATION_PROFILE_ID_2\": {\n        \"n\": 2,\n        \"desc\": \"Configuration profile 2.\"\n      }\n    }\n  },\n  \"ControlInterfacePreset\": {\n    \"label\": \"Configuration \u00b7 Control Interface Preset\",\n    \"domain\": \"Configuration\",\n    \"prefix\": \"CONTROL_INTERFACE_PRESET_\",\n    \"values\": {\n      \"CONTROL_INTERFACE_PRESET_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"CONTROL_INTERFACE_PRESET_IO_2_WIRE\": {\n        \"n\": 1,\n        \"desc\": \"Applies the 2-wire I\/O preset.\"\n      },\n      \"CONTROL_INTERFACE_PRESET_IO_3_WIRE\": {\n        \"n\": 2,\n        \"desc\": \"Applies the 3-wire I\/O preset.\"\n      },\n      \"CONTROL_INTERFACE_PRESET_MODBUS\": {\n        \"n\": 3,\n        \"desc\": \"Enables Modbus.\"\n      }\n    }\n  },\n  \"CoolingFanState\": {\n    \"label\": \"Cooling \u00b7 Fan State\",\n    \"domain\": \"Line & Cooling\",\n    \"prefix\": \"COOLING_FAN_STATE_\",\n    \"values\": {\n      \"COOLING_FAN_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"COOLING_FAN_STATE_ON\": {\n        \"n\": 1,\n        \"desc\": \"Fan is ON.\"\n      },\n      \"COOLING_FAN_STATE_OFF\": {\n        \"n\": 2,\n        \"desc\": \"Fan is OFF.\"\n      }\n    }\n  },\n  \"CoolingMode\": {\n    \"label\": \"Cooling \u00b7 Mode\",\n    \"domain\": \"Line & Cooling\",\n    \"prefix\": \"COOLING_MODE_\",\n    \"values\": {\n      \"COOLING_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"COOLING_MODE_NORMAL\": {\n        \"n\": 1,\n        \"desc\": \"Fans run at full speed when the drive is running.\"\n      },\n      \"COOLING_MODE_ADVANCED\": {\n        \"n\": 2,\n        \"desc\": \"Adaptive fan speed based on drive temperature.\"\n      },\n      \"COOLING_MODE_TEST\": {\n        \"n\": 3,\n        \"desc\": \"Fan test mode (available when system is out of service).\"\n      }\n    }\n  },\n  \"DateTimeUpdaterProtocol\": {\n    \"label\": \"Date\/Time \u00b7 Updater Protocol\",\n    \"domain\": \"Date\/Time\",\n    \"prefix\": \"DATE_TIME_UPDATER_PROTOCOL_\",\n    \"values\": {\n      \"DATE_TIME_UPDATER_PROTOCOL_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DATE_TIME_UPDATER_PROTOCOL_NONE\": {\n        \"n\": 1,\n        \"desc\": \"NTP disabled (set time manually).\"\n      },\n      \"DATE_TIME_UPDATER_PROTOCOL_NTP\": {\n        \"n\": 2,\n        \"desc\": \"NTP protocol.\"\n      }\n    }\n  },\n  \"DisplayFilter\": {\n    \"label\": \"Display \u00b7 Filter\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_FILTER_\",\n    \"values\": {\n      \"DISPLAY_FILTER_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_FILTER_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"No modification \u2014 display regular image.\"\n      },\n      \"DISPLAY_FILTER_BLUR\": {\n        \"n\": 2,\n        \"desc\": \"Blur the image.\"\n      }\n    }\n  },\n  \"DisplayScreensaverMode\": {\n    \"label\": \"Display \u00b7 Screensaver Mode\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_SCREENSAVER_MODE_\",\n    \"values\": {\n      \"DISPLAY_SCREENSAVER_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_SCREENSAVER_MODE_BACKLIGHT_OFF\": {\n        \"n\": 1,\n        \"desc\": \"Default screensaver \u2014 backlight off.\"\n      },\n      \"DISPLAY_SCREENSAVER_MODE_BLUR\": {\n        \"n\": 2,\n        \"desc\": \"Screensaver blurs the screen.\"\n      }\n    }\n  },\n  \"DisplayScreensaverState\": {\n    \"label\": \"Display \u00b7 Screensaver State\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_SCREENSAVER_STATE_\",\n    \"values\": {\n      \"DISPLAY_SCREENSAVER_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_SCREENSAVER_STATE_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Screensaver inactive.\"\n      },\n      \"DISPLAY_SCREENSAVER_STATE_ACTIVE\": {\n        \"n\": 2,\n        \"desc\": \"Screensaver active.\"\n      },\n      \"DISPLAY_SCREENSAVER_STATE_PIN_UNLOCK_SCREEN\": {\n        \"n\": 3,\n        \"desc\": \"PIN unlock screen shown.\"\n      }\n    }\n  },\n  \"DisplayScreenshotState\": {\n    \"label\": \"Display \u00b7 Screenshot State\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_SCREENSHOT_STATE_\",\n    \"values\": {\n      \"DISPLAY_SCREENSHOT_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_SCREENSHOT_STATE_IDLE\": {\n        \"n\": 1,\n        \"desc\": \"Idle.\"\n      },\n      \"DISPLAY_SCREENSHOT_STATE_REQUESTED\": {\n        \"n\": 2,\n        \"desc\": \"Server is awaiting a screenshot from the HMI.\"\n      }\n    }\n  },\n  \"DisplayUserActivityRequestType\": {\n    \"label\": \"Display \u00b7 User Activity Request Type\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_USER_ACTIVITY_REQUEST_TYPE_\",\n    \"values\": {\n      \"DISPLAY_USER_ACTIVITY_REQUEST_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_USER_ACTIVITY_REQUEST_TYPE_TOUCH\": {\n        \"n\": 1,\n        \"desc\": \"Touch activity.\"\n      },\n      \"DISPLAY_USER_ACTIVITY_REQUEST_TYPE_LOCK\": {\n        \"n\": 2,\n        \"desc\": \"Lock request.\"\n      }\n    }\n  },\n  \"DisplayUserActivitySource\": {\n    \"label\": \"Display \u00b7 User Activity Source\",\n    \"domain\": \"Display\",\n    \"prefix\": \"DISPLAY_USER_ACTIVITY_SOURCE_\",\n    \"values\": {\n      \"DISPLAY_USER_ACTIVITY_SOURCE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"DISPLAY_USER_ACTIVITY_SOURCE_HMI\": {\n        \"n\": 1,\n        \"desc\": \"Activity from the HMI application (touchscreen).\"\n      },\n      \"DISPLAY_USER_ACTIVITY_SOURCE_HMI_IO\": {\n        \"n\": 2,\n        \"desc\": \"Activity from the HMI I\/Os (buttons).\"\n      }\n    }\n  },\n  \"ControlSupervisorState\": {\n    \"label\": \"Industrial Interface \u00b7 Control Supervisor State\",\n    \"domain\": \"Industrial Interface\",\n    \"prefix\": \"CONTROL_SUPERVISOR_STATE_\",\n    \"values\": {\n      \"CONTROL_SUPERVISOR_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Control Supervisor state undetermined.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_STARTUP\": {\n        \"n\": 1,\n        \"desc\": \"Booting \/ initializing.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_NOT_READY\": {\n        \"n\": 2,\n        \"desc\": \"Not ready to start the motor.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_READY\": {\n        \"n\": 3,\n        \"desc\": \"Ready to start the motor.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_ENABLED\": {\n        \"n\": 4,\n        \"desc\": \"Motor is running.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_STOPPING\": {\n        \"n\": 5,\n        \"desc\": \"Motor is stopping.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_FAULT_STOP\": {\n        \"n\": 6,\n        \"desc\": \"Motor is stopping because of a fault.\"\n      },\n      \"CONTROL_SUPERVISOR_STATE_FAULTED\": {\n        \"n\": 7,\n        \"desc\": \"Motor stopped and Control Supervisor is in fault.\"\n      }\n    }\n  },\n  \"IOAnalogInputFunction\": {\n    \"label\": \"I\/O \u00b7 Analog Input Function\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_INPUT_FUNCTION_\",\n    \"values\": {\n      \"IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_FREQUENCY\": {\n        \"n\": 1,\n        \"desc\": \"Controls the motor frequency setpoint.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_VELOCITY\": {\n        \"n\": 2,\n        \"desc\": \"Controls the motor velocity setpoint.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_PID_1_PROCESS_FEEDBACK_VALUE\": {\n        \"n\": 3,\n        \"desc\": \"Sets the PID #1 process feedback value.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_PID_1_PROCESS_SETPOINT\": {\n        \"n\": 4,\n        \"desc\": \"Sets the PID #1 process setpoint.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_PID_1_FORCED_OUTPUT_VALUE\": {\n        \"n\": 5,\n        \"desc\": \"Sets the PID #1 forced output value.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SENSOR_PTC\": {\n        \"n\": 6,\n        \"desc\": \"Resistance measurement of a PTC sensor.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_TORQUE\": {\n        \"n\": 7,\n        \"desc\": \"Controls the motor torque setpoint.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_MIN_TORQUE\": {\n        \"n\": 8,\n        \"desc\": \"Controls the motor minimum torque setpoint.\"\n      },\n      \"IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_MAX_TORQUE\": {\n        \"n\": 9,\n        \"desc\": \"Controls the motor maximum torque setpoint.\"\n      }\n    }\n  },\n  \"IOAnalogInputMode\": {\n    \"label\": \"I\/O \u00b7 Analog Input Mode\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_INPUT_MODE_\",\n    \"values\": {\n      \"IO_ANALOG_INPUT_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_INPUT_MODE_4_20_MA\": {\n        \"n\": 1,\n        \"desc\": \"Input signal is a current between 4-20 mA.\"\n      },\n      \"IO_ANALOG_INPUT_MODE_0_10_V\": {\n        \"n\": 2,\n        \"desc\": \"Input signal is a voltage between 0-10 V.\"\n      },\n      \"IO_ANALOG_INPUT_MODE_RESISTOR\": {\n        \"n\": 3,\n        \"desc\": \"Input signal is a resistance from 500 to 10K ohms.\"\n      },\n      \"IO_ANALOG_INPUT_MODE_PTC\": {\n        \"n\": 4,\n        \"desc\": \"Input signal is a PTC resistance measurement in ohms.\"\n      }\n    }\n  },\n  \"IOAnalogInputUnit\": {\n    \"label\": \"I\/O \u00b7 Analog Input Unit\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_INPUT_UNIT_\",\n    \"values\": {\n      \"IO_ANALOG_INPUT_UNIT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_INPUT_UNIT_MA\": {\n        \"n\": 1,\n        \"desc\": \"Milliamps (mA).\"\n      },\n      \"IO_ANALOG_INPUT_UNIT_V\": {\n        \"n\": 2,\n        \"desc\": \"Volts (V).\"\n      },\n      \"IO_ANALOG_INPUT_UNIT_KOHM\": {\n        \"n\": 3,\n        \"desc\": \"Kiloohms (k\u03a9).\"\n      }\n    }\n  },\n  \"IOAnalogOutputFunction\": {\n    \"label\": \"I\/O \u00b7 Analog Output Function\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_OUTPUT_FUNCTION_\",\n    \"values\": {\n      \"IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_OUTPUT_FUNCTION_MOTOR_FREQUENCY\": {\n        \"n\": 1,\n        \"desc\": \"Outputs the motor frequency (Hz).\"\n      },\n      \"IO_ANALOG_OUTPUT_FUNCTION_MOTOR_CURRENT_RMS\": {\n        \"n\": 2,\n        \"desc\": \"Outputs the motor current RMS (A).\"\n      },\n      \"IO_ANALOG_OUTPUT_FUNCTION_MOTOR_VELOCITY\": {\n        \"n\": 3,\n        \"desc\": \"Outputs the motor velocity (rpm).\"\n      },\n      \"IO_ANALOG_OUTPUT_FUNCTION_DRIVE_OUTPUT_ACTIVE_POWER\": {\n        \"n\": 4,\n        \"desc\": \"Outputs the drive output active power (kW).\"\n      },\n      \"IO_ANALOG_OUTPUT_FUNCTION_MOTOR_TORQUE\": {\n        \"n\": 5,\n        \"desc\": \"Outputs the motor torque (%).\"\n      }\n    }\n  },\n  \"IOAnalogOutputMode\": {\n    \"label\": \"I\/O \u00b7 Analog Output Mode\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_OUTPUT_MODE_\",\n    \"values\": {\n      \"IO_ANALOG_OUTPUT_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_OUTPUT_MODE_4_20_MA\": {\n        \"n\": 1,\n        \"desc\": \"Output signal is a current between 4-20 mA.\"\n      },\n      \"IO_ANALOG_OUTPUT_MODE_0_10_V\": {\n        \"n\": 2,\n        \"desc\": \"Output signal is a voltage between 0-10 V.\"\n      }\n    }\n  },\n  \"IOAnalogOutputUnit\": {\n    \"label\": \"I\/O \u00b7 Analog Output Unit\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_ANALOG_OUTPUT_UNIT_\",\n    \"values\": {\n      \"IO_ANALOG_OUTPUT_UNIT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_ANALOG_OUTPUT_UNIT_MA\": {\n        \"n\": 1,\n        \"desc\": \"Milliamps (mA).\"\n      },\n      \"IO_ANALOG_OUTPUT_UNIT_V\": {\n        \"n\": 2,\n        \"desc\": \"Volts (V).\"\n      }\n    }\n  },\n  \"IODigitalInputFunction\": {\n    \"label\": \"I\/O \u00b7 Digital Input Function\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_DIGITAL_INPUT_FUNCTION_\",\n    \"values\": {\n      \"IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD\": {\n        \"n\": 1,\n        \"desc\": \"[Edge] 3-wire control: run motor forward.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE\": {\n        \"n\": 2,\n        \"desc\": \"[Edge] 3-wire control: run motor in reverse.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP\": {\n        \"n\": 3,\n        \"desc\": \"[Level] 3-wire control: stop the motor.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_SPEED_FREQUENCY_REFERENCE\": {\n        \"n\": 4,\n        \"desc\": \"[Level] Select the preset\/analog frequency reference.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_FORWARD\": {\n        \"n\": 5,\n        \"desc\": \"[Level] 2-wire control: run motor forward.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_REVERSE\": {\n        \"n\": 6,\n        \"desc\": \"[Level] 2-wire control: run motor in reverse.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_SPEED_VELOCITY_REFERENCE\": {\n        \"n\": 7,\n        \"desc\": \"[Level] Select the preset\/analog velocity reference.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_CONTROL_MODE\": {\n        \"n\": 8,\n        \"desc\": \"[Level] Select control mode: inactive=local, active=remote.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_REMOTE_CONTROL_SOURCE\": {\n        \"n\": 9,\n        \"desc\": \"[Level] Select remote source: inactive=source 1, active=source 2.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_PID_FORCED_OUTPUT_STATE\": {\n        \"n\": 10,\n        \"desc\": \"[Level] Enable\/disable PID forced output.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_ENCODER_INCREMENTAL\": {\n        \"n\": 11,\n        \"desc\": \"[None] Incremental (multi-turn) encoder feedback.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT\": {\n        \"n\": 12,\n        \"desc\": \"[Edge] Reset the lockout system state.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_UP_AND_RAMP_DOWN_CURVES\": {\n        \"n\": 13,\n        \"desc\": \"[Level] Switch both ramp-up and ramp-down curves (set 1 vs set 2).\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_UP_CURVE\": {\n        \"n\": 14,\n        \"desc\": \"[Level] Switch the ramp-up curve (1 vs 2).\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_DOWN_CURVE\": {\n        \"n\": 15,\n        \"desc\": \"[Level] Switch the ramp-down curve (1 vs 2).\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_EMERGENCY_MODE\": {\n        \"n\": 16,\n        \"desc\": \"[Level] Enter\/exit emergency mode.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_DAMPER_FEEDBACK\": {\n        \"n\": 17,\n        \"desc\": \"[Level] HVAC damper feedback \u2014 motor waits for this before ramping up.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_MAINTENANCE_MODE\": {\n        \"n\": 18,\n        \"desc\": \"[Level] Enter\/exit maintenance mode.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_1\": {\n        \"n\": 19,\n        \"desc\": \"[Level] Raises ALARM_CODE_EXTERNAL_1 when active.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_2\": {\n        \"n\": 20,\n        \"desc\": \"[Level] Raises ALARM_CODE_EXTERNAL_2 when active.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_3\": {\n        \"n\": 21,\n        \"desc\": \"[Level] Raises ALARM_CODE_EXTERNAL_3 when active.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_4\": {\n        \"n\": 22,\n        \"desc\": \"[Level] Raises ALARM_CODE_EXTERNAL_4 when active.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_INTERLOCK\": {\n        \"n\": 23,\n        \"desc\": \"[Level] When active, sets motor control as disabled.\"\n      },\n      \"IO_DIGITAL_INPUT_FUNCTION_SELECT_ACTIVE_CONFIGURATION\": {\n        \"n\": 24,\n        \"desc\": \"[Level] Switch active configuration profile (1 vs 2).\"\n      }\n    }\n  },\n  \"IODigitalInputsPowerDirection\": {\n    \"label\": \"I\/O \u00b7 Digital Inputs Power Direction\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_DIGITAL_INPUTS_POWER_DIRECTION_\",\n    \"values\": {\n      \"IO_DIGITAL_INPUTS_POWER_DIRECTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_DIGITAL_INPUTS_POWER_DIRECTION_SOURCE\": {\n        \"n\": 1,\n        \"desc\": \"Drive provides the power for the digital inputs (incl. STO).\"\n      },\n      \"IO_DIGITAL_INPUTS_POWER_DIRECTION_SINK_INTERNAL\": {\n        \"n\": 2,\n        \"desc\": \"Drive provides the ground; external power required.\"\n      },\n      \"IO_DIGITAL_INPUTS_POWER_DIRECTION_SINK_EXTERNAL\": {\n        \"n\": 3,\n        \"desc\": \"Ground provided externally; external power required.\"\n      }\n    }\n  },\n  \"IOIncrementalEncoderWiring\": {\n    \"label\": \"I\/O \u00b7 Incremental Encoder Wiring\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_INCREMENTAL_ENCODER_WIRING_\",\n    \"values\": {\n      \"IO_INCREMENTAL_ENCODER_WIRING_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_INCREMENTAL_ENCODER_WIRING_AB\": {\n        \"n\": 1,\n        \"desc\": \"Straight: Encoder_A on DI1, Encoder_B on DI2.\"\n      },\n      \"IO_INCREMENTAL_ENCODER_WIRING_BA\": {\n        \"n\": 2,\n        \"desc\": \"Reversed: Encoder_B on DI1, Encoder_A on DI2.\"\n      }\n    }\n  },\n  \"IORelayFunction\": {\n    \"label\": \"I\/O \u00b7 Relay Function\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_RELAY_FUNCTION_\",\n    \"values\": {\n      \"IO_RELAY_FUNCTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_RELAY_FUNCTION_MOTOR_READY_TO_RUN\": {\n        \"n\": 1,\n        \"desc\": \"Output when the motor is in READY_TO_RUN state.\"\n      },\n      \"IO_RELAY_FUNCTION_MOTOR_RUNNING\": {\n        \"n\": 2,\n        \"desc\": \"Output when the motor is in RUNNING state.\"\n      },\n      \"IO_RELAY_FUNCTION_ANY_ALARM\": {\n        \"n\": 3,\n        \"desc\": \"Output when any alarm (excluding minor) is active.\"\n      },\n      \"IO_RELAY_FUNCTION_MOTOR_LOCKOUT\": {\n        \"n\": 4,\n        \"desc\": \"Output when the motor is in LOCKOUT state.\"\n      },\n      \"IO_RELAY_FUNCTION_MOTOR_TORQUE\": {\n        \"n\": 5,\n        \"desc\": \"Output when torque is being applied to the motor.\"\n      },\n      \"IO_RELAY_FUNCTION_DAMPER_CONTROL\": {\n        \"n\": 6,\n        \"desc\": \"Output to activate a damper for an HVAC system.\"\n      },\n      \"IO_RELAY_FUNCTION_ANY_ALARM_NEEDS_ACKNOWLEDGE\": {\n        \"n\": 7,\n        \"desc\": \"Output when any alarm (excluding minor) needs acknowledgement.\"\n      },\n      \"IO_RELAY_FUNCTION_ACTIVE_SWITCHOVER_LIMIT\": {\n        \"n\": 8,\n        \"desc\": \"Output when the secondary switchover limit is being applied.\"\n      }\n    }\n  },\n  \"IORelayOutput\": {\n    \"label\": \"I\/O \u00b7 Relay Output\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_RELAY_OUTPUT_\",\n    \"values\": {\n      \"IO_RELAY_OUTPUT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_RELAY_OUTPUT_NORMALLY_OPEN\": {\n        \"n\": 1,\n        \"desc\": \"Deprecated \u2014 use DE_ENERGIZED_BY_DEFAULT.\"\n      },\n      \"IO_RELAY_OUTPUT_NORMALLY_CLOSED\": {\n        \"n\": 2,\n        \"desc\": \"Deprecated \u2014 use ENERGIZED_BY_DEFAULT.\"\n      },\n      \"IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT\": {\n        \"n\": 3,\n        \"desc\": \"Relay energized when the function condition is false.\"\n      },\n      \"IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT\": {\n        \"n\": 4,\n        \"desc\": \"Relay de-energized when the function condition is false.\"\n      }\n    }\n  },\n  \"IOState\": {\n    \"label\": \"I\/O \u00b7 State\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_STATE_\",\n    \"values\": {\n      \"IO_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_STATE_CONFIGURED\": {\n        \"n\": 1,\n        \"desc\": \"The I\/O has an effective configuration.\"\n      },\n      \"IO_STATE_NOT_CONFIGURED\": {\n        \"n\": 2,\n        \"desc\": \"The I\/O has no effective configuration.\"\n      },\n      \"IO_STATE_UNAVAILABLE\": {\n        \"n\": 3,\n        \"desc\": \"The I\/O is unavailable.\"\n      }\n    }\n  },\n  \"IOTriggerType\": {\n    \"label\": \"I\/O \u00b7 Trigger Type\",\n    \"domain\": \"I\/O\",\n    \"prefix\": \"IO_TRIGGER_TYPE_\",\n    \"values\": {\n      \"IO_TRIGGER_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"IO_TRIGGER_TYPE_RISING_EDGE\": {\n        \"n\": 1,\n        \"desc\": \"Signal transitioning LOW \u2192 HIGH.\"\n      },\n      \"IO_TRIGGER_TYPE_FALLING_EDGE\": {\n        \"n\": 2,\n        \"desc\": \"Signal transitioning HIGH \u2192 LOW.\"\n      },\n      \"IO_TRIGGER_TYPE_HIGH\": {\n        \"n\": 3,\n        \"desc\": \"Signal is HIGH.\"\n      },\n      \"IO_TRIGGER_TYPE_LOW\": {\n        \"n\": 4,\n        \"desc\": \"Signal is LOW.\"\n      }\n    }\n  },\n  \"LinePhases\": {\n    \"label\": \"Line \u00b7 Phases\",\n    \"domain\": \"Line & Cooling\",\n    \"prefix\": \"LINE_PHASES_\",\n    \"values\": {\n      \"LINE_PHASES_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"LINE_PHASES_ONE\": {\n        \"n\": 1,\n        \"desc\": \"One phase.\"\n      },\n      \"LINE_PHASES_THREE\": {\n        \"n\": 3,\n        \"desc\": \"Three phases.\"\n      }\n    }\n  },\n  \"MotorAutotuneMethod\": {\n    \"label\": \"Motor Autotune \u00b7 Method\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_AUTOTUNE_METHOD_\",\n    \"values\": {\n      \"MOTOR_AUTOTUNE_METHOD_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to STATIC.\"\n      },\n      \"MOTOR_AUTOTUNE_METHOD_STATIC\": {\n        \"n\": 1,\n        \"desc\": \"Standstill: estimates stator\/rotor resistance and inductances.\"\n      },\n      \"MOTOR_AUTOTUNE_METHOD_DYNAMIC\": {\n        \"n\": 2,\n        \"desc\": \"Shaft must turn: estimates moment of inertia.\"\n      }\n    }\n  },\n  \"MotorAutotuneParameterStatus\": {\n    \"label\": \"Motor Autotune \u00b7 Parameter Status\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_AUTOTUNE_PARAMETER_STATUS_\",\n    \"values\": {\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Never autotuned or previous run canceled.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_IN_PROGRESS\": {\n        \"n\": 1,\n        \"desc\": \"Parameter is being autotuned.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_OK\": {\n        \"n\": 2,\n        \"desc\": \"Autotuned successfully, no errors.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_DID_NOT_CONVERGE\": {\n        \"n\": 3,\n        \"desc\": \"Value did not converge.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_LOWER_BOUND\": {\n        \"n\": 4,\n        \"desc\": \"Value went under theoretical lower bound.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_UPPER_BOUND\": {\n        \"n\": 5,\n        \"desc\": \"Value exceeded theoretical upper bound.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_MOTOR_RATING_UNSUPPORTED\": {\n        \"n\": 6,\n        \"desc\": \"Estimated, but the drive does not support the motor.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_INTERRUPTED\": {\n        \"n\": 7,\n        \"desc\": \"Computation was interrupted.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_STATUS_ERROR_LVRT_OCCURRED\": {\n        \"n\": 8,\n        \"desc\": \"May be affected by grid conditions (LVRT).\"\n      }\n    }\n  },\n  \"MotorAutotuneParameterType\": {\n    \"label\": \"Motor Autotune \u00b7 Parameter Type\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_AUTOTUNE_PARAMETER_TYPE_\",\n    \"values\": {\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_STATOR_RESISTANCE\": {\n        \"n\": 1,\n        \"desc\": \"Stator resistance (ohms).\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_ROTOR_RESISTANCE\": {\n        \"n\": 2,\n        \"desc\": \"Rotor resistance (ohms).\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_MAGNETIZING_INDUCTANCE\": {\n        \"n\": 3,\n        \"desc\": \"Magnetizing inductance (H).\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_LEAKAGE_INDUCTANCE\": {\n        \"n\": 4,\n        \"desc\": \"Leakage inductance (H).\"\n      },\n      \"MOTOR_AUTOTUNE_PARAMETER_TYPE_MOMENT_OF_INERTIA\": {\n        \"n\": 5,\n        \"desc\": \"Moment of inertia (kg\u00b7m\u00b2).\"\n      }\n    }\n  },\n  \"MotorAutotuneState\": {\n    \"label\": \"Motor Autotune \u00b7 State\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_AUTOTUNE_STATE_\",\n    \"values\": {\n      \"MOTOR_AUTOTUNE_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_AUTOTUNE_STATE_NOT_ALLOWED_TO_RUN\": {\n        \"n\": 1,\n        \"desc\": \"Not allowed (unconfigured, critical fault active, or motor running).\"\n      },\n      \"MOTOR_AUTOTUNE_STATE_READY_TO_RUN\": {\n        \"n\": 2,\n        \"desc\": \"Configured and ready to run autotune.\"\n      },\n      \"MOTOR_AUTOTUNE_STATE_RUNNING\": {\n        \"n\": 3,\n        \"desc\": \"Autotuning process is running.\"\n      }\n    }\n  },\n  \"MotorControllerAlgorithmControlMode\": {\n    \"label\": \"Motor Controller \u00b7 Algorithm Control Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_FREQUENCY_V_F\": {\n        \"n\": 1,\n        \"desc\": \"V\/F control; setpoint mode must be FREQUENCY or PRESET_FREQUENCY.\"\n      },\n      \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_VELOCITY_V_F\": {\n        \"n\": 2,\n        \"desc\": \"V\/F control; setpoint mode must be VELOCITY or PRESET_VELOCITY.\"\n      },\n      \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_VELOCITY_FOC\": {\n        \"n\": 3,\n        \"desc\": \"FOC control; setpoint mode must be VELOCITY or PRESET_VELOCITY.\"\n      },\n      \"MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_TORQUE_FOC\": {\n        \"n\": 4,\n        \"desc\": \"FOC control; setpoint mode must be TORQUE.\"\n      }\n    }\n  },\n  \"MotorControllerAutoRestartMode\": {\n    \"label\": \"Motor Controller \u00b7 Auto Restart Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_AUTO_RESTART_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_AUTO_RESTART_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Behaves as DISABLED.\"\n      },\n      \"MOTOR_CONTROLLER_AUTO_RESTART_MODE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Never auto-restarts on alarm or brownout.\"\n      },\n      \"MOTOR_CONTROLLER_AUTO_RESTART_MODE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Attempts to auto-restart on alarm or brownout.\"\n      }\n    }\n  },\n  \"MotorControllerBumplessDirectionChange\": {\n    \"label\": \"Motor Controller \u00b7 Bumpless Direction Change\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to DISABLED.\"\n      },\n      \"MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Disabled.\"\n      },\n      \"MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Enabled (requires reverse_enabled=true).\"\n      }\n    }\n  },\n  \"MotorControllerCommand\": {\n    \"label\": \"Motor Controller \u00b7 Command\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_COMMAND_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_COMMAND_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_COMMAND_ON\": {\n        \"n\": 1,\n        \"desc\": \"Start motor.\"\n      },\n      \"MOTOR_CONTROLLER_COMMAND_OFF\": {\n        \"n\": 2,\n        \"desc\": \"Stop motor.\"\n      }\n    }\n  },\n  \"MotorControllerCoolingType\": {\n    \"label\": \"Motor Controller \u00b7 Cooling Type\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_COOLING_TYPE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_COOLING_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to SELF_COOLED.\"\n      },\n      \"MOTOR_CONTROLLER_COOLING_TYPE_SELF_COOLED\": {\n        \"n\": 1,\n        \"desc\": \"Rotor-mounted fan; zero-speed load 70% of nominal.\"\n      },\n      \"MOTOR_CONTROLLER_COOLING_TYPE_FORCED_COOLED\": {\n        \"n\": 2,\n        \"desc\": \"External fan; zero-speed load 100% of nominal.\"\n      }\n    }\n  },\n  \"MotorControllerCurveSelectable\": {\n    \"label\": \"Motor Controller \u00b7 Curve Selectable\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_CURVE_SELECTABLE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_CURVE_SELECTABLE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to ENABLED.\"\n      },\n      \"MOTOR_CONTROLLER_CURVE_SELECTABLE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Curve cannot be used to control the motor.\"\n      },\n      \"MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Curve can be used to control the motor.\"\n      }\n    }\n  },\n  \"MotorControllerDirection\": {\n    \"label\": \"Motor Controller \u00b7 Direction\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_DIRECTION_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_DIRECTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_DIRECTION_FORWARD\": {\n        \"n\": 1,\n        \"desc\": \"Forward.\"\n      },\n      \"MOTOR_CONTROLLER_DIRECTION_REVERSE\": {\n        \"n\": 2,\n        \"desc\": \"Reverse.\"\n      }\n    }\n  },\n  \"MotorControllerDisable\": {\n    \"label\": \"Motor Controller \u00b7 Disable\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_DISABLE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_DISABLE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_DISABLE_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Disable inactive \u2014 motor can be controlled.\"\n      },\n      \"MOTOR_CONTROLLER_DISABLE_ACTIVE\": {\n        \"n\": 2,\n        \"desc\": \"Disable active \u2014 motor cannot be controlled.\"\n      }\n    }\n  },\n  \"MotorControllerFOCEcoMode\": {\n    \"label\": \"Motor Controller \u00b7 FOC Eco Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_FOC_ECO_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_FOC_ECO_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to OFF.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_ECO_MODE_OFF\": {\n        \"n\": 1,\n        \"desc\": \"Favors dynamic response over efficiency.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_ECO_MODE_ON_WITH_MTPA\": {\n        \"n\": 2,\n        \"desc\": \"Eco mode with Maximum Torque Per Ampere (MTPA).\"\n      }\n    }\n  },\n  \"MotorControllerFOCSpeedResponse\": {\n    \"label\": \"Motor Controller \u00b7 FOC Speed Response\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_SLOW\": {\n        \"n\": 1,\n        \"desc\": \"Slow speed-loop response.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_NORMAL\": {\n        \"n\": 2,\n        \"desc\": \"Normal speed-loop response.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_FAST\": {\n        \"n\": 3,\n        \"desc\": \"Fast speed-loop response.\"\n      }\n    }\n  },\n  \"MotorControllerFOCSpeedSource\": {\n    \"label\": \"Motor Controller \u00b7 FOC Speed Source\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_FOC_SPEED_SOURCE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_FOC_SPEED_SOURCE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_SPEED_SOURCE_ESTIMATED\": {\n        \"n\": 1,\n        \"desc\": \"Estimated from the motor controller algorithm.\"\n      },\n      \"MOTOR_CONTROLLER_FOC_SPEED_SOURCE_MEASURED\": {\n        \"n\": 2,\n        \"desc\": \"Measured from user input (e.g. incremental encoder).\"\n      }\n    }\n  },\n  \"MotorControllerLimitCode\": {\n    \"label\": \"Motor Controller \u00b7 Limit Code\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_LIMIT_CODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_LIMIT_CODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MAX_CURRENT\": {\n        \"n\": 1,\n        \"desc\": \"Max current.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MAX_START_CURRENT\": {\n        \"n\": 2,\n        \"desc\": \"Max start current.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MAX_MOTORING_POWER\": {\n        \"n\": 4,\n        \"desc\": \"Max motoring power.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MAX_GENERATING_POWER\": {\n        \"n\": 5,\n        \"desc\": \"Max generating power.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_LOW_TORQUE\": {\n        \"n\": 6,\n        \"desc\": \"Low torque.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_HIGH_TORQUE\": {\n        \"n\": 7,\n        \"desc\": \"High torque.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MIN_VELOCITY\": {\n        \"n\": 8,\n        \"desc\": \"Min velocity.\"\n      },\n      \"MOTOR_CONTROLLER_LIMIT_CODE_MAX_VELOCITY\": {\n        \"n\": 9,\n        \"desc\": \"Max velocity.\"\n      }\n    }\n  },\n  \"MotorControllerLimitsSwitchoverMode\": {\n    \"label\": \"Motor Controller \u00b7 Limits Switchover Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_LIMITS_SWITCHOVER_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_LIMITS_SWITCHOVER_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_LIMITS_SWITCHOVER_MODE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Disabled.\"\n      },\n      \"MOTOR_CONTROLLER_LIMITS_SWITCHOVER_MODE_ENABLED_MAX_CURRENT_LIMIT_BY_FREQUENCY\": {\n        \"n\": 2,\n        \"desc\": \"Limit max current depending on output frequency.\"\n      }\n    }\n  },\n  \"MotorControllerLowVoltageRideThrough\": {\n    \"label\": \"Motor Controller \u00b7 Low Voltage Ride Through\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to ENABLED.\"\n      },\n      \"MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Never handles LVRT; raises the cannot-recover major alarm immediately.\"\n      },\n      \"MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Handles LVRT when supported; raises minor LVRT alarm on occurrence.\"\n      }\n    }\n  },\n  \"MotorControllerMode\": {\n    \"label\": \"Motor Controller \u00b7 Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified (defaults to FREQUENCY).\"\n      },\n      \"MOTOR_CONTROLLER_MODE_FREQUENCY\": {\n        \"n\": 1,\n        \"desc\": \"Frequency setpoint.\"\n      },\n      \"MOTOR_CONTROLLER_MODE_PRESET_FREQUENCY\": {\n        \"n\": 2,\n        \"desc\": \"Preset frequency index.\"\n      },\n      \"MOTOR_CONTROLLER_MODE_VELOCITY\": {\n        \"n\": 3,\n        \"desc\": \"Velocity setpoint.\"\n      },\n      \"MOTOR_CONTROLLER_MODE_PRESET_VELOCITY\": {\n        \"n\": 4,\n        \"desc\": \"Preset velocity index.\"\n      },\n      \"MOTOR_CONTROLLER_MODE_TORQUE\": {\n        \"n\": 5,\n        \"desc\": \"Torque setpoint.\"\n      }\n    }\n  },\n  \"MotorControllerNormalStopBehaviour\": {\n    \"label\": \"Motor Controller \u00b7 Normal Stop Behaviour\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Behaves as RAMP_DOWN.\"\n      },\n      \"MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_RAMP_DOWN\": {\n        \"n\": 1,\n        \"desc\": \"Ramp down after a stop command.\"\n      },\n      \"MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_FREEWHEEL\": {\n        \"n\": 2,\n        \"desc\": \"Freewheel after a stop command.\"\n      }\n    }\n  },\n  \"MotorControllerOverloadClassType\": {\n    \"label\": \"Motor Controller \u00b7 Overload Class Type\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to class 10.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_5\": {\n        \"n\": 1,\n        \"desc\": \"Class 5 \u2014 shortest overload trigger time.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_10\": {\n        \"n\": 2,\n        \"desc\": \"Class 10.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_20\": {\n        \"n\": 3,\n        \"desc\": \"Class 20.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_30\": {\n        \"n\": 4,\n        \"desc\": \"Class 30.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_40\": {\n        \"n\": 5,\n        \"desc\": \"Class 40 \u2014 longest overload trigger time.\"\n      },\n      \"MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_DISABLED\": {\n        \"n\": 255,\n        \"desc\": \"Prevents the thermal overload class alarm from running.\"\n      }\n    }\n  },\n  \"MotorControllerPause\": {\n    \"label\": \"Motor Controller \u00b7 Pause\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_PAUSE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_PAUSE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_PAUSE_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Exit PAUSED state.\"\n      },\n      \"MOTOR_CONTROLLER_PAUSE_ACTIVE\": {\n        \"n\": 2,\n        \"desc\": \"Enter PAUSED state.\"\n      }\n    }\n  },\n  \"MotorControllerPowerUnit\": {\n    \"label\": \"Motor Controller \u00b7 Power Unit\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_POWER_UNIT_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_POWER_UNIT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_POWER_UNIT_W\": {\n        \"n\": 1,\n        \"desc\": \"Watt (W).\"\n      },\n      \"MOTOR_CONTROLLER_POWER_UNIT_HP\": {\n        \"n\": 2,\n        \"desc\": \"Horsepower (HP).\"\n      }\n    }\n  },\n  \"MotorControllerSpinningDirectionPriority\": {\n    \"label\": \"Motor Controller \u00b7 Spinning Direction Priority\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_KEEP_ACTUAL\": {\n        \"n\": 1,\n        \"desc\": \"Continue spinning in the original direction.\"\n      },\n      \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_FORWARD\": {\n        \"n\": 2,\n        \"desc\": \"Forward command has priority.\"\n      },\n      \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_REVERSE\": {\n        \"n\": 3,\n        \"desc\": \"Reverse command has priority.\"\n      },\n      \"MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_RAISE_ALARM\": {\n        \"n\": 4,\n        \"desc\": \"Default \u2014 raise an alarm and stop the motor.\"\n      }\n    }\n  },\n  \"MotorControllerStartBehaviour\": {\n    \"label\": \"Motor Controller \u00b7 Start Behaviour\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_START_BEHAVIOUR_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_START_BEHAVIOUR_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"MOTOR_CONTROLLER_START_BEHAVIOUR_READY\": {\n        \"n\": 1,\n        \"desc\": \"Motor may continue starting after MAGNETIZING.\"\n      },\n      \"MOTOR_CONTROLLER_START_BEHAVIOUR_HOLD\": {\n        \"n\": 2,\n        \"desc\": \"Hold in MAGNETIZING until start behaviour changes to READY.\"\n      }\n    }\n  },\n  \"MotorControllerStartTorqueBoostState\": {\n    \"label\": \"Motor Controller \u00b7 Start Torque Boost State\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to DISABLED.\"\n      },\n      \"MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Start torque boost disabled.\"\n      },\n      \"MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Start torque boost enabled.\"\n      }\n    }\n  },\n  \"MotorControllerTemperatureAtPowerUp\": {\n    \"label\": \"Motor Controller \u00b7 Temperature At Power-Up\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to COLD_START.\"\n      },\n      \"MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_COLD_START\": {\n        \"n\": 1,\n        \"desc\": \"Thermal capacity fully reset on power-up.\"\n      },\n      \"MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_RETAIN_LAST_VALUE\": {\n        \"n\": 2,\n        \"desc\": \"Thermal capacity reinitialized to last retained value.\"\n      }\n    }\n  },\n  \"MotorControllerVFStartMode\": {\n    \"label\": \"Motor Controller \u00b7 V\/F Start Mode\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_VF_START_MODE_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_VF_START_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Behaves as START_FROM_ZERO.\"\n      },\n      \"MOTOR_CONTROLLER_VF_START_MODE_START_FROM_ZERO\": {\n        \"n\": 1,\n        \"desc\": \"Shaft at standstill; DC premagnetization starts immediately.\"\n      },\n      \"MOTOR_CONTROLLER_VF_START_MODE_SCAN_RESTART\": {\n        \"n\": 2,\n        \"desc\": \"Shaft may spin; scans from max to zero, ramps down, then premagnetizes.\"\n      },\n      \"MOTOR_CONTROLLER_VF_START_MODE_CATCH_ON_THE_FLY\": {\n        \"n\": 3,\n        \"desc\": \"Shaft may spin; motor starts when flux reference is reached.\"\n      }\n    }\n  },\n  \"MotorControllerVoltageDropCompensations\": {\n    \"label\": \"Motor Controller \u00b7 Voltage Drop Compensations\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to ENABLED.\"\n      },\n      \"MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"No output-voltage compensation for hardware losses.\"\n      },\n      \"MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_ENABLED\": {\n        \"n\": 2,\n        \"desc\": \"Compensates output voltage for hardware losses when supported.\"\n      }\n    }\n  },\n  \"MotorControllerZeroSpeedBehaviour\": {\n    \"label\": \"Motor Controller \u00b7 Zero Speed Behaviour\",\n    \"domain\": \"Motor Controller\",\n    \"prefix\": \"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_\",\n    \"values\": {\n      \"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Behaves as MOTOR_PAUSE.\"\n      },\n      \"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_PAUSE\": {\n        \"n\": 1,\n        \"desc\": \"Setpoint below minimum \u2192 0; no start command needed to resume.\"\n      },\n      \"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_STOP_RAMP_DOWN\": {\n        \"n\": 2,\n        \"desc\": \"Ramp-stop below minimum; a start command is required to restart.\"\n      },\n      \"MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_MINIMUM_SPEED\": {\n        \"n\": 3,\n        \"desc\": \"Keep motor running at min input frequency.\"\n      }\n    }\n  },\n  \"NetworkConfigurationMethod\": {\n    \"label\": \"Network \u00b7 Configuration Method\",\n    \"domain\": \"Network\",\n    \"prefix\": \"NETWORK_CONFIGURATION_METHOD_\",\n    \"values\": {\n      \"NETWORK_CONFIGURATION_METHOD_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"NETWORK_CONFIGURATION_METHOD_DHCP\": {\n        \"n\": 1,\n        \"desc\": \"DHCP.\"\n      },\n      \"NETWORK_CONFIGURATION_METHOD_STATIC_IPV4\": {\n        \"n\": 2,\n        \"desc\": \"Static IPv4.\"\n      }\n    }\n  },\n  \"NetworkInterfaceState\": {\n    \"label\": \"Network \u00b7 Interface State\",\n    \"domain\": \"Network\",\n    \"prefix\": \"NETWORK_INTERFACE_STATE_\",\n    \"values\": {\n      \"NETWORK_INTERFACE_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"NETWORK_INTERFACE_STATE_DOWN\": {\n        \"n\": 1,\n        \"desc\": \"Interface down.\"\n      },\n      \"NETWORK_INTERFACE_STATE_UP\": {\n        \"n\": 2,\n        \"desc\": \"Interface up.\"\n      }\n    }\n  },\n  \"NetworkPingMode\": {\n    \"label\": \"Network \u00b7 Ping Mode\",\n    \"domain\": \"Network\",\n    \"prefix\": \"NETWORK_PING_MODE_\",\n    \"values\": {\n      \"NETWORK_PING_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to DISABLED.\"\n      },\n      \"NETWORK_PING_MODE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"Drop ICMP ping requests.\"\n      },\n      \"NETWORK_PING_MODE_PERMANENT_ON\": {\n        \"n\": 2,\n        \"desc\": \"Answer ICMP ping requests.\"\n      }\n    }\n  },\n  \"NetworkSwitchMode\": {\n    \"label\": \"Network \u00b7 Switch Mode\",\n    \"domain\": \"Network\",\n    \"prefix\": \"NETWORK_SWITCH_MODE_\",\n    \"values\": {\n      \"NETWORK_SWITCH_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"NETWORK_SWITCH_MODE_SWITCH\": {\n        \"n\": 1,\n        \"desc\": \"Switch mode.\"\n      },\n      \"NETWORK_SWITCH_MODE_GATEWAY\": {\n        \"n\": 2,\n        \"desc\": \"Gateway mode.\"\n      }\n    }\n  },\n  \"PIDControlMode\": {\n    \"label\": \"PID \u00b7 Control Mode\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_CONTROL_MODE_\",\n    \"values\": {\n      \"PID_CONTROL_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_CONTROL_MODE_DIRECT\": {\n        \"n\": 1,\n        \"desc\": \"Output increases as the process feedback increases.\"\n      },\n      \"PID_CONTROL_MODE_REVERSE\": {\n        \"n\": 2,\n        \"desc\": \"Output decreases as the process feedback increases.\"\n      }\n    }\n  },\n  \"PIDEngineeringUnit\": {\n    \"label\": \"PID \u00b7 Engineering Unit\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_ENGINEERING_UNIT_\",\n    \"values\": {\n      \"PID_ENGINEERING_UNIT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_ENGINEERING_UNIT_PASCAL\": {\n        \"n\": 1,\n        \"desc\": \"Pascal (Pa).\"\n      },\n      \"PID_ENGINEERING_UNIT_BAR\": {\n        \"n\": 2,\n        \"desc\": \"Bar.\"\n      },\n      \"PID_ENGINEERING_UNIT_CUBIC_METER_PER_SECOND\": {\n        \"n\": 3,\n        \"desc\": \"m\u00b3\/s.\"\n      },\n      \"PID_ENGINEERING_UNIT_CUBIC_METER_PER_HOUR\": {\n        \"n\": 4,\n        \"desc\": \"m\u00b3\/h.\"\n      },\n      \"PID_ENGINEERING_UNIT_LITER_PER_SECOND\": {\n        \"n\": 5,\n        \"desc\": \"L\/s.\"\n      },\n      \"PID_ENGINEERING_UNIT_METER\": {\n        \"n\": 6,\n        \"desc\": \"Meter (m).\"\n      },\n      \"PID_ENGINEERING_UNIT_CUSTOM\": {\n        \"n\": 512,\n        \"desc\": \"Custom unit (see engineering_unit_custom).\"\n      },\n      \"PID_ENGINEERING_UNIT_POUND_PER_SQUARE_INCH\": {\n        \"n\": 1024,\n        \"desc\": \"PSI.\"\n      },\n      \"PID_ENGINEERING_UNIT_GALLON_PER_MINUTE\": {\n        \"n\": 1025,\n        \"desc\": \"GPM.\"\n      },\n      \"PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_SECOND\": {\n        \"n\": 1026,\n        \"desc\": \"CFS.\"\n      },\n      \"PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_MINUTE\": {\n        \"n\": 1027,\n        \"desc\": \"CFM.\"\n      },\n      \"PID_ENGINEERING_UNIT_FOOT\": {\n        \"n\": 1028,\n        \"desc\": \"Foot (ft).\"\n      }\n    }\n  },\n  \"PIDForcedOutputType\": {\n    \"label\": \"PID \u00b7 Forced Output Type\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_FORCED_OUTPUT_TYPE_\",\n    \"values\": {\n      \"PID_FORCED_OUTPUT_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_FORCED_OUTPUT_TYPE_VALUE\": {\n        \"n\": 1,\n        \"desc\": \"Variable setpoint from HMI\/mobile\/remote per control mode.\"\n      },\n      \"PID_FORCED_OUTPUT_TYPE_PRESET\": {\n        \"n\": 2,\n        \"desc\": \"Fixed configuration preset value.\"\n      }\n    }\n  },\n  \"PIDIndex\": {\n    \"label\": \"PID \u00b7 Index\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_INDEX_\",\n    \"values\": {\n      \"PID_INDEX_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_INDEX_1\": {\n        \"n\": 1,\n        \"desc\": \"PID #1 (only PID #1 supported for now).\"\n      }\n    }\n  },\n  \"PIDOutputMode\": {\n    \"label\": \"PID \u00b7 Output Mode\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_OUTPUT_MODE_\",\n    \"values\": {\n      \"PID_OUTPUT_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_OUTPUT_MODE_CONTROL\": {\n        \"n\": 1,\n        \"desc\": \"Closed-loop regulation.\"\n      },\n      \"PID_OUTPUT_MODE_FORCED_OUTPUT\": {\n        \"n\": 2,\n        \"desc\": \"Forced output mode.\"\n      },\n      \"PID_OUTPUT_MODE_CONSTANT_OUTPUT\": {\n        \"n\": 3,\n        \"desc\": \"Maintains a constant output once set.\"\n      },\n      \"PID_OUTPUT_MODE_FORCED_ALARM\": {\n        \"n\": 4,\n        \"desc\": \"Forced output taking value from setpoints (alarm action).\"\n      }\n    }\n  },\n  \"PIDSetpointsType\": {\n    \"label\": \"PID \u00b7 Setpoints Type\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_SETPOINTS_TYPE_\",\n    \"values\": {\n      \"PID_SETPOINTS_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Undefined.\"\n      },\n      \"PID_SETPOINTS_TYPE_PROCESS_VALUE\": {\n        \"n\": 1,\n        \"desc\": \"Single floating-point process value.\"\n      }\n    }\n  },\n  \"PIDState\": {\n    \"label\": \"PID \u00b7 State\",\n    \"domain\": \"PID\",\n    \"prefix\": \"PID_STATE_\",\n    \"values\": {\n      \"PID_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"PID_STATE_DISABLED\": {\n        \"n\": 1,\n        \"desc\": \"PID not enabled in configuration.\"\n      },\n      \"PID_STATE_OFF\": {\n        \"n\": 2,\n        \"desc\": \"PID is OFF.\"\n      },\n      \"PID_STATE_DIRECT_ON\": {\n        \"n\": 3,\n        \"desc\": \"Direct ON mode.\"\n      },\n      \"PID_STATE_REVERSE_ON\": {\n        \"n\": 4,\n        \"desc\": \"Reverse ON mode.\"\n      },\n      \"PID_STATE_SLEEP\": {\n        \"n\": 5,\n        \"desc\": \"Sleep mode.\"\n      },\n      \"PID_STATE_GOING_TO_SLEEP\": {\n        \"n\": 6,\n        \"desc\": \"Sleep threshold passed, delay running (deprecated 2024.15).\"\n      },\n      \"PID_STATE_WAKING_UP\": {\n        \"n\": 7,\n        \"desc\": \"Wakeup threshold passed, delay running (deprecated 2024.15).\"\n      },\n      \"PID_STATE_FORCED_OUTPUT\": {\n        \"n\": 8,\n        \"desc\": \"Forced output enabled.\"\n      },\n      \"PID_STATE_MOTOR_DISABLED\": {\n        \"n\": 9,\n        \"desc\": \"Motor disabled while PID enabled.\"\n      },\n      \"PID_STATE_DEADBAND\": {\n        \"n\": 10,\n        \"desc\": \"Holding constant within the deadband region.\"\n      },\n      \"PID_STATE_CONSTANT_OUTPUT\": {\n        \"n\": 11,\n        \"desc\": \"Keeping the output constant.\"\n      },\n      \"PID_STATE_STOPPING\": {\n        \"n\": 12,\n        \"desc\": \"Stop requested; PID stopped, motor stopping.\"\n      }\n    }\n  },\n  \"SoftwareUpdateFetchStatus\": {\n    \"label\": \"Software Update \u00b7 Fetch Status\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_FETCH_STATUS_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_FETCH_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_FETCH_STATUS_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"No fetch in progress.\"\n      },\n      \"SOFTWARE_UPDATE_FETCH_STATUS_IN_PROGRESS\": {\n        \"n\": 2,\n        \"desc\": \"Fetch in progress.\"\n      },\n      \"SOFTWARE_UPDATE_FETCH_STATUS_DONE\": {\n        \"n\": 3,\n        \"desc\": \"Fetch done.\"\n      }\n    }\n  },\n  \"SoftwareUpdateResult\": {\n    \"label\": \"Software Update \u00b7 Result\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_RESULT_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_RESULT_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_OK\": {\n        \"n\": 1,\n        \"desc\": \"Update succeeded.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_UPDATE_CATALOG\": {\n        \"n\": 2,\n        \"desc\": \"Error fetching\/validating the catalog.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_FETCH_PACKAGES\": {\n        \"n\": 3,\n        \"desc\": \"Error fetching packages.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_REQUEST_USER_START_AUTHORIZATION\": {\n        \"n\": 4,\n        \"desc\": \"Error requesting user start authorization.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_CLEANUP\": {\n        \"n\": 5,\n        \"desc\": \"Error during cleanup.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_REQUEST_USER_REBOOT_AUTHORIZATION\": {\n        \"n\": 6,\n        \"desc\": \"Error requesting user reboot authorization.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_BACKUP_CONFIGURATIONS\": {\n        \"n\": 7,\n        \"desc\": \"Error backing up configurations.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_RUN_PRE_UPDATE_SCRIPTS\": {\n        \"n\": 9,\n        \"desc\": \"Error running pre-update scripts.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_INSTALL_PACKAGES\": {\n        \"n\": 10,\n        \"desc\": \"Error installing packages.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_RUN_POST_UPDATE_SCRIPTS\": {\n        \"n\": 11,\n        \"desc\": \"Error running post-update scripts.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_REBOOT\": {\n        \"n\": 12,\n        \"desc\": \"Error during reboot.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_VALIDATION\": {\n        \"n\": 13,\n        \"desc\": \"Post-update validation failed.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_ROLLBACK\": {\n        \"n\": 14,\n        \"desc\": \"Error during rollback.\"\n      },\n      \"SOFTWARE_UPDATE_RESULT_ERROR_CONFIGURATION_IMPORT\": {\n        \"n\": 15,\n        \"desc\": \"Error importing configuration.\"\n      }\n    }\n  },\n  \"SoftwareUpdateSourceStatus\": {\n    \"label\": \"Software Update \u00b7 Source Status\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_SOURCE_STATUS_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_OK\": {\n        \"n\": 1,\n        \"desc\": \"Source OK.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_UNAVAILABLE\": {\n        \"n\": 2,\n        \"desc\": \"Source unavailable.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_INVALID_SIGNATURE\": {\n        \"n\": 3,\n        \"desc\": \"Invalid signature.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_NO_UPDATE_FOUND\": {\n        \"n\": 4,\n        \"desc\": \"No update found.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_STATUS_NO_NEW_VERSION\": {\n        \"n\": 5,\n        \"desc\": \"No new version available.\"\n      }\n    }\n  },\n  \"SoftwareUpdateSourceType\": {\n    \"label\": \"Software Update \u00b7 Source Type\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_SOURCE_TYPE_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_SOURCE_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_TYPE_SMARTD\": {\n        \"n\": 1,\n        \"desc\": \"Update from SmartD servers.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_TYPE_LAN\": {\n        \"n\": 2,\n        \"desc\": \"Update from a local server.\"\n      },\n      \"SOFTWARE_UPDATE_SOURCE_TYPE_USB\": {\n        \"n\": 3,\n        \"desc\": \"Update from a USB key.\"\n      }\n    }\n  },\n  \"SoftwareUpdateStatus\": {\n    \"label\": \"Software Update \u00b7 Status\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_STATUS_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Inactive.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_READY\": {\n        \"n\": 2,\n        \"desc\": \"Ready to start; waiting for user approval.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_RUNNING\": {\n        \"n\": 3,\n        \"desc\": \"Running.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_ERROR\": {\n        \"n\": 4,\n        \"desc\": \"Encountered an error.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_FINALIZING\": {\n        \"n\": 6,\n        \"desc\": \"Finalizing.\"\n      },\n      \"SOFTWARE_UPDATE_STATUS_CANCELLING\": {\n        \"n\": 7,\n        \"desc\": \"Cancelling as requested.\"\n      }\n    }\n  },\n  \"SoftwareUpdateStep\": {\n    \"label\": \"Software Update \u00b7 Step\",\n    \"domain\": \"Software Update\",\n    \"prefix\": \"SOFTWARE_UPDATE_STEP_\",\n    \"values\": {\n      \"SOFTWARE_UPDATE_STEP_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_UPDATE_CATALOG\": {\n        \"n\": 1,\n        \"desc\": \"Fetch manifest and validate signature.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_FETCH_PACKAGES\": {\n        \"n\": 2,\n        \"desc\": \"Fetch packages and validate signatures.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_REQUEST_USER_START_AUTHORIZATION\": {\n        \"n\": 3,\n        \"desc\": \"Wait for user to accept the update.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_CLEANUP\": {\n        \"n\": 4,\n        \"desc\": \"Cleanup after the update.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_REQUEST_USER_REBOOT_AUTHORIZATION\": {\n        \"n\": 5,\n        \"desc\": \"Wait for user to accept reboot.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_BACKUP_CONFIGURATIONS\": {\n        \"n\": 7,\n        \"desc\": \"Back up configurations.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_RUN_PRE_UPDATE_SCRIPTS\": {\n        \"n\": 9,\n        \"desc\": \"Run pre-update scripts.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_INSTALL_PACKAGES\": {\n        \"n\": 10,\n        \"desc\": \"Install the packages.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_RUN_POST_UPDATE_SCRIPTS\": {\n        \"n\": 11,\n        \"desc\": \"Run post-update scripts.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_REBOOT\": {\n        \"n\": 12,\n        \"desc\": \"Stop motor and reboot.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_VALIDATION\": {\n        \"n\": 13,\n        \"desc\": \"Validate update and system health.\"\n      },\n      \"SOFTWARE_UPDATE_STEP_ROLLBACK\": {\n        \"n\": 14,\n        \"desc\": \"Rollback (only if validation fails).\"\n      },\n      \"SOFTWARE_UPDATE_STEP_IMPORT_CONFIGURATIONS\": {\n        \"n\": 15,\n        \"desc\": \"Import configurations from the other bank.\"\n      }\n    }\n  },\n  \"SystemApiLanAccess\": {\n    \"label\": \"System \u00b7 API LAN Access\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_API_LAN_ACCESS_\",\n    \"values\": {\n      \"SYSTEM_API_LAN_ACCESS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Defaults to ENABLED.\"\n      },\n      \"SYSTEM_API_LAN_ACCESS_ENABLED\": {\n        \"n\": 1,\n        \"desc\": \"Any IP address can reach the API on the LAN.\"\n      },\n      \"SYSTEM_API_LAN_ACCESS_DISABLED\": {\n        \"n\": 2,\n        \"desc\": \"No external client can reach the API.\"\n      }\n    }\n  },\n  \"SystemControlMode\": {\n    \"label\": \"System \u00b7 Control Mode\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_CONTROL_MODE_\",\n    \"values\": {\n      \"SYSTEM_CONTROL_MODE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_CONTROL_MODE_LOCAL\": {\n        \"n\": 1,\n        \"desc\": \"Local control (HMI or mobile app).\"\n      },\n      \"SYSTEM_CONTROL_MODE_REMOTE\": {\n        \"n\": 2,\n        \"desc\": \"Remote control (I\/O, Modbus, \u2026).\"\n      }\n    }\n  },\n  \"SystemControlSourceSelector\": {\n    \"label\": \"System \u00b7 Control Source Selector\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_CONTROL_SOURCE_SELECTOR_\",\n    \"values\": {\n      \"SYSTEM_CONTROL_SOURCE_SELECTOR_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_LOCAL\": {\n        \"n\": 1,\n        \"desc\": \"Local source may change control mode\/source.\"\n      },\n      \"SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1\": {\n        \"n\": 2,\n        \"desc\": \"Remote source 1 may change control mode\/source.\"\n      },\n      \"SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_2\": {\n        \"n\": 3,\n        \"desc\": \"Remote source 2 may change control mode\/source.\"\n      }\n    }\n  },\n  \"SystemEmergencyModeAction\": {\n    \"label\": \"System \u00b7 Emergency Mode Action\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_EMERGENCY_MODE_ACTION_\",\n    \"values\": {\n      \"SYSTEM_EMERGENCY_MODE_ACTION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"N\/A.\"\n      },\n      \"SYSTEM_EMERGENCY_MODE_ACTION_KEEP_SETPOINT\": {\n        \"n\": 1,\n        \"desc\": \"Enable emergency mode; keep last setpoints.\"\n      },\n      \"SYSTEM_EMERGENCY_MODE_ACTION_RUN_MOTOR\": {\n        \"n\": 2,\n        \"desc\": \"Start motor at the configured emergency setpoints.\"\n      },\n      \"SYSTEM_EMERGENCY_MODE_ACTION_STOP_FREEWHEEL\": {\n        \"n\": 3,\n        \"desc\": \"Stop the motor by coasting (HALTED).\"\n      }\n    }\n  },\n  \"SystemEmergencyModeState\": {\n    \"label\": \"System \u00b7 Emergency Mode State\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_EMERGENCY_MODE_STATE_\",\n    \"values\": {\n      \"SYSTEM_EMERGENCY_MODE_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_EMERGENCY_MODE_STATE_ENABLED\": {\n        \"n\": 1,\n        \"desc\": \"Emergency mode feature enabled.\"\n      },\n      \"SYSTEM_EMERGENCY_MODE_STATE_DISABLED\": {\n        \"n\": 2,\n        \"desc\": \"Emergency mode feature disabled.\"\n      }\n    }\n  },\n  \"SystemMobilePermission\": {\n    \"label\": \"System \u00b7 Mobile Permission\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_MOBILE_PERMISSION_\",\n    \"values\": {\n      \"SYSTEM_MOBILE_PERMISSION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Since v2026.08.01, defaults to LIMITED.\"\n      },\n      \"SYSTEM_MOBILE_PERMISSION_FULL_CONTROL\": {\n        \"n\": 1,\n        \"desc\": \"No restrictions on the mobile source.\"\n      },\n      \"SYSTEM_MOBILE_PERMISSION_LIMITED\": {\n        \"n\": 2,\n        \"desc\": \"Limited \u2014 e.g. cannot change Motor\/PID\/System setpoints.\"\n      }\n    }\n  },\n  \"SystemRemoteControlSource\": {\n    \"label\": \"System \u00b7 Remote Control Source\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_REMOTE_CONTROL_SOURCE_\",\n    \"values\": {\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_1\": {\n        \"n\": 1,\n        \"desc\": \"Remote control source 1.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_2\": {\n        \"n\": 2,\n        \"desc\": \"Remote control source 2.\"\n      }\n    }\n  },\n  \"SystemRemoteControlSourceType\": {\n    \"label\": \"System \u00b7 Remote Control Source Type\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_\",\n    \"values\": {\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO\": {\n        \"n\": 1,\n        \"desc\": \"I\/O.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_MODBUS\": {\n        \"n\": 2,\n        \"desc\": \"Modbus\/TCP.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_BACNET\": {\n        \"n\": 3,\n        \"desc\": \"BACnet.\"\n      },\n      \"SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_ETHERNET_IP\": {\n        \"n\": 4,\n        \"desc\": \"EtherNet\/IP.\"\n      }\n    }\n  },\n  \"SystemRunModeState\": {\n    \"label\": \"System \u00b7 Run Mode State\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_RUN_MODE_STATE_\",\n    \"values\": {\n      \"SYSTEM_RUN_MODE_STATE_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_RUN_MODE_STATE_ACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Active.\"\n      },\n      \"SYSTEM_RUN_MODE_STATE_INACTIVE\": {\n        \"n\": 2,\n        \"desc\": \"Inactive.\"\n      }\n    }\n  },\n  \"SystemSwitchControlModeBehavior\": {\n    \"label\": \"System \u00b7 Switch Control Mode Behavior\",\n    \"domain\": \"System\",\n    \"prefix\": \"SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_\",\n    \"values\": {\n      \"SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_AUTOMATIC\": {\n        \"n\": 1,\n        \"desc\": \"Automatic (default). Not implemented yet.\"\n      },\n      \"SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_FORCED_STOP\": {\n        \"n\": 2,\n        \"desc\": \"Forced stop. Not implemented yet.\"\n      }\n    }\n  },\n  \"SupportPackageExportLocation\": {\n    \"label\": \"Technical Support \u00b7 Package Export Location\",\n    \"domain\": \"Technical Support\",\n    \"prefix\": \"SUPPORT_PACKAGE_EXPORT_LOCATION_\",\n    \"values\": {\n      \"SUPPORT_PACKAGE_EXPORT_LOCATION_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_LOCATION_USB\": {\n        \"n\": 1,\n        \"desc\": \"Default \u2014 USB.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_LOCATION_VFD_INTERNAL\": {\n        \"n\": 2,\n        \"desc\": \"VFD internal storage.\"\n      }\n    }\n  },\n  \"SupportPackageExportStatus\": {\n    \"label\": \"Technical Support \u00b7 Package Export Status\",\n    \"domain\": \"Technical Support\",\n    \"prefix\": \"SUPPORT_PACKAGE_EXPORT_STATUS_\",\n    \"values\": {\n      \"SUPPORT_PACKAGE_EXPORT_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_STATUS_INACTIVE\": {\n        \"n\": 1,\n        \"desc\": \"Inactive.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_STATUS_IN_PROGRESS\": {\n        \"n\": 2,\n        \"desc\": \"Export in progress.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_STATUS_FAILED\": {\n        \"n\": 3,\n        \"desc\": \"Export failed.\"\n      },\n      \"SUPPORT_PACKAGE_EXPORT_STATUS_COMPLETED\": {\n        \"n\": 4,\n        \"desc\": \"Export completed.\"\n      }\n    }\n  },\n  \"UserPinSetStatus\": {\n    \"label\": \"User Management \u00b7 PIN Set Status\",\n    \"domain\": \"User Management\",\n    \"prefix\": \"USER_PIN_SET_STATUS_\",\n    \"values\": {\n      \"USER_PIN_SET_STATUS_UNSPECIFIED\": {\n        \"n\": 0,\n        \"desc\": \"Unspecified.\"\n      },\n      \"USER_PIN_SET_STATUS_NOT_SET\": {\n        \"n\": 1,\n        \"desc\": \"PIN not set.\"\n      },\n      \"USER_PIN_SET_STATUS_SET\": {\n        \"n\": 2,\n        \"desc\": \"PIN set.\"\n      }\n    }\n  }\n};\n\n\/\/ Flat lookup: FULL_ENUM_VALUE \u2192 { type, label, domain, desc, n }. Enum values\n\/\/ carry their type prefix so they are globally unique \u2014 a single flat map works.\nconst CONFIG_ENUM_LOOKUP = (() => {\n  const m = Object.create(null);\n  for (const [type, def] of Object.entries(SMARTD_API.ConfigEnums)) {\n    for (const [full, v] of Object.entries(def.values)) {\n      m[full] = { type, label: def.label, domain: def.domain, desc: v.desc, n: v.n };\n    }\n  }\n  return m;\n})();\n\nfunction configEnumMeta(fullEnum) {\n  return (fullEnum && CONFIG_ENUM_LOOKUP[fullEnum]) || null;\n}\n\n\/\/ API-reference categories generated from the configuration enums, grouped by\n\/\/ domain (used to build the category dropdown as <optgroup>s).\nconst API_REF_CONFIG_CATEGORIES = Object.entries(SMARTD_API.ConfigEnums).map(\n  ([type, def]) => ({ key: 'cfg:' + type, label: def.label, domain: def.domain, codePrefix: '' })\n);\n\n\/\/ Lookup helpers\nfunction smartdEventDesc(short)   { return (SMARTD_API.AuditLogEvent[short]    || {}).desc || ''; }\nfunction smartdOriginDesc(short)  { return (SMARTD_API.AuditLogOrigin[short]   || {}).desc || ''; }\nfunction smartdSeverityDesc(short){ return (SMARTD_API.AuditLogSeverity[short] || {}).desc || ''; }\nfunction smartdAlarmName(code)    {\n  const entry = SMARTD_API.AlarmCode[code];\n  return entry ? entry.name : null;\n}\nfunction smartdAlarmDesc(code)    {\n  const entry = SMARTD_API.AlarmCode[code];\n  return entry ? entry.desc : '';\n}\nfunction smartdSystemStateDesc(name) {\n  \/\/ Accept \"SystemStateRunningNormal\", \"SystemStateRunning Normal\", or short \"RUNNING_NORMAL\"\n  const short = String(name || '')\n    .replace(\/^SystemState\/, '')\n    .replace(\/([a-z0-9])([A-Z])\/g, '$1_$2')\n    .toUpperCase();\n  return (SMARTD_API.SystemState[short] || {}).desc || '';\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ API Reference Modal \u2014 searchable browser of the embedded enum reference\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst API_REF_CATEGORIES = [\n  { key: 'AuditLogEvent',         label: 'Audit Log Events',        codePrefix: 'AUDIT_LOG_EVENT_' },\n  { key: 'AuditLogOrigin',        label: 'Audit Log Origins',       codePrefix: 'AUDIT_LOG_ORIGIN_' },\n  { key: 'AuditLogSeverity',      label: 'Audit Log Severities',    codePrefix: 'AUDIT_LOG_SEVERITY_' },\n  { key: 'AlarmCode',             label: 'Alarm Codes',             codePrefix: 'ALARM_CODE_' },\n  { key: 'AlarmSeverity',         label: 'Alarm Severities',        codePrefix: 'ALARM_SEVERITY_' },\n  { key: 'AlarmState',            label: 'Alarm States',            codePrefix: 'ALARM_STATE_' },\n  { key: 'SystemState',           label: 'System States',           codePrefix: 'SYSTEM_STATE_' },\n  { key: 'MotorControllerState',  label: 'Motor Controller States', codePrefix: 'MOTOR_CONTROLLER_STATE_' },\n  { key: 'ResponseStatus',        label: 'Response Statuses',       codePrefix: 'RESPONSE_STATUS_' },\n];\n\nfunction apiRefBuildRows(catKey, search) {\n  if (catKey && catKey.indexOf('cfg:') === 0) {\n    const def = SMARTD_API.ConfigEnums[catKey.slice(4)];\n    if (!def) return [];\n    let rows = Object.entries(def.values).map(([full, v]) => ({ n: v.n, name: full, desc: v.desc }));\n    rows.sort((a, b) => (a.n ?? 999) - (b.n ?? 999));\n    if (!search) return rows;\n    const sc = search.toLowerCase();\n    return rows.filter(r => `${r.name} ${r.desc} ${r.n}`.toLowerCase().includes(sc));\n  }\n  const enumObj = SMARTD_API[catKey];\n  if (!enumObj) return [];\n  const rows = [];\n  for (const [key, val] of Object.entries(enumObj)) {\n    \/\/ AlarmCode entries are { name, desc }; others are { n, desc }\n    const isNumeric = typeof key === 'string' && \/^\\d+$\/.test(key);\n    const n = isNumeric ? Number(key) : val.n;\n    const name = isNumeric ? val.name : key;\n    rows.push({ n, name, desc: val.desc || '' });\n  }\n  \/\/ Sort by numeric code\n  rows.sort((a, b) => (a.n ?? 999) - (b.n ?? 999));\n  if (!search) return rows;\n  const s = search.toLowerCase();\n  return rows.filter(r => {\n    const hay = `${r.name} ${r.desc} ${r.n}`.toLowerCase();\n    return hay.includes(s);\n  });\n}\n\nfunction apiRefHighlight(text, query) {\n  if (!query) return escHtml(text);\n  const q = query.trim();\n  if (!q) return escHtml(text);\n  const lower = text.toLowerCase();\n  const ql = q.toLowerCase();\n  let out = '';\n  let i = 0;\n  while (i < text.length) {\n    const idx = lower.indexOf(ql, i);\n    if (idx === -1) { out += escHtml(text.slice(i)); break; }\n    out += escHtml(text.slice(i, idx)) + '<mark>' + escHtml(text.slice(idx, idx + q.length)) + '<\/mark>';\n    i = idx + q.length;\n  }\n  return out;\n}\n\nfunction apiRefRender() {\n  const search = $('#apiRefSearch').value.trim();\n  const cat = $('#apiRefCategory').value;\n  const ALL_API_REF = API_REF_CATEGORIES.concat(API_REF_CONFIG_CATEGORIES);\n  const categories = cat ? ALL_API_REF.filter(c => c.key === cat) : ALL_API_REF;\n\n  let totalShown = 0;\n  let html = '';\n  for (const c of categories) {\n    const rows = apiRefBuildRows(c.key, search);\n    if (rows.length === 0) continue;\n    totalShown += rows.length;\n    html += `<div class=\"api-cat-group\">\n      <div class=\"api-cat-header\">${escHtml(c.label)}<span class=\"api-cat-count\">(${rows.length})<\/span><\/div>`;\n    for (const r of rows) {\n      const fullName = (r.name === 'UNSPECIFIED' || c.codePrefix === '') ? r.name : c.codePrefix + r.name;\n      html += `<div class=\"api-row\">\n        <div class=\"api-row-code\">${r.n != null ? r.n : ''}<\/div>\n        <div class=\"api-row-name\">${apiRefHighlight(fullName, search)}<\/div>\n        <div class=\"api-row-desc\">${apiRefHighlight(r.desc, search)}<\/div>\n      <\/div>`;\n    }\n    html += `<\/div>`;\n  }\n  if (totalShown === 0) {\n    html = `<div class=\"api-empty\">No matching entries.${search ? ' Try a broader search.' : ''}<\/div>`;\n  }\n  $('#apiRefBody').innerHTML = html;\n  $('#apiRefCount').textContent = totalShown + (totalShown === 1 ? ' entry' : ' entries');\n}\n\nfunction apiRefOpen() {\n  const baseKeys = Object.keys(SMARTD_API).filter(k => k !== 'apiVersion' && k !== 'ConfigEnums');\n  const baseEntries = baseKeys.reduce((a, k) => a + Object.keys(SMARTD_API[k]).length, 0);\n  const cfgTypes = Object.keys(SMARTD_API.ConfigEnums);\n  const cfgEntries = cfgTypes.reduce((a, t) => a + Object.keys(SMARTD_API.ConfigEnums[t].values).length, 0);\n  $('#apiRefVersion').textContent = `Spec: smartd_api (${SMARTD_API.apiVersion}) \u00b7 ${baseKeys.length + cfgTypes.length} enums, ${baseEntries + cfgEntries} entries`;\n  $('#apiRefSearch').value = '';\n  $('#apiRefCategory').value = '';\n  apiRefRender();\n  $('#apiRefModal').style.display = 'flex';\n  setTimeout(() => $('#apiRefSearch').focus(), 50);\n}\nfunction apiRefClose() { $('#apiRefModal').style.display = 'none'; }\n\nfunction apiRefPopulateCategories() {\n  const sel = $('#apiRefCategory');\n  if (!sel || sel.dataset.cfgPopulated === '1') return;\n  \/\/ Group base (audit\/system) options under one optgroup for clarity.\n  const baseOpts = Array.from(sel.querySelectorAll('option')).filter(o => o.value && o.value.indexOf('cfg:') !== 0);\n  if (baseOpts.length) {\n    const og = document.createElement('optgroup');\n    og.label = 'Audit & System';\n    baseOpts.forEach(o => og.appendChild(o));\n    sel.appendChild(og);\n  }\n  \/\/ Config enums grouped by domain, in first-seen order.\n  const order = [];\n  const byDomain = {};\n  API_REF_CONFIG_CATEGORIES.forEach(c => {\n    if (!byDomain[c.domain]) { byDomain[c.domain] = []; order.push(c.domain); }\n    byDomain[c.domain].push(c);\n  });\n  order.forEach(domain => {\n    const og = document.createElement('optgroup');\n    og.label = 'Config \u00b7 ' + domain;\n    byDomain[domain].forEach(c => {\n      const opt = document.createElement('option');\n      opt.value = c.key;\n      opt.textContent = c.label;\n      og.appendChild(opt);\n    });\n    sel.appendChild(og);\n  });\n  sel.dataset.cfgPopulated = '1';\n}\n\n(function wireApiRef() {\n  const btn = $('#apiRefBtn');\n  if (!btn) return;\n  apiRefPopulateCategories();\n  btn.addEventListener('click', apiRefOpen);\n  $('#apiRefClose').addEventListener('click', apiRefClose);\n  $('#apiRefBackdrop').addEventListener('click', apiRefClose);\n  $('#apiRefSearch').addEventListener('input', debounce(apiRefRender, 120));\n  $('#apiRefCategory').addEventListener('change', apiRefRender);\n  document.addEventListener('keydown', (ev) => {\n    if (ev.key === 'Escape' && $('#apiRefModal').style.display === 'flex') apiRefClose();\n  });\n})();\n\n(function wireAlarmHelp() {\n  $('#alarmHelpClose').addEventListener('click', hdCloseAlarmHelp);\n  $('#alarmHelpBackdrop').addEventListener('click', hdCloseAlarmHelp);\n  document.addEventListener('keydown', (ev) => {\n    if (ev.key === 'Escape' && $('#alarmHelpModal').style.display === 'flex') hdCloseAlarmHelp();\n  });\n  ['hd-recent-alarms', 'hd-frequent-alarms'].forEach(id => {\n    const el = document.getElementById(id);\n    if (!el) return;\n    el.addEventListener('click', (ev) => {\n      const row = ev.target.closest('.hd-alarm-row');\n      if (!row) return;\n      \/\/ (?) button \u2192 alarm-description popup\n      if (ev.target.closest('.hd-alarm-help-btn')) {\n        if (row.dataset.alarmCode) hdOpenAlarmHelp(row.dataset.alarmCode);\n        return;\n      }\n      \/\/ Row click \u2192 jump to the matching audit-log line\n      if (row.dataset.alarmTs) {\n        hdJumpToAuditEvent(row.dataset.alarmTs, row.dataset.alarmCode);\n      } else {\n        showToast('No timestamp recorded for this alarm \u2014 cannot locate it in the audit log.', 'error');\n      }\n    });\n  });\n})();\n\nfunction escHtml(s) {\n  if (s === null || s === undefined) return '';\n  return String(s).replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;');\n}\nfunction highlightText(text, query) {\n  const safe = (text === null || text === undefined) ? '' : String(text);\n  if (!query) return escHtml(safe);\n  const re = new RegExp('(' + query.replace(\/[.*+?^${}()|[\\]\\\\]\/g,'\\\\$&') + ')', 'gi');\n  return escHtml(safe).replace(re, '<mark>$1<\/mark>');\n}\nfunction formatTS(ts) {\n  const d = new Date(ts);\n  return { date: d.toISOString().slice(0,10), time: d.toISOString().slice(11,23) };\n}\n\n\/\/ \u2500\u2500 Power meter metrics renderer \u2500\u2500\nconst METRIC_LABELS = {\n  l1Irms:'L1 Irms', l2Irms:'L2 Irms', l3Irms:'L3 Irms',\n  l12Vrms:'L12 Vrms', l23Vrms:'L23 Vrms', l31Vrms:'L31 Vrms',\n  frequencyHz:'Freq', realPowerW:'Real Power',\n  l1Ipeak:'L1 Ipeak', l2Ipeak:'L2 Ipeak', l3Ipeak:'L3 Ipeak',\n  l12Vpeak:'L12 Vpeak', l23Vpeak:'L23 Vpeak', l31Vpeak:'L31 Vpeak',\n  uIrms:'U Irms', vIrms:'V Irms', wIrms:'W Irms',\n  uvVrms:'UV Vrms', vwVrms:'VW Vrms', wuVrms:'WU Vrms',\n  uIpeak:'U Ipeak', vIpeak:'V Ipeak', wIpeak:'W Ipeak',\n  currentBalancePercentage:'Curr Balance',\n  posV:'Pos V', negV:'Neg V', totalV:'Total V', unbalanceV:'Unbalance V'\n};\nconst METRIC_UNITS = {\n  l1Irms:'A', l2Irms:'A', l3Irms:'A',\n  l1Ipeak:'A', l2Ipeak:'A', l3Ipeak:'A',\n  uIrms:'A', vIrms:'A', wIrms:'A',\n  uIpeak:'A', vIpeak:'A', wIpeak:'A',\n  l12Vrms:'V', l23Vrms:'V', l31Vrms:'V',\n  l12Vpeak:'V', l23Vpeak:'V', l31Vpeak:'V',\n  uvVrms:'V', vwVrms:'V', wuVrms:'V',\n  posV:'V', negV:'V', totalV:'V', unbalanceV:'V',\n  frequencyHz:'Hz', realPowerW:'W',\n  currentBalancePercentage:'%'\n};\nfunction fmtMetric(v) {\n  if (v === null || v === undefined) return '\u2014';\n  const n = parseFloat(v);\n  if (isNaN(n)) return String(v);\n  return Math.abs(n) >= 1000 ? n.toFixed(0) : n.toFixed(2);\n}\nfunction renderMetricSection(title, fields) {\n  if (!fields || !Object.keys(fields).length) return '';\n  let rows = `<div class=\"metrics-grid\">\n    <div><\/div><div class=\"m-header\">Min<\/div><div class=\"m-header\">Max<\/div>`;\n  for (const [key, val] of Object.entries(fields)) {\n    const label = METRIC_LABELS[key] || key;\n    const unit  = METRIC_UNITS[key] ? ' ' + METRIC_UNITS[key] : '';\n    rows += `<div class=\"m-label\">${label}<\/div>\n      <div class=\"m-val m-val-min\">${fmtMetric(val.minValue)}${unit}<\/div>\n      <div class=\"m-val m-val-max\">${fmtMetric(val.maxValue)}${unit}<\/div>`;\n  }\n  rows += '<\/div>';\n  return `<div class=\"metrics-section\">\n    <div class=\"metrics-section-title\">${title}<\/div>${rows}<\/div>`;\n}\nfunction renderMetricsMessage(rawMsg) {\n  const jsonStart = rawMsg.indexOf('{');\n  if (jsonStart === -1) return null;\n  try {\n    const parsed = JSON.parse(rawMsg.slice(jsonStart));\n    const m = parsed.metrics || {};\n    const sections = [\n      renderMetricSection('Line', m.line),\n      renderMetricSection('Load', m.load),\n      renderMetricSection('DC',   m.dc)\n    ].filter(Boolean).join('');\n    return `<div class=\"metrics-card\">\n      <div class=\"metrics-title\">\u26a1 Power Meter Metrics<\/div>\n      <div class=\"metrics-sections\">${sections}<\/div>\n    <\/div>`;\n  } catch (e) { return null; }\n}\n\n\/\/ --- Debug info alarms: \"Debug info: { operationalData: { ... } }\" ----------\n\/\/ Parses the embedded JSON and renders a compact collapsed-by-default structured view.\n\/\/ If parsing fails, falls back to the raw text.\nfunction renderDebugInfoAlarm(rawMsg, search) {\n  const jsonStart = rawMsg.indexOf('{');\n  if (jsonStart === -1) return highlightText(rawMsg, search);\n  let parsed;\n  try { parsed = JSON.parse(rawMsg.slice(jsonStart)); }\n  catch (e) { return highlightText(rawMsg, search); }\n\n  \/\/ Pull a handful of signal-carrying fields into a summary strip.\n  \/\/ These are the common \"interesting\" values operators look at first in a debug dump.\n  const op = parsed.operationalData || parsed;\n  const fmtNum = (v, unit) => {\n    if (v === null || v === undefined) return null;\n    if (typeof v !== 'number') return String(v);\n    \/\/ Smart decimals: big numbers no decimals, small numbers 2 decimals\n    const abs = Math.abs(v);\n    const formatted = abs >= 100 ? v.toFixed(0) : abs >= 1 ? v.toFixed(2) : v.toFixed(4);\n    return formatted + (unit || '');\n  };\n  const summaryFields = [\n    ['VDC',       fmtNum(op.vdcFiltered, ' V'),      'accent'],\n    ['AFE State', op.afeState !== undefined ? String(op.afeState) : null, 'accent'],\n    ['Ready',     op.readyToRun === undefined ? null : (op.readyToRun ? 'Yes' : 'No'), op.readyToRun ? 'ok' : 'warn'],\n    ['Motor Power', fmtNum(op.motorActivePower, ' W'), 'accent'],\n    ['Critical Err', op.latchedCriticalErrors === undefined ? null : String(op.latchedCriticalErrors),\n      op.latchedCriticalErrors > 0 ? 'err' : 'ok'],\n    ['LVRT Err',  op.latchedLvrtErrors === undefined ? null : String(op.latchedLvrtErrors),\n      op.latchedLvrtErrors > 0 ? 'warn' : 'ok'],\n  ].filter(([, v]) => v !== null && v !== undefined);\n\n  \/\/ Build summary chips\n  const summaryHtml = summaryFields.length\n    ? `<div class=\"dbginfo-summary\">${summaryFields.map(([k, v, cls]) => `\n        <div class=\"dbginfo-chip dbginfo-${cls || 'accent'}\">\n          <span class=\"dbginfo-chip-label\">${escHtml(k)}<\/span>\n          <span class=\"dbginfo-chip-value\">${escHtml(String(v))}<\/span>\n        <\/div>`).join('')}<\/div>`\n    : '';\n\n  \/\/ Render full JSON tree (collapsed by default) using the config viewer renderer\n  let treeHtml;\n  if (parsed === null || typeof parsed !== 'object') {\n    treeHtml = `<div style=\"padding:8px 14px\">${renderConfigValue(parsed)}<\/div>`;\n  } else if (Array.isArray(parsed)) {\n    treeHtml = renderConfigArray(parsed, 'items');\n  } else {\n    let html = '';\n    for (const [k, v] of Object.entries(parsed)) {\n      if (Array.isArray(v)) html += renderConfigArray(v, humanizeKey(k));\n      else if (v !== null && typeof v === 'object') html += renderConfigGroup(v, humanizeKey(k));\n      else {\n        html += `<div class=\"cfg-group\"><div class=\"cfg-group-title\">${escHtml(humanizeKey(k))}<\/div><div style=\"padding:10px 14px\">${renderConfigValue(v)}<\/div><\/div>`;\n      }\n    }\n    treeHtml = html || '<div class=\"cfg-no-data\">(empty)<\/div>';\n  }\n\n  return `<div class=\"dbginfo-wrap\">\n    <details class=\"dbginfo-details\">\n      <summary class=\"dbginfo-summary-row\">\n        <svg class=\"dbginfo-chev\" width=\"10\" height=\"10\" viewBox=\"0 0 10 10\" fill=\"none\"><path d=\"M3 2l4 3-4 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"\/><\/svg>\n        <span class=\"dbginfo-label\">Debug info<\/span>\n        ${summaryHtml}\n      <\/summary>\n      <div class=\"dbginfo-tree cfg-render\">${treeHtml}<\/div>\n    <\/details>\n  <\/div>`;\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ METRICS VIEWER \u2014 renders smartdrive-metrics.json (SystemMetrics,\n\/\/ CoolingManagementMetrics, MotorControllerMetrics)\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet metricsData = null;\n\n\/\/ Convert a value in SECONDS to a compact human-readable duration.\n\/\/ Uses years \/ days \/ hours \/ minutes \/ seconds (skips zero components except the last).\n\/\/ Example: 63607 s \u2192 \"17h 40m 7s\" ; 1915 s \u2192 \"31m 55s\" ; 144000000 s \u2192 \"4y 210d 16h\".\nfunction mvFmtSeconds(seconds) {\n  if (seconds === null || seconds === undefined || isNaN(seconds)) return '\u2014';\n  let s = Math.floor(Number(seconds));\n  if (s <= 0) return '0s';\n  const SEC_PER_YEAR = 365 * 24 * 3600;\n  const SEC_PER_DAY  = 24 * 3600;\n  const SEC_PER_HOUR = 3600;\n  const SEC_PER_MIN  = 60;\n  const y = Math.floor(s \/ SEC_PER_YEAR); s -= y * SEC_PER_YEAR;\n  const d = Math.floor(s \/ SEC_PER_DAY);  s -= d * SEC_PER_DAY;\n  const h = Math.floor(s \/ SEC_PER_HOUR); s -= h * SEC_PER_HOUR;\n  const m = Math.floor(s \/ SEC_PER_MIN);  s -= m * SEC_PER_MIN;\n  const parts = [];\n  if (y) parts.push(y + 'y');\n  if (d) parts.push(d + 'd');\n  if (h) parts.push(h + 'h');\n  if (m) parts.push(m + 'm');\n  if (s || parts.length === 0) parts.push(s + 's');\n  return parts.join(' ');\n}\n\n\/\/ Convert Unix timestamp (seconds) to ISO datetime string\nfunction mvFmtUnix(ts) {\n  if (!ts || ts === 0) return '\u2014';\n  try {\n    return new Date(Number(ts) * 1000).toISOString().replace('T', ' ').replace('.000Z', 'Z');\n  } catch (e) { return String(ts); }\n}\n\n\/\/ Comma-format an integer\nfunction mvFmtInt(n) {\n  if (n === null || n === undefined) return '\u2014';\n  return Number(n).toLocaleString();\n}\n\n\/\/ Build a single metric tile: label + big value (+ optional subtitle)\n\/\/ v1.0.9 i18n label maps for Summary inner strings\nconst HD_LABEL_KEY = { 'VFD name':'hd.k.driveName','Model':'hd.k.deviceModel','Serial number':'hd.k.serial','Current firmware':'hd.k.curFw','Backup firmware':'hd.k.backupFw','Control mode':'hd.k.ctrlMode','Remote control source':'hd.k.remoteSrc','Method':'hd.k.method','IP address':'hd.k.ip','Subnet mask':'hd.k.mask','Gateway':'hd.k.gateway','DNS resolvers':'hd.k.dns','MAC address \u00b7 Eth0':'hd.k.mac0','MAC address \u00b7 Eth1':'hd.k.mac1','BLE MAC address':'hd.k.bleMac','BLE state':'hd.k.bleState','Mobile permissions':'hd.k.mobilePerm' };\nconst MV_LABEL_KEY = { 'Runtime since last boot':'hd.k.curUptime','Lifetime boot count':'hd.k.bootCount','Lifetime runtime':'hd.k.lifeRun','VFD start commands since last boot':'hd.k.vfdStarts','VFD runtime since last boot':'hd.k.vfdRun','VFD lifetime start commands':'hd.k.vfdLifeStarts','VFD lifetime runtime':'hd.k.vfdLifeRun','Motor lifetime start commands':'hd.k.motLifeStarts','Motor lifetime runtime':'hd.k.motLifeRun','Motor runtime since last boot':'hd.k.motRunCur','Auto-restart attempts':'hd.k.autoRestart' };\nfunction mvTile(label, value, subtitle) {\n  const subHtml = subtitle ? `<div class=\"mv-tile-sub\">${subtitle}<\/div>` : '';\n  const _mk = MV_LABEL_KEY[label];\n  const _mka = _mk ? ` data-i18n=\"${_mk}\"` : '';\n  const _mkt = (_mk && typeof t === 'function') ? t(_mk) : label;\n  return `<div class=\"mv-tile\">\n    <div class=\"mv-tile-label\"${_mka}>${escapeHtml(_mkt)}<\/div>\n    <div class=\"mv-tile-value\">${value}<\/div>\n    ${subHtml}\n  <\/div>`;\n}\n\n\/\/ Render one fan card with a health\/life-remaining bar\nfunction mvFanCard(fan) {\n  const runtime = fan.runtime || 0;\n  const remaining = fan.remaining_life_expectancy || 0;\n  const total = fan.total_life_expectancy || 0;\n  \/\/ Percentage USED = runtime \/ total_life_expectancy \u2014 bar fills as fan wears out.\n  const pct = total > 0 ? Math.max(0, Math.min(100, (runtime \/ total) * 100)) : 0;\n  const pctStr = pct.toFixed(2) + '%';\n  let severity = '';\n  if (pct >= 90) severity = 'crit';       \/\/ used >= 90% \u2192 red\n  else if (pct >= 70) severity = 'warn';  \/\/ used >= 70% \u2192 amber\n  \/\/ else green (default)\n  const lastResetHtml = fan.runtime_last_reset_time\n    ? `<span><span data-i18n=\"hd.k.lastReset\">Last reset:<\/span> <strong>${escapeHtml(mvFmtUnix(fan.runtime_last_reset_time))}<\/strong><\/span>`\n    : '';\n  return `<div class=\"mv-fan\">\n    <div class=\"mv-fan-head\">\n      <div class=\"mv-fan-title\"><span data-i18n=\"hd.k.fan\">Fan<\/span> ${fan.id ?? '?'}<\/div>\n      <div class=\"mv-fan-pct ${severity}\">${pctStr} <span data-i18n=\"hd.k.used\">used<\/span><\/div>\n    <\/div>\n    <div class=\"mv-fan-bar\"><div class=\"mv-fan-bar-fill ${severity}\" style=\"width:${pct}%\"><\/div><\/div>\n    <div class=\"mv-fan-stats\">\n      <span><span data-i18n=\"hd.k.runtime\">Runtime:<\/span> <strong>${escapeHtml(mvFmtSeconds(runtime))}<\/strong><\/span>\n      <span><span data-i18n=\"hd.k.remaining\">Remaining:<\/span> <strong>${escapeHtml(mvFmtSeconds(remaining))}<\/strong><\/span>\n    <\/div>\n    <div class=\"mv-fan-stats\">\n      <span><span data-i18n=\"hd.k.totalLife\">Total life:<\/span> <strong>${escapeHtml(mvFmtSeconds(total))}<\/strong><\/span>\n      ${lastResetHtml}\n    <\/div>\n  <\/div>`;\n}\n\n\/\/ Render one section (header + grid of tiles). Collapsible via a chevron in the header.\nfunction mvSection(title, subtitle, iconSvg, tilesHtml, extraClass) {\n  const cls = extraClass || 'mv-tiles';\n  const subHtml = subtitle ? `<div class=\"mv-section-subtitle\">${subtitle}<\/div>` : '';\n  const _tk = ({ 'System':'hd.system', 'Operational Data':'hd.motorctl', 'Cooling (Fan)':'hd.cooling' })[title];\n  const _tt = (_tk && typeof t === 'function') ? t(_tk) : title;\n  const _ta = _tk ? ` data-i18n=\"${_tk}\"` : '';\n  return `<div class=\"mv-section collapsible\">\n    <div class=\"mv-section-head\">\n      <div class=\"mv-section-icon\">${iconSvg}<\/div>\n      <div class=\"mv-section-title\"${_ta}>${escapeHtml(_tt)}<\/div>\n      ${subHtml}\n      <svg class=\"mv-section-chevron\" width=\"12\" height=\"12\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 6l4 4 4-4\"\/><\/svg>\n    <\/div>\n    <div class=\"${cls}\">${tilesHtml}<\/div>\n  <\/div>`;\n}\n\n\/\/ SVG icons (small, single-color via currentColor)\nconst MV_ICON_SYSTEM = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"3\" width=\"11\" height=\"10\" rx=\"1.5\"\/><path d=\"M5 6h6M5 8.5h6M5 11h3\"\/><\/svg>';\nconst MV_ICON_FAN    = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"><circle cx=\"8\" cy=\"8\" r=\"1.5\"\/><path d=\"M8 6.5C8 4 6 2.5 5 3.5 4 4.5 5.5 6.5 8 6.5zM8 9.5c0 2.5 2 4 3 3 1-1-.5-3-3-3zM6.5 8C4 8 2.5 10 3.5 11 4.5 12 6.5 10.5 6.5 8zM9.5 8c2.5 0 4-2 3-3-1-1-3 .5-3 3z\"\/><\/svg>';\nconst MV_ICON_MOTOR  = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><circle cx=\"8\" cy=\"8\" r=\"4\"\/><path d=\"M8 4v-1.5M8 12v1.5M4 8h-1.5M12 8h1.5M5.2 5.2l-1-1M10.8 10.8l1 1M5.2 10.8l-1 1M10.8 5.2l1-1\"\/><\/svg>';\n\n\/\/ Build the structured metrics sections HTML (System \/ Motor Controller \/ Cooling fans)\n\/\/ from a parsed smartdrive-metrics.json object. Shared by the Metrics tab and the\n\/\/ Health Dashboard's embedded \"Structured Metrics\" panel.\nfunction buildMetricsSectionsHtml(data) {\n  if (!data || typeof data !== 'object') {\n    return '<div class=\"mv-empty\">Metrics file is empty or malformed.<\/div>';\n  }\n\n  const sections = [];\n\n  \/\/ \u2500\u2500 Operational Data (merged: System + Motor Controller) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const _sys = data.SystemMetrics;\n  const _mc  = data.MotorControllerMetrics;\n  if ((_sys && typeof _sys === 'object') || (_mc && typeof _mc === 'object')) {\n    const up      = (_sys && _sys.uptime_metrics)   || {};\n    const life    = (_sys && _sys.lifetime_metrics) || {};\n    const vfdUp   = (_mc && _mc.vfd_uptime_metrics)   || {};\n    const vfdLife = (_mc && _mc.vfd_lifetime_metrics) || {};\n    const mLife   = (_mc && _mc.motor_lifetime_metrics) || {};\n    const mStart  = (_mc && _mc.motor_start_metrics)    || {};\n    const ar      = (_mc && _mc.motor_auto_restart_metrics) || {};\n    const uptimeS = up.uptime_up_time;\n    const runS    = life.lifetime_run_time;\n    const bootTiles = [\n      mvTile('Runtime since last boot', mvFmtSeconds(uptimeS), uptimeS != null ? mvFmtInt(uptimeS) + ' s' : ''),\n      mvTile('VFD start commands since last boot', mvFmtInt(vfdUp.uptime_start_count), ''),\n      mvTile('VFD runtime since last boot', mvFmtSeconds(vfdUp.uptime_run_time), vfdUp.uptime_run_time != null ? mvFmtInt(vfdUp.uptime_run_time) + ' s' : ''),\n      mvTile('Motor runtime since last boot', mvFmtSeconds(mStart.motor_runtime_run_time), mStart.motor_runtime_run_time != null ? mvFmtInt(mStart.motor_runtime_run_time) + ' s' : ''),\n    ].join('');\n    const lifeTiles = [\n      mvTile('Lifetime boot count', mvFmtInt(life.lifetime_boot_count), ''),\n      mvTile('Lifetime runtime', mvFmtSeconds(runS), runS != null ? mvFmtInt(runS) + ' s' : ''),\n      mvTile('VFD lifetime start commands', mvFmtInt(vfdLife.vfd_lifetime_start_count), ''),\n      mvTile('VFD lifetime runtime', mvFmtSeconds(vfdLife.vfd_lifetime_run_time), vfdLife.vfd_lifetime_run_time != null ? mvFmtInt(vfdLife.vfd_lifetime_run_time) + ' s' : ''),\n      mvTile('Motor lifetime start commands', mvFmtInt(mLife.motor_lifetime_start_count), ''),\n      mvTile('Motor lifetime runtime', mvFmtSeconds(mLife.motor_lifetime_run_time), mLife.motor_lifetime_run_time != null ? mvFmtInt(mLife.motor_lifetime_run_time) + ' s' : ''),\n      mvTile('Auto-restart attempts', mvFmtInt(ar.motor_autorestart_total_attempts), '<span data-i18n=\"hd.k.success\">Success:<\/span> ' + escapeHtml(String(mvFmtInt(ar.motor_autorestart_total_success))) + ' \u00b7 <span data-i18n=\"hd.k.current\">Current:<\/span> ' + escapeHtml(String(mvFmtInt(ar.motor_autorestart_current_attempts)))),\n    ].join('');\n    const _body =\n      '<div class=\"mv-subhead\" data-i18n=\"hd.sub.sinceBoot\">Since last VFD boot<\/div>'\n      + '<div class=\"mv-tiles\">' + bootTiles + '<\/div>'\n      + '<div class=\"mv-subsep\"><\/div>'\n      + '<div class=\"mv-subhead\" data-i18n=\"hd.sub.vfdLifetime\">VFD Lifetime<\/div>'\n      + '<div class=\"mv-tiles\">' + lifeTiles + '<\/div>';\n    const subtitle = (_mc && _mc.metrics_last_update_time)\n      ? '<span data-i18n=\"hd.k.lastUpdated\">Last updated<\/span> ' + escapeHtml(mvFmtUnix(_mc.metrics_last_update_time))\n      : ((_sys && _sys.setpoints_last_update_time)\n          ? '<span data-i18n=\"hd.k.setpointsUpdated\">Setpoints last updated<\/span> ' + escapeHtml(mvFmtUnix(_sys.setpoints_last_update_time))\n          : '');\n    sections.push(mvSection('Operational Data', subtitle, MV_ICON_MOTOR, _body, 'mv-body-sections'));\n  }\n\n  \/\/ \u2500\u2500 Cooling Management (fans) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const cool = data.CoolingManagementMetrics;\n  if (cool && typeof cool === 'object') {\n    const fans = Array.isArray(cool.fans_metrics) ? cool.fans_metrics : [];\n    const fansHtml = fans.length\n      ? fans.map(mvFanCard).join('')\n      : '<div class=\"mv-empty\" style=\"grid-column:1\/-1\">No fan data.<\/div>';\n    const subtitle = cool.last_update_time\n      ? '<span data-i18n=\"hd.k.lastUpdated\">Last updated<\/span> ' + escapeHtml(mvFmtUnix(cool.last_update_time))\n      : '';\n    sections.push(mvSection('Cooling (Fan)', subtitle, MV_ICON_FAN, fansHtml, 'mv-fans'));\n  }\n\n  if (!sections.length) {\n    return '<div class=\"mv-empty\">No recognized sections in metrics file (expected SystemMetrics, MotorControllerMetrics, or CoolingManagementMetrics).<\/div>';\n  }\n\n  return sections.join('');\n}\n\nfunction renderMetrics(data) {\n  $('#mv-body').innerHTML = buildMetricsSectionsHtml(data);\n  if (typeof applyLang === 'function') applyLang(LANG);\n}\n\n\/\/ Render the structured metrics (System \/ Motor Control \/ Cooling (Fan)) into the\n\/\/ Health Dashboard, below the five health scoring panels. Each panel is collapsible.\nfunction hdRenderMetrics() {\n  const el = $('#hd-metrics-body');\n  if (!el) return;\n  if (!healthMetrics) {\n    el.innerHTML = '<div class=\"mv-empty\">smartdrive-metrics.json not available in this package.<\/div>';\n    return;\n  }\n  el.innerHTML = buildMetricsSectionsHtml(healthMetrics);\n}\n\nfunction initMetricsViewer(data, filename) {\n  metricsData = data;\n  $('#mv-header-filename').textContent = filename;\n\n  \/\/ Pick the most recent \"last update\" time across sections for the header snapshot\n  let latest = 0;\n  const walk = (o) => {\n    if (!o || typeof o !== 'object') return;\n    for (const [k, v] of Object.entries(o)) {\n      if (typeof v === 'number' && \/_update_time$|_last_reset_time$\/.test(k) && v > latest) latest = v;\n      else if (typeof v === 'object') walk(v);\n    }\n  };\n  walk(data);\n  $('#mv-header-updated').textContent = latest ? mvFmtUnix(latest) : '\u2014';\n\n  \/\/ Pre-render the raw JSON view so it's ready when the user toggles\n  mvRenderRaw(data);\n\n  \/\/ Default to structured view on load\n  mvSetMode('structured');\n}\n\n\/\/ Render the raw pretty-printed JSON with the same syntax highlighting the code viewer uses\nfunction mvRenderRaw(data) {\n  const el = $('#mv-raw');\n  if (!el) return;\n  try {\n    const pretty = JSON.stringify(data, null, 2);\n    el.innerHTML = pretty.split('\\n').map(highlightJsonLine).join('\\n');\n  } catch (e) {\n    el.textContent = String(e && e.message || e);\n  }\n}\n\n\/\/ Toggle between structured cards and raw JSON\nfunction mvSetMode(mode) {\n  const body = $('#mv-body');\n  const raw = $('#mv-raw');\n  const btnS = $('#mv-mode-structured');\n  const btnR = $('#mv-mode-raw');\n  if (mode === 'raw') {\n    body.style.display = 'none';\n    raw.style.display = '';\n    btnS.classList.remove('active');\n    btnR.classList.add('active');\n  } else {\n    body.style.display = '';\n    raw.style.display = 'none';\n    btnS.classList.add('active');\n    btnR.classList.remove('active');\n  }\n}\n\n\/\/ Wire the toggle buttons once at load\n$('#mv-mode-structured').addEventListener('click', () => mvSetMode('structured'));\n$('#mv-mode-raw').addEventListener('click',        () => mvSetMode('raw'));\n\nfunction updateMetricsClock() {\n  const el = $('#mv-header-time');\n  if (el) el.textContent = new Date().toUTCString().replace(' GMT', 'Z');\n}\nsetInterval(updateMetricsClock, 1000);\nupdateMetricsClock();\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ VFD HEALTH DASHBOARD \u2014 aggregates config + metrics + device-info + hmi-info\n\/\/                       + version.log; computes health scores per the spec.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet healthConfig = null;\nlet healthMetrics = null;\nlet healthDeviceInfo = {};   \/\/ parsed from device-info.log\nlet healthDeviceStatus = {}; \/\/ parsed from device-status.log\nlet healthHmiInfo = {};      \/\/ parsed from hmi-info.log\nlet healthVersionInfo = {};  \/\/ parsed from version.log\nlet healthOpData = null;     \/\/ parsed from smartdrive-opdata.json\nlet healthAlarmsState = null; \/\/ parsed from system-state-and-alarms.json\n\n\/\/ Drive-stability thresholds \u2014 from user spec (V1.4.0 health panels)\nconst HD_DPR_GREEN_DAYS = 180;   \/\/ Days per reboot \u2265 180 d          \u2192 Stable\nconst HD_DPR_WARN_DAYS  = 30;    \/\/ 30\u2013180 d \u2192 Warning; < 30 d       \u2192 Unstable\nconst HD_UPT_GREEN_DAYS = 30;    \/\/ Uptime since last reboot \u2265 30 d  \u2192 Stable\nconst HD_UPT_WARN_DAYS  = 7;     \/\/ 7\u201330 d \u2192 Warning; < 7 d          \u2192 Unstable\nconst HD_SPH_GREEN      = 2;     \/\/ Starts\/hour \u2264 2                  \u2192 Stable\nconst HD_SPH_WARN       = 6;     \/\/ 2\u20136 \u2192 Warning; > 6               \u2192 Unstable\nconst HD_ART_GREEN_H    = 0.25;  \/\/ Avg runtime\/start \u2265 0.25 h (15m) \u2192 Stable\nconst HD_ART_WARN_H     = 0.083; \/\/ 5\u201315 min \u2192 Warning; < 5 min      \u2192 Unstable\nconst HD_RSR_GREEN      = 95;    \/\/ Restart success \u2265 95 %           \u2192 Stable (if auto-restart enabled)\nconst HD_RSR_WARN       = 80;    \/\/ 80\u201395 % \u2192 Warning; < 80 %        \u2192 Unstable\n\n\/\/ Simple \"key : value\" parser for log files like device-info.log \/ hmi-info.log.\n\/\/ Handles both \"key: value\" and \"key : value\" spacing. Ignores JSON blobs\n\/\/ (which start with '{' \u2014 we don't consume them here).\nfunction hdParseKvLog(text) {\n  const out = {};\n  if (!text) return out;\n  const lines = text.split('\\n');\n  for (const line of lines) {\n    if (!line || line.startsWith('#') || line.startsWith('{') || line.startsWith('}')) continue;\n    const idx = line.indexOf(':');\n    if (idx <= 0) continue;\n    const key = line.slice(0, idx).trim();\n    const val = line.slice(idx + 1).trim();\n    if (!key || val.startsWith('{')) continue;   \/\/ skip nested JSON blobs\n    if (val) out[key] = val;\n  }\n  return out;\n}\n\n\/\/ Parse the OLD system-alarms.log format used by older firmware:\n\/\/   ***sd-alarms [-a]***:\n\/\/   System opdata:  { \"operationalData\": { ... } }\n\/\/   Alarms opdata:  { \"operationalData\": { \"alarms\": [...] }, \"status\": \"...\" }\n\/\/ Extracts each labeled block by matching braces, unwraps the \"operationalData\"\n\/\/ layer, and returns a shape compatible with the new JSON format\n\/\/ ({ SystemOpdata, AlarmOperationalData, AlarmMetrics }).\nfunction hdParseOldAlarmsLog(text) {\n  const result = { SystemOpdata: null, AlarmOperationalData: null, AlarmMetrics: null };\n  if (!text) return result;\n\n  const sections = [\n    { key: \/System\\s+opdata\\s*:\/i,  target: 'SystemOpdata' },\n    { key: \/Alarms\\s+opdata\\s*:\/i,  target: 'AlarmOperationalData' },\n  ];\n\n  for (const sec of sections) {\n    const m = sec.key.exec(text);\n    if (!m) continue;\n    \/\/ Find opening brace after the label\n    const braceStart = text.indexOf('{', m.index + m[0].length);\n    if (braceStart < 0) continue;\n    \/\/ Scan forward tracking braces + strings to find matching close\n    let depth = 0, end = -1, inString = false, escape = false;\n    for (let i = braceStart; i < text.length; i++) {\n      const ch = text[i];\n      if (escape) { escape = false; continue; }\n      if (ch === '\\\\') { escape = true; continue; }\n      if (ch === '\"') { inString = !inString; continue; }\n      if (inString) continue;\n      if (ch === '{') depth++;\n      else if (ch === '}') {\n        depth--;\n        if (depth === 0) { end = i; break; }\n      }\n    }\n    if (end < 0) continue;\n    const jsonStr = text.slice(braceStart, end + 1);\n    try {\n      const parsed = JSON.parse(jsonStr);\n      \/\/ Unwrap \"operationalData\" wrapper if present (old format nests everything under it)\n      const inner = (parsed && typeof parsed === 'object' && parsed.operationalData)\n        ? parsed.operationalData : parsed;\n      result[sec.target] = inner;\n    } catch (e) {\n      console.warn('Old-alarms parser: JSON.parse failed for ' + sec.target, e);\n    }\n  }\n\n  return result;\n}\n\n\/\/ Parse version.log \u2014 format: \"Current: v...\" \/ \"Backup: v...\" lines\nfunction hdParseVersionLog(text) {\n  const out = {};\n  if (!text) return out;\n  for (const line of text.split('\\n')) {\n    const m = line.match(\/^\\s*(Current|Backup|BLE-Image|BLE-CoPro|Motor-CoPro|AFE-CoPro)\\s*:\\s*(.+?)\\s*$\/);\n    if (m) out[m[1]] = m[2];\n  }\n  return out;\n}\n\n\/\/ Strip common enum prefixes for human display: NETWORK_CONFIGURATION_METHOD_STATIC_IPV4 \u2192 Static Ipv4\nfunction hdStripEnum(v) {\n  if (!v || typeof v !== 'string') return v;\n  \/\/ Strip well-known prefixes (longest first); then convert SNAKE_CASE to Title Case\n  const stripped = v\n    .replace(\/^NETWORK_CONFIGURATION_METHOD_\/, '')\n    .replace(\/^SYSTEM_MOBILE_PERMISSION_\/, '')\n    .replace(\/^SYSTEM_REMOTE_CONTROL_SOURCE_\/, 'Source ')\n    .replace(\/^SYSTEM_CONTROL_MODE_\/, '')\n    .replace(\/^BLE_ADVERTISING_STATE_\/, '')\n    .replace(\/^ALARM_SEVERITY_\/, '')\n    .replace(\/^ALARM_STATE_\/, '')\n    .replace(\/^ALARM_CODE_\/, '')\n    .replace(\/^SYSTEM_\/, '')\n    .replace(\/^BLE_\/, '')\n    .replace(\/^NETWORK_\/, '');\n  return stripped.split('_').map(w =>\n    w.length ? w[0] + w.slice(1).toLowerCase() : w\n  ).join(' ');\n}\n\n\/\/ Health status classes: '' = Stable (green), 'warn' = Warning, 'crit' = Unstable, 'na' = No Data\nfunction hdStateLabel(state) {\n  if (state === '') return 'Stable';\n  if (state === 'warn') return 'Warning';\n  if (state === 'crit') return 'Unstable';\n  return 'No Data';\n}\n\n\/\/ Small numeric formatters for the stability metrics\nfunction hdFmtNum(v, dp) { return (v == null || isNaN(v)) ? '\u2014' : v.toFixed(dp != null ? dp : 2); }\nfunction hdFmtHoursH(h) {\n  if (h == null || isNaN(h)) return '\u2014';\n  if (h < 1) return (h * 60).toFixed(1) + ' min';\n  return h.toFixed(2) + ' h';\n}\nfunction hdFmtDaysD(dv) {\n  if (dv == null || isNaN(dv)) return '\u2014';\n  return dv >= 100 ? Math.round(dv).toLocaleString('en-US') + ' d' : dv.toFixed(1) + ' d';\n}\n\n\/\/ Compute the drive-stability metrics from smartdrive-metrics.json (+ config for\n\/\/ auto-restart enablement). Returns:\n\/\/   { rebootRate, daysPerReboot, uptimeDays, startsPerHour, startsPerDay,\n\/\/     avgRuntimePerStartH, restartRate, restartEnabled,\n\/\/     status ('' | 'warn' | 'crit' | 'na'), critReasons[], warnReasons[], details{} }\nfunction hdComputeScores(metrics) {\n  const out = {\n    rebootRate: null, daysPerReboot: null, uptimeDays: null,\n    startsPerHour: null, startsPerDay: null, avgRuntimePerStartH: null,\n    restartRate: null, restartEnabled: false,\n    status: 'na', critReasons: [], warnReasons: [], details: {},\n  };\n  if (!metrics || typeof metrics !== 'object') return out;\n\n  const sys = metrics.SystemMetrics || {};\n  const mc  = metrics.MotorControllerMetrics || {};\n\n  \/\/ \u2500\u2500 Drive stability inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \/\/ \"Total VFD uptime\" = SystemMetrics.lifetime_metrics.lifetime_run_time (seconds,\n  \/\/ total powered-on time) \u2014 paired with lifetime_boot_count from the same section.\n  const bootCount    = sys.lifetime_metrics?.lifetime_boot_count;\n  const totalUpSec   = sys.lifetime_metrics?.lifetime_run_time;\n  const currentUpSec = sys.uptime_metrics?.uptime_up_time;      \/\/ uptime since last reboot\n  const totalUpH     = totalUpSec != null ? totalUpSec \/ 3600 : null;\n  const totalUpDays  = totalUpH   != null ? totalUpH \/ 24 : null;\n\n  if (bootCount != null && totalUpDays != null && totalUpDays > 0) {\n    out.rebootRate    = Math.max(bootCount - 1, 0) \/ totalUpDays;      \/\/ #\/day\n    out.daysPerReboot = totalUpDays \/ Math.max(bootCount - 1, 1);      \/\/ days\n  }\n  if (currentUpSec != null) out.uptimeDays = currentUpSec \/ 86400;\n  out.details.stability = { bootCount, totalUpSec, totalUpH, totalUpDays, currentUpSec };\n\n  \/\/ \u2500\u2500 Mechanical \/ thermal stress inputs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const motorStarts = mc.motor_lifetime_metrics?.motor_lifetime_start_count;\n  const motorRunSec = mc.motor_lifetime_metrics?.motor_lifetime_run_time;\n  const motorRunH   = motorRunSec != null ? motorRunSec \/ 3600 : null;\n  if (motorStarts != null && motorRunH != null) {\n    out.startsPerHour       = motorStarts \/ Math.max(motorRunH, 1);\n    out.startsPerDay        = motorStarts \/ Math.max(motorRunH \/ 24, 1);\n    out.avgRuntimePerStartH = motorRunH \/ Math.max(motorStarts, 1);\n  }\n  out.details.stress = { motorStarts, motorRunSec, motorRunH };\n\n  \/\/ \u2500\u2500 Auto-restart reliability \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const ar = mc.motor_auto_restart_metrics || {};\n  const attempts = ar.motor_autorestart_total_attempts;\n  const success  = ar.motor_autorestart_total_success;\n  \/\/ Enabled if the config says so; fall back to \"has ever attempted a restart\".\n  const arMode = healthConfig?.configurations?.motorController?.autoRestart?.mode;\n  out.restartEnabled = arMode != null\n    ? !\/DISABLED|OFF|NONE\/i.test(String(arMode))\n    : (attempts != null && attempts > 0);\n  if (attempts != null && attempts > 0) {\n    out.restartRate = 100 * ((success || 0) \/ attempts);\n  }\n  out.details.restart = {\n    attempts, success,\n    currentAttempts: ar.motor_autorestart_current_attempts,\n    arMode,\n  };\n\n  \/\/ \u2500\u2500 Health status: Unstable \u25b8 Warning \u25b8 Stable \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const crit = [], warn = [];\n  if (out.daysPerReboot != null) {\n    if (out.daysPerReboot < HD_DPR_WARN_DAYS)\n      crit.push('Days per reboot ' + hdFmtDaysD(out.daysPerReboot) + ' < ' + HD_DPR_WARN_DAYS + ' d');\n    else if (out.daysPerReboot < HD_DPR_GREEN_DAYS)\n      warn.push('Days per reboot ' + hdFmtDaysD(out.daysPerReboot) + ' in 30\u2013180 d band');\n  }\n  if (out.uptimeDays != null) {\n    if (out.uptimeDays < HD_UPT_WARN_DAYS)\n      crit.push('Uptime since last boot ' + hdFmtDaysD(out.uptimeDays) + ' < ' + HD_UPT_WARN_DAYS + ' d');\n    else if (out.uptimeDays < HD_UPT_GREEN_DAYS)\n      warn.push('Uptime since last boot ' + hdFmtDaysD(out.uptimeDays) + ' in 7\u201330 d band');\n  }\n  if (out.startsPerHour != null) {\n    if (out.startsPerHour > HD_SPH_WARN)\n      crit.push('Starts per hour ' + hdFmtNum(out.startsPerHour) + ' > ' + HD_SPH_WARN);\n    else if (out.startsPerHour > HD_SPH_GREEN)\n      warn.push('Starts per hour ' + hdFmtNum(out.startsPerHour) + ' in 2\u20136 band');\n  }\n  if (out.avgRuntimePerStartH != null) {\n    if (out.avgRuntimePerStartH < HD_ART_WARN_H)\n      crit.push('Avg runtime per start ' + hdFmtHoursH(out.avgRuntimePerStartH) + ' < 5 min');\n    else if (out.avgRuntimePerStartH < HD_ART_GREEN_H)\n      warn.push('Avg runtime per start ' + hdFmtHoursH(out.avgRuntimePerStartH) + ' in 5\u201315 min band');\n  }\n  if (out.restartEnabled && out.restartRate != null) {\n    if (out.restartRate < HD_RSR_WARN)\n      crit.push('Restart success rate ' + hdFmtNum(out.restartRate, 1) + '% < ' + HD_RSR_WARN + '%');\n    else if (out.restartRate < HD_RSR_GREEN)\n      warn.push('Restart success rate ' + hdFmtNum(out.restartRate, 1) + '% in 80\u201395% band');\n  }\n  out.critReasons = crit;\n  out.warnReasons = warn;\n\n  const anyMetric = [out.daysPerReboot, out.uptimeDays, out.startsPerHour,\n                     out.avgRuntimePerStartH, out.restartRate].some(v => v != null);\n  if (!anyMetric) out.status = 'na';\n  else if (crit.length) out.status = 'crit';\n  else if (warn.length) out.status = 'warn';\n  else out.status = '';\n\n  return out;\n}\n\n\/\/ Build an info row (key + value) for the info sections\nfunction hdInfoRow(key, val, opts) {\n  opts = opts || {};\n  const isNa = val == null || val === '' || val === '\u2014';\n  const mono = opts.mono ? ' mono' : '';\n  const cls = isNa ? 'hd-info-val na' + mono : 'hd-info-val' + mono;\n  const _lk = HD_LABEL_KEY[key];\n  const _lka = _lk ? ` data-i18n=\"${_lk}\"` : '';\n  const _lkt = (_lk && typeof t === 'function') ? t(_lk) : key;\n  return `<div class=\"hd-info-key\"${_lka}>${escapeHtml(_lkt)}<\/div>`\n    + `<div class=\"${cls}\">${isNa ? '\u2014' : escapeHtml(String(val))}<\/div>`;\n}\n\nfunction hdRenderDeviceInfo() {\n  const cfg = healthConfig || {};\n  const meta = cfg._metadata || {};\n  const sys = (cfg.configurations && cfg.configurations.system) || {};\n  const di = healthDeviceInfo;\n  const ver = healthVersionInfo;\n  const sod = (healthAlarmsState && healthAlarmsState.SystemOpdata) || {};\n\n  const driveName = sys.driveName || '';\n  const model     = meta.deviceModel || di.model || '';\n  const serial    = meta.deviceSerialNumber || di.serial_number || '';\n  const current   = meta.deviceSoftwareVersion || ver.Current || '';\n  const backup    = meta.deviceBackupVersion || ver.Backup || '';\n  const ctrlMode  = hdStripEnum(sod.controlMode) || '';\n  const ctrlSrc   = hdStripEnum(sod.remoteControlSource) || '';\n\n  const rows = [\n    hdInfoRow('VFD name',                driveName),\n    hdInfoRow('Model',                   model, { mono: true }),\n    hdInfoRow('Serial number',           serial, { mono: true }),\n    hdInfoRow('Current firmware',        current, { mono: true }),\n    hdInfoRow('Backup firmware',         backup, { mono: true }),\n    hdInfoRow('Control mode',            ctrlMode),\n    hdInfoRow('Remote control source',   ctrlSrc),\n  ];\n  $('#hd-device-info').innerHTML = rows.join('');\n}\n\nfunction hdRenderNetworkInfo() {\n  const cfg = healthConfig || {};\n  const net = (cfg.configurations && cfg.configurations.network) || {};\n  const iface = (net.interfaces && net.interfaces[0]) || {};\n  const dns = net.dns || {};\n  const di = healthDeviceInfo;\n\n  const method  = hdStripEnum(iface.method) || '';\n  const ip      = iface.ipv4Address || '';\n  const mask    = iface.ipv4NetworkMask || '';\n  const gateway = iface.ipv4Gateway || '';\n  const resolvers = Array.isArray(dns.resolvers) && dns.resolvers.length\n    ? dns.resolvers.join(', ')\n    : '';\n  const mac0 = di.eth0_addr || '';\n  const mac1 = di.eth1_addr || '';\n\n  const rows = [\n    hdInfoRow('Method',              method),\n    hdInfoRow('IP address',          ip, { mono: true }),\n    hdInfoRow('Subnet mask',         mask, { mono: true }),\n    hdInfoRow('Gateway',             gateway, { mono: true }),\n    hdInfoRow('DNS resolvers',       resolvers, { mono: true }),\n    hdInfoRow('MAC address \u00b7 Eth0',  mac0, { mono: true }),\n    hdInfoRow('MAC address \u00b7 Eth1',  mac1, { mono: true }),\n  ];\n  $('#hd-network-info').innerHTML = rows.join('');\n}\n\nfunction hdRenderMobileInfo() {\n  const cfg = healthConfig || {};\n  const sys = (cfg.configurations && cfg.configurations.system) || {};\n  const ble = (cfg.configurations && cfg.configurations.ble) || {};\n  const hmi = healthHmiInfo;\n\n  const bleMac    = hmi.mac_address || '';\n  const bleState  = hdStripEnum(ble.advertisingState) || '';\n  const mobilePerm = hdStripEnum(sys.mobilePermissions) || '';\n\n  const rows = [\n    hdInfoRow('BLE MAC address',      bleMac, { mono: true }),\n    hdInfoRow('BLE state',            bleState),\n    hdInfoRow('Mobile permissions',   mobilePerm),\n  ];\n  $('#hd-mobile-info').innerHTML = rows.join('');\n}\n\n\/\/ Map alarm severity enum \u2192 chip color class\nfunction hdSeverityClass(sev) {\n  if (!sev) return '';\n  const s = sev.replace(\/^ALARM_SEVERITY_\/, '');\n  if (s === 'CRITICAL') return 'crit';\n  if (s === 'MAJOR')    return 'major';\n  if (s === 'MEDIUM')   return 'medium';\n  if (s === 'MINOR')    return 'minor';\n  if (s === 'WARNING')  return 'warning';\n  if (s === 'INFO' || s === 'INFORMATIONAL') return 'info';\n  return '';\n}\n\n\/\/ Format ISO timestamp compactly, treating 1970-* as \"never\"\nfunction hdFmtTs(iso) {\n  if (!iso || typeof iso !== 'string') return '\u2014';\n  if (iso.startsWith('1970-')) return '\u2014';\n  return iso.replace('T', ' ').replace(\/\\.\\d+Z$\/, 'Z').replace(\/Z$\/, 'Z');\n}\n\n\/\/ Online help link for alarm troubleshooting (used as \"view full article\" link in the popup)\nconst CLICKHELP_TROUBLESHOOTING_URL = 'https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-publication\/troubleshooting';\nconst CLICKHELP_ALARM_HELP_URL = 'https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-publication\/alarms-and-troubleshooting-suggestions\/';\nconst CLICKHELP_ALARM_HELP_BASE = 'https:\/\/smartd.clickhelp.co\/articles\/clean-power-vfd-publication\/alarms-and-troubleshooting-suggestions\/a\/';\n\n\/\/ Deep-link anchors for individual alarm codes on the ClickHelp alarms\/troubleshooting page.\n\/\/ Codes not listed here (or if the code can't be resolved) fall back to the general alarms page.\nconst ALARM_HELP_ANCHORS = {\n  1:  'h2__1336226717', 3:  'h2_2128401314',  5:  'h2_414420430',   6:  'h2__563329799',\n  7:  'h2_1009985039',  9:  'h2__1473736254', 11: 'h2_142627263',   12: 'h2__2112894391',\n  14: 'h2_1523102616',  15: 'h2_626175430',   16: 'h2__1681867939', 17: 'h2__234719959',\n  20: 'h2__1600931824', 21: 'h2__566060484',  22: 'h2_1453539979',\n  24: 'h2_624245363',   25: 'h2_624245363',   26: 'h2_624245363',   27: 'h2_624245363',\n  28: 'h2__182853868',  29: 'h2_999652170',   30: 'h2__1529045032', 31: 'h2__1460754512',\n  32: 'h2__885706767',  33: 'h2__1661790332', 34: 'h2__1994934078', 35: 'h2_1853967225',\n  36: 'h2_279338876',   37: 'h2__1600334762', 38: 'h2_1874209846',  39: 'h2_72003316',\n  40: 'h2_1535120169',  41: 'h2__1392194053', 42: 'h2__657967492',  43: 'h2__1569998707',\n  44: 'h2_834057796',   45: 'h2__550449301',  46: 'h2_404392193',   47: 'h2_404392193',\n  48: 'h2__933395141',  49: 'h2_639512569',   50: 'h2__1853304787', 51: 'h2__994417820',\n  52: 'h2_606735070',   53: 'h2_258188187',   54: 'h2_1508398271',  58: 'h2__1514127580',\n  59: 'h2__2011229936', 60: 'h2__2065413630', 61: 'h2__260059748',  62: 'h2__793187828',\n  63: 'h2_1793297493',  64: 'h2__1395780153', 65: 'h2__1823543049', 66: 'h2__1935437104',\n  67: 'h2_1891333455',  68: 'h2__1869725698', 69: 'h2_829593070',\n  70: 'h2__990159130',  71: 'h2__990159130',  72: 'h2_1525238971',  78: 'h2_812474193',\n  80: 'h2__2132790985',\n};\n\n\/\/ Resolve the best ClickHelp URL for a given numeric alarm code: a deep link to the\n\/\/ specific section if we have one, otherwise the general alarms\/troubleshooting page.\nfunction hdAlarmHelpUrl(num) {\n  if (num != null && ALARM_HELP_ANCHORS[num]) return CLICKHELP_ALARM_HELP_BASE + ALARM_HELP_ANCHORS[num];\n  return CLICKHELP_ALARM_HELP_URL;\n}\n\n\/\/ Open the alarm-help popup for a given raw alarm code string (e.g. \"ALARM_CODE_LINE_UNEXPECTED_VOLTAGE\")\nconst ALARM_DESC_FR={\"1\": \"Safe Torque Off d\u00e9clench\u00e9 par le mat\u00e9riel via l\u2019entr\u00e9e TOR STO1\/STO2.\", \"2\": \"D\u00e9faut dans le circuit STO1 ou STO2. Contactez le support.\", \"3\": \"R\u00e9seau mal c\u00e2bl\u00e9 \u2014 inversez deux phases.\", \"4\": \"Tension du bus CC trop basse pour maintenir le fonctionnement.\", \"5\": \"Tension du bus CC dangereusement \u00e9lev\u00e9e.\", \"6\": \"Tension du r\u00e9seau basse.\", \"7\": \"D\u00e9s\u00e9quilibre de tension du r\u00e9seau entre les phases.\", \"8\": \"NON IMPL\u00c9MENT\u00c9.\", \"9\": \"Probl\u00e8me de communication entre la carte de contr\u00f4le et l\u2019IHM.\", \"10\": \"NON IMPL\u00c9MENT\u00c9.\", \"11\": \"Protection mat\u00e9rielle contre la surintensit\u00e9 c\u00f4t\u00e9 r\u00e9seau.\", \"12\": \"D\u00e9s\u00e9quilibre du courant moteur entre les phases.\", \"13\": \"Courant soutenu \u2014 intensit\u00e9 plus \u00e9lev\u00e9e sur une dur\u00e9e plus courte que la SURCHARGE.\", \"14\": \"Surchauffe d\u00e9tect\u00e9e sur le moteur.\", \"15\": \"Courant soutenu au-dessus du nominal moteur \u2014 intensit\u00e9 plus faible sur une dur\u00e9e plus longue que la SURINTENSIT\u00c9.\", \"16\": \"Protection mat\u00e9rielle contre la surchauffe.\", \"17\": \"Une erreur s\u2019est produite au d\u00e9marrage ; red\u00e9marrage requis.\", \"18\": \"D\u00e9marr\u00e9 sur la version de secours compatible avec la configuration actuelle.\", \"19\": \"D\u00e9marr\u00e9 sur la version de secours incompatible avec la config \u2014 a provoqu\u00e9 une r\u00e9initialisation d\u2019usine.\", \"20\": \"La temp\u00e9rature de l\u2019\u00e9lectronique de puissance est trop \u00e9lev\u00e9e.\", \"21\": \"Tension du r\u00e9seau \u00e9lev\u00e9e.\", \"22\": \"La mise \u00e0 jour logicielle a \u00e9chou\u00e9.\", \"23\": \"Ondulation de tension sur le bus CC ET tension r\u00e9seau d\u00e9s\u00e9quilibr\u00e9e (p. ex. perte de phase).\", \"24\": \"Le ventilateur de refroidissement n\u00b0 1 ne fonctionne pas comme pr\u00e9vu.\", \"25\": \"Le ventilateur de refroidissement n\u00b0 2 ne fonctionne pas comme pr\u00e9vu.\", \"26\": \"Le ventilateur de refroidissement n\u00b0 3 ne fonctionne pas comme pr\u00e9vu.\", \"27\": \"Le ventilateur de refroidissement n\u00b0 4 ne fonctionne pas comme pr\u00e9vu.\", \"28\": \"Pas assez de ventilateurs en fonctionnement.\", \"29\": \"La tension r\u00e9seau mesur\u00e9e ne correspond pas \u00e0 la tension r\u00e9seau configur\u00e9e.\", \"30\": \"Erreurs de communication interne graves entre les composants du VFD. Contactez le support.\", \"31\": \"Tension du bus CC d\u00e9s\u00e9quilibr\u00e9e (CC+ vs CC-).\", \"32\": \"Protection mat\u00e9rielle contre la surintensit\u00e9 c\u00f4t\u00e9 charge.\", \"33\": \"Commandes E\/S avant et arri\u00e8re d\u00e9tect\u00e9es simultan\u00e9ment.\", \"34\": \"Le syst\u00e8me est rest\u00e9 trop longtemps en \u00e9tat de d\u00e9marrage.\", \"35\": \"Le syst\u00e8me est d\u00e9grad\u00e9.\", \"36\": \"Utilisation du processeur trop \u00e9lev\u00e9e.\", \"37\": \"Utilisation de la m\u00e9moire trop \u00e9lev\u00e9e.\", \"38\": \"Le VFD limite la d\u00e9c\u00e9l\u00e9ration car la tension du bus CC est trop \u00e9lev\u00e9e.\", \"39\": \"Le VFD limite l\u2019acc\u00e9l\u00e9ration car le courant moteur est trop \u00e9lev\u00e9.\", \"40\": \"La fr\u00e9quence r\u00e9seau mesur\u00e9e ne correspond pas \u00e0 la fr\u00e9quence configur\u00e9e.\", \"41\": \"Tension du bus CC insuffisante par rapport au r\u00e9seau mesur\u00e9 \u2014 non s\u00e9curitaire d\u2019activer le relais de bypass.\", \"42\": \"L\u2019\u00e9tage d\u2019entr\u00e9e actif (AFE) ne peut pas fonctionner en raison d\u2019un d\u00e9faut important.\", \"43\": \"L\u2019AFE a d\u00e9tect\u00e9 un creux ou un d\u00e9s\u00e9quilibre de tension r\u00e9seau \u2014 tenue aux creux de tension (LVRT). Contr\u00f4le moteur adapt\u00e9 pour maintenir le fonctionnement.\", \"44\": \"L\u2019utilisateur a intentionnellement forc\u00e9 le VFD en mode local alors que le contr\u00f4le \u00e0 distance \u00e9tait s\u00e9lectionn\u00e9.\", \"45\": \"Le capteur de sortie analogique est d\u00e9fectueux.\", \"46\": \"Valeur de r\u00e9troaction du PID n\u00b0 1 trop basse.\", \"47\": \"Valeur de r\u00e9troaction du PID n\u00b0 1 trop \u00e9lev\u00e9e.\", \"48\": \"L\u2019alimentation de la carte de contr\u00f4le a \u00e9t\u00e9 perdue.\", \"49\": \"Conditions de contr\u00f4le moteur instables d\u00e9tect\u00e9es ; le VFD doit s\u2019arr\u00eater pour se prot\u00e9ger.\", \"50\": \"Le moteur tourne trop vite.\", \"51\": \"Le moteur tourne trop lentement.\", \"52\": \"Le moteur tourne dans le mauvais sens.\", \"53\": \"L\u2019encodeur ne mesure pas correctement la vitesse du moteur.\", \"54\": \"Encodeur mal c\u00e2bl\u00e9 ou d\u00e9fectueux.\", \"55\": \"Capteur de l\u2019entr\u00e9e analogique 1 d\u00e9fectueux.\", \"56\": \"Capteur de l\u2019entr\u00e9e analogique 2 d\u00e9fectueux.\", \"57\": \"Capteur de l\u2019entr\u00e9e analogique 3 d\u00e9fectueux.\", \"58\": \"Communication Modbus interrompue.\", \"59\": \"Temp\u00e9rature ambiante trop \u00e9lev\u00e9e.\", \"60\": \"Le capteur de temp\u00e9rature du moteur est en court-circuit.\", \"61\": \"Aucune lecture de courant sur l\u2019une des phases du moteur.\", \"62\": \"L\u2019arbre du moteur est cal\u00e9 (avec consigne de vitesse).\", \"63\": \"Aucune lecture de courant sur les phases du moteur (circuit ouvert VFD\u2194moteur).\", \"64\": \"Sous-charge moteur d\u00e9finie par l\u2019application de l\u2019utilisateur.\", \"65\": \"La tentative de red\u00e9marrage automatique du moteur a \u00e9chou\u00e9.\", \"66\": \"L\u2019AFE a d\u00e9tect\u00e9 un creux\/d\u00e9s\u00e9quilibre de tension r\u00e9seau et n\u2019a pas pu r\u00e9cup\u00e9rer \u2014 \u00e9chec LVRT.\", \"67\": \"Courant soutenu au-dessus de la capacit\u00e9 du VFD.\", \"68\": \"L\u2019utilisateur a saisi un NIP invalide.\", \"69\": \"Signal de r\u00e9troaction du registre perdu pendant que le moteur tourne.\", \"70\": \"D\u00e9lai max d\u00e9pass\u00e9 pour la r\u00e9troaction externe, sous tension et en attente d\u2019entra\u00eener le moteur.\", \"71\": \"D\u00e9lai max d\u00e9pass\u00e9 pour la disparition de la r\u00e9troaction externe.\", \"72\": \"D\u00e9faut de mise \u00e0 la terre du moteur.\", \"73\": \"Mode d\u2019urgence interdit (verrouillage ou mode maintenance).\", \"74\": \"Alarme externe 1 (d\u00e9finie par l\u2019utilisateur via une entr\u00e9e TOR).\", \"75\": \"Alarme externe 2 (d\u00e9finie par l\u2019utilisateur via une entr\u00e9e TOR).\", \"76\": \"Alarme externe 3 (d\u00e9finie par l\u2019utilisateur via une entr\u00e9e TOR).\", \"77\": \"Alarme externe 4 (d\u00e9finie par l\u2019utilisateur via une entr\u00e9e TOR).\", \"78\": \"Protection contre la surcharge thermique du moteur.\", \"79\": \"Communication interne A7 \u2194 M4 perdue.\", \"80\": \"Communication BACnet interrompue.\"};\nconst ALARM_NAME_FR={\"9\": \"Perte de communication IHM\", \"14\": \"Surchauffe moteur\", \"15\": \"Surcharge moteur\", \"46\": \"PID 1 \u2014 Valeur de proc\u00e9d\u00e9 basse\", \"47\": \"PID 1 \u2014 Valeur de proc\u00e9d\u00e9 haute\", \"50\": \"Survitesse moteur\", \"51\": \"Sous-vitesse moteur\", \"58\": \"Perte de communication Modbus\", \"62\": \"Moteur cal\u00e9\", \"64\": \"Sous-charge applicative moteur\", \"74\": \"Alarme externe critique\", \"75\": \"Alarme externe majeure\", \"76\": \"Alarme externe moyenne\", \"77\": \"Alarme externe mineure\", \"78\": \"Surcharge thermique moteur\", \"80\": \"Perte de communication BACnet\"};\nfunction hdOpenAlarmHelp(codeRaw) {\n  const codeLabel = hdStripEnum(codeRaw) || codeRaw || 'Unknown Alarm';\n  const num = hdAlarmCodeNumber(codeRaw);\n  const entry = num != null ? SMARTD_API.AlarmCode[num] : null;\n  const fr = (typeof LANG !== 'undefined' && LANG === 'fr');\n  const descEn = entry ? entry.desc : 'No description available for this alarm code.';\n  const desc = fr ? ((num != null && ALARM_DESC_FR[num]) || descEn) : descEn;\n  const helpUrl = fr ? 'https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-manuel-d-utilisation\/liste-alarmes-par-gravite' : hdAlarmHelpUrl(num);\n  const linkLabel = fr ? 'Consulter la liste des alarmes \u2197' : 'View full troubleshooting article on ClickHelp \u2197';\n  const nameLbl = (fr && num != null && ALARM_NAME_FR[num]) ? ALARM_NAME_FR[num] : codeLabel;\n  $('#alarmHelpTitle').textContent = num != null ? `${num} - ${nameLbl}` : nameLbl;\n  $('#alarmHelpSub').textContent = codeRaw || '\u2014';\n  $('#alarmHelpBody').innerHTML = `\n    <p class=\"alarm-help-desc\">${escapeHtml(desc)}<\/p>\n    <a class=\"alarm-help-link\" href=\"${helpUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${linkLabel}<\/a>\n  `;\n  $('#alarmHelpModal').style.display = 'flex';\n}\nfunction hdCloseAlarmHelp() { $('#alarmHelpModal').style.display = 'none'; }\n\n\/\/ Reverse-lookup the numeric alarm code (from the SmartD API enum reference) for a\n\/\/ raw alarm code string like \"ALARM_CODE_LINE_UNEXPECTED_VOLTAGE\" \u2192 29\nfunction hdAlarmCodeNumber(codeStr) {\n  if (!codeStr || typeof codeStr !== 'string') return null;\n  const raw = codeStr.replace(\/^ALARM_CODE_\/, '');\n  for (const num of Object.keys(SMARTD_API.AlarmCode)) {\n    if (SMARTD_API.AlarmCode[num].name === raw) return Number(num);\n  }\n  return null;\n}\n\n\/\/ Build a single alarm row. `metaHtml` is the right-side info (timestamp or occurrences count).\n\/\/ Clicking the row jumps to the audit-log line matching the alarm's date & time\n\/\/ (see hdJumpToAuditEvent); the (?) button in the top-right corner opens the\n\/\/ alarm-description popup (see hdOpenAlarmHelp).\nfunction hdBuildAlarmRow(a, metaHtml) {\n  const codeName = a.displayName || hdStripEnum(a.code) || a.code || '\u2014';\n  const codeNum = hdAlarmCodeNumber(a.code);\n  const codeLabel = codeNum != null ? `${codeNum} - ${codeName}` : codeName;\n  const codeRaw = a.code || a.displayName || '';\n  const sevRaw = a.severity || '';\n  const sevLabel = a.sevLabel || hdStripEnum(sevRaw) || '\u2014';\n  const sevCls = a.sevCls != null ? a.sevCls : hdSeverityClass(sevRaw);\n  const ts = (a.lastOccurrenceTime && !a.lastOccurrenceTime.startsWith('1970-'))\n    ? a.lastOccurrenceTime : '';\n  const rowTitle = ts\n    ? 'Click to locate this alarm in the Audit Log'\n    : 'No timestamp recorded for this alarm';\n  return `<div class=\"hd-alarm-row\" data-alarm-code=\"${escapeHtml(codeRaw)}\" data-alarm-ts=\"${escapeHtml(ts)}\" title=\"${rowTitle}\">\n    <div class=\"hd-alarm-code\">${escapeHtml(codeLabel)}\n      <span class=\"code-raw\">${escapeHtml(a.code || a.subLabel || '')}<\/span>\n    <\/div>\n    <div class=\"hd-sev-chip ${sevCls}\">${escapeHtml(sevLabel)}<\/div>\n    <div class=\"hd-alarm-meta\">${metaHtml}<\/div>\n    <button class=\"hd-alarm-help-btn\" title=\"Alarm description\" aria-label=\"Alarm description\">?<\/button>\n  <\/div>`;\n}\n\n\/\/ Jump from a Health Dashboard alarm row to the audit-log line matching the\n\/\/ alarm's date & time: switch to the Audit Log tab, find the nearest-in-time\n\/\/ entry (preferring ALARM events), page to it, scroll it into view, and flash it.\nfunction hdJumpToAuditEvent(tsIso, codeRaw) {\n  if (!state.auditLoaded || !Array.isArray(auditAllData) || !auditAllData.length) {\n    showToast('audit-log.json is not available in this package.', 'error');\n    return;\n  }\n  const target = Date.parse(tsIso);\n  if (isNaN(target)) {\n    showToast('This alarm has no valid timestamp to match.', 'error');\n    return;\n  }\n\n  \/\/ Nearest entry by time; ALARM events get a bias so they win near-ties,\n  \/\/ and a bigger bias if the message actually mentions this alarm code.\n  const codeShort = (codeRaw || '').replace(\/^ALARM_CODE_\/, '');\n  let best = null, bestScore = Infinity;\n  for (const e of auditAllData) {\n    if (!e.timestamp) continue;\n    const t = Date.parse(e.timestamp);\n    if (isNaN(t)) continue;\n    let score = Math.abs(t - target);\n    if (e.event === 'AUDIT_LOG_EVENT_ALARM') {\n      score -= 1500;\n      if (codeShort && (e.message || '').includes(codeShort)) score -= 3000;\n    }\n    if (score < bestScore) { bestScore = score; best = e; }\n  }\n  if (!best) {\n    showToast('No audit-log entry found near this alarm time.', 'error');\n    return;\n  }\n\n  activateTab('audit');\n\n  \/\/ If the current filters hide the target line, clear them so it's reachable.\n  if (auditFilteredData.indexOf(best) === -1) auditClearFilters();\n  if (auditFilteredData.indexOf(best) === -1) {\n    showToast('Matching audit-log line could not be displayed.', 'error');\n    return;\n  }\n\n  \/\/ Pin the matched entry: mark it with * and show the surrounding context.\n  auditSetFocus(best);\n\n  \/\/ If the nearest line is far from the alarm time, say so.\n  const deltaS = Math.abs(Date.parse(best.timestamp) - target) \/ 1000;\n  if (deltaS > 5) {\n    showToast('Nearest audit-log entry is ' + (deltaS >= 120 ? Math.round(deltaS \/ 60) + ' min' : Math.round(deltaS) + ' s') + ' from the alarm time.');\n  }\n}\n\n\/\/ Reverse-lookup an ALARM_CODE_* enum from the human alarm name that appears in\n\/\/ audit-log messages (\"Alarm Line unexpected voltage transitioned from ...\").\n\/\/ Compares alphanumeric-normalized forms against the SmartD API AlarmCode names.\nfunction hdAlarmCodeFromName(name) {\n  if (!name || typeof name !== 'string') return null;\n  \/\/ Some drive-generated alarm names carry a parenthetical abbreviation that is\n  \/\/ NOT part of the API enum, e.g. \"AFE Low Voltage RideThrough (LVRT) Cannot\n  \/\/ Recover\". Strip \"(...)\" groups before normalizing so the name still resolves\n  \/\/ to its code (\u2192 code number, severity chip, and alarm-help link all populate).\n  const norm = s => String(s).replace(\/\\([^)]*\\)\/g, ' ').replace(\/[^a-z0-9]\/gi, '').toLowerCase();\n  const candidates = [norm(name)];\n  const rawNorm = name.replace(\/[^a-z0-9]\/gi, '').toLowerCase();   \/\/ fallback: keep parenthetical text\n  if (rawNorm && candidates.indexOf(rawNorm) === -1) candidates.push(rawNorm);\n  for (const target of candidates) {\n    if (!target) continue;\n    for (const num of Object.keys(SMARTD_API.AlarmCode)) {\n      const apiName = SMARTD_API.AlarmCode[num].name || '';\n      if (apiName.replace(\/[^a-z0-9]\/gi, '').toLowerCase() === target) {\n        return 'ALARM_CODE_' + apiName;\n      }\n    }\n  }\n  return null;\n}\n\n\/\/ Resolve the API alarm severity (ALARM_SEVERITY_*) for a given alarm code, using\n\/\/ the per-alarm severity reported in system-state-and-alarms.json. Returns null if\n\/\/ unknown. `codeSevMap` is a code \u2192 ALARM_SEVERITY_* lookup built once per render.\nfunction hdApiAlarmSeverity(alarmCode, codeSevMap) {\n  if (!alarmCode || !codeSevMap) return null;\n  return codeSevMap[alarmCode] || null;\n}\n\n\/\/ Build a code \u2192 ALARM_SEVERITY_* map from the alarms state (the authoritative\n\/\/ per-alarm severity, matching the SmartD AlarmSeverity API enum).\nfunction hdBuildCodeSeverityMap(alarmsState) {\n  const map = {};\n  const list = (alarmsState && alarmsState.AlarmOperationalData && Array.isArray(alarmsState.AlarmOperationalData.alarms))\n    ? alarmsState.AlarmOperationalData.alarms : [];\n  list.forEach(a => { if (a.code && a.severity) map[a.code] = a.severity; });\n  return map;\n}\n\n\/\/ Scan the loaded audit log for *real* alarm triggers \u2014 ALARM entries whose\n\/\/ message says the alarm transitioned from Normal to Active AND that are\n\/\/ preceded by a state-change log line leaving ReadyToRun:\n\/\/   \"System state changed from SystemStateReadyToRun to ...\"\n\/\/ If a Normal \u2192 Active transition is not preceded by such a line (within\n\/\/ HD_FAULT_PRECEDE_WINDOW_S before it), the alarm is ignored. A small forward\n\/\/ tolerance (HD_FAULT_JITTER_S) absorbs millisecond log-write ordering jitter.\nconst HD_ALARM_TRIGGER_RE = \/\\bAlarm\\s+(.+?)\\s+transitioned\\s+from\\s+Normal\\s+to\\s+Active\\b\/i;\nconst HD_FAULT_TRANSITION_RE = \/System state changed from SystemStateReadyToRun to \/;\nconst HD_FAULT_PRECEDE_WINDOW_S = 10;   \/\/ fault must precede the trigger by \u2264 10 s\nconst HD_FAULT_JITTER_S = 1;            \/\/ \u2026or trail it by \u2264 1 s (log ordering jitter)\n\nfunction hdCollectAuditTriggers() {\n  if (!state.auditLoaded || !Array.isArray(auditAllData)) return null;\n\n  \/\/ Pass 1: collect the timestamps (ms) of every fault state-change line\n  const faultTimes = [];\n  for (const e of auditAllData) {\n    if (!e || !e.message || !e.timestamp) continue;\n    if (HD_FAULT_TRANSITION_RE.test(e.message)) {\n      const t = Date.parse(e.timestamp);\n      if (!isNaN(t)) faultTimes.push(t);\n    }\n  }\n  faultTimes.sort((a, b) => a - b);\n\n  \/\/ Pass 2: keep only Normal \u2192 Active transitions preceded by a fault line\n  const before = HD_FAULT_PRECEDE_WINDOW_S * 1000;\n  const after = HD_FAULT_JITTER_S * 1000;\n  const isFaultPreceded = (trigT) => {\n    if (isNaN(trigT)) return false;\n    for (const ft of faultTimes) {\n      if (ft > trigT + after) break;              \/\/ sorted \u2014 nothing later can match\n      if (trigT - ft <= before && ft - trigT <= after) return true;\n    }\n    return false;\n  };\n\n  const triggers = [];\n  for (const e of auditAllData) {\n    if (!e || !e.message) continue;\n    \/\/ Must be an ALARM-type event (ignore non-alarm rows that merely quote the phrase)\n    if (e.event && e.event !== 'AUDIT_LOG_EVENT_ALARM') continue;\n    const m = HD_ALARM_TRIGGER_RE.exec(e.message);\n    if (!m) continue;\n    \/\/ Ignore the alarm unless it is preceded by a system state change line\n    if (!isFaultPreceded(Date.parse(e.timestamp || ''))) continue;\n    triggers.push({ name: m[1].trim(), timestamp: e.timestamp || '', severity: e.severity, entry: e });\n  }\n  triggers.sort((a, b) => (b.timestamp || '').localeCompare(a.timestamp || ''));\n  return triggers;\n}\n\nfunction hdRenderAlarms() {\n  const s = healthAlarmsState;\n  const recentEl = $('#hd-recent-alarms');\n  const frequentEl = $('#hd-frequent-alarms');\n  const recentCountEl = $('#hd-recent-alarms-count');\n  const frequentCountEl = $('#hd-frequent-alarms-count');\n\n  \/\/ \u2500\u2500 Recent Alarms \u2014 sourced from audit-log.json: each ALARM entry whose\n  \/\/    message says the alarm \"transitioned from Normal to Active\" AND that is\n  \/\/    preceded by a ReadyToRun state-change line is one trigger. Severity is\n  \/\/    taken from the API alarm severity (system-state-and-alarms.json), NOT the\n  \/\/    audit-log entry's severity.\n  const codeSevMap = hdBuildCodeSeverityMap(s);\n  const triggers = hdCollectAuditTriggers();\n  if (!triggers) {\n    recentCountEl.textContent = '';\n    recentEl.innerHTML = '<div class=\"hd-alarm-empty\">audit-log.json not available in this package.<\/div>';\n  } else if (triggers.length) {\n    const recentTop = triggers.slice(0, 10);\n    recentCountEl.textContent = `${recentTop.length} of ${triggers.length} triggers`;\n    recentEl.innerHTML = recentTop.map(t => {\n      const code = hdAlarmCodeFromName(t.name);   \/\/ may be null if the name has no API match\n      const apiSev = hdApiAlarmSeverity(code, codeSevMap);   \/\/ ALARM_SEVERITY_* or null\n      const row = {\n        code: code,\n        displayName: t.name,\n        subLabel: 'AUDIT_LOG_EVENT_ALARM',\n        sevLabel: hdStripEnum(apiSev) || '\u2014',\n        sevCls: hdSeverityClass(apiSev),\n        lastOccurrenceTime: t.timestamp,\n      };\n      return hdBuildAlarmRow(row, `<strong>${escapeHtml(hdFmtTs(t.timestamp))}<\/strong>`);\n    }).join('');\n  } else {\n    recentCountEl.textContent = '';\n    recentEl.innerHTML = '<div class=\"hd-alarm-empty\">No qualifying alarm triggers found in the audit log (Normal \u2192 Active transitions preceded by a state change out of ReadyToRun).<\/div>';\n  }\n\n  \/\/ \u2500\u2500 Alarm History \u2014 from system-state-and-alarms.json. Shows every alarm with\n  \/\/    recorded activity: a positive occurrence count, a real last-occurrence time,\n  \/\/    OR a real last-clear time (non-epoch). This surfaces latched faults such as\n  \/\/    AFE LVRT Cannot Recover (code 66) even on firmware that doesn't populate an\n  \/\/    occurrences counter, and even when the audit-log window doesn't cover them.\n  if (!s) {\n    frequentCountEl.textContent = '';\n    frequentEl.innerHTML = '<div class=\"hd-alarm-empty\">system-state-and-alarms.json not available.<\/div>';\n    return;\n  }\n  const opAlarms = (s.AlarmOperationalData && Array.isArray(s.AlarmOperationalData.alarms))\n    ? s.AlarmOperationalData.alarms : [];\n  const hasMetrics = s.AlarmMetrics && Array.isArray(s.AlarmMetrics.stats);\n  const stats = hasMetrics ? s.AlarmMetrics.stats : [];\n  \/\/ Merge metrics (occurrences + clear time) with opdata (state + severity) by code.\n  const sevByCode = {};\n  const tsByCode = {};\n  opAlarms.forEach(a => {\n    if (a.code && a.severity) sevByCode[a.code] = a.severity;\n    if (a.code && a.lastOccurrenceTime && !a.lastOccurrenceTime.startsWith('1970-')) tsByCode[a.code] = a.lastOccurrenceTime;\n  });\n\n  const isRealTs = t => !!t && !String(t).startsWith('1970-') && !String(t).startsWith('0001-');\n  const activityMs = a => {\n    const times = [a.lastOccurrenceTime, a.lastClearTime, tsByCode[a.code]]\n      .filter(isRealTs).map(t => Date.parse(t)).filter(n => !isNaN(n));\n    return times.length ? Math.max(...times) : 0;\n  };\n  \/\/ Metrics stats carry occurrences + clear time; fall back to opdata alarms when a\n  \/\/ package has no metrics block.\n  const source = hasMetrics ? stats : opAlarms;\n  const history = source\n    .filter(a => (a.occurrences || 0) > 0)\n    .slice()\n    .sort((a, b) => {\n      const oa = a.occurrences || 0, ob = b.occurrences || 0;\n      if (ob !== oa) return ob - oa;              \/\/ occurrence count first\n      return activityMs(b) - activityMs(a);        \/\/ then most-recent activity\n    });\n\n  if (!source.length) {\n    frequentCountEl.textContent = '';\n    frequentEl.innerHTML = '<div class=\"hd-alarm-empty\">No alarm inventory provided in this package.<\/div>';\n  } else if (history.length) {\n    frequentCountEl.textContent = `${history.length} ${history.length === 1 ? 'alarm' : 'alarms'} with occurrences`;\n    frequentEl.innerHTML = history.map(a => {\n      const occTs = isRealTs(a.lastOccurrenceTime) ? a.lastOccurrenceTime : tsByCode[a.code];\n      const enriched = Object.assign({}, a, {\n        severity: a.severity || sevByCode[a.code],\n        \/\/ give the row a timestamp to jump to: prefer occurrence, else clear time\n        lastOccurrenceTime: occTs || a.lastClearTime,\n      });\n      const count = a.occurrences || 0;\n      let meta;\n      if (count > 0) {\n        meta = `<strong>${mvFmtInt(count)}<\/strong> ${count === 1 ? 'occurrence' : 'occurrences'}`;\n      } else if (isRealTs(occTs)) {\n        meta = `last seen <strong>${escapeHtml(hdFmtTs(occTs))}<\/strong>`;\n      } else {\n        meta = `cleared <strong>${escapeHtml(hdFmtTs(a.lastClearTime))}<\/strong>`;\n      }\n      return hdBuildAlarmRow(enriched, meta);\n    }).join('');\n  } else {\n    frequentCountEl.textContent = '';\n    frequentEl.innerHTML = '<div class=\"hd-alarm-empty\">No alarms with recorded occurrence counts.<\/div>';\n  }\n}\n\n\nfunction renderHealthDashboard() {\n  const scores = hdComputeScores(healthMetrics);\n  hdRenderDeviceInfo();\n  hdRenderNetworkInfo();\n  hdRenderMobileInfo();\n  hdRenderAlarms();\n  hdRenderMetrics();\n\n  \/\/ Header subtitle: show config source (device model or a hint)\n  const meta = healthConfig?._metadata || {};\n  const sys = (healthConfig?.configurations && healthConfig.configurations.system) || {};\n  const di = healthDeviceInfo || {};\n  const sub = meta.deviceModel\n    ? `${meta.deviceModel}${meta.deviceSerialNumber ? ' \u00b7 ' + meta.deviceSerialNumber : ''}`\n    : 'Aggregated from support-package files';\n  $('#hd-header-sub').textContent = sub;\n\n  \/\/ Header stats: Drive Name + Serial Number\n  $('#hd-hdr-drivename').textContent = sys.driveName || '\u2014';\n  $('#hd-hdr-serial').textContent = meta.deviceSerialNumber || di.serial_number || '\u2014';\n  if (typeof applyLang === 'function') applyLang(LANG);\n}\n\n\/\/ Called by preInitRichViewers when a package loads. Pulls in whatever files are present:\n\/\/   smartdrive-config.json, smartdrive-metrics.json, smartdrive-opdata.json,\n\/\/   device-info.log, device-status.log, hmi-info.log, version.log,\n\/\/   system-state-and-alarms.json (or legacy system-alarms.log).\nfunction initHealthDashboard() {\n  \/\/ Config (already parsed elsewhere, but we re-parse here to avoid coupling)\n  healthConfig = null;\n  const cfgFile = state.files['smartdrive-config.json'];\n  if (cfgFile) {\n    try { healthConfig = JSON.parse(cfgFile.content); }\n    catch (e) { console.warn('Health: config parse failed:', e); }\n  }\n\n  \/\/ Metrics\n  healthMetrics = null;\n  const metFile = state.files['smartdrive-metrics.json'];\n  if (metFile) {\n    try { healthMetrics = JSON.parse(metFile.content); }\n    catch (e) { console.warn('Health: metrics parse failed:', e); }\n  }\n\n  \/\/ Operational data (system opdata snapshot \u2014 used as a fallback source for\n  \/\/ control mode \/ control source when system-state-and-alarms.json is missing)\n  healthOpData = null;\n  const odFile = state.files['smartdrive-opdata.json'];\n  if (odFile) {\n    try {\n      const parsed = JSON.parse(odFile.content);\n      \/\/ Accept either a bare opdata object or one wrapped in operationalData \/ System_OpData\n      healthOpData =\n        (parsed && parsed.System_OpData && (parsed.System_OpData.operationalData || parsed.System_OpData)) ||\n        (parsed && parsed.operationalData) ||\n        parsed;\n    } catch (e) { console.warn('Health: opdata parse failed:', e); }\n  }\n\n  \/\/ Log files (all optional)\n  healthDeviceInfo = {};\n  const diFile = state.files['device-info.log'];\n  if (diFile) healthDeviceInfo = hdParseKvLog(diFile.content);\n\n  \/\/ device-status.log \u2014 merged into device info as a fallback for any keys\n  \/\/ device-info.log doesn't provide\n  healthDeviceStatus = {};\n  const dsFile = state.files['device-status.log'];\n  if (dsFile) {\n    healthDeviceStatus = hdParseKvLog(dsFile.content);\n    for (const k of Object.keys(healthDeviceStatus)) {\n      if (healthDeviceInfo[k] == null || healthDeviceInfo[k] === '') {\n        healthDeviceInfo[k] = healthDeviceStatus[k];\n      }\n    }\n  }\n\n  healthHmiInfo = {};\n  const hmiFile = state.files['hmi-info.log'];\n  if (hmiFile) healthHmiInfo = hdParseKvLog(hmiFile.content);\n\n  healthVersionInfo = {};\n  const verFile = state.files['version.log'];\n  if (verFile) healthVersionInfo = hdParseVersionLog(verFile.content);\n\n  healthAlarmsState = null;\n  const saFile = state.files['system-state-and-alarms.json'] || state.files['system-alarms.log'];\n  if (saFile && saFile.content) {\n    const content = saFile.content;\n    \/\/ Try new JSON format first (single top-level object)\n    try {\n      healthAlarmsState = hdNormalizeAlarmsState(JSON.parse(content));\n    } catch (e) {\n      \/\/ Fall back to old text format:\n      \/\/   ***sd-alarms [-a]***:\n      \/\/   System opdata:  { ... }\n      \/\/   Alarms opdata:  { ... }\n      try {\n        healthAlarmsState = hdParseOldAlarmsLog(content);\n      } catch (e2) {\n        console.warn('Health: alarms\/state parse failed (both formats):', e2);\n      }\n    }\n  }\n  \/\/ If system opdata is missing from the alarms file, backfill it from smartdrive-opdata.json\n  if (healthOpData) {\n    if (!healthAlarmsState) healthAlarmsState = { SystemOpdata: healthOpData, AlarmOperationalData: null, AlarmMetrics: null };\n    else if (!healthAlarmsState.SystemOpdata) healthAlarmsState.SystemOpdata = healthOpData;\n  }\n\n  state.healthLoaded = true;\n  renderHealthDashboard();\n}\n\n\/\/ Normalize the two JSON key styles seen in system-state-and-alarms.json into the\n\/\/ canonical shape the dashboard renders from:\n\/\/   { SystemOpdata, AlarmOperationalData, AlarmMetrics }\n\/\/ Newer firmware emits System_OpData \/ Alarms_OpData \/ Alarms_Metrics with a\n\/\/ nested `operationalData` wrapper; older JSON already uses the canonical keys.\nfunction hdNormalizeAlarmsState(root) {\n  if (!root || typeof root !== 'object') return root;\n  const unwrap = (o) => (o && typeof o === 'object' && o.operationalData) ? o.operationalData : o;\n  const out = {\n    SystemOpdata:         unwrap(root.SystemOpdata         || root.System_OpData)  || null,\n    AlarmOperationalData: unwrap(root.AlarmOperationalData || root.Alarms_OpData)  || null,\n    AlarmMetrics:         unwrap(root.AlarmMetrics         || root.Alarms_Metrics) || null,\n  };\n  \/\/ If none of the known keys are present, keep the original object untouched\n  if (!out.SystemOpdata && !out.AlarmOperationalData && !out.AlarmMetrics) return root;\n  return out;\n}\n\nfunction updateHealthClock() {\n  const el = $('#hd-header-time');\n  if (el) el.textContent = new Date().toUTCString().replace(' GMT', 'Z');\n}\nsetInterval(updateHealthClock, 1000);\nupdateHealthClock();\n\n\/\/ \u2500\u2500 Init \u2500\u2500\nfunction initAuditViewer(data, filename) {\n  auditAllData = data;\n  buildAuditTxColorMap();\n  $('#auditSearch').value = '';\n  auditSortCol = 'timestamp'; auditSortDir = -1;\n  auditRendered = 0;\n\n  $('#audit-header-filename').textContent = filename;\n  $('#audit-total-count').textContent = auditAllData.length.toLocaleString();\n  const ts = auditAllData.map(e => e.timestamp).filter(Boolean).sort();\n  const fmt = t => t ? t.slice(0,10) : '?';\n  $('#audit-date-range').textContent = fmt(ts[0]) + ' \u2192 ' + fmt(ts[ts.length-1]);\n\n  \/\/ Initialize the Time Range inputs: reset values, set min\/max to the package's range\n  \/\/ so the native calendar picker opens at the right place. Input format is naive\n  \/\/ ISO (\"YYYY-MM-DDTHH:MM:SS\") \u2014 we treat it as UTC when filtering.\n  const dateFrom = $('#audit-date-from');\n  const dateTo = $('#audit-date-to');\n  if (dateFrom && dateTo) {\n    dateFrom.value = ''; dateFrom.removeAttribute('data-has-value');\n    dateTo.value = '';   dateTo.removeAttribute('data-has-value');\n    if (ts.length) {\n      const minAttr = ts[0].slice(0, 19);\n      const maxAttr = ts[ts.length - 1].slice(0, 19);\n      dateFrom.min = minAttr; dateFrom.max = maxAttr;\n      dateTo.min   = minAttr; dateTo.max   = maxAttr;\n    } else {\n      dateFrom.removeAttribute('min'); dateFrom.removeAttribute('max');\n      dateTo.removeAttribute('min');   dateTo.removeAttribute('max');\n    }\n  }\n\n  AUDIT_COL_FILTERS.forEach(buildColFilterList);\n\n  $$('#auditViewer th').forEach(th => th.classList.remove('sorted'));\n  $$('#auditViewer .sort-indicator').forEach(si => si.textContent = '');\n  $('#th-timestamp').classList.add('sorted');\n  auditUpdateTimestampHeader();\n\n  auditViewMode = 'formatted';\n  auditFocusEntry = null;\n  $('#btn-view-formatted').classList.add('active');\n  $('#btn-view-raw').classList.remove('active');\n  auditShowStartShutdown = false;\n  $('#btn-show-startshutdown').classList.remove('active');\n\n  auditApplyFilters();\n  updateAuditClock();\n}\n\n\/\/ \u2500\u2500 Column-header filters (Event Type, Severity, Origin, User) \u2500\u2500\nconst AUDIT_COL_FILTERS = [\n  { key:'event',    field:'event',    labelFn:v=>shortEvent(v) },\n  { key:'severity', field:'severity', labelFn:v=>shortSeverity(v) },\n  { key:'origin',   field:'origin',   labelFn:v=>shortOrigin(v) },\n  { key:'user',     field:'userId',   labelFn:v=>String(v) },\n];\nfunction auditColValues(field) {\n  return [...new Set(auditAllData.map(e => e[field]).filter(v => v!==null && v!==undefined && v!==''))]\n    .sort((a,b)=>String(a).localeCompare(String(b),undefined,{numeric:true}));\n}\nfunction buildColFilterList(cfg) {\n  const list = $('#cf-'+cfg.key+'-list');\n  if (!list) return;\n  list.innerHTML = '';\n  auditColValues(cfg.field).forEach(v => {\n    const item = document.createElement('label');\n    item.className = 'ms-item';\n    const cb = document.createElement('input');\n    cb.type = 'checkbox'; cb.value = String(v); cb.checked = true;\n    cb.addEventListener('change', () => { updateColBadge(cfg.key); auditApplyFilters(); });\n    const span = document.createElement('span');\n    span.className = 'ms-item-label';\n    span.textContent = cfg.labelFn(v);\n    item.appendChild(cb); item.appendChild(span);\n    list.appendChild(item);\n  });\n  updateColBadge(cfg.key);\n}\nfunction colSelectedSet(key) {\n  const checks = $$('#cf-'+key+'-list input[type=checkbox]');\n  if (checks.length === 0 || checks.every(c=>c.checked)) return null;   \/\/ all checked = no filter\n  return new Set(checks.filter(c=>c.checked).map(c=>c.value));\n}\n\/\/ \"Highlight color\" filter (dropdown): set of enabled row-color classes, or null when all enabled.\nfunction colorSelectedSet() {\n  const checks = $$('#cf-color-list input[type=checkbox]');\n  if (!checks.length || checks.every(c => c.checked)) return null;\n  return new Set(checks.filter(c => c.checked).map(c => c.getAttribute('data-color-key')));\n}\nfunction updateColorBadge() {\n  const checks = $$('#cf-color-list input[type=checkbox]');\n  const checked = checks.filter(c => c.checked).length;\n  const filtered = !(checks.length === 0 || checked === checks.length);\n  const badge = $('#cf-color-badge');\n  const trig  = $('#cf-color-trigger');\n  if (badge) badge.textContent = filtered ? String(checked) : '';\n  if (trig)  trig.classList.toggle('cf-active', filtered);\n}\nfunction updateColBadge(key) {\n  const checks = $$('#cf-'+key+'-list input[type=checkbox]');\n  const checked = checks.filter(c=>c.checked).length;\n  const filtered = !(checks.length===0 || checked===checks.length);\n  const badge = $('#cf-'+key+'-badge');\n  const trig  = $('#cf-'+key+'-trigger');\n  if (badge) badge.textContent = filtered ? String(checked) : '';\n  if (trig)  trig.classList.toggle('cf-active', filtered);\n}\n\nfunction auditApplyFilters() {\n  const search   = $('#auditSearch').value.trim().toLowerCase();\n  const setEvent    = colSelectedSet('event');\n  const setSeverity = colSelectedSet('severity');\n  const setOrigin   = colSelectedSet('origin');\n  const setUser     = colSelectedSet('user');\n  const setColor    = colorSelectedSet();\n  \/\/ Time range: datetime-local inputs give \"YYYY-MM-DDTHH:MM\" or \"YYYY-MM-DDTHH:MM:SS\".\n  \/\/ Normalize to seconds form and parse as UTC to match audit timestamps (UTC with 'Z').\n  const parseDtLocal = (s) => {\n    if (!s) return null;\n    \/\/ Add ':00' seconds if missing (format YYYY-MM-DDTHH:MM \u2192 YYYY-MM-DDTHH:MM:00)\n    if (s.length === 16) s = s + ':00';\n    const ms = Date.parse(s + 'Z');\n    return Number.isFinite(ms) ? ms : null;\n  };\n  const dtFromStr = $('#audit-date-from') ? $('#audit-date-from').value : '';\n  const dtToStr   = $('#audit-date-to')   ? $('#audit-date-to').value   : '';\n  const fromMs = parseDtLocal(dtFromStr);\n  const toMs   = parseDtLocal(dtToStr);\n  \/\/ Visual cue: highlight inputs when they have a usable value\n  if ($('#audit-date-from')) $('#audit-date-from').toggleAttribute('data-has-value', !!dtFromStr);\n  if ($('#audit-date-to'))   $('#audit-date-to').toggleAttribute('data-has-value', !!dtToStr);\n\n  auditFilteredData = auditAllData.filter(e => {\n    if (!auditShowStartShutdown && auditIsUntypedEvent(e)) return false;\n    if (setColor    && !setColor.has(auditColorClass(e)))    return false;\n    if (setEvent    && !setEvent.has(e.event))               return false;\n    if (setSeverity && !setSeverity.has(e.severity))         return false;\n    if (setOrigin   && !setOrigin.has(e.origin))             return false;\n    if (setUser     && !setUser.has(String(e.userId)))       return false;\n    if (fromMs !== null || toMs !== null) {\n      const ems = e.timestamp ? Date.parse(e.timestamp) : NaN;\n      if (!Number.isFinite(ems)) return false;\n      if (fromMs !== null && ems < fromMs) return false;\n      if (toMs   !== null && ems > toMs)   return false;\n    }\n    if (search) {\n      const hay = [e.message, e.userId, e.transactionId, shortEvent(e.event)]\n        .filter(v => v !== null && v !== undefined).join(' ').toLowerCase();\n      if (!hay.includes(search)) return false;\n    }\n    return true;\n  });\n\n  auditSortData();\n  \/\/ Focus mode: when an entry is pinned, narrow the view to a window around it.\n  if (auditFocusEntry) {\n    const fi = auditFilteredData.indexOf(auditFocusEntry);\n    if (fi === -1) {\n      auditFocusEntry = null;   \/\/ pinned entry no longer passes the filters\n    } else {\n      const s = Math.max(0, fi - AUDIT_FOCUS_WINDOW);\n      const en = Math.min(auditFilteredData.length, fi + AUDIT_FOCUS_WINDOW + 1);\n      auditFilteredData = auditFilteredData.slice(s, en);\n    }\n  }\n  updateFocusBanner();\n  updateAuditStats();\n  renderAuditPage();\n}\n\nfunction auditClearFilters() {\n  \/\/ NOTE (V1.8.0): the marked (pinned\/focused) event is intentionally preserved here \u2014\n  \/\/ clearing filters resets search\/column\/time\/highlight filters but keeps the user's marked event.\n  $$('#cf-color-list input[type=checkbox]').forEach(c => c.checked = true);\n  updateColorBadge();\n  $('#auditSearch').value = '';\n  AUDIT_COL_FILTERS.forEach(cfg => {\n    $$('#cf-'+cfg.key+'-list input[type=checkbox]').forEach(c => c.checked = true);\n    updateColBadge(cfg.key);\n  });\n  auditShowStartShutdown = false;\n  $('#btn-show-startshutdown').classList.remove('active');\n  \/\/ Clear Time Range inputs\n  if ($('#audit-date-from')) { $('#audit-date-from').value = ''; $('#audit-date-from').removeAttribute('data-has-value'); }\n  if ($('#audit-date-to'))   { $('#audit-date-to').value   = ''; $('#audit-date-to').removeAttribute('data-has-value'); }\n  auditApplyFilters();\n}\n\nfunction auditSortData() {\n  auditFilteredData.sort((a,b) => {\n    const va = a[auditSortCol] || '', vb = b[auditSortCol] || '';\n    return va < vb ? -auditSortDir : va > vb ? auditSortDir : 0;\n  });\n}\n\/\/ The timestamp column header doubles as a sort-order label: \"Order: latest first\"\n\/\/ (descending) \/ \"Order: oldest first\" (ascending). Reverts to \"Timestamp\" when a\n\/\/ different column drives the sort.\nfunction auditUpdateTimestampHeader() {\n  const lbl = $('#th-ts-label');\n  if (!lbl) return;\n  if (auditSortCol === 'timestamp') {\n    lbl.textContent = (auditSortDir === -1) ? t('audit.orderLatest') : t('audit.orderOldest');\n    $('#si-timestamp').textContent = '';   \/\/ order is stated in the label; no arrow needed\n  } else {\n    lbl.textContent = t('audit.tsColumn');\n  }\n}\nfunction auditSortBy(col) {\n  auditSortDir = (auditSortCol === col) ? -auditSortDir : (col === 'timestamp' ? -1 : 1);\n  auditSortCol = col;\n  $$('#auditViewer th').forEach(th => th.classList.remove('sorted'));\n  $$('#auditViewer .sort-indicator').forEach(si => si.textContent = '');\n  const th = $('#th-' + col);\n  if (th) { th.classList.add('sorted'); if (col !== 'timestamp') $('#si-' + col).textContent = auditSortDir===1?'\u25b2':'\u25bc'; }\n  auditUpdateTimestampHeader();\n  auditApplyFilters();\n}\n\nfunction updateAuditStats() {\n  const c = { INFO:0, WARNING:0, ERROR:0, EMERGENCY:0 }; let alarms = 0;\n  auditFilteredData.forEach(e => {\n    const s = shortSeverity(e.severity);\n    if (c[s] !== undefined) c[s]++;\n    if (e.event === 'AUDIT_LOG_EVENT_ALARM') alarms++;\n  });\n  $('#s-info').textContent  = c.INFO.toLocaleString();\n  $('#s-warn').textContent  = c.WARNING.toLocaleString();\n  $('#s-error').textContent = c.ERROR.toLocaleString();\n  $('#s-emerg').textContent = c.EMERGENCY.toLocaleString();\n  $('#s-alarm').textContent = alarms.toLocaleString();\n  $('#results-count').textContent = auditFilteredData.length.toLocaleString();\n  $('#results-total').textContent = auditAllData.length.toLocaleString();\n}\n\nfunction auditUpdateLoadedInfo() {\n  const el = $('#audit-loaded-info');\n  if (!el) return;\n  const total = auditFilteredData.length;\n  if (!total) { el.textContent = ''; return; }\n  el.textContent = (auditRendered >= total)\n    ? `All ${total.toLocaleString()} events loaded`\n    : `Showing ${auditRendered.toLocaleString()} of ${total.toLocaleString()} \u2014 scroll for more`;\n}\n\n\/\/ Append the next `n` rows of the filtered set to the table.\nfunction auditAppendRows(n) {\n  if (auditRendered >= auditFilteredData.length) return;\n  const search = $('#auditSearch').value.trim();\n  const end = Math.min(auditRendered + n, auditFilteredData.length);\n  const html = auditFilteredData.slice(auditRendered, end).map(e => buildAuditRow(e, search)).join('');\n  $('#log-body').insertAdjacentHTML('beforeend', html);\n  auditRendered = end;\n  auditUpdateLoadedInfo();\n}\n\n\/\/ Make sure at least `count` rows are rendered (used by jump-to-row).\nfunction auditEnsureRendered(count) {\n  if (count > auditRendered) auditAppendRows(count - auditRendered);\n}\n\n\/\/ (Re)render from the top \u2014 resets the infinite-scroll window to the first chunk.\nfunction renderAuditPage() {\n  const tbody = $('#log-body');\n  const empty = $('#audit-empty-state');\n  auditRendered = 0;\n  tbody.innerHTML = '';\n\n  if (!auditFilteredData.length) {\n    empty.style.display = '';\n    auditUpdateLoadedInfo();\n    return;\n  }\n  empty.style.display = 'none';\n  auditAppendRows(AUDIT_CHUNK);\n  const tc = $('#audit-table-container');\n  if (tc) {\n    \/\/ Keep filling until the content overflows (so the scroll handler can take over).\n    let guard = 0;\n    while (tc.scrollHeight <= tc.clientHeight && auditRendered < auditFilteredData.length && guard++ < 60) {\n      auditAppendRows(AUDIT_CHUNK);\n    }\n    tc.scrollTop = 0;\n  }\n}\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ CONFIG_CHANGE diff \u2014 parse the Set Configuration protobuf-text payload,\n\/\/ compare it with the previous config change in the log, and highlight what\n\/\/ changed. \"View all\" shows the full parsed configuration (changes still\n\/\/ highlighted). Response entries (\"\u2026request with status \u2026\") carry no payload.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst CC_REQUEST_MARK = 'Set Configuration request:';\n\nfunction ccIsConfigRequest(e) {\n  return e && e.event === 'AUDIT_LOG_EVENT_CONFIG_CHANGE'\n    && typeof e.message === 'string' && e.message.indexOf(CC_REQUEST_MARK) !== -1;\n}\nfunction ccExtractPayload(msg) {\n  const i = String(msg || '').indexOf(CC_REQUEST_MARK);\n  return i === -1 ? '' : msg.slice(i + CC_REQUEST_MARK.length).trim();\n}\n\/\/ All config-change requests in chronological order, with their index in auditAllData.\nfunction ccAllRequests() {\n  const list = [];\n  (auditAllData || []).forEach((e, idx) => { if (ccIsConfigRequest(e)) list.push({ e, idx }); });\n  list.sort((a, b) => String(a.e.timestamp || '').localeCompare(String(b.e.timestamp || '')));\n  return list;\n}\n\n\/\/ Tokenize protobuf TextFormat: names\/scalars, quoted strings, and { } < > : delimiters.\nfunction ccTokenize(s) {\n  const toks = []; let i = 0;\n  while (i < s.length) {\n    const c = s[i];\n    if (\/\\s\/.test(c)) { i++; continue; }\n    if (c === '{' || c === '<') { toks.push({ t: '{' }); i++; continue; }\n    if (c === '}' || c === '>') { toks.push({ t: '}' }); i++; continue; }\n    if (c === ':') { toks.push({ t: ':' }); i++; continue; }\n    if (c === '\"' || c === \"'\") {\n      const q = c; i++; let v = '';\n      while (i < s.length && s[i] !== q) {\n        if (s[i] === '\\\\' && i + 1 < s.length) { v += s[i + 1]; i += 2; }\n        else v += s[i++];\n      }\n      i++; toks.push({ t: 'val', v: '\"' + v + '\"' }); continue;\n    }\n    let v = '';\n    while (i < s.length && !\/[\\s:{}<>]\/.test(s[i])) v += s[i++];\n    toks.push({ t: 'word', v });\n  }\n  return toks;\n}\n\/\/ Parse tokens into a nested object; repeated fields become arrays.\nfunction ccParse(text) {\n  const toks = ccTokenize(String(text || ''));\n  let p = 0;\n  function parseObj(top) {\n    const o = {};\n    while (p < toks.length) {\n      const tk = toks[p];\n      if (tk.t === '}') { if (!top) p++; break; }\n      if (tk.t !== 'word') { p++; continue; }\n      const key = tk.v; p++;\n      if (toks[p] && toks[p].t === ':') p++;\n      let val;\n      if (toks[p] && toks[p].t === '{') { p++; val = parseObj(false); }\n      else if (toks[p] && (toks[p].t === 'val' || toks[p].t === 'word')) { val = toks[p].v; p++; }\n      else val = '';\n      if (Object.prototype.hasOwnProperty.call(o, key)) {\n        if (!Array.isArray(o[key])) o[key] = [o[key]];\n        o[key].push(val);\n      } else o[key] = val;\n    }\n    return o;\n  }\n  return parseObj(true);\n}\nfunction ccScalarText(v) { return typeof v === 'string' ? v.replace(\/^\"|\"$\/g, '') : String(v); }\n\n\/\/ Identity tag for a repeated\/list-like entry, so matching survives reordering\n\/\/ AND survives the protobuf quirk where a repeated field with a single element\n\/\/ parses as one object instead of an array.\nfunction ccIdTag(el) {\n  const idKeys = ['code', 'index', 'filename', 'mac_address', 'id', 'fan_id'];\n  if (el && typeof el === 'object' && !Array.isArray(el)) {\n    for (const k of idKeys) { if (el[k] != null && typeof el[k] !== 'object') return ccScalarText(el[k]); }\n  }\n  return null;\n}\n\/\/ Flatten a parsed tree to leaf path \u2192 value.\nfunction ccFlatten(node, prefix, out) {\n  out = out || {};\n  if (Array.isArray(node)) {\n    const seen = {};\n    node.forEach((el, i) => {\n      let tag = ccIdTag(el);\n      if (tag == null) tag = '#' + i;\n      if (seen[tag] != null) { seen[tag]++; tag = tag + '~' + seen[tag]; } else seen[tag] = 0;\n      ccFlatten(el, prefix + '[' + tag + ']', out);\n    });\n  } else if (node && typeof node === 'object') {\n    const keys = Object.keys(node);\n    if (!keys.length && prefix) { out[prefix] = '{}'; return out; }\n    keys.forEach(k => {\n      const child = node[k];\n      const childPrefix = prefix ? prefix + '.' + k : k;\n      const tag = ccIdTag(child);   \/\/ single identity-bearing object \u2192 key like a 1-element list\n      if (tag != null) ccFlatten(child, childPrefix + '[' + tag + ']', out);\n      else ccFlatten(child, childPrefix, out);\n    });\n  } else {\n    out[prefix] = node;\n  }\n  return out;\n}\n\/\/ Fields that change on every config write and aren't meaningful edits.\nfunction ccIsIgnoredKey(k) {\n  const s = String(k).toLowerCase();\n  return s.includes('last_update_time') || s.includes('lastupdatetime');\n}\nfunction ccValHtml(v) {\n  const s = ccScalarText(v);\n  const meta = (typeof ENUM_REGEX !== 'undefined' && ENUM_REGEX.test(s)) ? configEnumMeta(s) : null;\n  if (meta) return `<span class=\"cc-enum-known\" title=\"${escHtml(s + ' \u2014 ' + meta.desc + '  \u00b7  [' + meta.label + ']')}\">${escHtml(s)}<\/span>`;\n  return escHtml(s);\n}\nfunction ccDiff(oldFlat, newFlat) {\n  const keys = new Set([...Object.keys(oldFlat || {}), ...Object.keys(newFlat || {})]);\n  const added = [], removed = [], changed = [];\n  [...keys].sort().forEach(k => {\n    if (ccIsIgnoredKey(k)) return;   \/\/ ignore last_update_time churn\n    const inOld = oldFlat && Object.prototype.hasOwnProperty.call(oldFlat, k);\n    const inNew = newFlat && Object.prototype.hasOwnProperty.call(newFlat, k);\n    if (inNew && !inOld) added.push({ k, nv: newFlat[k] });\n    else if (inOld && !inNew) removed.push({ k, ov: oldFlat[k] });\n    else if (String(oldFlat[k]) !== String(newFlat[k])) changed.push({ k, ov: oldFlat[k], nv: newFlat[k] });\n  });\n  return { added, removed, changed };\n}\n\/\/ Short summary of the top-level sections touched by a payload.\nfunction ccSummary(payload) {\n  try {\n    const t = ccParse(payload);\n    const cfg = t.configurations || t;\n    const secs = Object.keys(cfg || {}).filter(k => k !== 'last_update_time');\n    return secs.length ? 'sections: ' + secs.join(', ') : '(empty payload)';\n  } catch (_) { return ''; }\n}\n\/\/ Full parsed configuration payload rendered inline (added\/changed vs the previous\n\/\/ config change highlighted) so the CONFIG_CHANGE request content is ALWAYS shown\n\/\/ in the formatted view rather than hidden behind the modal.\nfunction ccInlineContent(e) {\n  try {\n    const reqs = ccAllRequests();\n    const idx = auditAllData.indexOf(e);\n    const pos = reqs.findIndex(r => r.idx === idx);\n    const prev = pos > 0 ? reqs[pos - 1] : null;\n    const newFlat = ccFlatten(ccParse(ccExtractPayload(e.message)), '', {});\n    const diff = prev\n      ? ccDiff(ccFlatten(ccParse(ccExtractPayload(prev.e.message)), '', {}), newFlat)\n      : ccDiff({}, newFlat);\n    const addedSet = new Set(diff.added.map(d => d.k));\n    const chgMap = {}; diff.changed.forEach(d => { chgMap[d.k] = d.ov; });\n    const rows = [];\n    Object.keys(newFlat).sort().forEach(k => {\n      const cls = addedSet.has(k) ? 'cc-add' : (k in chgMap ? 'cc-chg' : '');\n      const sign = cls === 'cc-add' ? '+' : (cls === 'cc-chg' ? '~' : '');\n      const old = (k in chgMap) ? chgMap[k] : null;\n      rows.push('<div class=\"cc-row ' + cls + '\"><span class=\"cc-sign\">' + sign + '<\/span>' +\n        '<span class=\"cc-path\">' + escHtml(k) + '<\/span>' +\n        '<span class=\"cc-val\">' + (old != null ? '<span class=\"cc-old\">' + ccValHtml(old) + '<\/span> \\u2192 ' : '') +\n        ccValHtml(newFlat[k]) + '<\/span><\/div>');\n    });\n    return rows.length ? rows.join('') : '<div class=\"cc-empty\">Empty configuration payload.<\/div>';\n  } catch (_) {\n    return '<div class=\"cc-empty\">Could not parse configuration payload.<\/div>';\n  }\n}\n\/\/ CONFIG_CHANGE response \u2014 concise request\/status label, plus the returned error\n\/\/ string when the request failed (status != OK), shown in the formatted view.\nfunction ccResponseFormatted(msg, search) {\n  const m = \/responded to\\s+(.+?)\\s+request with status\\s+([A-Za-z_]+)([\\s\\S]*)$\/.exec(String(msg || ''));\n  if (!m) return null;\n  const status = m[2].replace(\/^RESPONSE_STATUS_\/, '');\n  const ok = status === 'OK';\n  const err = (m[3] || '').replace(\/^[\\s:,.\\-]+\/, '').trim();\n  let html = '<span class=\"svc-concise ' + (ok ? 'svc-ok' : 'svc-err') + '\">' + escHtml(m[1] + ': ' + status) + '<\/span>';\n  if (!ok && err) html += '<div class=\"cc-resp-err\">\\u26A0 ' + highlightText(err, search) + '<\/div>';\n  return html;\n}\n\/\/ SETPOINTS_CHANGE request \u2014 structured view: the originating Service and the Request\n\/\/ content (command, control_mode, start_behavior, \u2026) parsed from the protobuf-text message.\nfunction renderSetpointsRequest(msg, search) {\n  const m = \/^(.+?)\\s+(?:service\\s+)?received\\s+(.+?)\\s+request\\s*:?\\s*([\\s\\S]*)$\/.exec(String(msg || ''));\n  if (!m) return null;\n  const service = m[1].trim();\n  const reqName = m[2].trim();\n  const content = (m[3] || '').trim();\n  const fields = [];\n  const re = \/([A-Za-z_][A-Za-z0-9_]*)\\s*:\\s*(\"(?:[^\"\\\\]|\\\\.)*\"|\\S+)\/g;\n  let mm;\n  while ((mm = re.exec(content)) !== null) {\n    let v = mm[2];\n    if (v.length > 1 && v[0] === '\"' && v[v.length - 1] === '\"') v = v.slice(1, -1);\n    fields.push([mm[1], v]);\n  }\n  const head = '<div class=\"sp-req-head\">' +\n    '<div class=\"sp-req-row\"><span class=\"sp-req-k\">Service<\/span><span class=\"sp-req-v\">' + highlightText(service, search) + '<\/span><\/div>' +\n    '<div class=\"sp-req-row\"><span class=\"sp-req-k\">Request<\/span><span class=\"sp-req-v\">' + highlightText(reqName, search) + '<\/span><\/div>' +\n    '<\/div>';\n  let body = '';\n  if (fields.length) {\n    body = '<div class=\"sp-req-fields\">' + fields.map(function(kv) {\n      const k = kv[0], v = kv[1];\n      const isEnum = \/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$\/.test(v);\n      const vh = isEnum ? '<span class=\"val-enum\" title=\"' + escHtml(v) + '\">' + escHtml(v) + '<\/span>' : highlightText(v, search);\n      return '<div class=\"sp-req-row\"><span class=\"sp-req-k\">' + escHtml(humanizeKey(k)) + '<\/span><span class=\"sp-req-v\">' + vh + '<\/span><\/div>';\n    }).join('') + '<\/div>';\n  } else if (content) {\n    body = '<div class=\"sp-req-fields\"><div class=\"sp-req-row\"><span class=\"sp-req-k\">Content<\/span><span class=\"sp-req-v\">' + highlightText(content, search) + '<\/span><\/div><\/div>';\n  }\n  return '<div class=\"sp-req\">' + head + body + '<\/div>';\n}\n\/\/ SETPOINTS_CHANGE 'system state changed from X to Y by action Z' -> structured view.\nfunction renderSystemStateChange(msg, search) {\n  const m = \/System state chang(?:e|ed)\\s+from\\s+(\\S+)\\s+to\\s+(\\S+)(?:\\s+by\\s+action\\s+(\\S+))?\/i.exec(String(msg || ''));\n  if (!m) return null;\n  const fromFull = m[1], toFull = m[2], actFull = m[3] || '';\n  const fromS = fromFull.replace(\/^SystemState\/, '');\n  const toS   = toFull.replace(\/^SystemState\/, '');\n  const actS  = actFull ? actFull.replace(\/^SystemStateAction\/, '') : '';\n  const stateSpan = (full, short) => {\n    const d = smartdSystemStateDesc(full);\n    return d ? '<span class=\"st-tip\" title=\"' + escHtml(full + ' \\u2014 ' + d) + '\">' + escHtml(short) + '<\/span>' : escHtml(short);\n  };\n  let html = '<div class=\"sp-req\"><div class=\"sp-req-title\">Change system state<\/div>';\n  html += '<div class=\"sp-req-head\"><div class=\"sp-req-row\"><span class=\"sp-req-k\">Action<\/span><span class=\"sp-req-v\">' +\n          (actS ? highlightText(actS, search) : '<span class=\"null-cell\">\\u2014<\/span>') + '<\/span><\/div><\/div>';\n  html += '<div class=\"sp-req-fields\">';\n  html += '<div class=\"sp-req-row\"><span class=\"sp-req-k\">From<\/span><span class=\"sp-req-v\">' + stateSpan(fromFull, fromS) + '<\/span><\/div>';\n  html += '<div class=\"sp-req-row\"><span class=\"sp-req-k\">To<\/span><span class=\"sp-req-v\">' + stateSpan(toFull, toS) + '<\/span><\/div>';\n  html += '<\/div><\/div>';\n  return html;\n}\n\/\/ CONFIG_CHANGE: pull configurations.system.config_name.value from the payload, if present.\nfunction ccConfigName(rawMsg) {\n  try {\n    const flat = ccFlatten(ccParse(ccExtractPayload(rawMsg)), '', {});\n    for (const k of Object.keys(flat)) {\n      if (\/(^|\\.)system\\.config_name\\.value$\/.test(k)) {\n        const v = ccScalarText(flat[k]);\n        if (v && v !== '\"\"') return v;\n      }\n    }\n    return null;\n  } catch (_) { return null; }\n}\n\nlet ccState = { idx: null, mode: 'changes' };\nfunction ccOpen(idx, mode) {\n  ccState.idx = idx;\n  ccState.mode = (mode === 'all') ? 'all' : (mode === 'raw') ? 'raw' : 'changes';\n  ccRender();\n  $('#ccModal').style.display = 'flex';\n}\nfunction ccClose() { $('#ccModal').style.display = 'none'; }\nfunction ccSetMode(m) { ccState.mode = m; ccRender(); }\n\nfunction ccRender() {\n  const e = auditAllData[ccState.idx];\n  if (!e) return;\n  const reqs = ccAllRequests();\n  const pos = reqs.findIndex(r => r.idx === ccState.idx);\n  const prev = pos > 0 ? reqs[pos - 1] : null;\n  const t = formatTS(e.timestamp);\n\n  $('#ccTitle').textContent = 'Configuration Change' + (pos >= 0 ? ` \u00b7 #${pos + 1} of ${reqs.length}` : '');\n  $('#ccSub').textContent = `${t.date} ${t.time}Z` + (e.transactionId ? ' \u00b7 tx ' + e.transactionId.slice(0, 12) : '');\n\n  const newFlat = ccFlatten(ccParse(ccExtractPayload(e.message)), '', {});\n  let diff;\n  if (prev) {\n    const oldFlat = ccFlatten(ccParse(ccExtractPayload(prev.e.message)), '', {});\n    diff = ccDiff(oldFlat, newFlat);\n    const pf = formatTS(prev.e.timestamp);\n    $('#ccPrevInfo').innerHTML = `Compared against the previous configuration change \u00b7 <strong>${pf.date} ${pf.time}Z<\/strong>` +\n      (prev.e.transactionId ? ` \u00b7 tx ${escHtml(prev.e.transactionId.slice(0, 12))}` : '');\n  } else {\n    diff = ccDiff({}, newFlat);\n    $('#ccPrevInfo').innerHTML = 'No earlier configuration change in this log \u2014 showing the full configuration as the baseline.';\n  }\n\n  $('#ccCounts').innerHTML =\n    `<span class=\"cc-badge cc-add\">+${diff.added.length} added<\/span>` +\n    `<span class=\"cc-badge cc-chg\">~${diff.changed.length} changed<\/span>`;\n  $('#ccModeChanges').classList.toggle('active', ccState.mode === 'changes');\n  $('#ccModeAll').classList.toggle('active', ccState.mode === 'all');\n  $('#ccModeRaw').classList.toggle('active', ccState.mode === 'raw');\n\n  if (ccState.mode === 'raw') {\n    const payload = ccExtractPayload(e.message) || e.message || '';\n    $('#ccBody').innerHTML = `<pre class=\"cc-raw\">${escHtml(payload)}<\/pre>`;\n    $('#ccBody').scrollTop = 0;\n    return;\n  }\n\n  const rows = [];\n  const row = (cls, sign, k, val, old) => rows.push(\n    `<div class=\"cc-row ${cls}\"><span class=\"cc-sign\">${sign}<\/span>` +\n    `<span class=\"cc-path\">${escHtml(k)}<\/span>` +\n    `<span class=\"cc-val\">${old != null ? `<span class=\"cc-old\">${ccValHtml(old)}<\/span> \u2192 ` : ''}${ccValHtml(val)}<\/span><\/div>`\n  );\n\n  if (ccState.mode === 'changes') {\n    diff.added.forEach(d => row('cc-add', '+', d.k, d.nv));\n    diff.changed.forEach(d => row('cc-chg', '~', d.k, d.nv, d.ov));\n    if (!rows.length) rows.push('<div class=\"cc-empty\">No differences from the previous configuration change.<\/div>');\n  } else {\n    const addedSet = new Set(diff.added.map(d => d.k));\n    const chgMap = {}; diff.changed.forEach(d => { chgMap[d.k] = d.ov; });\n    Object.keys(newFlat).sort().forEach(k => {\n      const cls = addedSet.has(k) ? 'cc-add' : (k in chgMap ? 'cc-chg' : '');\n      const sign = cls === 'cc-add' ? '+' : (cls === 'cc-chg' ? '~' : '');\n      row(cls, sign, k, newFlat[k], (k in chgMap) ? chgMap[k] : null);\n    });\n    if (!rows.length) rows.push('<div class=\"cc-empty\">Empty configuration payload.<\/div>');\n  }\n  $('#ccBody').innerHTML = rows.join('');\n  $('#ccBody').scrollTop = 0;\n}\n\n\/\/ Wiring (static DOM, so bind once at load).\n$('#ccClose').addEventListener('click', ccClose);\n$('#ccBackdrop').addEventListener('click', ccClose);\n$('#ccModeChanges').addEventListener('click', () => ccSetMode('changes'));\n$('#ccModeAll').addEventListener('click', () => ccSetMode('all'));\n$('#ccModeRaw').addEventListener('click', () => ccSetMode('raw'));\ndocument.addEventListener('keydown', ev => {\n  if (ev.key === 'Escape' && $('#ccModal') && $('#ccModal').style.display === 'flex') ccClose();\n});\n\/\/ Delegated: the audit table body is re-rendered on every page\/ filter change.\n$('#log-body').addEventListener('click', ev => {\n  const btn = ev.target.closest && ev.target.closest('.cc-btn');\n  if (!btn) return;\n  const idx = Number(btn.getAttribute('data-cc-idx'));\n  if (!isNaN(idx)) ccOpen(idx, btn.getAttribute('data-cc-mode') || 'changes');\n});\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ STARTUP \u2014 view the event message as Structured (software VersionInfo fields)\n\/\/ or Raw (verbatim text). The structured view always lists the VersionInfo\n\/\/ fields; values are extracted from the message payload when present, else \"\u2014\".\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nconst ST_FIELDS = [\n  ['Current Version',     ['current_version']],\n  ['Backup Version',      ['backup_version']],\n  ['Target Version',      ['target_version']],\n  ['Update Size',         ['update_size']],\n  ['Up To Date',          ['up_to_date']],\n  ['New Bootloader',      ['new_bootloader']],\n  ['Rollback Available',  ['rollback_available_after_update', 'rollback_available']],\n];\nfunction atRenderRaw(msg) {\n  return `<pre class=\"at-raw\">${escHtml(String(msg || ''))}<\/pre>`;\n}\n\/\/ Pull the VersionInfo values out of a STARTUP message. Robust to prose prefixes\n\/\/ and to plain scalars (current_version:\"\u2026\"), quoted\/bare values, protobuf\n\/\/ wrappers (current_version:{value:\u2026}), and snake_case or camelCase names.\nfunction stExtractVersionInfo(msg) {\n  const s = String(msg || '');\n  const find = cands => {\n    for (const c of cands) {\n      const variants = [c, c.replace(\/_([a-z])\/g, (_, x) => x.toUpperCase())];\n      for (const name of variants) {\n        const re = new RegExp(name + '\\\\s*:\\\\s*(?:\\\\{\\\\s*value\\\\s*:\\\\s*)?(?:\"([^\"]*)\"|([^\\\\s{}]+))', 'i');\n        const m = s.match(re);\n        if (m) return (m[1] !== undefined ? m[1] : m[2]);\n      }\n    }\n    return null;\n  };\n  return ST_FIELDS.map(([label, cands]) => [label, find(cands)]);\n}\nfunction stFmtVal(v) {\n  if (v == null || v === '') return '\u2014';\n  const s = ccScalarText(v);\n  if (\/^true$\/i.test(s)) return 'Yes';\n  if (\/^false$\/i.test(s)) return 'No';\n  return s;\n}\nfunction stRenderStructured(msg) {\n  const rows = stExtractVersionInfo(msg).map(([label, v]) =>\n    `<div class=\"at-row\"><span class=\"at-path\">${escHtml(label)}<\/span><span class=\"at-val\">${escHtml(stFmtVal(v))}<\/span><\/div>`\n  ).join('');\n  return `<div class=\"at-prefix\">Software Version Info<\/div><div class=\"at-rows\">${rows}<\/div>`;\n}\n\n\/\/ \u2500\u2500 ALARM message \u2192 concise label \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ \"Alarm <name> transitioned from Normal to Active\"       \u2192 \"ACTIVE: <name>\"\n\/\/ \"Alarm <name> transitioned from Active to Normal.\"       \u2192 \"CLEARED: <name>\"\n\/\/ \"Alarm <name> transitioned from Active to OOS.\"          \u2192 \"DISABLE (OOS): <name>\"\n\/\/ \"Alarm <name> transitioned from unspecified to Normal\"   \u2192 \"ENABLE: <name>\"\n\/\/ \"Alarm <name> transitioned from unspecified to OOS\"      \u2192 \"DISABLE: <name>\"\n\/\/ \"Alarm <name> has been acknowledged by user N\"           \u2192 \"ACKNOWLEDGED: <name> by user N\"\nfunction alarmConciseLabel(msg) {\n  const m = String(msg || '');\n  let r;\n  if ((r = \/^\\s*Alarm (.+?) transitioned from Normal to Active\\b\/.exec(m)))          return 'ACTIVE: ' + r[1];\n  if ((r = \/^\\s*Alarm (.+?) transitioned from Active to Normal\\b\/.exec(m)))           return 'CLEARED: ' + r[1];\n  if ((r = \/^\\s*Alarm (.+?) transitioned from Active to OOS\\b\/.exec(m)))              return 'DISABLE (OOS): ' + r[1];\n  if ((r = \/^\\s*Alarm (.+?) transitioned from [Uu]nspecified to Normal\\b\/.exec(m)))   return 'ENABLE: ' + r[1];\n  if ((r = \/^\\s*Alarm (.+?) transitioned from [Uu]nspecified to OOS\\b\/.exec(m)))      return 'DISABLE: ' + r[1];\n  if ((r = \/^\\s*(?:Instance of Alarm )?(.+?) has changed from (\\S+) to ([A-Za-z0-9_]+)\/.exec(m))) return 'Change ' + r[1] + ' state from ' + r[2] + ' to ' + r[3];\n  if ((r = \/^\\s*Alarm (.+?) has been acknowledged by user (\\d+)\/.exec(m)))            return 'ACKNOWLEDGED: ' + r[1] + ' by user ' + r[2];\n  return null;\n}\nfunction alarmStatusClass(label) {\n  if (label.startsWith('ACTIVE:'))       return 'al-active';\n  if (label.startsWith('CLEARED:'))      return 'al-cleared';\n  if (label.startsWith('ENABLE:'))       return 'al-enable';\n  if (label.startsWith('DISABLE (OOS)')) return 'al-oos';\n  if (label.startsWith('DISABLE:'))      return 'al-disable';\n  if (label.startsWith('ACKNOWLEDGED:')) return 'al-ack';\n  return '';\n}\n\/\/ An alarm \"has power meter metrics\" (or other embedded data) when it carries a\n\/\/ JSON\/protobuf payload \u2014 e.g. a \"Debug info:\" electrical snapshot.\nfunction alarmHasPayload(msg) {\n  const m = String(msg || '');\n  return m.indexOf('{') !== -1 || \/power meter\/i.test(m) || \/Debug info:\/i.test(m) || \/operationalData\/i.test(m);\n}\nfunction alarmConciseHtml(msg) {\n  const label = alarmConciseLabel(msg);\n  if (label) return `<span class=\"alarm-concise ${alarmStatusClass(label)}\">${escHtml(label)}<\/span>`;\n  \/\/ Unrecognized pattern (e.g. Debug info \/ state-machine transition): show the\n  \/\/ prose part before any JSON payload as the formatted view.\n  const m = String(msg || '');\n  const b = m.indexOf('{');\n  const prose = (b > 0 ? m.slice(0, b) : m).trim().replace(\/[:\\s]+$\/, '');\n  return `<span class=\"alarm-concise\">${escHtml(prose || m)}<\/span>`;\n}\n\/\/ Formatted view = concise label + the structured Power Meter metrics card (or a\n\/\/ structured Debug-info snapshot) when the alarm carries such a payload.\nfunction alarmFormattedView(msg) {\n  const head = alarmConciseHtml(msg);\n  let structured = null;\n  if (String(msg || '').indexOf('{') !== -1) {\n    structured = renderMetricsMessage(msg);                        \/\/ \u26a1 Power Meter Metrics, or null\n    if (!structured) structured = renderDebugInfoAlarm(msg, '');   \/\/ else structured debug-info snapshot\n  }\n  return structured ? `${head}<div class=\"alarm-metrics-wrap\">${structured}<\/div>` : head;\n}\n\/\/ \u2500\u2500 Service request\/response messages \u2192 concise labels \u2500\u2500\u2500\u2500\u2500\n\/\/ \"<svc> received <request name> request: \u2026\"                          \u2192 \"<request name>: requested by user\"\n\/\/ \"<svc> responded to <request name> request with status <STATUS>\"    \u2192 \"<request name>: OK\" (or the status code)\nfunction svcConciseLabel(msg) {\n  const m = String(msg || '');\n  let r;\n  if ((r = \/responded to (.+?) request with status ([A-Za-z_]+)\/.exec(m))) {\n    const status = r[2].replace(\/^RESPONSE_STATUS_\/, '');\n    return { label: r[1] + ': ' + status, cls: status === 'OK' ? 'svc-ok' : 'svc-err' };\n  }\n  if ((r = \/received (.+?) request\\b\/.exec(m))) {\n    return { label: r[1] + ': requested by user', cls: 'svc-req' };\n  }\n  return null;\n}\nfunction svcConciseHtml(msg) {\n  const s = svcConciseLabel(msg);\n  return s ? `<span class=\"svc-concise ${s.cls}\">${escHtml(s.label)}<\/span>` : null;\n}\n\/\/ Formatted (non-raw) view for the toggle, dispatched by event type.\nfunction auditFormattedFor(e) {\n  if (e.event === 'AUDIT_LOG_EVENT_ALARM') return alarmFormattedView(e.message);\n  return stRenderStructured(e.message);   \/\/ STARTUP\n}\n\n\/\/ Delegated toggle (audit body is re-rendered on paging\/filtering).\n$('#log-body').addEventListener('click', ev => {\n  const btn = ev.target.closest && ev.target.closest('.at-btn');\n  if (!btn || btn.disabled) return;\n  const cell = btn.closest('.at-cell'); if (!cell) return;\n  const e = auditAllData[Number(cell.getAttribute('data-at-idx'))]; if (!e) return;\n  const mode = btn.getAttribute('data-at-mode');\n  cell.querySelectorAll('.at-btn').forEach(b => b.classList.toggle('active', b === btn));\n  cell.querySelector('[data-at-view]').innerHTML =\n    (mode === 'raw') ? atRenderRaw(e.message) : auditFormattedFor(e);\n});\n\n\/\/ Semantic highlight class for an event row (independent of focus\/pin state).\n\/\/ Shared by row rendering and the \"Highlight color\" toolbar filter.\n\/\/ SETPOINTS_CHANGE request rows carry the command text; the matching response row\n\/\/ shares the same transaction id but has no command text. Precompute the colour of\n\/\/ each transaction from its request so responses can inherit it.\nfunction setpointsColorFromMsg(m) {\n  if (!m) return '';\n  if (m.includes('command:MOTOR_CONTROLLER_COMMAND_ON'))  return 'row-motor-on';\n  if (m.includes('command:MOTOR_CONTROLLER_COMMAND_OFF')) return 'row-motor-off';\n  if (m.includes('System state change'))                 return 'row-state-change';\n  if (m.includes('Set Control Mode'))                    return 'row-control-mode';\n  return '';\n}\nfunction buildAuditTxColorMap() {\n  auditTxColor = {};\n  for (const e of auditAllData) {\n    if (e.event !== 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE') continue;\n    const tid = e.transactionId;\n    if (!tid) continue;\n    const cls = setpointsColorFromMsg(e.message || '');\n    if (cls && !auditTxColor[tid]) auditTxColor[tid] = cls;\n  }\n}\nfunction auditColorClass(e) {\n  const sev = shortSeverity(e.severity);\n  const m = e.message || '';\n  if (e.event === 'AUDIT_LOG_EVENT_ALARM') return 'row-alarm';\n  if (e.event === 'AUDIT_LOG_EVENT_LOGIN_ATTEMPT') return 'row-login';   \/\/ login attempts get their own colour, even at ERROR severity\n  if (e.event === 'AUDIT_LOG_EVENT_LOCKOUT_RESET') return 'row-lockout';\n  if (e.event === 'AUDIT_LOG_EVENT_CONFIG_CHANGE') return 'row-config';\n  if (m.includes('SystemStateBooting') && m.includes('SystemStateReadyToRun') && m.includes('SystemStateActionSystemdBootingComplete')) return 'row-ready';\n  if (e.event === 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE') {\n    const direct = setpointsColorFromMsg(m);\n    if (direct) return direct;                                   \/\/ request row (carries the command)\n    if (e.transactionId && auditTxColor[e.transactionId])        \/\/ response row inherits its request's colour\n      return auditTxColor[e.transactionId];\n  }\n  return '';\n}\n\nfunction buildAuditRow(e, search) {\n  const sev = shortSeverity(e.severity), evt = shortEvent(e.event), orig = shortOrigin(e.origin);\n  const {date, time} = formatTS(e.timestamp);\n  \/\/ Row highlight class is computed by auditColorClass(); a pinned\/marked row overrides it visually.\n  let rc = (auditFocusEntry && e === auditFocusEntry) ? 'row-focus' : auditColorClass(e);\n\n  const rawMsg = e.message || '';\n  const isMetrics = rawMsg.startsWith('Getting power meter metrics');\n  const isDebugInfo = rawMsg.startsWith('Debug info:');\n  let msgHtml;\n  const formattedView = (auditViewMode !== 'raw');\n  if (!formattedView) {\n    msgHtml = highlightText(rawMsg, search);   \/\/ global RAW view \u2014 verbatim for every event\n  } else if (isDebugInfo) {\n    msgHtml = renderDebugInfoAlarm(rawMsg, search);\n  } else if (isMetrics) {\n    const rendered = renderMetricsMessage(rawMsg);\n    msgHtml = rendered || highlightText(rawMsg, search);\n  } else {\n    \/\/ SETPOINTS_CHANGE requests always show their full content (raw, no concise label);\n    \/\/ SETPOINTS_CHANGE responses are formatted further down (status + error string on failure).\n    \/\/ CONFIG_CHANGE requests (Set Configuration, Switching to configuration profile,\n    \/\/ date & time change, \u2026) always show their full content \u2014 skip the concise\n    \/\/ \"requested by user\" label. CONFIG_CHANGE responses keep their concise OK\/ERR status.\n    const ccReqFull = (e.event === 'AUDIT_LOG_EVENT_CONFIG_CHANGE') && !\/responded to .+ request with status\/.test(rawMsg);\n    const svc = (e.event === 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE' || ccReqFull) ? null : svcConciseHtml(rawMsg);\n    if (svc) {\n      msgHtml = svc;\n    } else {\n      msgHtml = highlightText(rawMsg, search);\n      \/\/ Wrap SystemState* tokens in spans with tooltips from the API reference.\n      \/\/ Skips already-marked content (search highlights) to avoid breaking those.\n      msgHtml = msgHtml.replace(\/\\bSystemState([A-Z][a-zA-Z0-9]+)\\b\/g, (match, suffix) => {\n        const desc = smartdSystemStateDesc(match);\n        if (!desc) return match;\n        return `<span class=\"st-tip\" title=\"${escHtml(match + ' \u2014 ' + desc)}\">${match}<\/span>`;\n      });\n    }\n  }\n\n  \/\/ CONFIG_CHANGE requests carry a large protobuf payload \u2014 instead of dumping it\n  \/\/ into the cell, show a compact summary with Compare (diff vs previous) \/ View all.\n  \/\/ Only when the payload actually carries a structured body (\"{\"); otherwise the\n  \/\/ default rendering shows the raw message with no structured\/raw option.\n  if (formattedView && ccIsConfigRequest(e) && rawMsg.indexOf('{') !== -1) {\n    const ccIdx = auditAllData.indexOf(e);\n    const secs = ccSummary(ccExtractPayload(rawMsg));\n    const ccName = ccConfigName(rawMsg);\n    msgHtml = `<div class=\"cc-cell\">\n      <div class=\"cc-cell-head\">\n        <span class=\"cc-tag\">Set Configuration${ccName ? ': ' + escHtml(ccName) : ''}<\/span>\n        <button class=\"cc-btn cc-compare-btn\" data-cc-idx=\"${ccIdx}\" data-cc-mode=\"changes\" title=\"Compare this configuration change with the previous one and highlight what changed\">\u21c4 Compare changes<\/button>\n        <button class=\"cc-btn cc-viewall-btn\" data-cc-idx=\"${ccIdx}\" data-cc-mode=\"all\" title=\"View the full parsed configuration payload\">View all<\/button>\n        <button class=\"cc-btn cc-raw-btn\" data-cc-idx=\"${ccIdx}\" data-cc-mode=\"raw\" title=\"View the raw protobuf-text payload\">Raw<\/button>\n      <\/div>\n      ${secs ? `<div class=\"cc-cell-sec\">${escHtml(secs)}<\/div>` : ''}\n      <div class=\"cc-cell-content\">${ccInlineContent(e)}<\/div>\n    <\/div>`;\n  }\n\n  \/\/ STARTUP \u2014 Structured (software VersionInfo) vs Raw. When the message carries\n  \/\/ no VersionInfo, default to Raw (structured would be all \"\u2014\") but keep both options.\n  \/\/ CONFIG_CHANGE \/ SETPOINTS_CHANGE response \u2014 keep the concise status; when it failed, include the error string.\n  if (formattedView && (e.event === 'AUDIT_LOG_EVENT_CONFIG_CHANGE' || e.event === 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE') && \/responded to .+ request with status\/.test(rawMsg)) {\n    const respHtml = ccResponseFormatted(rawMsg, search);\n    if (respHtml) msgHtml = respHtml;\n  }\n\n  \/\/ SETPOINTS_CHANGE request \u2014 structured Service \/ Request(content) view.\n  if (formattedView && e.event === 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE' && \/received .+ request\/.test(rawMsg)) {\n    const spHtml = renderSetpointsRequest(rawMsg, search);\n    if (spHtml) msgHtml = spHtml;\n  }\n\n  \/\/ SETPOINTS_CHANGE 'system state changed from X to Y by action Z' -> Action \/ From \/ To.\n  if (formattedView && e.event === 'AUDIT_LOG_EVENT_SETPOINTS_CHANGE' && \/System state chang(?:e|ed)\\s+from\/i.test(rawMsg)) {\n    const ssHtml = renderSystemStateChange(rawMsg, search);\n    if (ssHtml) msgHtml = ssHtml;\n  }\n\n  if (formattedView && e.event === 'AUDIT_LOG_EVENT_STARTUP') {\n    const stIdx = auditAllData.indexOf(e);\n    const hasVersion = stExtractVersionInfo(rawMsg).some(([, v]) => v != null && v !== '');\n    const initial = hasVersion ? stRenderStructured(rawMsg) : atRenderRaw(rawMsg);\n    msgHtml = `<div class=\"at-cell\" data-at-idx=\"${stIdx}\">\n      <div class=\"at-head\">\n        <span class=\"at-tag at-tag-startup\">Startup<\/span>\n        <button class=\"at-btn ${hasVersion ? 'active' : ''}\" data-at-mode=\"structured\">Structured<\/button>\n        <button class=\"at-btn ${hasVersion ? '' : 'active'}\" data-at-mode=\"raw\">Raw<\/button>\n      <\/div>\n      <div class=\"at-view\" data-at-view>${initial}<\/div>\n    <\/div>`;\n  }\n\n  \/\/ ALARM \u2014 show a concise label (ACTIVE\/CLEARED\/DISABLE (OOS)\/ACKNOWLEDGED).\n  \/\/ When the alarm also carries power-meter\/electrical metrics, offer a Raw view.\n  if (formattedView && e.event === 'AUDIT_LOG_EVENT_ALARM') {\n    const label = alarmConciseLabel(rawMsg);\n    if (label && label.startsWith('Change ')) {\n      \/\/ Reformatted alarm state-change (\"Change <name> state from X to Y\"):\n      \/\/ show the single line only \u2014 no Formatted\/Raw toggle, no context-data card.\n      msgHtml = alarmConciseHtml(rawMsg);\n    } else if (alarmHasPayload(rawMsg)) {\n      const alIdx = auditAllData.indexOf(e);\n      msgHtml = `<div class=\"at-cell\" data-at-idx=\"${alIdx}\">\n        <div class=\"at-head\">\n          <span class=\"at-tag at-tag-alarm\">Alarm<\/span>\n          <button class=\"at-btn active\" data-at-mode=\"structured\">Formatted<\/button>\n          <button class=\"at-btn\" data-at-mode=\"raw\">Raw<\/button>\n        <\/div>\n        <div class=\"at-view\" data-at-view>${alarmFormattedView(rawMsg)}<\/div>\n      <\/div>`;\n    } else if (label) {\n      msgHtml = alarmConciseHtml(rawMsg);\n    }\n    \/\/ else: unrecognized alarm text with no payload \u2192 keep default rendering\n  }\n\n  const userId = (e.userId !== null && e.userId !== undefined) ? highlightText(String(e.userId), search) : '<span class=\"null-cell\">\u2014<\/span>';\n  const txId   = e.transactionId ? highlightText(e.transactionId.slice(0,12)+(e.transactionId.length>12?'\u2026':''), search) : '<span class=\"null-cell\">\u2014<\/span>';\n\n  \/\/ Build descriptive tooltips from the embedded SmartD API reference\n  const evtDesc = smartdEventDesc(evt);\n  const origDesc = smartdOriginDesc(orig);\n  const sevDesc = smartdSeverityDesc(sev);\n  const evtTip = evtDesc ? `AUDIT_LOG_EVENT_${evt} \u2014 ${evtDesc}` : `AUDIT_LOG_EVENT_${evt}`;\n  const origTip = origDesc ? `AUDIT_LOG_ORIGIN_${orig} \u2014 ${origDesc}` : `AUDIT_LOG_ORIGIN_${orig}`;\n  const sevTip = sevDesc ? `AUDIT_LOG_SEVERITY_${sev} \u2014 ${sevDesc}` : `AUDIT_LOG_SEVERITY_${sev}`;\n\n  return `<tr class=\"${rc}\" title=\"${escHtml(e.transactionId||'')}\">\n    <td class=\"col-ts ts-cell\">${(auditFocusEntry && e === auditFocusEntry) ? '<span class=\"focus-star\">*<\/span>' : ''}<div class=\"ts-date\">${date}<\/div><div class=\"ts-time\">${time}Z<\/div><\/td>\n    <td class=\"col-evt\"><span class=\"evt-chip evt-${evt}\" title=\"${escHtml(evtTip)}\">${evt}<\/span><\/td>\n    <td class=\"col-sev\"><span class=\"badge badge-${sev}\" title=\"${escHtml(sevTip)}\"><span class=\"badge-dot badge-dot-${sev}\"><\/span>${sev}<\/span><\/td>\n    <td class=\"col-orig\"><span class=\"origin-chip origin-${orig}\" title=\"${escHtml(origTip)}\">${orig}<\/span><\/td>\n    <td class=\"col-user\">${userId}<\/td>\n    <td class=\"col-tid\" title=\"${escHtml(e.transactionId||'')}\">${txId}<\/td>\n    <td class=\"col-msg\">${msgHtml}<\/td>\n  <\/tr>`;\n}\n\nfunction updateAuditClock() {\n  const el = $('#audit-current-time');\n  if (el) el.textContent = new Date().toUTCString().replace(' GMT','Z');\n}\nsetInterval(() => {\n  if ($('#auditViewer') && $('#auditViewer').classList.contains('active')) updateAuditClock();\n}, 1000);\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ Export to PDF \u2014 opens a printable window, triggers print.\n\/\/ Used by both Audit Log and Timeline tabs.\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nfunction escPdfText(s) {\n  return String(s == null ? '' : s).replace(\/[&<>\"']\/g, c => ({\n    '&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'\n  }[c]));\n}\n\nfunction openPrintWindow(title, bodyHtml) {\n  const css = `\n    * { box-sizing: border-box; }\n    body {\n      margin: 0;\n      font-family: Arial, sans-serif;\n      font-size: 10.5px; line-height: 1.4;\n      color: #1f2937; background: #fff;\n    }\n    .pdf-wrap { padding: 22px 24px 28px; }\n    h1.pdf-title { font-size: 16px; margin: 0 0 4px; color: #0f172a; }\n    .pdf-subtitle { font-size: 10.5px; color: #64748b; margin: 0 0 16px; }\n    .pdf-meta {\n      display: flex; flex-wrap: wrap; gap: 6px 18px;\n      border: 1px solid #e5e7eb; border-radius: 4px;\n      padding: 8px 12px; margin-bottom: 14px;\n      font-family: 'SF Mono', Consolas, Menlo, monospace;\n      font-size: 10px; color: #334155;\n      background: #f8fafc;\n    }\n    .pdf-meta strong { color: #0f172a; font-weight: 600; }\n    table.pdf-table {\n      width: 100%;\n      border-collapse: collapse;\n      font-family: 'SF Mono', Consolas, Menlo, monospace;\n      font-size: 9.5px;\n    }\n    table.pdf-table thead th {\n      background: #f1f5f9;\n      text-align: left;\n      padding: 6px 8px;\n      border-bottom: 1.5px solid #cbd5e1;\n      font-size: 8.5px; font-weight: 700;\n      letter-spacing: 0.06em; text-transform: uppercase;\n      color: #475569;\n    }\n    table.pdf-table tbody td {\n      padding: 4px 8px;\n      border-bottom: 1px solid #e5e7eb;\n      vertical-align: top;\n      word-break: break-word;\n    }\n    table.pdf-table tbody tr:nth-child(even) td { background: #fafbfc; }\n    .pdf-ts-date { color: #64748b; font-size: 8.5px; display: block; }\n    .pdf-ts-time { font-weight: 500; }\n    .pdf-pill {\n      display: inline-block;\n      padding: 1px 5px; border-radius: 2px;\n      font-size: 8px; font-weight: 700; letter-spacing: 0.06em;\n      text-transform: uppercase;\n      border: 1px solid transparent;\n      white-space: nowrap;\n    }\n    .pdf-pill-INFO      { color: #02AF4C; background: #D4EDDA; border-color: #c8ebd8; }\n    .pdf-pill-WARN, .pdf-pill-WARNING { color: #b45309; background: #fef3c7; border-color: #fde68a; }\n    .pdf-pill-ERROR     { color: #b91c1c; background: #fee2e2; border-color: #fecaca; }\n    .pdf-pill-EMERGENCY { color: #7f1d1d; background: #fecaca; border-color: #fca5a5; }\n    .pdf-pill-src-audit { color: #4338ca; background: #e0e7ff; border-color: #c7d2fe; }\n    .pdf-pill-src-debug { color: #92400e; background: #fef3c7; border-color: #fde68a; }\n    .pdf-chip {\n      display: inline-block;\n      padding: 0 5px;\n      border: 1px solid #e5e7eb;\n      border-radius: 2px;\n      background: #f8fafc;\n      color: #334155;\n      font-size: 8.5px;\n      white-space: nowrap;\n      max-width: 100%;\n      overflow: hidden;\n      text-overflow: ellipsis;\n    }\n    .pdf-chip-event { color: #4338ca; border-color: #c7d2fe; background: #eef2ff; }\n    .pdf-chip-origin { color: #0891b2; border-color: #a5f3fc; background: #ecfeff; }\n    .pdf-muted { color: #94a3b8; }\n    .pdf-msg { white-space: pre-wrap; word-break: break-word; font-size: 9.5px; }\n    tr.pdf-row-error td { background: #fef2f2 !important; }\n    tr.pdf-row-warn td  { background: #fffbeb !important; }\n    tr.pdf-row-alarm td { background: #fef2f2 !important; }\n\n    @page { size: A4 landscape; margin: 14mm 10mm 12mm; }\n    @media print {\n      thead { display: table-header-group; }\n      tr { break-inside: avoid; page-break-inside: avoid; }\n      tr.pdf-row-error td, tr.pdf-row-warn td, tr.pdf-row-alarm td,\n      table.pdf-table tbody tr:nth-child(even) td,\n      table.pdf-table thead th {\n        -webkit-print-color-adjust: exact;\n        print-color-adjust: exact;\n      }\n    }\n  `;\n\n  const html = `<!DOCTYPE html><html><head><meta charset=\"utf-8\"><title>${escPdfText(title)}<\/title><style>${css}<\/style><\/head><body><div class=\"pdf-wrap\">${bodyHtml}<\/div><script>\n    window.addEventListener('load', function() {\n      setTimeout(function() { window.focus(); window.print(); }, 250);\n    });\n  <\\\/script><\/body><\/html>`;\n\n  const win = window.open('', '_blank');\n  if (!win) {\n    showToast('Popup blocked \u2014 please allow popups to export PDF.', 'error');\n    return null;\n  }\n  win.document.open();\n  win.document.write(html);\n  win.document.close();\n  return win;\n}\n\nfunction pdfSplitTs(iso) {\n  if (!iso) return { date: '', time: '' };\n  const t = iso.indexOf('T');\n  if (t < 0) return { date: iso, time: '' };\n  const date = iso.slice(0, t);\n  let rest = iso.slice(t + 1).replace(\/Z$\/, '').replace(\/[+-]\\d{2}:\\d{2}$\/, '');\n  const dot = rest.indexOf('.');\n  if (dot > 0 && rest.length > dot + 4) rest = rest.slice(0, dot + 4);\n  return { date, time: rest };\n}\n\n\/\/ ---- Audit Log PDF ----------------------------------------------------\nfunction exportAuditLogToPdf(order \/* 'asc' | 'desc' *\/) {\n  if (!Array.isArray(auditFilteredData) || auditFilteredData.length === 0) {\n    showToast('No entries to export. Adjust filters to select some events.', 'error');\n    return;\n  }\n  \/\/ auditFilteredData is already in the user's current on-screen sort; we just re-order by timestamp for the report\n  const rows = auditFilteredData.slice().sort((a, b) => {\n    const ak = a.timestamp || '', bk = b.timestamp || '';\n    if (ak === bk) return 0;\n    if (order === 'desc') return ak < bk ? 1 : -1;\n    return ak < bk ? -1 : 1;\n  });\n\n  const pkg = state.packageName || 'support-package';\n  const tsNow = new Date().toISOString().replace(\/\\.\\d+Z$\/, 'Z');\n  const orderLabel = order === 'desc' ? 'Newest \u2192 Oldest' : 'Oldest \u2192 Newest';\n\n  let firstTs = null, lastTs = null;\n  for (const e of rows) {\n    if (!e.timestamp) continue;\n    if (firstTs === null || e.timestamp < firstTs) firstTs = e.timestamp;\n    if (lastTs === null || e.timestamp > lastTs) lastTs = e.timestamp;\n  }\n  const range = (firstTs && lastTs)\n    ? `${firstTs.slice(0,19).replace('T',' ')} \u2192 ${lastTs.slice(0,19).replace('T',' ')}`\n    : '\u2014';\n\n  const headerHtml = `\n    <h1 class=\"pdf-title\">Audit Log Export<\/h1>\n    <div class=\"pdf-subtitle\">Support package <strong>${escPdfText(pkg)}<\/strong> \u00b7 generated ${escPdfText(tsNow)}<\/div>\n    <div class=\"pdf-meta\">\n      <div><strong>${rows.length.toLocaleString()}<\/strong> entries displayed<\/div>\n      <div>Range: <strong>${escPdfText(range)}<\/strong><\/div>\n      <div>Order: <strong>${escPdfText(orderLabel)}<\/strong><\/div>\n      <div>Source: <strong>audit-log.json<\/strong><\/div>\n    <\/div>`;\n\n  const trs = rows.map(e => {\n    const sev = shortSeverity(e.severity);\n    const evt = shortEvent(e.event);\n    const orig = shortOrigin(e.origin);\n    const { date, time } = pdfSplitTs(e.timestamp);\n    let rc = '';\n    if (e.event === 'AUDIT_LOG_EVENT_ALARM') rc = 'pdf-row-alarm';\n    else if (sev === 'EMERGENCY' || sev === 'ERROR') rc = 'pdf-row-error';\n    else if (sev === 'WARNING' || sev === 'WARN') rc = 'pdf-row-warn';\n    const user = (e.userId !== null && e.userId !== undefined) ? String(e.userId) : '\u2014';\n    const tid = e.transactionId || '';\n    return `<tr class=\"${rc}\">\n      <td><span class=\"pdf-ts-date\">${escPdfText(date)}<\/span><span class=\"pdf-ts-time\">${escPdfText(time)}Z<\/span><\/td>\n      <td><span class=\"pdf-pill pdf-pill-${escPdfText(sev)}\">${escPdfText(sev || '\u2014')}<\/span><\/td>\n      <td><span class=\"pdf-chip pdf-chip-event\">${escPdfText(evt || '\u2014')}<\/span><\/td>\n      <td><span class=\"pdf-chip pdf-chip-origin\">${escPdfText(orig || '\u2014')}<\/span><\/td>\n      <td>${user === '\u2014' ? '<span class=\"pdf-muted\">\u2014<\/span>' : escPdfText(user)}<\/td>\n      <td>${tid ? escPdfText(tid) : '<span class=\"pdf-muted\">\u2014<\/span>'}<\/td>\n      <td class=\"pdf-msg\">${escPdfText(e.message || '')}<\/td>\n    <\/tr>`;\n  }).join('');\n\n  const tableHtml = `<table class=\"pdf-table\">\n    <colgroup>\n      <col style=\"width:105px\"><col style=\"width:70px\"><col style=\"width:110px\"><col style=\"width:80px\">\n      <col style=\"width:55px\"><col style=\"width:90px\"><col>\n    <\/colgroup>\n    <thead><tr>\n      <th>Timestamp<\/th><th>Severity<\/th><th>Event Type<\/th><th>Origin<\/th>\n      <th>User<\/th><th>Transaction ID<\/th><th>Message<\/th>\n    <\/tr><\/thead>\n    <tbody>${trs}<\/tbody>\n  <\/table>`;\n\n  openPrintWindow(`Audit Log Export \u2014 ${pkg}`, headerHtml + tableHtml);\n}\n\n\n\/\/ ---- Config PDF -------------------------------------------------------\nfunction exportConfigToPdf(scope \/* 'full' | 'section' *\/) {\n  if (!configData || !configData.configurations) {\n    showToast('No configuration loaded to export.', 'error');\n    return;\n  }\n  const configs = configData.configurations;\n  const meta = configData._metadata || {};\n  const pkg = state.packageName || 'support-package';\n  const tsNow = new Date().toISOString().replace(\/\\.\\d+Z$\/, 'Z');\n  const fmtTs = s => typeof s === 'string' ? s.replace(\/\\.\\d+Z$\/, 'Z') : s;\n  const lastUpdated = meta.lastUpdateTime || meta.lastUpdateDate || '';\n  const extra = meta.extraInformation || {};\n  const versionStr = meta.version ? (meta.version + (meta.minorVersion ? '.' + meta.minorVersion : '')) : '\u2014';\n\n  \/\/ Decide which sections to include\n  let sectionKeys;\n  let scopeLabel;\n  if (scope === 'section') {\n    if (!configActiveSection || !configs[configActiveSection]) {\n      showToast('No section currently selected. Pick a section in the sidebar first.', 'error');\n      return;\n    }\n    sectionKeys = [configActiveSection];\n    scopeLabel = `Section: ${humanizeKey(configActiveSection)}`;\n  } else {\n    sectionKeys = orderedConfigKeys(configs);\n    scopeLabel = `Full Configuration (${sectionKeys.length} sections)`;\n  }\n\n  \/\/ Metadata items (mirroring the strip in the live viewer, only those with values)\n  const metaItems = [\n    { label: 'Device Model',     value: meta.deviceModel },\n    { label: 'Serial Number',    value: meta.deviceSerialNumber },\n    { label: 'Product',          value: extra.product_name },\n    { label: 'Vendor',           value: extra.vendor_name },\n    { label: 'Software Version', value: meta.deviceSoftwareVersion },\n    { label: 'Backup Version',   value: meta.deviceBackupVersion },\n    { label: 'Config Version',   value: meta.version ? versionStr : null },\n    { label: 'Label',            value: meta.label },\n    { label: 'Description',      value: meta.description },\n    { label: 'Created',          value: fmtTs(meta.creationDate) },\n    { label: 'Last Updated',     value: fmtTs(lastUpdated) },\n  ].filter(m => m.value !== undefined && m.value !== null && m.value !== '');\n\n  \/\/ Build metadata grid HTML\n  const metaHtml = metaItems.length ? `<div class=\"pdf-cfg-meta\">${\n    metaItems.map(m => `<div class=\"pdf-cfg-meta-item\">\n      <div class=\"pdf-cfg-meta-label\">${escPdfText(m.label)}<\/div>\n      <div class=\"pdf-cfg-meta-value\">${escPdfText(String(m.value))}<\/div>\n    <\/div>`).join('')\n  }<\/div>` : '';\n\n  \/\/ Render each section using the live viewer's config renderer.\n  \/\/ The output uses .cfg-group \/ .cfg-field-row \/ etc. CSS classes which we provide below.\n  let sectionsHtml = '';\n  for (const k of sectionKeys) {\n    const v = configs[k];\n    const title = humanizeKey(k);\n    sectionsHtml += `<div class=\"pdf-section\">`;\n    sectionsHtml += `<h2 class=\"pdf-section-title\">${escPdfText(title)}<\/h2>`;\n    sectionsHtml += `<div class=\"pdf-section-path\">configurations.${escPdfText(k)}<\/div>`;\n    if (Array.isArray(v)) {\n      sectionsHtml += `<div class=\"cfg-render\">${renderConfigArray(v, title)}<\/div>`;\n    } else if (v !== null && typeof v === 'object') {\n      sectionsHtml += `<div class=\"cfg-render\">${renderConfigGroup(v, title)}<\/div>`;\n    } else {\n      sectionsHtml += `<div class=\"cfg-render\"><div style=\"padding:8px 14px\">${renderConfigValue(v)}<\/div><\/div>`;\n    }\n    sectionsHtml += `<\/div>`;\n  }\n\n  \/\/ Custom CSS for the config-flavoured PDF (extends openPrintWindow's base styles)\n  const customStyles = `\n    \/* Override openPrintWindow base styles for config-shaped output *\/\n    h1.pdf-title { font-size: 18px; }\n    .pdf-cfg-meta {\n      display: grid;\n      grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));\n      gap: 8px 14px;\n      border: 1px solid #cbd5e1;\n      border-radius: 4px;\n      padding: 10px 12px;\n      margin-bottom: 16px;\n      background: #f8fafc;\n    }\n    .pdf-cfg-meta-item { display: flex; flex-direction: column; gap: 2px; }\n    .pdf-cfg-meta-label {\n      font-family: 'SF Mono', Consolas, Menlo, monospace;\n      font-size: 8.5px; font-weight: 700;\n      letter-spacing: 0.06em; text-transform: uppercase;\n      color: #64748b;\n    }\n    .pdf-cfg-meta-value {\n      font-family: 'SF Mono', Consolas, Menlo, monospace;\n      font-size: 10.5px; font-weight: 500;\n      color: #0f172a;\n      word-break: break-all;\n    }\n    .pdf-section { margin-bottom: 18px; break-inside: avoid-page; }\n    .pdf-section-title {\n      font-size: 13px; font-weight: 600;\n      color: #1e3a8a;\n      margin: 0 0 2px;\n      padding-bottom: 4px;\n      border-bottom: 1.5px solid #1e3a8a;\n    }\n    .pdf-section-path {\n      font-family: 'SF Mono', Consolas, Menlo, monospace;\n      font-size: 8.5px;\n      color: #94a3b8;\n      margin-bottom: 8px;\n    }\n    \/* Config tree styling (mirrors live viewer .cfg-render) *\/\n    .cfg-render { font-family: 'SF Mono', Consolas, Menlo, monospace; font-size: 9.5px; }\n    .cfg-render .cfg-group {\n      border: 1px solid #e5e7eb;\n      border-radius: 3px;\n      margin-bottom: 6px;\n      background: #fff;\n      break-inside: avoid;\n    }\n    .cfg-render .cfg-group-title {\n      background: #f1f5f9;\n      padding: 4px 8px;\n      font-size: 9px; font-weight: 700;\n      letter-spacing: 0.06em; text-transform: uppercase;\n      color: #475569;\n      border-bottom: 1px solid #e5e7eb;\n    }\n    .cfg-render .cfg-fields {\n      display: grid;\n      grid-template-columns: minmax(120px, 1fr) 2fr;\n      gap: 0;\n    }\n    .cfg-render .cfg-field-row { display: contents; }\n    .cfg-render .cfg-field-label {\n      padding: 3px 8px;\n      color: #64748b;\n      border-bottom: 1px solid #f1f5f9;\n      background: #fafbfc;\n    }\n    .cfg-render .cfg-field-value {\n      padding: 3px 8px;\n      color: #0f172a;\n      border-bottom: 1px solid #f1f5f9;\n      font-weight: 500;\n      word-break: break-word;\n    }\n    .cfg-render .cfg-field-row:last-child .cfg-field-label,\n    .cfg-render .cfg-field-row:last-child .cfg-field-value { border-bottom: none; }\n    .cfg-render .val-string { color: #047857; }\n    .cfg-render .val-number { color: #02AF4C; font-weight: 600; }\n    .cfg-render .val-bool-true {\n      display: inline-block; padding: 0 5px;\n      color: #047857; background: #d1fae5; border: 1px solid #6ee7b7;\n      border-radius: 2px; font-size: 8.5px; font-weight: 600;\n    }\n    .cfg-render .val-bool-false {\n      display: inline-block; padding: 0 5px;\n      color: #b91c1c; background: #fee2e2; border: 1px solid #fca5a5;\n      border-radius: 2px; font-size: 8.5px; font-weight: 600;\n    }\n    .cfg-render .val-null { color: #94a3b8; font-style: italic; }\n    .cfg-render .val-enum {\n      display: inline-block; padding: 0 5px;\n      color: #4338ca; background: #e0e7ff; border: 1px solid #c7d2fe;\n      border-radius: 2px; font-size: 8.5px; font-weight: 600;\n    }\n    .cfg-render .val-enum-disabled { color: #94a3b8; background: #f1f5f9; border-color: #e5e7eb; }\n    .cfg-render .val-duration {\n      display: inline-block; padding: 0 5px;\n      color: #6b21a8; background: #f3e8ff; border: 1px solid #d8b4fe;\n      border-radius: 2px; font-size: 8.5px; font-weight: 600;\n    }\n    .cfg-render .val-date { color: #7c2d12; }\n    .cfg-render .val-empty { color: #94a3b8; font-style: italic; font-size: 8.5px; }\n    \/* Arrays *\/\n    .cfg-render .cfg-array {\n      border: 1px solid #e5e7eb;\n      border-radius: 3px;\n      margin-bottom: 6px;\n      background: #fff;\n    }\n    .cfg-render .cfg-array-title {\n      background: #f1f5f9;\n      padding: 4px 8px;\n      font-size: 9px; font-weight: 700;\n      letter-spacing: 0.06em; text-transform: uppercase;\n      color: #475569;\n      border-bottom: 1px solid #e5e7eb;\n      display: flex; justify-content: space-between; align-items: center;\n    }\n    .cfg-render .cfg-array-count {\n      font-size: 8.5px; color: #94a3b8; font-weight: 500;\n      letter-spacing: 0; text-transform: none;\n    }\n    .cfg-render .cfg-array-item {\n      border-bottom: 1px dashed #e5e7eb;\n      padding: 4px 8px;\n    }\n    .cfg-render .cfg-array-item:last-child { border-bottom: none; }\n    .cfg-render .cfg-array-item-title {\n      font-size: 9px; font-weight: 600;\n      color: #4338ca;\n      margin-bottom: 3px;\n    }\n    .cfg-render .cfg-inline-fields {\n      display: grid;\n      grid-template-columns: auto 1fr;\n      gap: 1px 8px;\n      font-size: 9px;\n    }\n    .cfg-render .cfg-inline-label { color: #64748b; }\n    .cfg-render .cfg-inline-value { color: #0f172a; font-weight: 500; word-break: break-word; }\n    .cfg-render .cfg-no-data { color: #94a3b8; font-style: italic; padding: 4px 8px; font-size: 9px; }\n  `;\n\n  \/\/ Build the PDF body, then wrap in the standard print template via openPrintWindow.\n  \/\/ We append our customStyles after the base by injecting a <style> at the top of body content.\n  const headerHtml = `\n    <style>${customStyles}<\/style>\n    <h1 class=\"pdf-title\">Configuration Export<\/h1>\n    <div class=\"pdf-subtitle\">Support package <strong>${escPdfText(pkg)}<\/strong> \u00b7 generated ${escPdfText(tsNow)}<\/div>\n    <div class=\"pdf-meta\">\n      <div><strong>${scopeLabel}<\/strong><\/div>\n      <div>Source: <strong>${escPdfText(meta.deviceModel || 'smartdrive-config.json')}<\/strong><\/div>\n    <\/div>\n    ${metaHtml}\n    ${sectionsHtml}\n  `;\n\n  openPrintWindow(`Config Export \u2014 ${pkg}`, headerHtml);\n}\n\n\/\/ ---- Dropdown wiring --------------------------------------------------\nfunction wireExportDropdown(btnId, menuId, onExport) {\n  const btn = $('#' + btnId);\n  const menu = $('#' + menuId);\n  if (!btn || !menu) return;\n  btn.setAttribute('aria-expanded', 'false');\n\n  \/\/ Portal the menu to <body> so it's not clipped by the table container,\n  \/\/ sticky thead, or any other stacking context in the audit\/timeline viewer.\n  (SMARTD_SPV_ROOT || document.body).appendChild(menu);\n  menu.style.position = 'fixed';\n  menu.style.zIndex = '10000';\n\n  function positionMenu() {\n    const rect = btn.getBoundingClientRect();\n    \/\/ Measure the menu to decide if it fits below; flip above if needed\n    menu.style.visibility = 'hidden';\n    menu.style.display = 'block';\n    const mh = menu.offsetHeight;\n    const mw = menu.offsetWidth;\n    menu.style.display = '';\n    menu.style.visibility = '';\n    const vpH = window.innerHeight;\n    const vpW = window.innerWidth;\n    let top = rect.bottom + 4;\n    if (top + mh > vpH - 8) {\n      \/\/ Not enough room below \u2192 place above the button\n      top = Math.max(8, rect.top - 4 - mh);\n    }\n    let left = rect.right - mw; \/\/ right-align under the button\n    if (left < 8) left = 8;\n    if (left + mw > vpW - 8) left = vpW - 8 - mw;\n    menu.style.top = top + 'px';\n    menu.style.left = left + 'px';\n  }\n\n  function openMenu() {\n    \/\/ Close all other export menus first\n    $$('.export-menu.open').forEach(m => {\n      if (m !== menu) {\n        m.classList.remove('open');\n        const otherBtn = document.querySelector('[aria-controls=\"' + m.id + '\"]') ||\n                         document.querySelector('.audit-btn-export[aria-expanded=\"true\"]');\n        if (otherBtn) otherBtn.setAttribute('aria-expanded', 'false');\n      }\n    });\n    menu.classList.add('open');\n    btn.setAttribute('aria-expanded', 'true');\n    positionMenu();\n  }\n  function closeMenu() {\n    menu.classList.remove('open');\n    btn.setAttribute('aria-expanded', 'false');\n  }\n\n  btn.setAttribute('aria-controls', menuId);\n  btn.addEventListener('click', (ev) => {\n    ev.stopPropagation();\n    if (menu.classList.contains('open')) closeMenu();\n    else openMenu();\n  });\n  menu.querySelectorAll('.export-menu-item').forEach(item => {\n    item.addEventListener('click', (ev) => {\n      ev.stopPropagation();\n      const order = item.getAttribute('data-export-order')\n                 || item.getAttribute('data-export-scope');\n      closeMenu();\n      try { onExport(order); }\n      catch (err) {\n        console.error('Export failed:', err);\n        showToast('Export failed: ' + err.message, 'error');\n      }\n    });\n  });\n  \/\/ Reposition on scroll \/ resize while menu is open\n  window.addEventListener('scroll', () => { if (menu.classList.contains('open')) positionMenu(); }, true);\n  window.addEventListener('resize', () => { if (menu.classList.contains('open')) positionMenu(); });\n}\n\/\/ Close any open export menu on outside click\ndocument.addEventListener('click', (ev) => {\n  if (ev.target.closest('.export-wrap') || ev.target.closest('.export-menu')) return;\n  $$('.export-menu.open').forEach(m => {\n    m.classList.remove('open');\n    const btnId = m.id.replace('-menu', '-btn');\n    const btn = $('#' + btnId);\n    if (btn) btn.setAttribute('aria-expanded', 'false');\n  });\n});\n\/\/ Close on Escape\ndocument.addEventListener('keydown', (ev) => {\n  if (ev.key === 'Escape') {\n    $$('.export-menu.open').forEach(m => {\n      m.classList.remove('open');\n      const btnId = m.id.replace('-menu', '-btn');\n      const btn = $('#' + btnId);\n      if (btn) btn.setAttribute('aria-expanded', 'false');\n    });\n  }\n});\n\n\/\/ Audit viewer event wiring (attach once on load)\n$('#auditSearch').addEventListener('input', debounce(auditApplyFilters, 250));\n\/\/ (column filters are wired via their header dropdowns below)\n['audit-date-from','audit-date-to'].forEach(id => {\n  const el = $('#' + id);\n  if (el) {\n    \/\/ Only listen to 'change' (fires on commit). 'input' fires mid-interaction with\n    \/\/ the calendar widget and can cause thrashing with partial values.\n    el.addEventListener('change', auditApplyFilters);\n  }\n});\n$('#btn-clear').addEventListener('click', auditClearFilters);\nwireExportDropdown('audit-export-btn', 'audit-export-menu', exportAuditLogToPdf);\n\/\/ Config-tab \"Export PDF\" was replaced by Commissioning Report; open the modal on click.\n(function(){\n  const btn = document.getElementById('commissioning-report-btn');\n  if (btn) btn.addEventListener('click', () => {\n    if (!window.openCommissioningReport) return;\n    if (!configData || !configData.configurations) {\n      showToast('No configuration loaded.', 'error');\n      return;\n    }\n    window.openCommissioningReport(configData);\n  });\n})();\n$('#btn-show-startshutdown').addEventListener('click', () => {\n  auditShowStartShutdown = !auditShowStartShutdown;\n  $('#btn-show-startshutdown').classList.toggle('active', auditShowStartShutdown);\n  auditApplyFilters();\n});\n\/\/ Index (into the filtered set) of the row currently at the top of the viewport.\nfunction auditTopVisibleIndex() {\n  const tc = $('#audit-table-container');\n  const rows = $('#log-body') && $('#log-body').children;\n  if (!tc || !rows || !rows.length) return 0;\n  const top = tc.getBoundingClientRect().top;\n  for (let i = 0; i < rows.length; i++) {\n    if (rows[i].getBoundingClientRect().bottom > top + 1) return i;\n  }\n  return 0;\n}\nfunction auditSetViewMode(mode) {\n  const next = (mode === 'raw') ? 'raw' : 'formatted';\n  if (next === auditViewMode) return;\n  const topIdx = auditTopVisibleIndex();     \/\/ remember where the user is\n  auditViewMode = next;\n  $('#btn-view-formatted').classList.toggle('active', auditViewMode === 'formatted');\n  $('#btn-view-raw').classList.toggle('active', auditViewMode === 'raw');\n  renderAuditPage();                                          \/\/ re-render in the new mode\n  auditEnsureRendered(Math.min(auditFilteredData.length, topIdx + AUDIT_CHUNK));\n  requestAnimationFrame(() => {\n    const tc = $('#audit-table-container');\n    const row = $('#log-body') && $('#log-body').children[topIdx];\n    if (tc && row) {\n      tc.scrollTop += row.getBoundingClientRect().top - tc.getBoundingClientRect().top;\n    }\n  });\n}\n$('#btn-view-formatted').addEventListener('click', () => auditSetViewMode('formatted'));\n$('#btn-view-raw').addEventListener('click', () => auditSetViewMode('raw'));\n\/\/ Infinite scroll: append the next chunk as the user nears the bottom.\n(function () {\n  const tc = $('#audit-table-container');\n  if (!tc) return;\n  tc.addEventListener('scroll', () => {\n    if (tc.scrollTop + tc.clientHeight >= tc.scrollHeight - 500) auditAppendRows(AUDIT_CHUNK);\n  });\n})();\n\/\/ First \/ Last entry navigation.\n$('#btn-first-entry').addEventListener('click', () => {\n  const tc = $('#audit-table-container');\n  if (tc) tc.scrollTop = 0;\n});\n$('#btn-last-entry').addEventListener('click', () => {\n  auditEnsureRendered(auditFilteredData.length);   \/\/ render all remaining rows\n  requestAnimationFrame(() => {\n    const tc = $('#audit-table-container');\n    if (tc) tc.scrollTop = tc.scrollHeight;\n  });\n});\n\n\/\/ \u2500\u2500 Focus \/ context mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Click a row to pin it (marked with *), lock the view on it, and filter the log\n\/\/ down to a window of surrounding entries. Click it again (or Clear) to release.\nfunction auditSetFocus(entry) {\n  auditFocusEntry = entry;\n  auditApplyFilters();\n  requestAnimationFrame(auditCenterFocused);\n}\nfunction auditClearFocus() {\n  if (!auditFocusEntry) return;\n  auditFocusEntry = null;\n  auditApplyFilters();\n}\nfunction auditCenterFocused() {\n  const tc = $('#audit-table-container');\n  const row = tc && tc.querySelector('tr.row-focus');\n  if (row) row.scrollIntoView({ block: 'center' });\n}\nfunction updateFocusBanner() {\n  const banner = $('#audit-focus-banner');\n  if (!banner) return;\n  banner.style.display = auditFocusEntry ? 'flex' : 'none';\n}\n$('#btn-clear-focus').addEventListener('click', auditClearFocus);\n$('#btn-goto-marked').addEventListener('click', () => {\n  auditEnsureRendered(auditFilteredData.length);\n  requestAnimationFrame(auditCenterFocused);\n});\n$('#log-body').addEventListener('click', ev => {\n  \/\/ Ignore clicks on interactive elements inside a row.\n  if (ev.target.closest('.at-btn, .cc-btn, button, a, input, label, select, .st-tip')) return;\n  const tr = ev.target.closest('tr');\n  if (!tr || !tr.parentNode) return;\n  const pos = Array.prototype.indexOf.call(tr.parentNode.children, tr);\n  const entry = auditFilteredData[pos];\n  if (!entry) return;\n  if (entry === auditFocusEntry) auditClearFocus();   \/\/ click the pinned row again to release\n  else auditSetFocus(entry);\n});\n$$('#auditViewer th[data-sort]').forEach(th => {\n  th.addEventListener('click', () => auditSortBy(th.dataset.sort));\n});\n\n\/\/ Column-header filter dropdowns (portalled) \u2014 Event Type, Severity, Origin, User\nAUDIT_COL_FILTERS.forEach(cfg => {\n  const trigger  = $('#cf-'+cfg.key+'-trigger');\n  const dropdown = $('#cf-'+cfg.key+'-dropdown');\n  const chevron  = $('#cf-'+cfg.key+'-chevron');\n  if (!trigger || !dropdown) return;\n  let openedAt = 0;\n  function openDropdown() {\n    openedAt = Date.now();\n    const rect = trigger.getBoundingClientRect();\n    dropdown.style.top  = (rect.bottom + 4) + 'px';\n    dropdown.style.left = rect.left + 'px';\n    dropdown.style.minWidth = Math.max(rect.width, 170) + 'px';\n    dropdown.classList.add('open');\n    trigger.classList.add('open');\n    if (chevron) chevron.style.transform = 'rotate(180deg)';\n    const list = dropdown.querySelector('.ms-list');\n    if (list) {\n      const avail = window.innerHeight - rect.bottom - 24 - 40;\n      list.style.maxHeight = Math.max(160, Math.min(360, avail)) + 'px';\n    }\n  }\n  function closeDropdown() {\n    dropdown.classList.remove('open');\n    trigger.classList.remove('open');\n    if (chevron) chevron.style.transform = '';\n  }\n  trigger.addEventListener('click', (e) => {\n    e.stopPropagation();\n    \/\/ close any other open column dropdowns first\n    $$('.multiselect-dropdown.open').forEach(d => { if (d !== dropdown) d.classList.remove('open'); });\n    dropdown.classList.contains('open') ? closeDropdown() : openDropdown();\n  });\n  window.addEventListener('scroll', (e) => {\n    if (!dropdown.classList.contains('open')) return;\n    if (Date.now() - openedAt < 300) return;   \/\/ ignore focus\/layout scroll right after opening\n    if (dropdown.contains(e.target)) return;\n    \/\/ Ignore scrolls originating inside the audit table (e.g. programmatic scrollTop\n    \/\/ reset on re-render after toggling a filter) \u2014 the toolbar trigger doesn't move,\n    \/\/ so the dropdown must stay open.\n    if (e.target && e.target.closest && e.target.closest('#audit-table-container')) return;\n    closeDropdown();\n  }, true);\n  window.addEventListener('resize', closeDropdown);\n  document.addEventListener('click', (e) => {\n    if (!trigger.contains(e.target) && !dropdown.contains(e.target)) closeDropdown();\n  });\n  $('#cf-'+cfg.key+'-all').addEventListener('click', (e) => {\n    e.stopPropagation();\n    $$('#cf-'+cfg.key+'-list input').forEach(c => c.checked = true);\n    updateColBadge(cfg.key); auditApplyFilters();\n  });\n  $('#cf-'+cfg.key+'-none').addEventListener('click', (e) => {\n    e.stopPropagation();\n    $$('#cf-'+cfg.key+'-list input').forEach(c => c.checked = false);\n    updateColBadge(cfg.key); auditApplyFilters();\n  });\n});\n\n\/\/ \"Highlight color\" filter \u2014 portalled dropdown with Select All \/ Deselect All.\n(function wireColorFilter(){\n  const trigger  = $('#cf-color-trigger');\n  const dropdown = $('#cf-color-dropdown');\n  const chevron  = $('#cf-color-chevron');\n  if (!trigger || !dropdown) return;\n  let openedAt = 0;\n  function openDropdown() {\n    openedAt = Date.now();\n    const rect = trigger.getBoundingClientRect();\n    dropdown.style.top  = (rect.bottom + 4) + 'px';\n    dropdown.style.left = rect.left + 'px';\n    dropdown.style.minWidth = Math.max(rect.width, 190) + 'px';\n    dropdown.classList.add('open');\n    trigger.classList.add('open');\n    if (chevron) chevron.style.transform = 'rotate(180deg)';\n    const list = dropdown.querySelector('.ms-list');\n    if (list) {\n      const avail = window.innerHeight - rect.bottom - 24 - 40;\n      list.style.maxHeight = Math.max(160, Math.min(360, avail)) + 'px';\n    }\n  }\n  function closeDropdown() {\n    dropdown.classList.remove('open');\n    trigger.classList.remove('open');\n    if (chevron) chevron.style.transform = '';\n  }\n  trigger.addEventListener('click', (e) => {\n    e.stopPropagation();\n    $$('.multiselect-dropdown.open').forEach(d => { if (d !== dropdown) d.classList.remove('open'); });\n    dropdown.classList.contains('open') ? closeDropdown() : openDropdown();\n  });\n  window.addEventListener('scroll', (e) => {\n    if (!dropdown.classList.contains('open')) return;\n    if (Date.now() - openedAt < 300) return;\n    if (dropdown.contains(e.target)) return;\n    \/\/ Ignore scrolls originating inside the audit table (e.g. programmatic scrollTop\n    \/\/ reset on re-render after toggling a filter) \u2014 the toolbar trigger doesn't move,\n    \/\/ so the dropdown must stay open.\n    if (e.target && e.target.closest && e.target.closest('#audit-table-container')) return;\n    closeDropdown();\n  }, true);\n  window.addEventListener('resize', closeDropdown);\n  document.addEventListener('click', (e) => {\n    if (!trigger.contains(e.target) && !dropdown.contains(e.target)) closeDropdown();\n  });\n  $$('#cf-color-list input[type=checkbox]').forEach(cb => {\n    cb.addEventListener('change', () => { updateColorBadge(); auditApplyFilters(); });\n  });\n  $('#cf-color-all').addEventListener('click', (e) => {\n    e.stopPropagation();\n    $$('#cf-color-list input').forEach(c => c.checked = true);\n    updateColorBadge(); auditApplyFilters();\n  });\n  $('#cf-color-none').addEventListener('click', (e) => {\n    e.stopPropagation();\n    $$('#cf-color-list input').forEach(c => c.checked = false);\n    updateColorBadge(); auditApplyFilters();\n  });\n  updateColorBadge();\n})();\n\nfunction detectLevel(service, msg) {\n  if (!msg) return null;\n  \/\/ logfmt style: level=info \/ level=warning \/ level=error \/ level=debug\n  const m = \/(?:^|\\s)level=(info|warn|warning|error|fatal|panic|debug)\\b\/i.exec(msg);\n  if (m) {\n    const v = m[1].toLowerCase();\n    if (v === 'warn' || v === 'warning') return 'WARN';\n    if (v === 'error' || v === 'fatal' || v === 'panic') return 'ERROR';\n    if (v === 'debug') return 'DEBUG';\n    return 'INFO';\n  }\n  \/\/ Bare uppercase keywords\n  if (\/\\b(FATAL|PANIC)\\b\/.test(msg)) return 'ERROR';\n  if (\/\\bERROR\\b\/.test(msg) || \/\\b(err)=\/i.test(msg)) return 'ERROR';\n  if (\/\\b(WARNING|WARN)\\b\/.test(msg)) return 'WARN';\n  if (\/\\bfailed\\b\/i.test(msg)) return 'ERROR';\n  if (\/\\b(DEBUG|dbg)\\b\/i.test(msg)) return 'DEBUG';\n  \/\/ Service-based hints\n  if (service === 'kernel') return 'INFO';\n  return 'INFO';\n}\n\n\/\/ Classify service into a colour bucket (for the service chip styling)\nfunction serviceClass(svc) {\n  if (svc === 'kernel')           return 'dl-svc-kernel';\n  if (svc === 'systemd')          return 'dl-svc-systemd';\n  if (svc.startsWith('iot-'))     return 'dl-svc-iot';\n  if (svc.startsWith('sd-') ||\n      svc === 'smartd-vfd-firmware') return 'dl-svc-sd';\n  if (svc === 'redis' ||\n      svc === 'redis-server')     return 'dl-svc-redis';\n  return '';\n}\n   \n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ CONFIGURATION VIEWER\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\nlet configData = null;\nlet configActiveSection = null;\nlet configSearchQuery = '';\n\nfunction humanizeKey(k) {\n  if (!k) return '';\n  return String(k)\n    .replace(\/([a-z])([A-Z])\/g, '$1 $2')\n    .replace(\/[_-]\/g, ' ')\n    .replace(\/\\b\\w\/g, c => c.toUpperCase())\n    .replace(\/\\bL(\\d+)\\b\/g, 'L$1')\n    .replace(\/\\bPid\\b\/g, 'PID')\n    .replace(\/\\bPids\\b\/g, 'PIDs')\n    .replace(\/\\bIos\\b\/g, 'IOs')\n    .replace(\/\\bBacnet\\b\/g, 'BACnet')\n    .replace(\/\\bIo\\b\/gi, 'I\/O')\n    .replace(\/\\bBle\\b\/g, 'BLE')\n    .replace(\/\\bIp\\b\/g, 'IP')\n    .replace(\/\\bDns\\b\/g, 'DNS')\n    .replace(\/\\bDhcp\\b\/g, 'DHCP')\n    .replace(\/\\bMac\\b\/g, 'MAC')\n    .replace(\/\\bUi\\b\/g, 'UI')\n    .replace(\/\\bHz\\b\/gi, 'Hz')\n    .replace(\/\\bRms\\b\/gi, 'RMS')\n    .replace(\/\\bDc\\b\/g, 'DC')\n    .replace(\/\\bAc\\b\/g, 'AC');\n}\n\nfunction shortenEnum(v) {\n  if (!v || typeof v !== 'string') return v;\n  const parts = v.split('_');\n  if (parts.length <= 2) return v;\n  const markers = ['TYPE','MODE','STATE','ACTION','FUNCTION','SOURCE','SELECTOR','UNIT','PHASES','SELECTABLE','OUTPUT','BEHAVIOR','LEVEL','KIND','ENGINEERING','INDEX','CODE','CURVE','BOOT','TRIGGER','DISABLED','UNSPECIFIED'];\n  let lastMarkerIdx = -1;\n  for (let i = 0; i < parts.length; i++) {\n    if (markers.includes(parts[i])) lastMarkerIdx = i;\n  }\n  if (lastMarkerIdx >= 0 && lastMarkerIdx < parts.length - 1) {\n    return parts.slice(lastMarkerIdx + 1).join(' ');\n  }\n  return parts.slice(2).join(' ') || v;\n}\n\nconst ENUM_REGEX = \/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$\/;\nconst ISO_DATE_REGEX = \/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d+)?Z?$\/;\nconst DURATION_REGEX = \/^-?\\d+(\\.\\d+)?s$\/;\n\nfunction renderConfigValue(v) {\n  if (v === null || v === undefined) return '<span class=\"val-null\">null<\/span>';\n  if (typeof v === 'boolean') {\n    return v\n      ? '<span class=\"val-bool-true\">\u2713 True<\/span>'\n      : '<span class=\"val-bool-false\">\u2717 False<\/span>';\n  }\n  if (typeof v === 'number') {\n    const str = Number.isInteger(v) ? v.toLocaleString() :\n                Math.abs(v) >= 1000 ? v.toFixed(2) : String(v);\n    return `<span class=\"val-number\">${escHtml(str)}<\/span>`;\n  }\n  if (typeof v === 'string') {\n    if (v === '') return '<span class=\"val-empty\">(empty)<\/span>';\n    if (ENUM_REGEX.test(v)) {\n      const short = shortenEnum(v);\n      const isUnspec = \/UNSPECIFIED$\/.test(v) || \/DISABLED$\/.test(v);\n      const meta = configEnumMeta(v);\n      let cls = isUnspec ? 'val-enum val-enum-disabled' : 'val-enum';\n      if (meta) cls += ' val-enum-known';\n      const title = meta ? `${v} \u2014 ${meta.desc}  \u00b7  [${meta.label}]` : v;\n      return `<span class=\"${cls}\" title=\"${escHtml(title)}\">${escHtml(short)}<\/span>`;\n    }\n    if (ISO_DATE_REGEX.test(v)) {\n      try {\n        const d = new Date(v);\n        const formatted = d.toISOString().slice(0,19).replace('T',' ') + 'Z';\n        return `<span class=\"val-date\" title=\"${escHtml(v)}\">${escHtml(formatted)}<\/span>`;\n      } catch {}\n    }\n    if (DURATION_REGEX.test(v)) {\n      return `<span class=\"val-duration\">${escHtml(v)}<\/span>`;\n    }\n    return `<span class=\"val-string\">${escHtml(v)}<\/span>`;\n  }\n  return `<span class=\"val-string\">${escHtml(JSON.stringify(v))}<\/span>`;\n}\n\nfunction isSimpleValue(v) { return v === null || typeof v !== 'object'; }\n\nfunction matchesConfigSearch(label, value) {\n  if (!configSearchQuery) return true;\n  const q = configSearchQuery.toLowerCase();\n  const labelStr = String(label || '').toLowerCase();\n  const valueStr = typeof value === 'object' ? JSON.stringify(value || '').toLowerCase() : String(value == null ? '' : value).toLowerCase();\n  return labelStr.includes(q) || valueStr.includes(q);\n}\n\nfunction renderConfigGroup(obj, title) {\n  if (obj === null || obj === undefined || typeof obj !== 'object') return '';\n  const simple = [], nestedObjects = [], arrays = [];\n  for (const [k, v] of Object.entries(obj)) {\n    if (Array.isArray(v)) arrays.push({ k, v });\n    else if (v !== null && typeof v === 'object') nestedObjects.push({ k, v });\n    else simple.push({ k, v });\n  }\n  const filteredSimple = simple.filter(f => matchesConfigSearch(f.k, f.v));\n\n  let html = '';\n  const openWrap = title ? `<div class=\"cfg-group\"><div class=\"cfg-group-title\">${escHtml(title)}<\/div>` : '';\n  const closeWrap = title ? '<\/div>' : '';\n\n  if (filteredSimple.length > 0) {\n    const rows = filteredSimple.map(f => `\n      <div class=\"cfg-field-row\">\n        <div class=\"cfg-field-label\">${escHtml(humanizeKey(f.k))}<\/div>\n        <div class=\"cfg-field-value\">${renderConfigValue(f.v)}<\/div>\n      <\/div>`).join('');\n    html += `${openWrap}<div class=\"cfg-fields\">${rows}<\/div>${closeWrap}`;\n  } else if (title && nestedObjects.length === 0 && arrays.length === 0) {\n    return '';\n  } else if (title && simple.length > 0 && filteredSimple.length === 0) {\n    html += '';\n  } else if (title) {\n    html += openWrap;\n  }\n\n  for (const { k, v } of nestedObjects) {\n    html += renderConfigGroup(v, humanizeKey(k));\n  }\n  for (const { k, v } of arrays) {\n    html += renderConfigArray(v, humanizeKey(k));\n  }\n\n  if (title && filteredSimple.length === 0 && (nestedObjects.length > 0 || arrays.length > 0)) {\n    html += closeWrap;\n  }\n  return html;\n}\n\nfunction renderConfigArray(arr, title) {\n  if (!Array.isArray(arr) || arr.length === 0) return '';\n\n  if (arr.every(isSimpleValue)) {\n    if (!matchesConfigSearch(title, arr)) return '';\n    const items = arr.map(v => renderConfigValue(v)).join(' ');\n    return `<div class=\"cfg-group\">\n      <div class=\"cfg-group-title\">${escHtml(title)} <span class=\"cfg-array-count\" style=\"margin-left:8px\">${arr.length}<\/span><\/div>\n      <div style=\"padding:10px 14px\">${items || '<span class=\"val-null\">(empty)<\/span>'}<\/div>\n    <\/div>`;\n  }\n\n  const items = arr.map((item, idx) => {\n    if (!item || typeof item !== 'object') {\n      return `<div class=\"cfg-array-item\">${renderConfigValue(item)}<\/div>`;\n    }\n    const indexField = item.index !== undefined ? item.index\n                    : item.code !== undefined ? item.code\n                    : item.name !== undefined ? item.name\n                    : (idx + 1);\n    const itemTitle = typeof indexField === 'string' ? shortenEnum(indexField) : `#${indexField}`;\n\n    const simpleFields = [], nestedParts = [];\n    for (const [k, v] of Object.entries(item)) {\n      if (k === 'index' || k === 'code') continue;\n      if (Array.isArray(v) || (v !== null && typeof v === 'object')) nestedParts.push({ k, v });\n      else simpleFields.push({ k, v });\n    }\n\n    const anyMatch = matchesConfigSearch(itemTitle, '') ||\n                     simpleFields.some(f => matchesConfigSearch(f.k, f.v)) ||\n                     nestedParts.some(p => matchesConfigSearch(p.k, p.v));\n    if (!anyMatch) return '';\n\n    const inline = simpleFields.length > 0 ? `\n      <div class=\"cfg-inline-fields\">\n        ${simpleFields.map(f => `\n          <div class=\"cfg-inline-label\">${escHtml(humanizeKey(f.k))}<\/div>\n          <div class=\"cfg-inline-value\">${renderConfigValue(f.v)}<\/div>\n        `).join('')}\n      <\/div>` : '';\n\n    const nested = nestedParts.map(p => {\n      if (Array.isArray(p.v)) return renderConfigArray(p.v, humanizeKey(p.k));\n      return renderConfigGroup(p.v, humanizeKey(p.k));\n    }).join('');\n\n    return `<div class=\"cfg-array-item\">\n      <div class=\"cfg-array-item-title\">${escHtml(String(itemTitle))}<\/div>\n      ${inline}\n      ${nested}\n    <\/div>`;\n  }).filter(Boolean).join('');\n\n  if (!items) return '';\n  return `<div class=\"cfg-array\">\n    <div class=\"cfg-array-title\">\n      <span>${escHtml(title)}<\/span>\n      <span class=\"cfg-array-count\">${arr.length} item${arr.length!==1?'s':''}<\/span>\n    <\/div>\n    ${items}\n  <\/div>`;\n}\n\nfunction countLeafFields(v) {\n  if (v === null || v === undefined) return 1;\n  if (Array.isArray(v)) return v.reduce((sum, item) => sum + countLeafFields(item), 0);\n  if (typeof v === 'object') return Object.values(v).reduce((sum, child) => sum + countLeafFields(child), 0);\n  return 1;\n}\n\n\/\/ Preferred display order for config sections (keys handled both as camelCase and snake_case)\nconst CONFIG_SECTION_ORDER = [\n  'system', 'line', 'motor', 'motorAutotune', 'ios',\n  'alarms', 'dateTime', 'network', 'ble', 'modbus',\n  'bacnet', 'ethernetIp', 'display', 'pids', 'softwareUpdate',\n];\nfunction _canonKey(k) { return String(k).toLowerCase().replace(\/[_-]\/g, ''); }\nfunction orderedConfigKeys(configs) {\n  const all = Object.keys(configs || {});\n  const byCanon = {};\n  for (const k of all) byCanon[_canonKey(k)] = k;\n  const out = [];\n  const used = new Set();\n  for (const p of CONFIG_SECTION_ORDER) {\n    const k = byCanon[_canonKey(p)];\n    if (k && !used.has(k)) { out.push(k); used.add(k); }\n  }\n  for (const k of all) if (!used.has(k)) out.push(k);\n  return out;\n}\n\nfunction initConfigViewer(data, filename) {\n  configData = data;\n  configSearchQuery = '';\n  $('#cfg-search').value = '';\n\n  $('#config-filename').textContent = filename;\n  const meta = data._metadata || {};\n  $('#config-device').textContent = meta.deviceModel || '\u2014';\n  const versionStr = meta.version ? (meta.version + (meta.minorVersion ? '.' + meta.minorVersion : '')) : '\u2014';\n  $('#config-version').textContent = versionStr;\n\n  \/\/ Strip nanosecond precision from ISO timestamps for readability\n  \/\/ '2026-04-20T18:15:38.736411323Z' -> '2026-04-20T18:15:38Z'\n  const fmtTs = s => typeof s === 'string' ? s.replace(\/\\.\\d+Z$\/, 'Z') : s;\n  const lastUpdated = meta.lastUpdateTime || meta.lastUpdateDate || '';\n  const extra = meta.extraInformation || {};\n\n  const metaEl = $('#cfg-metadata');\n  const metaItems = [\n    { label: 'Device Model',     value: meta.deviceModel },\n    { label: 'Serial Number',    value: meta.deviceSerialNumber },\n    { label: 'Product',          value: extra.product_name },\n    { label: 'Vendor',           value: extra.vendor_name },\n    { label: 'Software Version', value: meta.deviceSoftwareVersion },\n    { label: 'Backup Version',   value: meta.deviceBackupVersion },\n    { label: 'Config Version',   value: meta.version ? versionStr : null },\n    { label: 'Label',            value: meta.label },\n    { label: 'Description',      value: meta.description },\n    { label: 'Created',          value: fmtTs(meta.creationDate) },\n    { label: 'Last Updated',     value: fmtTs(lastUpdated) },\n  ].filter(m => m.value !== undefined && m.value !== null && m.value !== '');\n\n  const CFG_META_KEY = { 'Device Model':'cfg.m.model','Serial Number':'cfg.m.serial','Product':'cfg.m.product','Vendor':'cfg.m.vendor','Software Version':'cfg.m.sw','Backup Version':'cfg.m.backup','Config Version':'cfg.m.cfgver','Label':'cfg.m.label','Description':'cfg.m.desc','Created':'cfg.m.created','Last Updated':'cfg.m.updated' };\n  metaEl.innerHTML = metaItems.map(m => {\n    const _k = CFG_META_KEY[m.label];\n    const _a = _k ? ` data-i18n=\"${_k}\"` : '';\n    const _l = (_k && typeof t === 'function') ? t(_k) : m.label;\n    return `\n    <div class=\"cfg-meta-item\">\n      <div class=\"cfg-meta-label\"${_a}>${escHtml(_l)}<\/div>\n      <div class=\"cfg-meta-value\">${escHtml(String(m.value))}<\/div>\n    <\/div>`;\n  }).join('');\n\n  const configs = data.configurations || {};\n  const sectionNames = orderedConfigKeys(configs);\n  configActiveSection = sectionNames[0] || null;\n  renderConfigSidebar(configs);\n  renderConfigMain();\n  if (typeof applyLang === 'function') applyLang(LANG);\n  updateConfigClock();\n}\n\nfunction renderConfigSidebar(configs) {\n  const nav = $('#cfg-nav');\n  nav.innerHTML = orderedConfigKeys(configs).map(key => {\n    const count = countLeafFields(configs[key]);\n    const active = key === configActiveSection ? 'active' : '';\n    return `<div class=\"cfg-nav-item ${active}\" data-section=\"${escHtml(key)}\">\n      <span>${escHtml(humanizeKey(key))}<\/span>\n      <span class=\"cfg-nav-count\">${count}<\/span>\n    <\/div>`;\n  }).join('');\n\n  nav.querySelectorAll('.cfg-nav-item').forEach(el => {\n    el.addEventListener('click', () => {\n      configActiveSection = el.getAttribute('data-section');\n      nav.querySelectorAll('.cfg-nav-item').forEach(n => n.classList.remove('active'));\n      el.classList.add('active');\n      renderConfigMain();\n      $('#cfg-main').scrollTop = 0;\n      updateConfigExportSectionLabel();\n    });\n  });\n  updateConfigExportSectionLabel();\n}\n\nfunction updateConfigExportSectionLabel() {\n  const lbl = $('#config-export-section-name');\n  if (!lbl) return;\n  lbl.textContent = configActiveSection ? humanizeKey(configActiveSection) : '\u2014 no section selected';\n}\n\nfunction renderConfigMain() {\n  const main = $('#cfg-main');\n  if (!configData || !configActiveSection) {\n    main.innerHTML = '<div class=\"cfg-no-data\">No configuration loaded.<\/div>';\n    return;\n  }\n  const section = configData.configurations[configActiveSection];\n  const title = humanizeKey(configActiveSection);\n  let body = '';\n  if (section === null || section === undefined) {\n    body = '<div class=\"cfg-no-data\">This section is empty.<\/div>';\n  } else if (Array.isArray(section)) {\n    body = renderConfigArray(section, title);\n  } else if (typeof section === 'object') {\n    body = renderConfigGroup(section, null);\n  } else {\n    body = `<div class=\"cfg-group\"><div class=\"cfg-group-title\">${escHtml(title)}<\/div>\n      <div style=\"padding:10px 14px\">${renderConfigValue(section)}<\/div><\/div>`;\n  }\n  if (!body || body.trim() === '') {\n    body = `<div class=\"cfg-no-data\">No fields match \"${escHtml(configSearchQuery)}\".<\/div>`;\n  }\n  main.innerHTML = `\n    <div class=\"cfg-section-title\">${escHtml(title)}<\/div>\n    <div class=\"cfg-section-path\">configurations.${escHtml(configActiveSection)}<\/div>\n    ${body}`;\n}\n\nfunction updateConfigClock() {\n  const el = $('#config-current-time');\n  if (el) el.textContent = new Date().toUTCString().replace(' GMT','Z');\n}\nsetInterval(() => {\n  if ($('#configViewer') && $('#configViewer').classList.contains('active')) updateConfigClock();\n}, 1000);\n\n$('#cfg-search').addEventListener('input', debounce((e) => {\n  configSearchQuery = e.target.value.trim();\n  renderConfigMain();\n}, 180));\n\n\/\/ ---------- Shell event wiring ----------\n$('#dropBtn').addEventListener('click', () => { const fi = $('#fileInput'); fi.accept = FULL_ACCEPT; state.landingIntent = null; fi.click(); });\n$('#unloadBtn').addEventListener('click', resetView);\n$$('#healthViewer .hd-section.collapsible .hd-section-head').forEach(head => {\n  head.addEventListener('click', () => {\n    \/\/ VFD Information \/ Network \/ Mobile Application expand and collapse together.\n    const clicked = head.closest('.hd-section.collapsible');\n    const expand = clicked.classList.contains('collapsed');   \/\/ currently collapsed \u2192 expand all three\n    $$('#healthViewer .hd-section.collapsible').forEach(sec => sec.classList.toggle('collapsed', !expand));\n  });\n});\ndocument.addEventListener('click', (ev) => {\n  const head = ev.target.closest('.mv-section.collapsible .mv-section-head');\n  if (head) head.closest('.mv-section.collapsible').classList.toggle('collapsed');\n});\n$('#fileInput').addEventListener('change', (e) => {\n  const f = e.target.files[0];\n  if (f) loadPackage(f);\n  e.target.value = '';\n  e.target.accept = FULL_ACCEPT;\n});\n$('#copyBtn').addEventListener('click', copyActiveFile);\n$('#downloadBtn').addEventListener('click', downloadActiveFile);\n$('#wrapBtn').addEventListener('click', () => { state.wrap = !state.wrap; applyWrap(); });\n$('#viewModeStructured').addEventListener('click', () => { state.fileViewMode = 'structured'; applyFileViewMode(); });\n$('#viewModeRaw').addEventListener('click',        () => { state.fileViewMode = 'raw';        applyFileViewMode(); });\n$('#codeSearchInput').addEventListener('input', (e) => runCodeSearch(e.target.value));\n$('#codeSearchInput').addEventListener('keydown', (e) => {\n  if (e.key === 'Escape') { e.target.value = ''; runCodeSearch(''); }\n});\n\nconst dz = $('#dropZone');\n['dragenter', 'dragover'].forEach(ev => {\n  document.addEventListener(ev, (e) => { e.preventDefault(); dz.classList.add('dragging'); });\n});\n['dragleave', 'drop'].forEach(ev => {\n  document.addEventListener(ev, (e) => {\n    e.preventDefault();\n    if (ev === 'drop') {\n      dz.classList.remove('dragging');\n      const f = e.dataTransfer.files && e.dataTransfer.files[0];\n      if (f) loadPackage(f);\n    } else if (e.target === document || e.relatedTarget === null) {\n      dz.classList.remove('dragging');\n    }\n  });\n});\n\nconst emptyStateEl = $('#emptyState');\nif (emptyStateEl) emptyStateEl.addEventListener('click', (e) => {\n  const btn = e.target.closest('[data-lc-action]');\n  if (!btn) return;\n  const action = btn.getAttribute('data-lc-action');\n  if (action === 'new') { startFromScratch(); return; }\n  const fi = $('#fileInput');\n  state.landingIntent = btn.getAttribute('data-lc-tab') || null;\n  fi.accept = btn.getAttribute('data-lc-accept') || FULL_ACCEPT;\n  fi.click();\n});\n\n\/\/ ---------- Top navigation bar + support-package sub-menu (v1.0.9) ----------\nfunction _tnFileAvailable(id){ const t = TAB_DEFS.find(x => x.id === id); return t ? !!getFileForTab(t) : false; }\n\nfunction renderTopNav(){\n  const pkg = state.loadKind === 'package';\n  const mode = state.topMode || 'home';\n  $$('#tnMenu .tn-item').forEach(el => el.classList.toggle('active', el.dataset.nav === mode));\n  const sub = $('#subNav');\n  if (sub){\n    if (pkg && mode === 'package'){\n      sub.style.display = 'flex';\n      $$('#subNav .sn-item').forEach(el => {\n        const avail = _tnFileAvailable(el.dataset.sub);\n        el.classList.toggle('active', state.activeTabId === el.dataset.sub);\n        el.classList.toggle('sn-missing', !avail);\n        el.disabled = !avail;\n      });\n    } else {\n      sub.style.display = 'none';\n    }\n  }\n}\n\nfunction topNavOpenPicker(accept, intent, mode){\n  const fi = $('#fileInput');\n  state.pendingTopMode = mode || null;\n  state.landingIntent = intent || null;\n  fi.accept = accept || FULL_ACCEPT;\n  fi.click();\n}\n\nfunction topNavGo(nav){\n  switch (nav){\n    case 'home':\n      state.topMode = 'home';\n      resetView();\n      break;\n    case 'create':\n      startFromScratch();\n      state.topMode = 'create';\n      break;\n    case 'edit':\n      if (state.configLoaded && _tnFileAvailable('editor')){ state.topMode = 'edit'; activateTab('editor'); }\n      else topNavOpenPicker('.json,application\/json', 'editor', 'edit');\n      break;\n    case 'viewreport':\n      if (_tnFileAvailable('config')){ state.topMode = (state.loadKind === 'package' ? 'package' : 'config'); activateTab('config'); }\n      else topNavOpenPicker('.json,application\/json', 'config', 'config');\n      break;\n    case 'audit':\n      if (_tnFileAvailable('audit')){ state.topMode = (state.loadKind === 'package' ? 'package' : 'audit'); activateTab('audit'); }\n      else topNavOpenPicker('.json,application\/json', 'audit', 'audit');\n      break;\n    case 'package':\n      if (state.loadKind === 'package'){ state.topMode = 'package'; activateTab(_tnFileAvailable('health') ? 'health' : 'config'); }\n      else topNavOpenPicker('.tar.gz,.tgz,.zip,.gz', 'health', 'package');\n      break;\n  }\n  renderTopNav();\n}\n\n(function(){\n  const menu = $('#tnMenu');\n  if (menu) menu.addEventListener('click', (e) => {\n    const btn = e.target.closest('.tn-item'); if (!btn) return;\n    topNavGo(btn.dataset.nav);\n  });\n  const sub = $('#subNav');\n  if (sub) sub.addEventListener('click', (e) => {\n    const btn = e.target.closest('.sn-item'); if (!btn || btn.disabled) return;\n    state.topMode = 'package';\n    activateTab(btn.dataset.sub);\n  });\n  renderTopNav();\n})();\n\n\/\/ ---------- Edit Config: thin upload \/ drag-drop strip (v1.0.9) ----------\n(function(){\n  const ed = $('#editorDrop');\n  if (!ed) return;\n  const openPick = () => {\n    state.pendingTopMode = 'edit';\n    state.landingIntent = 'editor';\n    const fi = $('#fileInput');\n    fi.accept = '.json,application\/json';\n    fi.click();\n  };\n  ed.addEventListener('click', openPick);\n  ed.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openPick(); } });\n  ['dragenter','dragover'].forEach(evt => ed.addEventListener(evt, (e) => { e.preventDefault(); ed.classList.add('dragging'); }));\n  ed.addEventListener('dragleave', () => ed.classList.remove('dragging'));\n  ed.addEventListener('drop', (e) => {\n    e.preventDefault(); e.stopPropagation();           \/\/ stopPropagation: prevent the global drop handler from double-loading\n    ed.classList.remove('dragging');\n    const f = e.dataTransfer.files && e.dataTransfer.files[0];\n    if (f) { state.landingIntent = 'editor'; state.pendingTopMode = 'edit'; loadPackage(f); }\n  });\n})();\n\nconst I18N = {\"rawfx.nodisp\": {\"en\": \"This file type can't be displayed as raw text (listed only).\", \"fr\": \"Ce type de fichier ne peut pas \u00eatre affich\u00e9 en texte brut (list\u00e9 seulement).\"}, \"rawfx.title\": {\"en\": \"Raw package files\", \"fr\": \"Fichiers bruts du dossier\"}, \"rawfx.empty\": {\"en\": \"No text files in this package.\", \"fr\": \"Aucun fichier texte dans ce dossier.\"}, \"rawfx.more\": {\"en\": \"View other raw files (JSON \/ LOG)\", \"fr\": \"Voir les autres fichiers bruts (JSON \/ LOG)\"}, \"editor.reset\": {\"en\": \"Reset\", \"fr\": \"R\u00e9initialiser\"}, \"editor.preset\": {\"en\": \"Preset Application Config.\", \"fr\": \"Config. d'application pr\u00e9d\u00e9finie\"}, \"editor.save\": {\"en\": \"Save JSON\", \"fr\": \"Enregistrer JSON\"}, \"editor.wizCancel\": {\"en\": \"Cancel\", \"fr\": \"Annuler\"}, \"editor.wizTitle\": {\"en\": \"Preset Application Configuration \\u2014 Select Application\", \"fr\": \"Configuration d'application pr\\u00e9d\\u00e9finie \\u2014 S\\u00e9lectionner l'application\"}, \"editor.wizDesc\": {\"en\": \"Choose the closest application. Key parameters will be set and <strong style=\\\"color:var(--t)\\\">highlighted in the editor<\/strong> for review.\", \"fr\": \"Choisissez l'application la plus proche. Les param\\u00e8tres cl\\u00e9s seront d\\u00e9finis et <strong style=\\\"color:var(--t)\\\">surlign\\u00e9s dans l'\\u00e9diteur<\/strong> pour r\\u00e9vision.\"}, \"editor.presetTitle\": {\"en\": \"Apply a preset application configuration\", \"fr\": \"Appliquer une configuration d'application pr\\u00e9d\\u00e9finie\"}, \"editor.wizApply\": {\"en\": \"Apply & Edit\", \"fr\": \"Appliquer et modifier\"}, \"editor.disclaimer\": {\"en\": \"<b>DISCLAIMER:<\/b> This editor is provided \\\"as is\\\" without support. It does not validate parameter ranges or inter-dependencies. Always review all settings against official SmartD documentation before use. Save the configuration, copy it to a USB drive as <code>smartdrive-config.json<\/code>, and import it into the Clean Power VFD.\", \"fr\": \"<b>AVERTISSEMENT :<\/b> Cet \u00e9diteur est fourni \u00ab tel quel \u00bb, sans support. Il ne valide pas les plages de param\u00e8tres ni les interd\u00e9pendances. V\u00e9rifiez toujours tous les r\u00e9glages par rapport \u00e0 la documentation officielle SmartD avant utilisation. Enregistrez la configuration, copiez-la sur une cl\u00e9 USB sous le nom <code>smartdrive-config.json<\/code>, puis importez-la dans le VFD Clean Power.\"}, \"cfg.m.model\": {\"en\": \"Model\", \"fr\": \"Mod\u00e8le\"}, \"cfg.m.serial\": {\"en\": \"Serial Number\", \"fr\": \"Num\u00e9ro de s\u00e9rie\"}, \"cfg.m.product\": {\"en\": \"Product\", \"fr\": \"Produit\"}, \"cfg.m.vendor\": {\"en\": \"Vendor\", \"fr\": \"Fournisseur\"}, \"cfg.m.sw\": {\"en\": \"Software Version\", \"fr\": \"Version logicielle\"}, \"cfg.m.backup\": {\"en\": \"Backup Version\", \"fr\": \"Version de secours\"}, \"cfg.m.cfgver\": {\"en\": \"Config Version\", \"fr\": \"Version de configuration\"}, \"cfg.m.label\": {\"en\": \"Label\", \"fr\": \"Libell\u00e9\"}, \"cfg.m.desc\": {\"en\": \"Description\", \"fr\": \"Description\"}, \"cfg.m.created\": {\"en\": \"Created\", \"fr\": \"Cr\u00e9\u00e9 le\"}, \"cfg.m.updated\": {\"en\": \"Last Updated\", \"fr\": \"Derni\u00e8re mise \u00e0 jour\"}, \"audit.orderLatest\": {\"en\": \"Order: latest first\", \"fr\": \"Ordre: plus r\u00e9cente\"}, \"audit.orderOldest\": {\"en\": \"Order: oldest first\", \"fr\": \"Ordre: plus ancienne\"}, \"audit.tsColumn\": {\"en\": \"Timestamp\", \"fr\": \"Horodatage\"}, \"report.driveName\": {\"en\": \"VFD Name\", \"fr\": \"Nom du VFD\"}, \"report.configName\": {\"en\": \"Configuration Name\", \"fr\": \"Nom de configuration\"}, \"hd.k.driveName\": {\"en\": \"VFD name\", \"fr\": \"Nom du VFD\"}, \"hd.k.deviceModel\": {\"en\": \"Model\", \"fr\": \"Mod\u00e8le\"}, \"hd.k.serial\": {\"en\": \"Serial number\", \"fr\": \"Num\u00e9ro de s\u00e9rie\"}, \"hd.k.curFw\": {\"en\": \"Current firmware\", \"fr\": \"Micrologiciel actuel\"}, \"hd.k.backupFw\": {\"en\": \"Backup firmware\", \"fr\": \"Micrologiciel de secours\"}, \"hd.k.ctrlMode\": {\"en\": \"Control mode\", \"fr\": \"Mode de contr\u00f4le\"}, \"hd.k.remoteSrc\": {\"en\": \"Remote control source\", \"fr\": \"Source de contr\u00f4le distante\"}, \"hd.k.method\": {\"en\": \"Method\", \"fr\": \"M\u00e9thode\"}, \"hd.k.ip\": {\"en\": \"IP address\", \"fr\": \"Adresse IP\"}, \"hd.k.mask\": {\"en\": \"Subnet mask\", \"fr\": \"Masque de sous-r\u00e9seau\"}, \"hd.k.gateway\": {\"en\": \"Gateway\", \"fr\": \"Passerelle\"}, \"hd.k.dns\": {\"en\": \"DNS resolvers\", \"fr\": \"R\u00e9solveurs DNS\"}, \"hd.k.mac0\": {\"en\": \"MAC address \u00b7 Eth0\", \"fr\": \"Adresse MAC \u00b7 Eth0\"}, \"hd.k.mac1\": {\"en\": \"MAC address \u00b7 Eth1\", \"fr\": \"Adresse MAC \u00b7 Eth1\"}, \"hd.k.bleMac\": {\"en\": \"BLE MAC address\", \"fr\": \"Adresse MAC BLE\"}, \"hd.k.bleState\": {\"en\": \"BLE state\", \"fr\": \"\u00c9tat BLE\"}, \"hd.k.mobilePerm\": {\"en\": \"Mobile permissions\", \"fr\": \"Autorisations mobiles\"}, \"hd.k.curUptime\": {\"en\": \"Runtime\", \"fr\": \"Temps de fonctionnement\"}, \"hd.k.bootCount\": {\"en\": \"Boot count\", \"fr\": \"Nombre de d\u00e9marrages\"}, \"hd.k.lifeRun\": {\"en\": \"Runtime\", \"fr\": \"Temps de fonctionnement\"}, \"hd.k.vfdStarts\": {\"en\": \"VFD start commands\", \"fr\": \"Commandes de marche VFD\"}, \"hd.k.vfdRun\": {\"en\": \"VFD runtime\", \"fr\": \"Temps de marche VFD\"}, \"hd.k.vfdLifeStarts\": {\"en\": \"VFD start commands\", \"fr\": \"Commandes de marche VFD\"}, \"hd.k.vfdLifeRun\": {\"en\": \"VFD runtime\", \"fr\": \"Temps de marche VFD\"}, \"hd.k.motLifeStarts\": {\"en\": \"Motor start commands\", \"fr\": \"Commandes de marche moteur\"}, \"hd.k.motLifeRun\": {\"en\": \"Motor runtime\", \"fr\": \"Temps de marche moteur\"}, \"hd.k.motRunCur\": {\"en\": \"Motor runtime\", \"fr\": \"Temps de marche moteur\"}, \"hd.k.autoRestart\": {\"en\": \"Auto-restart attempts\", \"fr\": \"Tentatives de red\u00e9marrage automatique\"}, \"hd.k.fan\": {\"en\": \"Fan\", \"fr\": \"Ventilateur\"}, \"hd.k.used\": {\"en\": \"used\", \"fr\": \"utilis\u00e9\"}, \"hd.k.runtime\": {\"en\": \"Runtime:\", \"fr\": \"Temps de marche :\"}, \"hd.k.remaining\": {\"en\": \"Remaining:\", \"fr\": \"Restant :\"}, \"hd.k.totalLife\": {\"en\": \"Total life:\", \"fr\": \"Dur\u00e9e de vie totale :\"}, \"hd.k.lastReset\": {\"en\": \"Last reset:\", \"fr\": \"Derni\u00e8re r\u00e9init. :\"}, \"hd.k.lastUpdated\": {\"en\": \"Last updated\", \"fr\": \"Derni\u00e8re mise \u00e0 jour\"}, \"hd.k.setpointsUpdated\": {\"en\": \"Setpoints last updated\", \"fr\": \"Consignes mises \u00e0 jour\"}, \"hd.k.success\": {\"en\": \"Success:\", \"fr\": \"R\u00e9ussis :\"}, \"hd.k.current\": {\"en\": \"Current:\", \"fr\": \"Actuels :\"}, \"hd.device\": {\"en\": \"VFD Information\", \"fr\": \"Informations sur le VFD\"}, \"hd.network\": {\"en\": \"Network\", \"fr\": \"R\u00e9seau\"}, \"hd.mobile\": {\"en\": \"Mobile Application\", \"fr\": \"Application Mobile\"}, \"hd.recentAlarms\": {\"en\": \"Recent Alarms \u00b7 Last 10 Triggered (Audit Log)\", \"fr\": \"Alarmes r\u00e9centes \u00b7 10 derni\u00e8res d\u00e9clench\u00e9es (journal d'audit)\"}, \"hd.alarmHistory\": {\"en\": \"Alarm History \u00b7 Occurrences\", \"fr\": \"Historique des alarmes \u00b7 occurrences\"}, \"hd.system\": {\"en\": \"System\", \"fr\": \"Syst\u00e8me\"}, \"hd.sub.sinceBoot\": {\"en\": \"Since last VFD boot\", \"fr\": \"Depuis le dernier d\u00e9marrage du VFD\"}, \"hd.sub.vfdLifetime\": {\"en\": \"VFD Lifetime\", \"fr\": \"Dur\u00e9e de vie du VFD\"}, \"hd.motorctl\": {\"en\": \"Operational Data\", \"fr\": \"Donn\u00e9es op\u00e9rationnelles\"}, \"hd.cooling\": {\"en\": \"Cooling (Fan)\", \"fr\": \"Refroidissement (ventilateur)\"}, \"nav.home\": {\"en\": \"Reset\", \"fr\": \"R\u00e9initialiser\"}, \"nav.create\": {\"en\": \"Create Config\", \"fr\": \"Cr\u00e9er config\"}, \"nav.edit\": {\"en\": \"Edit Config\", \"fr\": \"Modifier config\"}, \"nav.viewreport\": {\"en\": \"View Config & Report\", \"fr\": \"Voir config et rapport\"}, \"nav.audit\": {\"en\": \"View Audit Log\", \"fr\": \"Voir journal d'audit\"}, \"nav.package\": {\"en\": \"View Support Package\", \"fr\": \"Voir dossier de support\"}, \"sub.summary\": {\"en\": \"Summary\", \"fr\": \"Sommaire\"}, \"sub.config\": {\"en\": \"Configuration\", \"fr\": \"Configuration\"}, \"sub.audit\": {\"en\": \"Audit Log\", \"fr\": \"Journal d'audit\"}, \"home.title\": {\"en\": \"SmartD - Clean Power VFD Tools Suite\", \"fr\": \"SmartD - Suite d'outils du Clean Power VFD\"}, \"home.desc\": {\"en\": \"Access configuration tools, audit logs and support package.\", \"fr\": \"Acc\u00e9dez aux outils de configuration, du journal d'audit ou du dossier de support.\"}, \"home.drop\": {\"en\": \"Drag & Drop a file here\", \"fr\": \"Glissez-d\u00e9posez un fichier ici\"}, \"home.supported\": {\"en\": \"Supported files: Configuration JSON, Audit Log JSON, Support Package\", \"fr\": \"Fichiers pris en charge : Configuration JSON, Journal d'audit JSON, Dossier de support\"}, \"editor.drop\": {\"en\": \"Drag &amp; drop a configuration <b>.json<\/b> here, or <span class=\\\"ce-dropbar-link\\\">click to upload<\/span> a file to edit\", \"fr\": \"Glissez-d\u00e9posez une configuration <b>.json<\/b> ici, ou <span class=\\\"ce-dropbar-link\\\">cliquez pour t\u00e9l\u00e9verser<\/span> un fichier \u00e0 modifier\"}, \"cfg.device\": {\"en\": \"Model:\", \"fr\": \"Mod\u00e8le :\"}, \"cfg.version\": {\"en\": \"Version:\", \"fr\": \"Version :\"}, \"cfg.searchFields\": {\"en\": \"Search fields\u2026\", \"fr\": \"Rechercher des champs\u2026\"}, \"cfg.commReport\": {\"en\": \"Commissioning Report\", \"fr\": \"Rapport de mise en service\"}, \"audit.title\": {\"en\": \"Audit Log\", \"fr\": \"Journal d'audit\"}, \"audit.view\": {\"en\": \"View\", \"fr\": \"Affichage\"}, \"audit.timerange\": {\"en\": \"Time Range (UTC)\", \"fr\": \"Plage horaire (UTC)\"}, \"audit.highlight\": {\"en\": \"Highlight color\", \"fr\": \"Couleur de surlignage\"}, \"audit.search\": {\"en\": \"Search\", \"fr\": \"Recherche\"}, \"audit.formatted\": {\"en\": \"Formatted\", \"fr\": \"Format\u00e9\"}, \"audit.raw\": {\"en\": \"Raw\", \"fr\": \"Brut\"}, \"audit.showunspec\": {\"en\": \"Show Unspecified Events\", \"fr\": \"Afficher les \u00e9v\u00e9nements non sp\u00e9cifi\u00e9s\"}, \"audit.clear\": {\"en\": \"Clear Filters\", \"fr\": \"Effacer les filtres\"}, \"audit.first\": {\"en\": \"\u2191 First Entry\", \"fr\": \"\u2191 Premi\u00e8re entr\u00e9e\"}, \"audit.last\": {\"en\": \"\u2193 Last Entry\", \"fr\": \"\u2193 Derni\u00e8re entr\u00e9e\"}, \"audit.evtype\": {\"en\": \"Event Type\", \"fr\": \"Type d'\u00e9v\u00e9nement\"}, \"audit.severity\": {\"en\": \"Severity\", \"fr\": \"Gravit\u00e9\"}, \"audit.origin\": {\"en\": \"Origin\", \"fr\": \"Origine\"}, \"audit.user\": {\"en\": \"User\", \"fr\": \"Utilisateur\"}, \"audit.tid\": {\"en\": \"Transaction ID\", \"fr\": \"ID de transaction\"}, \"audit.message\": {\"en\": \"Message\", \"fr\": \"Message\"}, \"report.title\": {\"en\": \"Commissioning Report\", \"fr\": \"Rapport de mise en service\"}, \"report.desc\": {\"en\": \"Generate an IEEE 3007.2 \u00b7 IEC 61800 \u00b7 NEMA MG-1 commissioning report from the loaded configuration. Pick the sections, add commissioning details, and export as PDF or XML.\", \"fr\": \"G\u00e9n\u00e9rez un rapport de mise en service IEEE 3007.2 \u00b7 IEC 61800 \u00b7 NEMA MG-1 \u00e0 partir de la configuration charg\u00e9e. S\u00e9lectionnez les sections, ajoutez les d\u00e9tails de mise en service, puis exportez en PDF ou XML.\"}, \"report.open\": {\"en\": \"Open Report Generator\", \"fr\": \"Ouvrir le g\u00e9n\u00e9rateur de rapport\"}, \"metrics.title\": {\"en\": \"Metrics\", \"fr\": \"Mesures\"}, \"ft.searchfile\": {\"en\": \"Search in file...\", \"fr\": \"Rechercher dans le fichier...\"}};\nlet LANG = 'en';\nfunction t(key){ const e = I18N[key]; return e ? (e[LANG] || e.en) : key; }\nfunction applyLang(lang){\n  LANG = (lang === 'fr') ? 'fr' : 'en';\n  $$('[data-i18n]').forEach(el => { const k = el.getAttribute('data-i18n'); if (I18N[k]) el.textContent = t(k); });\n  $$('[data-i18n-ph]').forEach(el => { const k = el.getAttribute('data-i18n-ph'); if (I18N[k]) el.setAttribute('placeholder', t(k)); });\n  $$('[data-i18n-html]').forEach(el => { const k = el.getAttribute('data-i18n-html'); if (I18N[k]) el.innerHTML = t(k); });\n  $$('[data-i18n-title]').forEach(el => { const k = el.getAttribute('data-i18n-title'); if (I18N[k]) el.title = t(k); });\n  $$('h3[data-sec-en]').forEach(el => { const en=el.getAttribute('data-sec-en'); const fr=el.getAttribute('data-sec-fr'); el.textContent=(LANG==='fr'&&fr)?fr:en; });\n  $$('.fld-t[data-fld-en]').forEach(el => { const en=el.getAttribute('data-fld-en'); const fr=el.getAttribute('data-fld-fr'); el.textContent=(LANG==='fr'&&fr)?fr:en; });\n  const av = $('#tnAvatar'); if (av) av.textContent = (LANG === 'fr' ? 'FR' : 'EN');\n  if (typeof auditUpdateTimestampHeader === 'function') auditUpdateTimestampHeader();\n  if (SMARTD_SPV_ROOT) SMARTD_SPV_ROOT.setAttribute('lang', LANG);\n}\n(function(){\n  const av = $('#tnAvatar');\n  if (av){\n    av.removeAttribute('aria-hidden');\n    av.setAttribute('role','button');\n    av.setAttribute('tabindex','0');\n    av.title = 'Language \/ Langue \u2014 English \/ Fran\u00e7ais';\n    const toggle = () => applyLang(LANG === 'en' ? 'fr' : 'en');\n    av.addEventListener('click', toggle);\n    av.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } });\n  }\n  applyLang('en');\n})();\n\n\/\/ ---------- Raw package files popup (v1.0.9) ----------\nfunction _rawfxBytes(n){ n=+n||0; return n<1024 ? n+' B' : (n<1048576 ? (n\/1024).toFixed(1)+' KB' : (n\/1048576).toFixed(2)+' MB'); }\nfunction openRawFilesModal(){\n  const ov = $('#rawFilesOverlay'); if(!ov) return;\n  const list=$('#rawFilesList'), pre=$('#rawFilePre'), nameEl=$('#rawFileName'), sizeEl=$('#rawFileSize');\n  const canView = (name)=>\/\\.(json|log)$\/i.test(name||'');\n  \/\/ List EVERY file in the package; only .json \/ .log are viewable, others are listed but not displayable.\n  const files = (state.allFiles || []).slice().sort((a,b)=>a.name.localeCompare(b.name));\n  list.innerHTML=''; pre.textContent=''; nameEl.textContent='\\u2014'; sizeEl.textContent='';\n  if(!files.length){\n    list.innerHTML = '<div class=\"rawfx-empty\">'+escapeHtml(t('rawfx.empty'))+'<\/div>';\n  } else {\n    list.innerHTML = files.map((f,i)=>`<button type=\"button\" class=\"rawfx-item${canView(f.name)?'':' rawfx-nodisp'}\" data-idx=\"${i}\" title=\"${escapeHtml(f.path||f.name)}\">${escapeHtml(f.name)}<\/button>`).join('');\n    const show=(i)=>{ const f=files[i]; if(!f) return;\n      Array.from(list.children).forEach((c,ci)=>{ if(c.classList) c.classList.toggle('active', ci===i); });\n      nameEl.textContent=f.name; sizeEl.textContent=_rawfxBytes(f.size);\n      if(canView(f.name)){ pre.classList.remove('rawfx-pre-msg'); pre.textContent=f.content||''; }\n      else { pre.classList.add('rawfx-pre-msg'); pre.textContent=t('rawfx.nodisp'); }\n      pre.scrollTop=0; };\n    Array.from(list.children).forEach(c=>c.addEventListener('click', ()=>show(+c.getAttribute('data-idx'))));\n    let first = files.findIndex(f=>canView(f.name)); if(first<0) first=0;\n    show(first);\n  }\n  ov.style.display='flex';\n}\nfunction closeRawFilesModal(){ const ov=$('#rawFilesOverlay'); if(ov) ov.style.display='none'; }\n(function(){\n  const more=$('#snMore'); if(more) more.addEventListener('click', openRawFilesModal);\n  const ov=$('#rawFilesOverlay'); if(ov) ov.addEventListener('click', e=>{ if(e.target===ov) closeRawFilesModal(); });\n  const cl=$('#rawFilesClose'); if(cl) cl.addEventListener('click', closeRawFilesModal);\n  document.addEventListener('keydown', e=>{ if(e.key==='Escape'){ const o=$('#rawFilesOverlay'); if(o && o.style.display!=='none') closeRawFilesModal(); } });\n})();\n\ndocument.addEventListener('keydown', (e) => {\n  if ((e.ctrlKey || e.metaKey) && e.key === 'f') {\n    const av = $('#auditViewer');\n    const cv = $('#configViewer');\n    if ($('#fileView').style.display !== 'none') {\n      e.preventDefault(); $('#codeSearchInput').focus(); $('#codeSearchInput').select();\n    } else if (av && av.classList.contains('active')) {\n      e.preventDefault(); const s = $('#auditSearch'); if (s) { s.focus(); s.select(); }\n    } else if (cv && cv.classList.contains('active')) {\n      e.preventDefault(); const s = $('#cfg-search'); if (s) { s.focus(); s.select(); }\n    }\n  }\n});\n\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\/\/ COMMISSIONING REPORT MODULE\n\/\/ Ported from SD_VFD_Commissioning_Report_Generator v1.3\n\/\/ Self-contained IIFE \u2014 exposes only window.openCommissioningReport(cfg).\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n(function(){\n  \/\/ Local $ that mirrors CR's convention (bare element ID, no # prefix).\n  \/\/ This is scoped so it does NOT shadow the SP's outer $ (which takes CSS selectors).\n  const $ = id => document.getElementById(id);\n  \/\/ No-op stubs \u2014 the ported CR code refers to a progress bar that the SP modal doesn't have.\n  const setProgress = () => {};\n\n  let cfg = null;\n  let fmt = 'pdf';\n\nconst REPORT_SECTIONS = [\n  { n: 1,  title: 'Project Information',     defaultOn: true },\n  { n: 2,  title: 'Equipment Information',   defaultOn: true },\n  { n: 3,  title: 'Motor Information',       defaultOn: true },\n  { n: 4,  title: 'Line Information',        defaultOn: true },\n  { n: 5,  title: 'Network & Communications',defaultOn: true },\n  { n: 6,  title: 'I\/O Configuration',       defaultOn: true },\n  { n: 7,  title: 'Active Alarms',           defaultOn: true },\n  { n: 8,  title: 'PID Configuration',       defaultOn: true },\n  { n: 9,  title: 'Motor Autotune',          defaultOn: true },\n  { n: 10, title: 'Display & Localization',  defaultOn: true },\n  { n: 11, title: 'System Configuration',    defaultOn: true },\n  { n: 12, title: 'Function Verification',   defaultOn: true },\n  { n: 13, title: 'Issues & Warnings',       defaultOn: true },\n  { n: 14, title: 'Commissioning Status',    defaultOn: true },\n];\n\/\/ Section numbers currently selected for export. Pre-populated on chip render.\nlet selectedSections = new Set(REPORT_SECTIONS.filter(s=>s.defaultOn).map(s=>s.n));\n\/\/ Returns true when section `n` should be emitted into the current export.\nconst inc = n => selectedSections.has(n);\n\n\n  \/\/ NOTE: `selectedSections` is declared above by the extracted CR section block.\n\nfunction fmtDateISO(iso){\n  if(!iso||iso.startsWith('1970'))return'N\/A';\n  const d=new Date(iso),p=n=>String(n).padStart(2,'0');\n  return`${p(d.getMonth()+1)}\/${p(d.getDate())}\/${d.getFullYear()} ${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;\n}\nfunction fmtDateLong(iso){\n  if(!iso)return'\u2014';\n  const months=['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];\n  const d=new Date(iso+'T00:00:00');\n  return`${months[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`;\n}\n\n\nfunction stripEnum(v){ return enumLabel(v); }\n\n\/\/ \u2500\u2500\u2500 Enum labeling \u2014 authoritative to smartd_api 2026-07-02 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Full set of configuration\/alarm enum prefixes from the 2026-07-02 spec (longest\n\/\/ first for correct longest-match). enumLabel() strips the type prefix and renders\n\/\/ a clean, acronym-aware short label used across the preview, PDF and XML output.\nconst CONFIG_ENUM_PREFIXES = [\n  'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_',\n  'MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_',\n  'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_',\n  'MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_',\n  'MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_',\n  'MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_',\n  'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_',\n  'MOTOR_CONTROLLER_LIMITS_SWITCHOVER_MODE_',\n  'MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_',\n  'MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_',\n  'CONFIGURATION_IMPORT_EXPORT_LOCATION_',\n  'MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_',\n  'MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_',\n  'SYSTEM_SWITCH_CONTROL_MODE_BEHAVIOR_',\n  'DISPLAY_USER_ACTIVITY_REQUEST_TYPE_',\n  'MOTOR_CONTROLLER_AUTO_RESTART_MODE_',\n  'AUDIT_LOG_TIMESTAMP_PRESET_FILTER_',\n  'IO_DIGITAL_INPUTS_POWER_DIRECTION_',\n  'MOTOR_CONTROLLER_CURVE_SELECTABLE_',\n  'MOTOR_CONTROLLER_FOC_SPEED_SOURCE_',\n  'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_',\n  'MOTOR_CONTROLLER_START_BEHAVIOUR_',\n  'MOTOR_AUTOTUNE_PARAMETER_STATUS_',\n  'SUPPORT_PACKAGE_EXPORT_LOCATION_',\n  'MOTOR_CONTROLLER_VF_START_MODE_',\n  'SYSTEM_CONTROL_SOURCE_SELECTOR_',\n  'IO_INCREMENTAL_ENCODER_WIRING_',\n  'MOTOR_AUTOTUNE_PARAMETER_TYPE_',\n  'MOTOR_CONTROLLER_COOLING_TYPE_',\n  'MOTOR_CONTROLLER_FOC_ECO_MODE_',\n  'SOFTWARE_UPDATE_SOURCE_STATUS_',\n  'SUPPORT_PACKAGE_EXPORT_STATUS_',\n  'DISPLAY_USER_ACTIVITY_SOURCE_',\n  'NETWORK_CONFIGURATION_METHOD_',\n  'SOFTWARE_UPDATE_FETCH_STATUS_',\n  'SYSTEM_EMERGENCY_MODE_ACTION_',\n  'SYSTEM_REMOTE_CONTROL_SOURCE_',\n  'MOTOR_CONTROLLER_LIMIT_CODE_',\n  'MOTOR_CONTROLLER_POWER_UNIT_',\n  'SOFTWARE_UPDATE_SOURCE_TYPE_',\n  'SYSTEM_EMERGENCY_MODE_STATE_',\n  'AUTH_ADMIN_PASSWORD_STATUS_',\n  'DATE_TIME_UPDATER_PROTOCOL_',\n  'MOTOR_CONTROLLER_DIRECTION_',\n  'DISPLAY_SCREENSAVER_STATE_',\n  'IO_ANALOG_OUTPUT_FUNCTION_',\n  'IO_DIGITAL_INPUT_FUNCTION_',\n  'CONFIGURATION_PROFILE_ID_',\n  'CONTROL_INTERFACE_PRESET_',\n  'CONTROL_SUPERVISOR_STATE_',\n  'DISPLAY_SCREENSAVER_MODE_',\n  'DISPLAY_SCREENSHOT_STATE_',\n  'IO_ANALOG_INPUT_FUNCTION_',\n  'MOTOR_CONTROLLER_COMMAND_',\n  'MOTOR_CONTROLLER_DISABLE_',\n  'SYSTEM_MOBILE_PERMISSION_',\n  'AUDIT_LOG_EXPORT_STATUS_',\n  'NETWORK_INTERFACE_STATE_',\n  'MOTOR_CONTROLLER_PAUSE_',\n  'PID_FORCED_OUTPUT_TYPE_',\n  'SOFTWARE_UPDATE_RESULT_',\n  'SOFTWARE_UPDATE_STATUS_',\n  'BLE_ADVERTISING_STATE_',\n  'IO_ANALOG_OUTPUT_MODE_',\n  'IO_ANALOG_OUTPUT_UNIT_',\n  'MOTOR_AUTOTUNE_METHOD_',\n  'MOTOR_CONTROLLER_MODE_',\n  'SYSTEM_API_LAN_ACCESS_',\n  'SYSTEM_RUN_MODE_STATE_',\n  'IO_ANALOG_INPUT_MODE_',\n  'IO_ANALOG_INPUT_UNIT_',\n  'MOTOR_AUTOTUNE_STATE_',\n  'PID_ENGINEERING_UNIT_',\n  'SOFTWARE_UPDATE_STEP_',\n  'ALARM_SENSOR_SOURCE_',\n  'NETWORK_SWITCH_MODE_',\n  'SYSTEM_CONTROL_MODE_',\n  'USER_PIN_SET_STATUS_',\n  'PID_SETPOINTS_TYPE_',\n  'COOLING_FAN_STATE_',\n  'IO_RELAY_FUNCTION_',\n  'NETWORK_PING_MODE_',\n  'ALARM_BOOT_STATE_',\n  'PID_CONTROL_MODE_',\n  'IO_RELAY_OUTPUT_',\n  'IO_TRIGGER_TYPE_',\n  'PID_OUTPUT_MODE_',\n  'ALARM_SEVERITY_',\n  'AUTH_LOCK_MODE_',\n  'DISPLAY_FILTER_',\n  'BLE_TEST_MODE_',\n  'ALARM_ACTION_',\n  'COOLING_MODE_',\n  'ALARM_STATE_',\n  'LINE_PHASES_',\n  'ALARM_CODE_',\n  'BLE_STATUS_',\n  'PID_INDEX_',\n  'PID_STATE_',\n  'IO_STATE_'\n];\nconst ENUM_ACRONYMS = {\n  FOC:'FOC', PID:'PID', RPM:'RPM', RMS:'RMS', DC:'DC', AC:'AC', IO:'I\/O',\n  IP:'IP', IPV4:'IPv4', IPV6:'IPv6', DNS:'DNS', DHCP:'DHCP', MAC:'MAC',\n  BLE:'BLE', BACNET:'BACnet', PTC:'PTC', HVAC:'HVAC', STO:'STO', AFE:'AFE',\n  LVRT:'LVRT', MTPA:'MTPA', NTP:'NTP', PSI:'PSI', GPM:'GPM', CFS:'CFS',\n  CFM:'CFM', HP:'HP', MA:'mA', KOHM:'k\\u03A9', HMI:'HMI', USB:'USB', LAN:'LAN',\n  V:'V', F:'F', W:'W'\n};\nconst ENUM_TOKEN_RE = \/^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+$\/;\n\/\/ Exact-value overrides where the conventional label is a symbol\/abbreviation.\nconst ENUM_VALUE_LABELS = {\n  PID_ENGINEERING_UNIT_UNSPECIFIED:'\\u2014', PID_ENGINEERING_UNIT_PASCAL:'Pa',\n  PID_ENGINEERING_UNIT_BAR:'Bar', PID_ENGINEERING_UNIT_CUBIC_METER_PER_SECOND:'m\\u00B3\/s',\n  PID_ENGINEERING_UNIT_CUBIC_METER_PER_HOUR:'m\\u00B3\/h', PID_ENGINEERING_UNIT_LITER_PER_SECOND:'L\/s',\n  PID_ENGINEERING_UNIT_METER:'m', PID_ENGINEERING_UNIT_CUSTOM:'Custom',\n  PID_ENGINEERING_UNIT_POUND_PER_SQUARE_INCH:'PSI', PID_ENGINEERING_UNIT_GALLON_PER_MINUTE:'GPM',\n  PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_SECOND:'CFS', PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_MINUTE:'CFM',\n  PID_ENGINEERING_UNIT_FOOT:'ft',\n  MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_UNSPECIFIED:'Class 10 (default)',\n  MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_5:'Class 5', MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_10:'Class 10',\n  MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_20:'Class 20', MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_30:'Class 30',\n  MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_40:'Class 40', MOTOR_CONTROLLER_OVERLOAD_CLASS_TYPE_DISABLED:'Disabled'\n};\nfunction enumLabel(v){\n  if(v==null||v==='') return '\\u2014';\n  const str=String(v);\n  if(ENUM_VALUE_LABELS[str]!==undefined) return ENUM_VALUE_LABELS[str];\n  if(!ENUM_TOKEN_RE.test(str)) return str;      \/\/ not an enum token (durations, models, plain text)\n  let tail=str;\n  for(const p of CONFIG_ENUM_PREFIXES){ if(str.startsWith(p)){ tail=str.slice(p.length); break; } }\n  const words=tail.split('_').filter(Boolean).map(w=>{\n    if(ENUM_ACRONYMS[w]!==undefined) return ENUM_ACRONYMS[w];\n    if(\/^\\d+$\/.test(w)) return w;\n    return w.charAt(0)+w.slice(1).toLowerCase();\n  });\n  let out=words.join(' ').trim()\n    .replace(\/\\bV F\\b\/g,'V\/F')\n    .replace(\/\\b2 Wire\\b\/g,'2-Wire').replace(\/\\b3 Wire\\b\/g,'3-Wire')\n    .replace(\/\\b4 20 mA\\b\/g,'4-20 mA').replace(\/\\b0 10 V\\b\/g,'0-10 V');\n  return out||str;\n}\n\n\/\/ Determine ramp rows for section 3.3 based on control mode\nfunction getRampRows(algo, ramps, kv) {\n  const mode = String(algo?.controlMode||'').toUpperCase();\n  const r = ramps||{};\n  if (mode.includes('TORQUE')) {\n    return [\n      kv('Torque Ramp Up', (r.torqueRampUpCurves||[])[0]?.linear?.rampDuration||'\u2014'),\n      kv('Torque Ramp Down', (r.torqueRampDownCurves||[])[0]?.linear?.rampDuration||'\u2014', true),\n    ].join('');\n  } else if (mode.includes('VELOCITY') || mode.includes('SPEED')) {\n    return [\n      kv('Velocity Ramp Up', (r.velocityRampUpCurves||[])[0]?.linear?.rampDuration||'\u2014'),\n      kv('Velocity Ramp Down', (r.velocityRampDownCurves||[])[0]?.linear?.rampDuration||'\u2014', true),\n    ].join('');\n  } else {\n    \/\/ Default: V\/F Frequency\n    return [\n      kv('Frequency Ramp Up', (r.frequencyRampUpCurves||[])[0]?.linear?.rampDuration||'\u2014'),\n      kv('Frequency Ramp Down', (r.frequencyRampDownCurves||[])[0]?.linear?.rampDuration||'\u2014', true),\n    ].join('');\n  }\n}\n\n\/\/ Same for XML\nfunction getRampXML(algo, ramps, tagFn, openFn, closeFn) {\n  const mode = String(algo?.controlMode||'').toUpperCase();\n  const r = ramps||{};\n  let x = '';\n  if (mode.includes('TORQUE')) {\n    x+=openFn('TorqueRampUp',null,3); x+=tagFn('Duration',(r.torqueRampUpCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('TorqueRampUp',3);\n    x+=openFn('TorqueRampDown',null,3); x+=tagFn('Duration',(r.torqueRampDownCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('TorqueRampDown',3);\n  } else if (mode.includes('VELOCITY') || mode.includes('SPEED')) {\n    x+=openFn('VelocityRampUp',null,3); x+=tagFn('Duration',(r.velocityRampUpCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('VelocityRampUp',3);\n    x+=openFn('VelocityRampDown',null,3); x+=tagFn('Duration',(r.velocityRampDownCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('VelocityRampDown',3);\n  } else {\n    x+=openFn('FrequencyRampUp',null,3); x+=tagFn('Duration',(r.frequencyRampUpCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('FrequencyRampUp',3);\n    x+=openFn('FrequencyRampDown',null,3); x+=tagFn('Duration',(r.frequencyRampDownCurves||[])[0]?.linear?.rampDuration||'',null,4); x+=closeFn('FrequencyRampDown',3);\n  }\n  return x;\n}\n\n\nasync function generatePDF(c,f){\n  const meta=c._metadata||{},conf=c.configurations||{};\n  const sys=conf.system||{},mot=conf.motor||{};\n  const mnp=mot.motorNameplate||{},ramps=mot.ramps||{};\n  const algo=mot.algorithm||{},vf=algo.constantVf||{},foc=algo.foc||{};\n  const opts=mot.options||{},therm=mot.thermalProtection||{};\n  const autoR=mot.autoRestart||{},emRun=mot.emergencyRun||{};\n  const presets=mot.presets||{},limits=mot.limits||{};\n  const advParams=mot.advancedParameters||{};\n  const line=conf.line||{},net=conf.network||{},mb=conf.modbus||{},bn=conf.bacnet||{};\n  const ios=conf.ios||{};\n  const di=[...(ios.digitalInputs||[])].sort((a,b)=>a.index-b.index);\n  const relays=[...(ios.relays||[])].sort((a,b)=>a.index-b.index);\n  const ai=[...(ios.analogInputs||[])].sort((a,b)=>a.index-b.index);\n  const ao=[...(ios.analogOutputs||[])].sort((a,b)=>a.index-b.index);\n  const enc=ios.incrementalEncoder||{};\n  const activeAlarms=(conf.alarms?.configs||[]).filter(a=>!a.disabled);\n  const pid0=(conf.pids?.instances||[])[0]||{};\n  const atd=conf.motorAutotune?.dynamic||{};\n  const disp=conf.display||{},loc=disp.localizationConfiguration||{},ss=disp.screensaverConfiguration||{};\n  const ble=conf.ble||{},dt=conf.dateTime||{},softUpd=conf.softwareUpdate||{};\n  const cm=sys.controlMode||{},em=sys.emergencyMode||{};\n  const iface=(net.interfaces||[])[0]||{};\n  \/\/ Firmware version: prefer running deviceSoftwareVersion (set by newer configs).\n  \/\/ Fall back to the config schema version (v + minor) used by older configs.\n  const fwRunning=meta.deviceSoftwareVersion||'';\n  const fwSchema=meta.version?(meta.version+(meta.minorVersion?'.'+meta.minorVersion:'')):'';\n  const fw=fwRunning||fwSchema||'\u2014';\n  \/\/ Whitelabel info from newer configs (extraInformation block).\n  const extra=meta.extraInformation||{};\n  \/\/ Last-update timestamp: newer configs use `lastUpdateTime`, older used `lastUpdateDate`.\n  const lastUpdate=meta.lastUpdateTime||meta.lastUpdateDate||'';\n  const s=stripEnum;\n  const b=v=>v?'Yes':'No';\n  const curvePts=pts=>(pts||[]).map(p=>`(${p.x}\u2192${p.y})`).join('  ')||'\u2014';\n  const commDate=f.date?fmtDateLong(f.date):'\u2014';\n\n  const G='#163F2D',BLK='#101010',GR='#5f6b63',LG='#A6A6A6',GL='#F0FFF5',GM='#DDFBE7',DK='#1B1B1B';\n  const kv=(k,v,alt=false)=>`<tr class=\"${alt?'alt':''}\"><td class=\"lbl\">${k}<\/td><td class=\"val\">${v!=null&&v!==''?v:'&nbsp;'}<\/td><\/tr>`;\n  const sec=(n,t)=>`<h2 class=\"sec-head\"><span class=\"sec-num\">${n}<\/span>${t}<\/h2>`;\n  const sub=t=>`<h3 class=\"sub-head\">${t}<\/h3>`;\n  const th=(...cols)=>`<tr>${cols.map(c=>`<th>${c}<\/th>`).join('')}<\/tr>`;\n\n  const rampRows = getRampRows(algo, ramps, kv);\n\n  const verItems=['Power-up sequence and HMI display verified','Motor rotation direction \u2014 Forward','Motor rotation direction \u2014 Reverse','Forward command DI-3 (Rising Edge)','Reverse command DI-4 (Rising Edge)','Stop command DI-5 (High Level)','Emergency mode DI-1 (High Level)','Lockout reset DI-2 (Rising Edge)','Speed reference selection DI-6','Analog Input 1 (0\u201310V) calibrated at 0V and 10V','Analog Input 2 PTC sensor verified','Analog Output 1 (4\u201320mA) calibrated at 1Hz and 60Hz','Analog Output 2 (4\u201320mA) calibrated at 0A and 35A','Encoder feedback 1600 PPR AB wiring','Relay 1 \u2014 Any Alarm output tested','Relay 2 \u2014 Motor Ready to Run tested','Relay 3 \u2014 Motor Running tested','Overspeed alarm triggered & cleared','Overload alarm triggered & cleared','Underspeed alarm triggered & cleared','Encoder speed error alarm tested','Thermal overload protection verified','External Start Feedback Timeout tested',`BACnet communication verified (ID ${bn.deviceId} \/ Port ${bn.port})`,'Start ramp and stop ramp measured','Preset frequencies verified','Emergency run (30Hz Forward via DI-1) tested','PID disabled state & default setpoint confirmed','Auto-restart disabled confirmed','Low voltage ride-through tested','BLE connectivity verified via mobile app'];\n\n  const html=`<!DOCTYPE html><html><head><meta charset=\"UTF-8\"\/>\n<style>\n@page{size:letter;margin:0.55in 0.5in 0.55in 0.5in}html{-webkit-print-color-adjust:exact;print-color-adjust:exact}\n*{box-sizing:border-box;margin:0;padding:0}\nbody{font-family:Arial,sans-serif;font-size:9.5pt;color:${DK}}\n.cover{min-height:9in;display:flex;flex-direction:column;justify-content:center;padding:0.5in 0;page-break-after:always}\n.cover-logo{height:52px;width:auto;margin-bottom:28px;display:block}\n.cover-title{font-size:26pt;font-weight:700;color:${DK};line-height:1.2;margin-bottom:8px}\n.cover-std{font-size:11pt;color:${GR};font-style:italic;margin-bottom:36px}\n.cover-table{width:100%;border-collapse:collapse}\n.cover-table td{padding:9px 12px;font-size:10pt;border-bottom:1px solid #ddd}\n.cover-table td:first-child{font-weight:700;color:${DK};background:${GM};width:38%}\n.cover-table td:last-child{color:${DK}}\n.cover-conf{font-size:8.5pt;color:${LG};font-style:italic;margin-top:32px}\n.sec-head{font-size:16pt;font-weight:700;color:${G};border-bottom:2.5px solid ${G};padding-bottom:5px;margin:24px 0 10px;page-break-after:avoid}\n.sec-num{display:inline-block;background:${G};color:#fff;font-size:10pt;border-radius:4px;padding:1px 8px;margin-right:10px;vertical-align:middle}\n.sub-head{font-size:11pt;font-weight:700;color:${G};margin:16px 0 6px;page-break-after:avoid}\ntable{width:100%;border-collapse:collapse;margin-bottom:10px;font-size:9pt}\ntd,th{padding:6px 10px;vertical-align:top}\n.kv-tbl td.lbl{width:38%;font-weight:700;background:${GM};color:${DK};border-bottom:1px solid #cde8d6}\n.kv-tbl td.val{background:#fff;color:${DK};border-bottom:1px solid #e8e8e8}\n.kv-tbl tr.alt td.lbl{background:${GM}}\n.kv-tbl tr.alt td.val{background:${GL}}\n.data-tbl th{background:${G};color:#fff;font-size:8.5pt;font-weight:700;text-align:left;padding:6px 10px}\n.data-tbl td{border-bottom:1px solid #e0e0e0;font-size:8.5pt}\n.data-tbl tr:nth-child(even) td{background:${GL}}\n.ver-tbl th{background:${G};color:#fff;font-size:8pt;font-weight:700}\n.ver-tbl td{border-bottom:1px solid #e0e0e0;font-size:8.5pt;padding:5px 8px}\n.ver-tbl tr:nth-child(even) td{background:${GL}}\n.check-cell{white-space:nowrap;color:${LG};font-size:8pt}\n.iss-tbl th{background:${G};color:#fff;font-size:8pt;font-weight:700}\n.iss-tbl td{border-bottom:1px solid #e0e0e0;font-size:8.5pt;padding:6px 8px}\n.iss-tbl tr:nth-child(even) td{background:${GL}}\n.iss-num{font-weight:700;color:${G}}\n.status-box{border:2px solid ${G};border-radius:8px;padding:16px 20px;margin:10px 0}\n.status-checks{display:flex;gap:32px;margin:12px 0;font-size:11pt}\n.checkbox{width:16px;height:16px;border:2px solid ${G};border-radius:3px;display:inline-block;flex-shrink:0;vertical-align:middle;margin-right:6px}\n.sig-row{display:flex;gap:24px;margin-top:12px}\n.sig-field{flex:1}\n.sig-label{font-size:8pt;color:${GR};margin-bottom:4px}\n.sig-line{border-bottom:1.5px solid #ccc;height:24px}\n.disclaimer{font-size:7.5pt;color:${LG};font-style:italic;margin-top:20px;padding-top:10px;border-top:1px solid #eee}\n.note{font-size:8.5pt;color:${GR};font-style:italic;margin-bottom:8px}\n<\/style><\/head><body>\n\n<div class=\"cover\">\n  <div class=\"cover-title\">Clean Power VFD Commissioning Report<\/div>\n  <div class=\"cover-std\">IEEE 3007.2  \u00b7  IEC 61800  \u00b7  NEMA MG-1<\/div>\n  <table class=\"cover-table\">\n    <tr><td>VFD Name<\/td><td>${sys.driveName||'\u2014'}<\/td><\/tr>\n    <tr><td>Configuration Name<\/td><td>${sys.configName||meta.label||'\u2014'}<\/td><\/tr>\n    <tr><td>Model<\/td><td>${meta.deviceModel||'\u2014'}<\/td><\/tr>\n    <tr><td>Serial Number<\/td><td>${crSerial(c)||'\u2014'}<\/td><\/tr>\n    <tr><td>Firmware Version<\/td><td>${fw}<\/td><\/tr>\n    <tr><td>Commissioning Date<\/td><td>${commDate}<\/td><\/tr>\n    <tr><td>Technician<\/td><td>${f.tech||'\u2014'}<\/td><\/tr>\n    <tr><td>Company<\/td><td>${f.company||'\u2014'}<\/td><\/tr>\n    <tr><td>Site \/ Location<\/td><td>${f.location||'\u2014'}<\/td><\/tr>\n    <tr><td>Work Order \/ PO<\/td><td>${f.wo||'\u2014'}<\/td><\/tr>\n  <\/table>\n  <div class=\"cover-conf\">CONFIDENTIAL \u2014 For Authorized Personnel Only<\/div>\n<\/div>\n\n${inc(1)?`${sec(1,'Project Information')}\n<table class=\"kv-tbl\">\n  ${kv('VFD Name',sys.driveName||'\u2014')}\n  ${kv('Configuration Name',sys.configName||'\u2014',true)}\n  ${kv('Config Label',meta.label||'\u2014')}\n  ${kv('Commissioning Date',commDate,true)}\n  ${kv('Config Created',fmtDateISO(meta.creationDate))}\n  ${kv('Config Last Updated',fmtDateISO(lastUpdate),true)}\n  ${kv('Model',meta.deviceModel||'\u2014')}\n  ${kv('Firmware Version',fw,true)}\n  ${kv('Technician Name',f.tech||'\u2014')}\n  ${kv('Company',f.company||'\u2014',true)}\n  ${kv('Location',f.location||'\u2014')}\n  ${kv('Work Order \/ PO',f.wo||'\u2014',true)}\n<\/table>`:''}\n\n${inc(2)?`${sec(2,'Equipment Information')}\n<table class=\"kv-tbl\">\n  ${kv('VFD Model',meta.deviceModel||'\u2014')}\n  ${kv('Serial Number',crSerial(c)||'\u2014',true)}\n  ${kv('Vendor',extra.vendor_name||'\u2014')}\n  ${kv('Firmware Version (running)',fw,true)}\n  ${kv('Backup Firmware Version',meta.deviceBackupVersion||'\u2014')}\n  ${kv('Config Schema Version',fwSchema||'\u2014',true)}\n  ${kv('VFD Name',sys.driveName||'\u2014')}\n  ${kv('BLE Advertising State',s(ble.advertisingState),true)}\n  ${kv('BLE Pairing Timeout',ble.pairingTimeout||'0s')}\n<\/table>`:''}\n\n${inc(3)?`${sec(3,'Motor Information')}\n${sub('3.1  Motor Nameplate')}\n<table class=\"kv-tbl\">\n  ${kv('Nominal Power',`${mnp.nominalPower||'?'} ${s(mnp.nominalPowerUnit)}`)}\n  ${kv('Nominal Current',`${mnp.nominalCurrent||'?'} A`,true)}\n  ${kv('Nominal Voltage',`${mnp.nominalVoltage||'?'} V`)}\n  ${kv('Nominal Speed',`${mnp.nominalVelocity||'?'} RPM`,true)}\n  ${kv('Nominal Frequency',`${mnp.nominalInputFrequency||'?'} Hz`)}\n  ${kv('Min \/ Max Input Frequency',`${mnp.minInputFrequency||'?'} Hz  \/  ${mnp.maxInputFrequency||'?'} Hz`,true)}\n  ${kv('Number of Poles',mnp.numberOfPoles||'?')}\n  ${kv('Power Factor',mnp.powerFactor||'?',true)}\n  ${kv('Phase Inversion',b(mnp.phaseInversion))}\n  ${kv('Time Between Starts',mnp.timeBetweenStarts||'0s',true)}\n  ${kv('Brand \/ Reference',`${mnp.brand||'\u2014'} \/ ${mnp.reference||'\u2014'}`)}\n<\/table>\n${sub('3.2  Control Algorithm')}\n<table class=\"kv-tbl\">\n  ${kv('Control Mode',s(algo.controlMode))}\n  ${kv('V\/F Overmodulation',b(vf.enableOvermodulation),true)}\n  ${kv('Closed Loop Control',b(vf.enableClosedLoopControl))}\n  ${kv('Start Mode',s(vf.start?.mode),true)}\n  ${kv('Start Torque Boost',s(vf.startTorqueBoost?.state))}\n  ${kv('FOC Speed Response',s(foc.speedControllerResponse),true)}\n  ${kv('FOC Speed Source',s(foc.speedSource))}\n  ${kv('FOC Eco Mode',s(foc.ecoMode),true)}\n<\/table>\n${sub('3.3  Ramps')}\n<table class=\"kv-tbl\">${rampRows}<\/table>\n${sub('3.4  Motor Options & Protection')}\n<table class=\"kv-tbl\">\n  ${kv('Reverse Enabled',b(opts.reverseEnabled))}\n  ${kv('Normal Stop',s(opts.normalStopBehaviour),true)}\n  ${kv('Zero Speed Behaviour',s(opts.zeroSpeedBehaviour))}\n  ${kv('Low Voltage Ride-Through',s(opts.lowVoltageRideThrough),true)}\n  ${kv('Bumpless Direction Change',s(opts.bumplessDirectionChange))}\n  ${kv('Voltage Drop Compensation',s(opts.voltageDropCompensations),true)}\n  ${kv('Cooling Type',s(therm.coolingType))}\n  ${kv('Nominal Load',`${therm.nominalLoad||100}%`,true)}\n  ${kv('Temp at Power-Up',s(therm.temperatureAtPowerUp))}\n  ${kv('Overload Class Type',s(therm.overloadClassType),true)}\n  ${kv('Thermal Time Constant',therm.thermalTimeConstant!=null?`${therm.thermalTimeConstant} s`:'\u2014')}\n  ${kv('Auto Restart Mode',s(autoR.mode),true)}\n  ${kv('Restart Delay \/ Max Retries',`${autoR.restartDelay||'\u2014'}  \/  ${autoR.maximumRetries??'\u2014'}`)}\n<\/table>\n${sub('3.5  Preset Frequencies')}\n<table class=\"kv-tbl\">\n  ${(presets.frequencies||[]).map((f2,i)=>kv(`Preset ${i+1}`,`${f2} Hz`,i%2===1)).join('')}\n<\/table>\n${sub('3.6  Emergency Run')}\n<table class=\"kv-tbl\">\n  ${kv('Frequency',`${emRun.frequency||'?'} Hz`)}\n  ${kv('Direction',s(emRun.direction),true)}\n<\/table>\n${sub('3.7  Motor Limits')}\n<table class=\"kv-tbl\">\n  ${kv('Max Current',`${limits.maxCurrent?.threshold??0} A`)}\n  ${kv('Max Motoring \/ Generating Power',`${limits.maxMotoringPower?.threshold??0} W  \/  ${limits.maxGeneratingPower?.threshold??0} W`,true)}\n  ${kv('Min \/ Max Velocity',`${limits.minVelocity?.threshold??0} RPM  \/  ${limits.maxVelocity?.threshold??0} RPM`)}\n  ${kv('Low \/ High Torque',`${limits.lowTorque?.threshold??0}  \/  ${limits.highTorque?.threshold??0}`,true)}\n  ${kv('Limits Switchover',s(mot.limitsSwitchover?.mode))}\n<\/table>\n${sub('3.8  Motor Static Autotuning Parameters')}\n<table class=\"kv-tbl\">\n  ${kv('Stator Resistance',advParams.statorResistance!=null?`${advParams.statorResistance} \u03a9`:'\u2014')}\n  ${kv('Rotor Resistance',advParams.rotorResistance!=null?`${advParams.rotorResistance} \u03a9`:'\u2014',true)}\n  ${kv('Magnetizing Inductance',advParams.magnetizingInductance!=null?`${advParams.magnetizingInductance} H`:'\u2014')}\n  ${kv('Leakage Inductance',advParams.leakageInductance!=null?`${advParams.leakageInductance} H`:'\u2014',true)}\n  ${kv('Moment of Inertia',advParams.momentOfInertia!=null?`${advParams.momentOfInertia} kg\u00b7m\u00b2`:'\u2014')}\n<\/table>`:''}\n\n${inc(4)?`${sec(4,'Line Information')}\n<table class=\"kv-tbl\">\n  ${kv('Nominal Voltage',`${line.nominalVoltage||'?'} V`)}\n  ${kv('Phases',String(line.phases||'').includes('THREE')?'Three-Phase (3\u03c6)':s(line.phases),true)}\n  ${kv('Line Frequency',`${line.frequency||'?'} Hz`)}\n<\/table>`:''}\n\n${inc(5)?`${sec(5,'Network & Communications')}\n${sub('5.1  Ethernet')}\n<table class=\"kv-tbl\">\n  ${kv('IP Configuration',s(iface.method))}\n  ${kv('IPv4 Address',iface.ipv4Address||'(DHCP)',true)}\n  ${kv('Network Mask',iface.ipv4NetworkMask||'(DHCP)')}\n  ${kv('Gateway',iface.ipv4Gateway||'(DHCP)',true)}\n  ${kv('DNS Resolvers',(net.dns?.resolvers||[]).join(', ')||'\u2014')}\n  ${kv('Ping Mode',s(net.ping?.mode),true)}\n<\/table>\n${sub('5.2  Modbus')}\n<table class=\"kv-tbl\">${kv('Modbus Active',b(mb.active))}<\/table>\n${sub('5.3  BACnet')}\n<table class=\"kv-tbl\">\n  ${kv('BACnet Active',b(bn.active))}\n  ${kv('Device ID',bn.deviceId??'\u2014',true)}\n  ${kv('Device Name',bn.deviceName||'\u2014')}\n  ${kv('UDP Port',bn.port??47808,true)}\n<\/table>\n${sub('5.4  EtherNet\/IP')}\n<table class=\"kv-tbl\">${kv('EtherNet\/IP',conf.ethernetIp!=null?'Configured':'Not Configured')}<\/table>`:''}\n\n${inc(6)?`${sec(6,'I\/O Configuration')}\n${sub('6.1  Digital Inputs  \u2014  Power Direction: '+s(ios.digitalInputsPowerDirection))}\n<table class=\"data-tbl\">\n  ${th('Index','Function','Trigger Type','Debounce')}\n  ${di.map(d=>`<tr><td>${d.index}<\/td><td>${s(d.function)}<\/td><td>${s(d.triggerType)}<\/td><td>${d.debounceDelay||'\u2014'}<\/td><\/tr>`).join('')}\n<\/table>\n${sub('6.2  Relay Outputs')}\n<table class=\"data-tbl\">\n  ${th('Index','Function','Default Output State')}\n  ${relays.map(r=>`<tr><td>${r.index}<\/td><td>${s(r.function)}<\/td><td>${s(r.output)}<\/td><\/tr>`).join('')}\n<\/table>\n${sub('6.3  Analog Inputs')}\n<table class=\"data-tbl\">\n  ${th('Index','Function','Mode','Curve Points')}\n  ${ai.map(a=>`<tr><td>${a.index}<\/td><td>${s(a.function)}<\/td><td>${s(a.mode)}<\/td><td>${curvePts(a.curvePoints)}<\/td><\/tr>`).join('')}\n<\/table>\n${sub('6.4  Analog Outputs')}\n<table class=\"data-tbl\">\n  ${th('Index','Function','Mode','Curve Points')}\n  ${ao.map(a=>`<tr><td>${a.index}<\/td><td>${s(a.function)}<\/td><td>${s(a.mode)}<\/td><td>${curvePts(a.curvePoints)}<\/td><\/tr>`).join('')}\n<\/table>\n${sub('6.5  Incremental Encoder')}\n<table class=\"kv-tbl\">\n  ${kv('Pulses per Revolution',enc.pulsesPerRevolution||'\u2014')}\n  ${kv('Wiring Mode',s(enc.wiring),true)}\n<\/table>`:''}\n\n${inc(7)?`${sec(7,'Alarm Configuration')}\n<p class=\"note\">Only enabled alarms are listed. Disabled alarms are excluded from this report.<\/p>\n${activeAlarms.length>0?`<table class=\"data-tbl\">\n  ${th('Alarm Code','Trig. Threshold','Trig. Delay','Reset Threshold','Reset Delay','Action')}\n  ${activeAlarms.map(a=>`<tr>\n    <td>${String(a.code||'').replace('ALARM_CODE_','').replace(\/_\/g,' ')}<\/td>\n    <td>${a.triggerThreshold!=null?a.triggerThreshold:'\u2014'}<\/td>\n    <td>${a.triggerDelay||'\u2014'}<\/td>\n    <td>${a.resetThreshold!=null?a.resetThreshold:'\u2014'}<\/td>\n    <td>${a.resetDelay||'\u2014'}<\/td>\n    <td>${(a.triggerActions||[]).map(t=>s(t.action)).join(', ')||'None'}<\/td>\n  <\/tr>`).join('')}\n<\/table>`:`<p class=\"note\">No alarms are currently enabled.<\/p>`}`:''}\n\n${inc(8)?`${sec(8,'PID Configuration')}\n<table class=\"kv-tbl\">\n  ${kv('Enabled',b(pid0.enable))}\n  ${kv('Proportional Gain (kP)',pid0.kP??'\u2014',true)}\n  ${kv('Integration Time (tI)',pid0.tI??'\u2014')}\n  ${kv('Derivative Time (tD)',pid0.tD??'\u2014',true)}\n  ${kv('Control Mode',s(pid0.controlMode))}\n  ${kv('Min \/ Max Process Value',`${pid0.minProcessValue??0}  \/  ${pid0.maxProcessValue??100}`,true)}\n  ${kv('Min \/ Max Control Output',`${pid0.minControlOutput??0}  \/  ${pid0.maxControlOutput??100}`)}\n  ${kv('Default Setpoint',pid0.defaultSetpoint??'\u2014',true)}\n  ${kv('Deadband Value \/ Delay',`${pid0.deadbandValue??0}  \/  ${pid0.deadbandDelay||'0s'}`)}\n  ${kv('Sensor Offset',pid0.sensorOffset??0,true)}\n<\/table>`:''}\n\n${inc(9)?`${sec(9,'Motor Autotune')}\n<table class=\"kv-tbl\">\n  ${kv('Min Speed',`${atd.minSpeed??'\u2014'}%`)}\n  ${kv('Max Speed',`${atd.maxSpeed??'\u2014'}%`,true)}\n  ${kv('Max Torque',`${atd.maxTorque??'\u2014'}%`)}\n  ${kv('Rise \/ Fall Time',atd.riseFallTime||'\u2014',true)}\n<\/table>`:''}\n\n${inc(10)?`${sec(10,'Display & Localization')}\n<table class=\"kv-tbl\">\n  ${kv('Screen Brightness',`${disp.screenBrightness??100}%`)}\n  ${kv('Language \/ Country',`${loc.languageCode||'\u2014'} \/ ${loc.countryCode||'\u2014'}`,true)}\n  ${kv('Screensaver Timeout',ss.timeout||'\u2014')}\n  ${kv('Screensaver Mode',s(ss.mode),true)}\n<\/table>`:''}\n\n${inc(11)?`${sec(11,'System Configuration')}\n<table class=\"kv-tbl\">\n  ${kv('Configuration Name',sys.configName||'\u2014')}\n  ${kv('VFD Name',sys.driveName||'\u2014',true)}\n  ${kv('Drive Notes',sys.driveNotes||'\u2014')}\n  ${kv('Control Source',s(cm.controlSourceSelector),true)}\n  ${kv('Remote Source 1',s(cm.remoteControlSource1))}\n  ${kv('Remote Source 2',s(cm.remoteControlSource2),true)}\n  ${kv('Emergency Mode State',s(em.state))}\n  ${kv('Emergency Mode Action',s(em.action),true)}\n  ${kv('Mobile Permissions',s(sys.mobilePermissions))}\n  ${kv('API LAN Access',s(sys.apiLanAccess),true)}\n  ${kv('Software Update Source',s(softUpd.selectedSource))}\n  ${kv('Timezone',dt.timezone||'\u2014',true)}\n  ${kv('BLE Sync Disabled',b(dt.disableBleSynchronization))}\n<\/table>`:''}\n\n${inc(12)?`<div style=\"page-break-before:always\"><\/div>${sec(12,'Function Verification')}\n<p class=\"note\">All items must be verified on-site by a qualified commissioning technician.<\/p>\n<table class=\"ver-tbl\">\n  ${th('Verification Item','Observed Value \/ Measurement','Result')}\n  ${verItems.map(item=>`<tr><td>${item}<\/td><td style=\"min-width:130px\">&nbsp;<\/td><td class=\"check-cell\" style=\"min-width:115px\">\u2610 Pass &nbsp; \u2610 Fail &nbsp; \u2610 N\/A<\/td><\/tr>`).join('')}\n<\/table>`:''}\n\n${inc(13)?`<div style=\"page-break-before:always\"><\/div>${sec(13,'Issues & Warnings')}\n<p class=\"note\">Record all issues and observations identified during commissioning.<\/p>\n<table class=\"iss-tbl\">\n  ${th('#','Severity','Description','Status \/ Resolution')}\n  ${[1,2,3,4,5,6,7,8,9,10].map(n=>`<tr><td class=\"iss-num\" style=\"width:28px\">${n}<\/td><td style=\"width:80px\">&nbsp;<\/td><td>&nbsp;<\/td><td style=\"width:160px\">&nbsp;<\/td><\/tr>`).join('')}\n<\/table>`:''}\n\n${inc(14)?`${sec(14,'Commissioning Status')}\n<div class=\"status-box\">\n  <div style=\"font-weight:700;font-size:11pt;margin-bottom:10px\">Overall Result<\/div>\n  <div class=\"status-checks\">\n    <div><span class=\"checkbox\"><\/span>Passed<\/div>\n    <div><span class=\"checkbox\"><\/span>Failed<\/div>\n    <div><span class=\"checkbox\"><\/span>Conditional<\/div>\n  <\/div>\n  <div style=\"font-size:8.5pt;color:${GR};margin-bottom:4px\">Conditions \/ Remarks<\/div>\n  <div style=\"border-bottom:1px solid #ccc;height:22px;margin-bottom:8px\"><\/div>\n  <div style=\"border-bottom:1px solid #ccc;height:22px;margin-bottom:16px\"><\/div>\n  <div class=\"sig-row\">\n    <div class=\"sig-field\"><div class=\"sig-label\">Technician Name<\/div><div class=\"sig-line\"><\/div><\/div>\n    <div class=\"sig-field\"><div class=\"sig-label\">Signature<\/div><div class=\"sig-line\"><\/div><\/div>\n    <div class=\"sig-field\"><div class=\"sig-label\">Date<\/div><div class=\"sig-line\"><\/div><\/div>\n  <\/div>\n  <div class=\"sig-row\" style=\"margin-top:12px\">\n    <div class=\"sig-field\"><div class=\"sig-label\">Reviewed By<\/div><div class=\"sig-line\"><\/div><\/div>\n    <div class=\"sig-field\"><div class=\"sig-label\">Reviewer Signature<\/div><div class=\"sig-line\"><\/div><\/div>\n    <div class=\"sig-field\"><div class=\"sig-label\">Review Date<\/div><div class=\"sig-line\"><\/div><\/div>\n  <\/div>\n<\/div>`:''}\n<div class=\"disclaimer\">This report was generated automatically from the device configuration file. All values must be verified on-site by a qualified technician. SmartD Technologies Inc. accepts no liability for inaccuracies resulting from incorrect or incomplete configuration data.<\/div>\n<\/body><\/html>`;\n\n  setProgress(70);\n  const win=window.open('','_blank','width=900,height=700');\n  if(!win)throw new Error('Popup blocked \u2014 please allow popups and try again.');\n  win.document.write(html);win.document.close();\n  win.onload=()=>{win.focus();setTimeout(()=>win.print(),600)};\n  setProgress(90);\n}\n\n\nfunction buildXML(c,f){\n  const meta=c._metadata||{},conf=c.configurations||{};\n  const sys=conf.system||{},mot=conf.motor||{};\n  const mnp=mot.motorNameplate||{},ramps=mot.ramps||{};\n  const algo=mot.algorithm||{},vf=algo.constantVf||{},foc=algo.foc||{};\n  const opts=mot.options||{},therm=mot.thermalProtection||{};\n  const autoR=mot.autoRestart||{},emRun=mot.emergencyRun||{};\n  const presets=mot.presets||{},limits=mot.limits||{};\n  const advParams=mot.advancedParameters||{};\n  const line=conf.line||{},net=conf.network||{},mb=conf.modbus||{},bn=conf.bacnet||{};\n  const ios=conf.ios||{};\n  const di=[...(ios.digitalInputs||[])].sort((a,b)=>a.index-b.index);\n  const relays=[...(ios.relays||[])].sort((a,b)=>a.index-b.index);\n  const ai=[...(ios.analogInputs||[])].sort((a,b)=>a.index-b.index);\n  const ao=[...(ios.analogOutputs||[])].sort((a,b)=>a.index-b.index);\n  const enc=ios.incrementalEncoder||{};\n  const activeAlarms=(conf.alarms?.configs||[]).filter(a=>!a.disabled);\n  const pid0=(conf.pids?.instances||[])[0]||{};\n  const atd=conf.motorAutotune?.dynamic||{};\n  const disp=conf.display||{},loc=disp.localizationConfiguration||{},ss=disp.screensaverConfiguration||{};\n  const ble=conf.ble||{},dt=conf.dateTime||{},softUpd=conf.softwareUpdate||{};\n  const cm=sys.controlMode||{},em=sys.emergencyMode||{};\n  const iface=(net.interfaces||[])[0]||{};\n  \/\/ Firmware: prefer running version (newer configs), fall back to schema version+minor.\n  const fwRunning=meta.deviceSoftwareVersion||'';\n  const fwSchema=meta.version?(meta.version+(meta.minorVersion?'.'+meta.minorVersion:'')):'';\n  const fw=fwRunning||fwSchema||'';\n  const extra=meta.extraInformation||{};\n  \/\/ Last-update timestamp: newer configs use `lastUpdateTime`, older used `lastUpdateDate`.\n  const lastUpdate=meta.lastUpdateTime||meta.lastUpdateDate||'';\n  const s=stripEnum;\n  const bool=v=>v?'true':'false';\n  const now=new Date().toISOString();\n  const commDate=f.date?fmtDateLong(f.date):'';\n\n  const esc=v=>{if(v==null)return'';return String(v).replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;').replace(\/\"\/g,'&quot;').replace(\/'\/g,'&apos;')};\n  let x='';\n  const ind=(n,str)=>' '.repeat(n*2)+str+'\\n';\n  const tag=(name,value,attrs,depth=2)=>{const a=attrs?` ${attrs}`:'';return ind(depth,`<${name}${a}>${esc(String(value??''))}<\/${name}>`)};\n  const open=(name,attrs,depth=1)=>{const a=attrs?` ${attrs}`:'';return ind(depth,`<${name}${a}>`)};\n  const close=(name,depth=1)=>ind(depth,`<\/${name}>`);\n  const cmt=(text,depth=1)=>ind(depth,`<!-- ${text} -->`);\n  const nl=()=>'\\n';\n\n  x+='<?xml version=\"1.0\" encoding=\"UTF-8\"?>\\n';\n  x+=`<!-- SmartD VFD Commissioning Report -->\\n<!-- Generated: ${now} -->\\n<!-- Standards: IEEE 3007.2 | IEC 61800 | NEMA MG-1 -->\\n`;\n  x+=`<CommissioningReport generatedAt=\"${esc(now)}\" standard=\"IEEE 3007.2 \/ IEC 61800 \/ NEMA MG-1\" schemaVersion=\"1.0\" vendor=\"SmartD Technologies Inc.\">\\n\\n`;\n\n  if(inc(1)){\n  x+=cmt('SECTION 1 \u2014 Project Information');\n  x+=open('ProjectInformation');\n  x+=tag('DriveName',sys.driveName||'',null,2);x+=tag('ConfigurationName',sys.configName||'',null,2);x+=tag('CommissioningDate',commDate,null,2);x+=tag('ConfigCreated',fmtDateISO(meta.creationDate),null,2);x+=tag('ConfigLastUpdated',fmtDateISO(lastUpdate),null,2);x+=tag('TechnicianName',f.tech||'',null,2);x+=tag('Company',f.company||'',null,2);x+=tag('Location',f.location||'',null,2);x+=tag('WorkOrderPO',f.wo||'',null,2);\n  x+=close('ProjectInformation');x+=nl();\n  }\n\n  if(inc(2)){\n  x+=cmt('SECTION 2 \u2014 Equipment Information');\n  x+=open('EquipmentInformation');\n  x+=tag('Model',meta.deviceModel||'',null,2);x+=tag('SerialNumber',crSerial(c)||'',null,2);x+=tag('Vendor',extra.vendor_name||'',null,2);x+=tag('FirmwareVersion',fw,null,2);x+=tag('BackupFirmwareVersion',meta.deviceBackupVersion||'',null,2);x+=tag('ConfigSchemaVersion',fwSchema||'',null,2);x+=tag('ConfigLabel',meta.label||'',null,2);x+=tag('DeviceName',sys.driveName||'',null,2);\n  x+=open('BLE',null,2);x+=tag('AdvertisingState',s(ble.advertisingState),null,3);x+=tag('PairingTimeout',ble.pairingTimeout||'0s',null,3);x+=close('BLE',2);\n  x+=close('EquipmentInformation');x+=nl();\n  }\n\n  if(inc(3)){\n  x+=cmt('SECTION 3 \u2014 Motor Information');\n  x+=open('MotorInformation');\n  x+=open('Nameplate',null,2);x+=tag('NominalPower',mnp.nominalPower??'',`unit=\"${s(mnp.nominalPowerUnit)}\"`,3);x+=tag('NominalCurrent',mnp.nominalCurrent??'','unit=\"A\"',3);x+=tag('NominalVoltage',mnp.nominalVoltage??'','unit=\"V\"',3);x+=tag('NominalSpeed',mnp.nominalVelocity??'','unit=\"RPM\"',3);x+=tag('NominalFrequency',mnp.nominalInputFrequency??'','unit=\"Hz\"',3);x+=tag('MinInputFrequency',mnp.minInputFrequency??'','unit=\"Hz\"',3);x+=tag('MaxInputFrequency',mnp.maxInputFrequency??'','unit=\"Hz\"',3);x+=tag('NumberOfPoles',mnp.numberOfPoles??'',null,3);x+=tag('PowerFactor',mnp.powerFactor??'',null,3);x+=tag('PhaseInversion',bool(mnp.phaseInversion),null,3);x+=tag('TimeBetweenStarts',mnp.timeBetweenStarts||'0s',null,3);x+=close('Nameplate',2);\n  x+=open('Algorithm',null,2);x+=tag('ControlMode',s(algo.controlMode),null,3);x+=open('ConstantVF',null,3);x+=tag('Overmodulation',bool(vf.enableOvermodulation),null,4);x+=tag('ClosedLoopControl',bool(vf.enableClosedLoopControl),null,4);x+=tag('StartMode',s(vf.start?.mode),null,4);x+=open('StartTorqueBoost',null,4);x+=tag('State',s(vf.startTorqueBoost?.state),null,5);x+=close('StartTorqueBoost',4);x+=close('ConstantVF',3);x+=open('FOC',null,3);x+=tag('Overmodulation',bool(foc.enableOvermodulation),null,4);x+=tag('SpeedResponse',s(foc.speedControllerResponse),null,4);x+=tag('SpeedSource',s(foc.speedSource),null,4);x+=tag('EcoMode',s(foc.ecoMode),null,4);x+=close('FOC',3);x+=close('Algorithm',2);\n  x+=open('Ramps',null,2);\n  x+=getRampXML(algo,ramps,tag,open,close);\n  x+=close('Ramps',2);\n  x+=open('Options',null,2);x+=tag('ReverseEnabled',bool(opts.reverseEnabled),null,3);x+=tag('NormalStop',s(opts.normalStopBehaviour),null,3);x+=tag('ZeroSpeedBehaviour',s(opts.zeroSpeedBehaviour),null,3);x+=tag('LowVoltageRideThrough',s(opts.lowVoltageRideThrough),null,3);x+=tag('BumplessDirectionChange',s(opts.bumplessDirectionChange),null,3);x+=tag('VoltageDropCompensation',s(opts.voltageDropCompensations),null,3);x+=close('Options',2);\n  x+=open('ThermalProtection',null,2);x+=tag('NominalLoad',therm.nominalLoad??100,'unit=\"%\"',3);x+=tag('CoolingType',s(therm.coolingType),null,3);x+=tag('TempAtPowerUp',s(therm.temperatureAtPowerUp),null,3);x+=tag('OverloadClassType',therm.overloadClassType?s(therm.overloadClassType):'',null,3);x+=tag('ThermalTimeConstant',therm.thermalTimeConstant??'',null,3);x+=close('ThermalProtection',2);\n  x+=open('AutoRestart',null,2);x+=tag('Mode',s(autoR.mode),null,3);x+=tag('RestartDelay',autoR.restartDelay||'',null,3);x+=tag('MaximumRetries',autoR.maximumRetries??'',null,3);x+=close('AutoRestart',2);\n  x+=open('EmergencyRun',null,2);x+=tag('Frequency',emRun.frequency??'','unit=\"Hz\"',3);x+=tag('Direction',s(emRun.direction),null,3);x+=close('EmergencyRun',2);\n  x+=open('PresetFrequencies',null,2);(presets.frequencies||[]).forEach((f2,i)=>{x+=tag('Preset',f2,`index=\"${i+1}\" unit=\"Hz\"`,3)});x+=close('PresetFrequencies',2);\n  x+=open('Limits',null,2);[['MaxCurrent',limits.maxCurrent,'A'],['MaxStartCurrent',limits.maxStartCurrent,'A'],['MaxMotoringPower',limits.maxMotoringPower,'W'],['MaxGeneratingPower',limits.maxGeneratingPower,'W'],['LowTorque',limits.lowTorque,''],['HighTorque',limits.highTorque,''],['MinVelocity',limits.minVelocity,'RPM'],['MaxVelocity',limits.maxVelocity,'RPM']].forEach(([n,obj,unit])=>{x+=tag(n,obj?.threshold??0,unit?`unit=\"${unit}\"`:null,3)});x+=tag('LimitsSwitchoverMode',mot.limitsSwitchover?.mode?s(mot.limitsSwitchover.mode):'',null,3);x+=close('Limits',2);\n  x+=open('AdvancedParameters',null,2);x+=tag('StatorResistance',advParams.statorResistance??'','unit=\"Ohm\"',3);x+=tag('RotorResistance',advParams.rotorResistance??'','unit=\"Ohm\"',3);x+=tag('MagnetizingInductance',advParams.magnetizingInductance??'','unit=\"H\"',3);x+=tag('LeakageInductance',advParams.leakageInductance??'','unit=\"H\"',3);x+=tag('MomentOfInertia',advParams.momentOfInertia??'','unit=\"kg*m^2\"',3);x+=close('AdvancedParameters',2);\n  x+=close('MotorInformation');x+=nl();\n  }\n\n  if(inc(4)){\n  x+=cmt('SECTION 4 \u2014 Line Information');\n  x+=open('LineInformation');x+=tag('NominalVoltage',line.nominalVoltage??'','unit=\"V\"',2);x+=tag('Phases',String(line.phases||'').includes('THREE')?'Three-Phase':s(line.phases),null,2);x+=tag('Frequency',line.frequency??'','unit=\"Hz\"',2);x+=close('LineInformation');x+=nl();\n  }\n\n  if(inc(5)){\n  x+=cmt('SECTION 5 \u2014 Network & Communications');\n  x+=open('NetworkConfiguration');\n  x+=open('Ethernet',null,2);x+=tag('IPConfigMethod',s(iface.method),null,3);x+=tag('IPv4Address',iface.ipv4Address||'',null,3);x+=tag('NetworkMask',iface.ipv4NetworkMask||'',null,3);x+=tag('Gateway',iface.ipv4Gateway||'',null,3);x+=open('DNSResolvers',null,3);(net.dns?.resolvers||[]).forEach(r=>{x+=tag('Resolver',r,null,4)});x+=close('DNSResolvers',3);x+=tag('PingMode',s(net.ping?.mode),null,3);x+=close('Ethernet',2);\n  x+=open('Modbus',`active=\"${bool(mb.active)}\"`,2);x+=close('Modbus',2);\n  x+=open('BACnet',`active=\"${bool(bn.active)}\"`,2);x+=tag('DeviceID',bn.deviceId??'',null,3);x+=tag('DeviceName',bn.deviceName||'',null,3);x+=tag('UDPPort',bn.port??47808,null,3);x+=close('BACnet',2);\n  x+=tag('EtherNetIP',conf.ethernetIp!=null?'Configured':'NotConfigured',null,2);\n  x+=close('NetworkConfiguration');x+=nl();\n  }\n\n  if(inc(6)){\n  x+=cmt('SECTION 6 \u2014 I\/O Configuration');\n  x+=open('IOConfiguration');\n  x+=open('DigitalInputs',`powerDirection=\"${s(ios.digitalInputsPowerDirection)}\"`,2);di.forEach(d=>{x+=open('DigitalInput',`index=\"${d.index}\"`,3);x+=tag('Function',s(d.function),null,4);x+=tag('TriggerType',s(d.triggerType),null,4);x+=tag('DebounceDelay',d.debounceDelay||'0s',null,4);x+=close('DigitalInput',3)});x+=close('DigitalInputs',2);\n  x+=open('Relays',null,2);relays.forEach(r=>{x+=open('Relay',`index=\"${r.index}\"`,3);x+=tag('Function',s(r.function),null,4);x+=tag('OutputState',s(r.output),null,4);x+=close('Relay',3)});x+=close('Relays',2);\n  x+=open('AnalogInputs',null,2);ai.forEach(a=>{x+=open('AnalogInput',`index=\"${a.index}\"`,3);x+=tag('Function',s(a.function),null,4);x+=tag('Mode',s(a.mode),null,4);x+=open('CurvePoints',null,4);(a.curvePoints||[]).forEach(p=>{x+=tag('Point',`${p.x}:${p.y}`,`x=\"${p.x}\" y=\"${p.y}\"`,5)});x+=close('CurvePoints',4);x+=close('AnalogInput',3)});x+=close('AnalogInputs',2);\n  x+=open('AnalogOutputs',null,2);ao.forEach(a=>{x+=open('AnalogOutput',`index=\"${a.index}\"`,3);x+=tag('Function',s(a.function),null,4);x+=tag('Mode',s(a.mode),null,4);x+=open('CurvePoints',null,4);(a.curvePoints||[]).forEach(p=>{x+=tag('Point',`${p.x}:${p.y}`,`x=\"${p.x}\" y=\"${p.y}\"`,5)});x+=close('CurvePoints',4);x+=close('AnalogOutput',3)});x+=close('AnalogOutputs',2);\n  x+=open('IncrementalEncoder',null,2);x+=tag('PulsesPerRevolution',enc.pulsesPerRevolution||'',null,3);x+=tag('Wiring',s(enc.wiring),null,3);x+=close('IncrementalEncoder',2);\n  x+=close('IOConfiguration');x+=nl();\n  }\n\n  if(inc(7)){\n  x+=cmt('SECTION 7 \u2014 Active Alarms Only');\n  x+=open('AlarmConfiguration',`activeCount=\"${activeAlarms.length}\"`);\n  activeAlarms.forEach(a=>{x+=open('Alarm',`code=\"${esc(String(a.code||'').replace('ALARM_CODE_',''))}\"`,2);x+=tag('TriggerThreshold',a.triggerThreshold??'',null,3);x+=tag('TriggerDelay',a.triggerDelay||'',null,3);x+=tag('ResetThreshold',a.resetThreshold??'',null,3);x+=tag('ResetDelay',a.resetDelay||'',null,3);x+=open('TriggerActions',null,3);(a.triggerActions||[]).forEach(t=>{x+=tag('Action',s(t.action),null,4)});x+=close('TriggerActions',3);x+=close('Alarm',2)});\n  x+=close('AlarmConfiguration');x+=nl();\n  }\n\n  if(inc(8)){\n  x+=cmt('SECTION 8 \u2014 PID');\n  x+=open('PIDConfiguration');x+=open('PIDInstance',`index=\"1\" enabled=\"${bool(pid0.enable)}\"`,2);x+=tag('ProportionalGain',pid0.kP??'',null,3);x+=tag('IntegrationTime',pid0.tI??'',null,3);x+=tag('DerivativeTime',pid0.tD??'',null,3);x+=tag('ControlMode',s(pid0.controlMode),null,3);x+=tag('MinProcessValue',pid0.minProcessValue??'',null,3);x+=tag('MaxProcessValue',pid0.maxProcessValue??'',null,3);x+=tag('DefaultSetpoint',pid0.defaultSetpoint??'',null,3);x+=tag('SensorOffset',pid0.sensorOffset??0,null,3);x+=close('PIDInstance',2);x+=close('PIDConfiguration');x+=nl();\n  }\n\n  if(inc(9)){\n  x+=cmt('SECTION 9 \u2014 Motor Autotune');\n  x+=open('MotorAutotune');x+=tag('MinSpeed',atd.minSpeed??'','unit=\"%\"',2);x+=tag('MaxSpeed',atd.maxSpeed??'','unit=\"%\"',2);x+=tag('MaxTorque',atd.maxTorque??'','unit=\"%\"',2);x+=tag('RiseFallTime',atd.riseFallTime||'',null,2);x+=close('MotorAutotune');x+=nl();\n  }\n\n  if(inc(10)){\n  x+=cmt('SECTION 10 \u2014 Display');\n  x+=open('DisplayLocalization');x+=tag('ScreenBrightness',disp.screenBrightness??100,'unit=\"%\"',2);x+=tag('LanguageCode',loc.languageCode||'',null,2);x+=tag('CountryCode',loc.countryCode||'',null,2);x+=open('Screensaver',null,2);x+=tag('Timeout',ss.timeout||'',null,3);x+=tag('Mode',s(ss.mode),null,3);x+=close('Screensaver',2);x+=close('DisplayLocalization');x+=nl();\n  }\n\n  if(inc(11)){\n  x+=cmt('SECTION 11 \u2014 System');\n  x+=open('SystemConfiguration');x+=tag('DriveName',sys.driveName||'',null,2);x+=tag('ConfigurationName',sys.configName||'',null,2);x+=tag('DriveNotes',sys.driveNotes||'',null,2);x+=open('ControlMode',null,2);x+=tag('ControlSourceSelector',s(cm.controlSourceSelector),null,3);x+=tag('RemoteSource1',s(cm.remoteControlSource1),null,3);x+=tag('RemoteSource2',s(cm.remoteControlSource2),null,3);x+=close('ControlMode',2);x+=open('EmergencyMode',null,2);x+=tag('State',s(em.state),null,3);x+=tag('Action',s(em.action),null,3);x+=close('EmergencyMode',2);x+=tag('MobilePermissions',s(sys.mobilePermissions),null,2);x+=tag('ApiLanAccess',sys.apiLanAccess?s(sys.apiLanAccess):'',null,2);x+=tag('SoftwareUpdateSource',s(softUpd.selectedSource),null,2);x+=open('DateTime',null,2);x+=tag('Timezone',dt.timezone||'',null,3);x+=tag('BLESyncDisabled',bool(dt.disableBleSynchronization),null,3);x+=close('DateTime',2);x+=close('SystemConfiguration');x+=nl();\n  }\n\n  if(inc(12)){\n  x+=cmt('SECTION 12 \u2014 Function Verification');\n  x+=open('FunctionVerification');\n  ['Power-up sequence verified','Motor Forward direction','Motor Reverse direction','Forward command DI-3','Reverse command DI-4','Stop command DI-5','Emergency mode DI-1','Lockout reset DI-2','Speed reference DI-6','Analog Input 1 calibrated','Analog Input 2 PTC verified','Analog Output 1 calibrated','Analog Output 2 calibrated','Encoder 1600 PPR AB','Relay 1 Any Alarm','Relay 2 Motor Ready','Relay 3 Motor Running','Overspeed alarm tested','Overload alarm tested','Underspeed alarm tested','Encoder speed error tested','Thermal overload verified','External feedback timeout tested','BACnet communication verified','Ramps measured','Preset frequencies verified','Emergency run tested','PID state confirmed','Auto-restart confirmed','Low voltage ride-through tested','BLE connectivity verified'].forEach((item,i)=>{\n    x+=open('VerificationItem',`id=\"VER-${String(i+1).padStart(3,'0')}\" result=\"\"`,2);\n    x+=tag('Description',item,null,3);x+=tag('ObservedValue','',null,3);x+=tag('Result','',null,3);\n    x+=close('VerificationItem',2);\n  });\n  x+=close('FunctionVerification');x+=nl();\n  }\n\n  if(inc(13)){\n  x+=cmt('SECTION 13 \u2014 Issues & Warnings');\n  x+=open('IssuesWarnings');\n  for(let i=1;i<=10;i++){x+=open('Issue',`id=\"ISS-${String(i).padStart(3,'0')}\" severity=\"\"`,2);x+=tag('Description','',null,3);x+=tag('Resolution','',null,3);x+=tag('Status','',null,3);x+=close('Issue',2)}\n  x+=close('IssuesWarnings');x+=nl();\n  }\n\n  if(inc(14)){\n  x+=cmt('SECTION 14 \u2014 Commissioning Status');\n  x+=open('CommissioningStatus');\n  x+=tag('OverallResult','',`options=\"Passed|Failed|Conditional\"`,2);x+=tag('ConditionsRemarks','',null,2);\n  x+=open('Technician',null,2);x+=tag('Name',f.tech||'',null,3);x+=tag('Signature','',null,3);x+=tag('Date',commDate,null,3);x+=close('Technician',2);\n  x+=open('ReviewedBy',null,2);x+=tag('Name','',null,3);x+=tag('Signature','',null,3);x+=tag('Date','',null,3);x+=close('ReviewedBy',2);\n  x+=close('CommissioningStatus');x+=nl();\n  }\n\n  x+=`<\/CommissioningReport>\\n`;\n  return x;\n}\n\n\n  function downloadBlob(content,mime,ext,c){\n    const blob=new Blob([content],{type:mime});\n    const url=URL.createObjectURL(blob);\n    const a=document.createElement('a');a.href=url;\n    const dn=((c?.configurations?.system?.driveName)||'VFD').replace(\/[^a-zA-Z0-9_-]\/g,'_');\n    a.download=`SmartD_Commissioning_Report_${dn}_${new Date().toISOString().slice(0,10)}.${ext}`;\n    a.click();URL.revokeObjectURL(url);\n  }\n\n  function getUserFields(){\n    return{\n      tech:    $('crTech').value.trim(),\n      company: $('crCompany').value.trim(),\n      location:$('crLocation').value.trim(),\n      wo:      $('crWO').value.trim(),\n      date:    $('crDate').value\n    };\n  }\n\n  function setStatus(cls, msg){\n    const el = $('crStatus');\n    el.className = 'cr-status' + (cls ? ' ' + cls : '');\n    el.textContent = msg;\n  }\n\n  function crSerial(c){\n    const m=(c&&c._metadata)||{}, ex=m.extraInformation||{};\n    return m.deviceSerialNumber||m.serialNumber||m.serial_number||m.serial||\n           (c&&c.deviceSerialNumber)||(c&&c.serialNumber)||\n           ex.serial_number||ex.serialNumber||ex.deviceSerialNumber||'';\n  }\n  function renderPreview(c){\n    const m = c._metadata || {}, conf = c.configurations || {};\n    const sys = conf.system || {}, mnp = conf.motor?.motorNameplate || {};\n    const line = conf.line || {};\n    const activeAlarms = (conf.alarms?.configs || []).filter(a=>!a.disabled).length;\n    const fwRunning = m.deviceSoftwareVersion || '';\n    const fwSchema  = m.version ? (m.version + (m.minorVersion ? '.' + m.minorVersion : '')) : '';\n    const fw = fwRunning || fwSchema || '\u2014';\n    const extra = m.extraInformation || {};\n    const rows = [\n      ['VFD Name',    sys.driveName || '\u2014'],\n      ['Configuration Name', sys.configName || m.label || '\u2014'],\n      ['Model',         m.deviceModel || '\u2014'],\n      ['Serial Number', crSerial(c) || '\u2014'],\n      ['Firmware',      fw],\n      ['Motor',         `${mnp.nominalPower||'?'} ${(mnp.nominalPowerUnit||'').replace('MOTOR_CONTROLLER_POWER_UNIT_','')} \/ ${mnp.nominalVoltage||'?'}V \/ ${mnp.nominalVelocity||'?'} RPM`],\n      ['Line',          `${line.nominalVoltage||'?'}V \/ ${line.frequency||'?'} Hz`],\n      ['Active Alarms', `${activeAlarms} enabled`],\n    ];\n    $('crPreview').innerHTML = rows.map(([k,v]) =>\n      `<div class=\"cr-preview-row\"><div class=\"cr-preview-k\">${k}<\/div><div class=\"cr-preview-v\">${String(v).replace(\/<\/g,'&lt;')}<\/div><\/div>`\n    ).join('');\n  }\n\n  function updateSecCount(){\n    const total = REPORT_SECTIONS.length, on = selectedSections.size;\n    $('crSecCount').textContent = `(${on} of ${total} selected)`;\n  }\n\n  function renderSectionToggles(){\n    const container = $('crSecGrid');\n    container.innerHTML = REPORT_SECTIONS.map(s => {\n      const on = selectedSections.has(s.n);\n      return `<label class=\"cr-sec-toggle${on ? '' : ' unchecked'}\"><input type=\"checkbox\" data-sec=\"${s.n}\"${on ? ' checked' : ''}><span>${s.n}. ${s.title}<\/span><\/label>`;\n    }).join('');\n    container.querySelectorAll('input[type=\"checkbox\"]').forEach(cb => {\n      cb.addEventListener('change', ()=>{\n        const n = +cb.getAttribute('data-sec');\n        if (cb.checked) selectedSections.add(n); else selectedSections.delete(n);\n        cb.closest('.cr-sec-toggle').classList.toggle('unchecked', !cb.checked);\n        updateSecCount();\n      });\n    });\n    updateSecCount();\n  }\n\n  function selectFmt(f){\n    fmt = f;\n    $('crFmtPDF').classList.toggle('active', f === 'pdf');\n    $('crFmtXML').classList.toggle('active', f === 'xml');\n  }\n\n  function closeModal(){ $('crModal').style.display = 'none'; }\n\n  \/\/ Public entry point\n  window.openCommissioningReport = function(config){\n    cfg = config;\n    \/\/ Reset section selection on each open\n    selectedSections = new Set(REPORT_SECTIONS.filter(s=>s.defaultOn).map(s=>s.n));\n    renderPreview(cfg);\n    renderSectionToggles();\n    if (!$('crDate').value) $('crDate').value = new Date().toISOString().slice(0,10);\n    setStatus('', '');\n    selectFmt('pdf');\n    $('crModal').style.display = 'flex';\n  };\n\n  \/\/ Wire up modal controls on load. Elements exist in the initial HTML.\n  window.addEventListener('DOMContentLoaded', () => {\n    $('crClose').addEventListener('click', closeModal);\n    $('crBackdrop').addEventListener('click', closeModal);\n    $('crFmtPDF').addEventListener('click', () => selectFmt('pdf'));\n    $('crFmtXML').addEventListener('click', () => selectFmt('xml'));\n    $('crAllBtn').addEventListener('click', () => {\n      selectedSections = new Set(REPORT_SECTIONS.map(s => s.n));\n      renderSectionToggles();\n    });\n    $('crNoneBtn').addEventListener('click', () => {\n      selectedSections = new Set();\n      renderSectionToggles();\n    });\n    $('crGenBtn').addEventListener('click', async () => {\n      if (!cfg) return;\n      if (selectedSections.size === 0) {\n        setStatus('err', 'Select at least one section.');\n        return;\n      }\n      const btn = $('crGenBtn');\n      btn.disabled = true;\n      setStatus('', fmt === 'pdf' ? 'Building PDF\u2026' : 'Building XML\u2026');\n      try {\n        const fields = getUserFields();\n        if (fmt === 'pdf') {\n          await generatePDF(cfg, fields);\n          setStatus('ok', '\u2705 PDF opened \u2014 use browser Print \u203a Save as PDF.');\n        } else {\n          const xml = buildXML(cfg, fields);\n          downloadBlob(xml, 'application\/xml', 'xml', cfg);\n          setStatus('ok', '\u2705 XML downloaded successfully.');\n        }\n      } catch(e) {\n        setStatus('err', 'Error: ' + e.message);\n      } finally {\n        btn.disabled = false;\n      }\n    });\n    document.addEventListener('keydown', (ev) => {\n      if (ev.key === 'Escape' && $('crModal').style.display === 'flex') closeModal();\n    });\n  });\n})();\n\n<\/script>\n\n<script>\n(function(){\n\nconst get = (o, p) => p.split('.').reduce((x,k) => x==null ? undefined : Array.isArray(x) ? x[+k] : x[k], o);\nfunction set(o, p, v) {\n  const parts = p.split('.'); let cur = o;\n  for (let i=0; i<parts.length-1; i++) { const k=parts[i]; cur = Array.isArray(cur) ? cur[+k] : (cur[k] ??= {}); }\n  const last = parts[parts.length-1]; Array.isArray(cur) ? cur[+last]=v : cur[last]=v;\n}\n\n\/\/ \u2500\u2500 Shared I\/O helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\/\/ Common motor control settings shared by all three SmartD presets\nfunction smartdMotorBase() {\n  return [\n    {path:'configurations.motor.algorithm.controlMode',                           value:'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_FREQUENCY_V_F'},\n    {path:'configurations.motor.algorithm.constantVf.startTorqueBoost.state',     value:'MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_ENABLED'},\n    {path:'configurations.motor.algorithm.constantVf.startTorqueBoost.voltagePercentage', value:3},\n    {path:'configurations.motor.algorithm.constantVf.startTorqueBoost.cutoffFrequencyPercentage', value:25},\n    {path:'configurations.motor.algorithm.constantVf.enableOvermodulation',       value:true},\n    {path:'configurations.motor.ramps.frequencyRampUpCurves.0',                   value:{linear:{rampDuration:'20s'},selectable:'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'}},\n    {path:'configurations.motor.ramps.frequencyRampDownCurves.0',                 value:{linear:{rampDuration:'20s'},selectable:'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'}},\n    {path:'configurations.motor.options.reverseEnabled',                          value:true},\n    {path:'configurations.motor.options.zeroSpeedBehaviour',                      value:'MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_PAUSE'},\n    {path:'configurations.motor.options.lowVoltageRideThrough',                   value:'MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_ENABLED'},\n    {path:'configurations.motor.options.normalStopBehaviour',                     value:'MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_RAMP_DOWN'},\n    {path:'configurations.motor.options.voltageDropCompensations',                value:'MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_ENABLED'},\n    {path:'configurations.motor.autoRestart.mode',                                value:'MOTOR_CONTROLLER_AUTO_RESTART_MODE_DISABLED'},\n    {path:'configurations.modbus.active',                                         value:false},\n    {path:'configurations.bacnet.active',                                         value:false},\n    {path:'configurations.display.screensaverConfiguration.mode',                 value:'DISPLAY_SCREENSAVER_MODE_BACKLIGHT_OFF'},\n    {path:'configurations.display.screensaverConfiguration.timeout',              value:'300s'},\n    {path:'configurations.pids.instances.0.enable',                               value:false},\n  ];\n}\n\n\/\/ Common analog I\/O shared by all SmartD presets\nfunction smartdAnalogIO() {\n  return [\n    {path:'configurations.ios.analogInputs.0.function',  value:'IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_FREQUENCY'},\n    {path:'configurations.ios.analogInputs.0.mode',      value:'IO_ANALOG_INPUT_MODE_0_10_V'},\n    {path:'configurations.ios.analogInputs.0.curvePoints.0.x', value:0},\n    {path:'configurations.ios.analogInputs.0.curvePoints.0.y', value:0},\n    {path:'configurations.ios.analogInputs.0.curvePoints.1.x', value:10},\n    {path:'configurations.ios.analogInputs.0.curvePoints.1.y', value:60},\n    {path:'configurations.ios.analogInputs.1.function',  value:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'},\n    {path:'configurations.ios.analogInputs.1.mode',      value:'IO_ANALOG_INPUT_MODE_UNSPECIFIED'},\n    {path:'configurations.ios.analogInputs.2.function',  value:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'},\n    {path:'configurations.ios.analogInputs.2.mode',      value:'IO_ANALOG_INPUT_MODE_UNSPECIFIED'},\n    {path:'configurations.ios.analogOutputs.0.function', value:'IO_ANALOG_OUTPUT_FUNCTION_MOTOR_FREQUENCY'},\n    {path:'configurations.ios.analogOutputs.0.mode',     value:'IO_ANALOG_OUTPUT_MODE_0_10_V'},\n    {path:'configurations.ios.analogOutputs.0.curvePoints.0.x', value:0},\n    {path:'configurations.ios.analogOutputs.0.curvePoints.0.y', value:0},\n    {path:'configurations.ios.analogOutputs.0.curvePoints.1.x', value:60},\n    {path:'configurations.ios.analogOutputs.0.curvePoints.1.y', value:10},\n    {path:'configurations.ios.analogOutputs.1.function', value:'IO_ANALOG_OUTPUT_FUNCTION_MOTOR_CURRENT_RMS'},\n    {path:'configurations.ios.analogOutputs.1.mode',     value:'IO_ANALOG_OUTPUT_MODE_0_10_V'},\n    {path:'configurations.ios.analogOutputs.1.curvePoints.0.x', value:0},\n    {path:'configurations.ios.analogOutputs.1.curvePoints.0.y', value:0},\n    {path:'configurations.ios.analogOutputs.1.curvePoints.1.x', value:100},\n    {path:'configurations.ios.analogOutputs.1.curvePoints.1.y', value:10},\n    {path:'configurations.ios.digitalInputsPowerDirection', value:'IO_DIGITAL_INPUTS_POWER_DIRECTION_SOURCE'},\n  ];\n}\n\nconst APP_PRESETS = [\n  \/\/ \u2500\u2500 SmartD 2-Wire \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  {\n    id:'smartd_2wire',\n    label:'SmartD 2-Wire',\n    icon:'\ud83d\udd0c',\n    desc:'V\/F frequency, 2-wire I\/O remote control. DI3=Run Fwd, DI4=Run Rev, DI5=unassigned, DI6=Reset. Relay 1=Any Alarm, Relay 2=Ready, Relay 3=Running.',\n    label_fr:'SmartD 2 fils',\n    desc_fr:'Fr\u00e9quence V\/F, commande \u00e0 distance par E\/S 2 fils. DI3=Marche AV, DI4=Marche AR, DI5=non assign\u00e9e, DI6=R\u00e9initialisation. Relais 1=Toute alarme, Relais 2=Pr\u00eat, Relais 3=En marche.',\n    changes: [\n      ...smartdMotorBase(),\n      ...smartdAnalogIO(),\n      \/\/ Control source\n      {path:'configurations.system.controlMode.controlSourceSelector', value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n      {path:'configurations.system.controlMode.remoteControlSource1',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},\n      {path:'configurations.system.controlMode.remoteControlSource2',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED'},\n      \/\/ Digital inputs \u2014 2-wire mapping\n      {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_FORWARD'},\n      {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n      {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_REVERSE'},\n      {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n      {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'},\n      {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n      \/\/ Relays\n      {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_ANY_ALARM'},\n      {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_MOTOR_READY_TO_RUN'},\n      {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_MOTOR_RUNNING'},\n      {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n    ]\n  },\n\n  \/\/ \u2500\u2500 SmartD 3-Wire \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  {\n    id:'smartd_3wire',\n    label:'SmartD 3-Wire',\n    icon:'\ud83d\udd0c',\n    desc:'V\/F frequency, 3-wire I\/O remote control. DI3=Run Fwd, DI4=Run Rev, DI5=Stop, DI6=Reset. Relay 1=Any Alarm, Relay 2=Ready, Relay 3=Running.',\n    label_fr:'SmartD 3 fils',\n    desc_fr:'Fr\u00e9quence V\/F, commande \u00e0 distance par E\/S 3 fils. DI3=Marche AV, DI4=Marche AR, DI5=Arr\u00eat, DI6=R\u00e9initialisation. Relais 1=Toute alarme, Relais 2=Pr\u00eat, Relais 3=En marche.',\n    changes: [\n      ...smartdMotorBase(),\n      ...smartdAnalogIO(),\n      \/\/ Control source\n      {path:'configurations.system.controlMode.controlSourceSelector', value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n      {path:'configurations.system.controlMode.remoteControlSource1',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},\n      {path:'configurations.system.controlMode.remoteControlSource2',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED'},\n      \/\/ Digital inputs \u2014 3-wire mapping\n      {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD'},\n      {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n      {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE'},\n      {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n      {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP'},\n      {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n      {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'},\n      {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n      \/\/ Relays\n      {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_ANY_ALARM'},\n      {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_MOTOR_READY_TO_RUN'},\n      {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_MOTOR_RUNNING'},\n      {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n    ]\n  },\n\n  \/\/ \u2500\u2500 SmartD Modbus \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  {\n    id:'smartd_modbus',\n    label:'SmartD Modbus',\n    icon:'\ud83c\udf10',\n    desc:'V\/F frequency, Modbus TCP remote control. All DIs unassigned. Modbus TCP enabled. I\/Os not pre-configured.',\n    label_fr:'SmartD Modbus',\n    desc_fr:'Fr\u00e9quence V\/F, commande \u00e0 distance par Modbus TCP. Toutes les DI non assign\u00e9es. Modbus TCP activ\u00e9. E\/S non pr\u00e9configur\u00e9es.',\n    changes: [\n      ...smartdMotorBase(),\n      \/\/ Control source \u2014 Modbus\n      {path:'configurations.system.controlMode.controlSourceSelector', value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n      {path:'configurations.system.controlMode.remoteControlSource1',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_MODBUS'},\n      {path:'configurations.system.controlMode.remoteControlSource2',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED'},\n      \/\/ Modbus enabled\n      {path:'configurations.modbus.active', value:true},\n      \/\/ Digital inputs \u2014 all unassigned\n      {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n      \/\/ Relays \u2014 all unassigned\n      {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n      {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n      \/\/ Analog I\/Os \u2014 all unassigned\n      {path:'configurations.ios.analogInputs.0.function',  value:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.analogInputs.0.mode',      value:'IO_ANALOG_INPUT_MODE_UNSPECIFIED'},\n      {path:'configurations.ios.analogInputs.1.function',  value:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.analogInputs.1.mode',      value:'IO_ANALOG_INPUT_MODE_UNSPECIFIED'},\n      {path:'configurations.ios.analogInputs.2.function',  value:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.analogInputs.2.mode',      value:'IO_ANALOG_INPUT_MODE_UNSPECIFIED'},\n      {path:'configurations.ios.analogOutputs.0.function', value:'IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.analogOutputs.0.mode',     value:'IO_ANALOG_OUTPUT_MODE_UNSPECIFIED'},\n      {path:'configurations.ios.analogOutputs.1.function', value:'IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED'},\n      {path:'configurations.ios.analogOutputs.1.mode',     value:'IO_ANALOG_OUTPUT_MODE_UNSPECIFIED'},\n      {path:'configurations.ios.digitalInputsPowerDirection', value:'IO_DIGITAL_INPUTS_POWER_DIRECTION_SOURCE'},\n    ]\n  },\n\n  \/\/ \u2500\u2500 Application-specific presets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  {\n    id:'cooling_tower_fan',\n    label:'Cooling Tower Fan',\n    icon:'\u2744\ufe0f',\n    desc:'With defrost (reverse) cycle. Enables reverse rotation, bumpless direction change, and sets direction priority to Reverse.',\n    label_fr:'Ventilateur de tour de refroidissement',\n    desc_fr:'Avec cycle de d\u00e9givrage (inversion). Active la rotation inverse et le changement de sens sans \u00e0-coup, et r\u00e8gle la priorit\u00e9 de sens sur Inverse.',\n    changes:[\n        {path:'configurations.motor.options.reverseEnabled',            value:true},\n        {path:'configurations.motor.options.bumplessDirectionChange',   value:'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_ENABLED'},\n        {path:'configurations.motor.options.spinningDirectionPriority', value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_REVERSE'},\n    ]\n  },\n  {\n    id:'supply_fan_fire',\n    label:'Supply Fan (Fire\/Smoke)',\n    icon:'\ud83c\udf00',\n    desc:'3-wire I\/O with damper feedback, emergency fire mode enabled, reverse direction on emergency run.',\n    label_fr:'Ventilateur d\u2019alimentation (incendie\/fum\u00e9e)',\n    desc_fr:'E\/S 3 fils avec retour de registre, mode d\u2019incendie activ\u00e9, sens inverse en marche d\u2019urgence.',\n    changes: function(cfg) {\n      const nomVel = get(cfg, 'configurations.motor.motorNameplate.nominalVelocity') ?? 1755;\n      return [\n        {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_EMERGENCY_MODE'},\n        {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_DAMPER_FEEDBACK'},\n        {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD'},\n        {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE'},\n        {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP'},\n        {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'},\n        {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_DAMPER_CONTROL'},\n        {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_ANY_ALARM_NEEDS_ACKNOWLEDGE'},\n        {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_MOTOR_RUNNING'},\n        {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.system.controlMode.controlSourceSelector',  value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n        {path:'configurations.system.controlMode.remoteControlSource1',   value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},\n        {path:'configurations.system.emergencyMode.state',  value:'SYSTEM_EMERGENCY_MODE_STATE_ENABLED'},\n        {path:'configurations.system.emergencyMode.action', value:'SYSTEM_EMERGENCY_MODE_ACTION_RUN_MOTOR'},\n        {path:'configurations.motor.emergencyRun.frequency',  value:60},\n        {path:'configurations.motor.emergencyRun.velocity',   value:nomVel},\n        {path:'configurations.motor.emergencyRun.direction',  value:'MOTOR_CONTROLLER_DIRECTION_REVERSE'},\n        {path:'configurations.motor.options.reverseEnabled', value:true},\n      ];\n    }\n  },\n  {\n    id:'exhaust_fan',\n    label:'Exhaust Fan (Fire\/Smoke)',\n    icon:'\ud83c\udf00',\n    desc:'3-wire I\/O with damper feedback, emergency fire mode enabled, forward direction on emergency run.',\n    label_fr:'Ventilateur d\u2019extraction (incendie\/fum\u00e9e)',\n    desc_fr:'E\/S 3 fils avec retour de registre, mode d\u2019incendie activ\u00e9, sens avant en marche d\u2019urgence.',\n    changes: function(cfg) {\n      const nomVel = get(cfg, 'configurations.motor.motorNameplate.nominalVelocity') ?? 1755;\n      return [\n        {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_EMERGENCY_MODE'},\n        {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_DAMPER_FEEDBACK'},\n        {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD'},\n        {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE'},\n        {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP'},\n        {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'},\n        {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_DAMPER_CONTROL'},\n        {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_ANY_ALARM_NEEDS_ACKNOWLEDGE'},\n        {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_MOTOR_RUNNING'},\n        {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.system.controlMode.controlSourceSelector', value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n        {path:'configurations.system.controlMode.remoteControlSource1',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},\n        {path:'configurations.system.emergencyMode.state',  value:'SYSTEM_EMERGENCY_MODE_STATE_ENABLED'},\n        {path:'configurations.system.emergencyMode.action', value:'SYSTEM_EMERGENCY_MODE_ACTION_RUN_MOTOR'},\n        {path:'configurations.motor.emergencyRun.frequency', value:60},\n        {path:'configurations.motor.emergencyRun.velocity',  value:nomVel},\n        {path:'configurations.motor.emergencyRun.direction', value:'MOTOR_CONTROLLER_DIRECTION_FORWARD'},\n        {path:'configurations.motor.options.reverseEnabled', value:true},\n      ];\n    }\n  },\n  {\n    id:'submersible_pump',\n    label:'Submersible Pump',\n    icon:'\ud83d\udca7',\n    desc:'3-wire I\/O, no reverse, multi-segment ramps to prevent water hammer, float switch interlock, external alarm for dry-run protection.',\n    label_fr:'Pompe submersible',\n    desc_fr:'E\/S 3 fils, sans inversion, rampes multi-segments pour pr\u00e9venir les coups de b\u00e9lier, verrouillage par interrupteur \u00e0 flotteur, alarme externe pour la protection contre la marche \u00e0 sec.',\n    changes: function(cfg) {\n      const nomFreq = get(cfg, 'configurations.motor.motorNameplate.nominalInputFrequency') ?? 60;\n      return [\n        {path:'configurations.ios.digitalInputs.0.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD'},\n        {path:'configurations.ios.digitalInputs.0.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.digitalInputs.1.function',    value:'IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP'},\n        {path:'configurations.ios.digitalInputs.1.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.2.function',    value:'IO_DIGITAL_INPUT_FUNCTION_INTERLOCK'},\n        {path:'configurations.ios.digitalInputs.2.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.3.function',    value:'IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_1'},\n        {path:'configurations.ios.digitalInputs.3.triggerType', value:'IO_TRIGGER_TYPE_HIGH'},\n        {path:'configurations.ios.digitalInputs.4.function',    value:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'},\n        {path:'configurations.ios.digitalInputs.4.triggerType', value:'IO_TRIGGER_TYPE_UNSPECIFIED'},\n        {path:'configurations.ios.digitalInputs.5.function',    value:'IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'},\n        {path:'configurations.ios.digitalInputs.5.triggerType', value:'IO_TRIGGER_TYPE_RISING_EDGE'},\n        {path:'configurations.ios.relays.0.function', value:'IO_RELAY_FUNCTION_ANY_ALARM'},\n        {path:'configurations.ios.relays.0.output',   value:'IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.1.function', value:'IO_RELAY_FUNCTION_MOTOR_RUNNING'},\n        {path:'configurations.ios.relays.1.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.ios.relays.2.function', value:'IO_RELAY_FUNCTION_MOTOR_READY_TO_RUN'},\n        {path:'configurations.ios.relays.2.output',   value:'IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'},\n        {path:'configurations.motor.options.reverseEnabled',          value:false},\n        {path:'configurations.motor.options.bumplessDirectionChange', value:'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_DISABLED'},\n        {path:'configurations.motor.options.spinningDirectionPriority', value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_RAISE_ALARM'},\n        {path:'configurations.motor.options.normalStopBehaviour', value:'MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_RAMP_DOWN'},\n        {path:'configurations.motor.algorithm.constantVf.start.mode', value:'MOTOR_CONTROLLER_VF_START_MODE_SCAN_RESTART'},\n        {path:'configurations.motor.thermalProtection.temperatureAtPowerUp', value:'MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_RETAIN_LAST_VALUE'},\n        {path:'configurations.motor.thermalProtection.coolingType', value:'MOTOR_CONTROLLER_COOLING_TYPE_FORCED_COOLED'},\n        {path:'configurations.motor.ramps.frequencyRampUpCurves.0', value:{linearSegments:{segments:[{duration:'8s',endValue:nomFreq*0.50},{duration:'12s',endValue:nomFreq*0.917},{duration:'5s',endValue:null}]},selectable:'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'}},\n        {path:'configurations.motor.ramps.frequencyRampDownCurves.0', value:{linearSegments:{segments:[{duration:'8s',endValue:nomFreq*0.667},{duration:'12s',endValue:nomFreq*0.25},{duration:'5s',endValue:null}]},selectable:'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'}},\n        {path:'configurations.system.controlMode.controlSourceSelector', value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},\n        {path:'configurations.system.controlMode.remoteControlSource1',  value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},\n        {path:'configurations.system.emergencyMode.state', value:'SYSTEM_EMERGENCY_MODE_STATE_DISABLED'},\n      ];\n    }\n  },\n  {id:'other_application',label:'Other application',label_fr:'Autre application',icon:'\u2699\ufe0f',desc:'Coming soon.',desc_fr:'Bient\u00f4t disponible.',changes:[]},\n];\n\nconst MODELS={\n  'SDB-1-2110-A':{vo:[480,400],motor:{v:460,i:22,kw:11,hp:15},frame:'S3'},\n  'SDB-1-2150-A':{vo:[480,400],motor:{v:460,i:28,kw:15,hp:20},frame:'S3'},\n  'SDB-1-2185-A':{vo:[480,400],motor:{v:460,i:34,kw:18.5,hp:25},frame:'S3'},\n  'SDB-1-2220-AL':{vo:[480,400],motor:{v:460,i:41,kw:22,hp:30},frame:'S3'},\n  'SDB-1-2220-A':{vo:[480,400],motor:{v:460,i:41,kw:22,hp:30},frame:'S4'},\n  'SDB-1-2300-A':{vo:[480,400],motor:{v:460,i:55,kw:30,hp:40},frame:'S4'},\n  'SDB-1-2375-A':{vo:[480,400],motor:{v:460,i:68,kw:37.5,hp:50},frame:'S4'},\n  'SDB-1-2450-A':{vo:[480,400],motor:{v:460,i:82,kw:45,hp:60},frame:'S4'},\n  'SDB-2-2150-A':{vo:[600],motor:{v:575,i:22,kw:15,hp:20},frame:'S3'},\n  'SDB-2-2185-A':{vo:[600],motor:{v:575,i:28,kw:18.5,hp:20},frame:'S3'},\n  'SDB-2-2220-A':{vo:[600],motor:{v:575,i:34,kw:22,hp:30},frame:'S3'},\n  'SDB-2-2300-A':{vo:[600],motor:{v:575,i:44,kw:30,hp:40},frame:'S4'},\n  'SDB-2-2375-A':{vo:[600],motor:{v:575,i:54,kw:37.5,hp:50},frame:'S4'},\n  'SDB-2-2450-A':{vo:[600],motor:{v:575,i:66,kw:45,hp:60},frame:'S4'},\n  'SDB-2-2550-A':{vo:[600],motor:{v:575,i:79,kw:55,hp:75},frame:'S4'},\n};\nconst DI_FNS=[\n  ['UNSPECIFIED','IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'],\n  ['3-WIRE RUN FORWARD','IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD'],\n  ['3-WIRE RUN REVERSE','IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE'],\n  ['3-WIRE STOP','IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_STOP'],\n  ['SELECT FREQUENCY REFERENCE','IO_DIGITAL_INPUT_FUNCTION_SELECT_SPEED_FREQUENCY_REFERENCE'],\n  ['2-WIRE RUN FORWARD','IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_FORWARD'],\n  ['2-WIRE RUN REVERSE','IO_DIGITAL_INPUT_FUNCTION_MOTOR_2_WIRE_REVERSE'],\n  ['SELECT VELOCITY REFERENCE','IO_DIGITAL_INPUT_FUNCTION_SELECT_SPEED_VELOCITY_REFERENCE'],\n  ['SELECT LOCAL\/REMOTE CONTROL','IO_DIGITAL_INPUT_FUNCTION_SELECT_CONTROL_MODE'],\n  ['SELECT REMOTE CONTROL SOURCE','IO_DIGITAL_INPUT_FUNCTION_SELECT_REMOTE_CONTROL_SOURCE'],\n  ['PID FORCED OUTPUT','IO_DIGITAL_INPUT_FUNCTION_SELECT_PID_FORCED_OUTPUT_STATE'],\n  ['INCREMENTAL ENCODER','IO_DIGITAL_INPUT_FUNCTION_ENCODER_INCREMENTAL'],\n  ['RESET LOCKOUT','IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT'],\n  ['SELECT ACC. & DEC. RAMP','IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_UP_AND_RAMP_DOWN_CURVES'],\n  ['SELECT ACCELERATION RAMP','IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_UP_CURVE'],\n  ['SELECT DECELERATION RAMP','IO_DIGITAL_INPUT_FUNCTION_SELECT_RAMP_DOWN_CURVE'],\n  ['SELECT ACTIVE CONFIGURATION','IO_DIGITAL_INPUT_FUNCTION_SELECT_ACTIVE_CONFIGURATION'],\n  ['TRIGGER EMERGENCY FIRE MODE','IO_DIGITAL_INPUT_FUNCTION_EMERGENCY_MODE'],\n  ['DAMPER FEEDBACK','IO_DIGITAL_INPUT_FUNCTION_DAMPER_FEEDBACK'],\n  ['TRIGGER MAINTENANCE MODE','IO_DIGITAL_INPUT_FUNCTION_MAINTENANCE_MODE'],\n  ['TRIGGER EXTERNAL CRITICAL ALARM','IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_1'],\n  ['TRIGGER EXTERNAL MAJOR ALARM','IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_2'],\n  ['TRIGGER EXTERNAL MEDIUM ALARM','IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_3'],\n  ['TRIGGER EXTERNAL MINOR ALARM','IO_DIGITAL_INPUT_FUNCTION_EXTERNAL_ALARM_4'],\n  ['INTERLOCK','IO_DIGITAL_INPUT_FUNCTION_INTERLOCK'],\n].map(([l,v])=>({label:l,value:v}));\nconst DI_RISING=new Set(['IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_FORWARD','IO_DIGITAL_INPUT_FUNCTION_MOTOR_3_WIRE_REVERSE','IO_DIGITAL_INPUT_FUNCTION_RESET_LOCKOUT']);\nconst DI_NOTRIG=new Set(['IO_DIGITAL_INPUT_FUNCTION_ENCODER_INCREMENTAL']);\nfunction trigOptsFor(fn){\n  if(fn==='IO_DIGITAL_INPUT_FUNCTION_ENCODER_INCREMENTAL'||fn==='IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'||!fn) return {opts:[{label:'UNSPECIFIED',value:'IO_TRIGGER_TYPE_UNSPECIFIED'}],disabled:true};\n  if(DI_RISING.has(fn)) return {opts:[{label:'RISING EDGE',value:'IO_TRIGGER_TYPE_RISING_EDGE'},{label:'FALLING EDGE',value:'IO_TRIGGER_TYPE_FALLING_EDGE'}],disabled:false};\n  return {opts:[{label:'HIGH LEVEL',value:'IO_TRIGGER_TYPE_HIGH'},{label:'LOW LEVEL',value:'IO_TRIGGER_TYPE_LOW'}],disabled:false};\n}\nconst RLY_FNS=[['UNSPECIFIED','IO_RELAY_FUNCTION_UNSPECIFIED'],['READY TO RUN','IO_RELAY_FUNCTION_MOTOR_READY_TO_RUN'],['MOTOR RUNNING','IO_RELAY_FUNCTION_MOTOR_RUNNING'],['ALARM(S) ABOVE MINOR','IO_RELAY_FUNCTION_ANY_ALARM'],['MOTOR LOCKOUT','IO_RELAY_FUNCTION_MOTOR_LOCKOUT'],['DAMPER CONTROL','IO_RELAY_FUNCTION_DAMPER_CONTROL'],['ALARM(S) NEED ACKNOWLEDGED','IO_RELAY_FUNCTION_ANY_ALARM_NEEDS_ACKNOWLEDGE']].map(([l,v])=>({label:l,value:v}));\nconst RLY_OUTS=[['UNSPECIFIED','IO_RELAY_OUTPUT_UNSPECIFIED'],['ENERGIZED by default','IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT'],['DE-ENERGIZED by default','IO_RELAY_OUTPUT_DE_ENERGIZED_BY_DEFAULT'],['NORMALLY OPEN (fw<2025.12.00)','IO_RELAY_OUTPUT_NORMALLY_OPEN'],['NORMALLY CLOSED (fw<2025.12.00)','IO_RELAY_OUTPUT_NORMALLY_CLOSED']].map(([l,v])=>({label:l,value:v}));\nconst AI_FNS=[['UNSPECIFIED','IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED'],['SET MOTOR FREQUENCY','IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_FREQUENCY'],['SET MOTOR VELOCITY','IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_VELOCITY'],['SET MOTOR TORQUE','IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_TORQUE'],['SET MOTOR MIN TORQUE','IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_MIN_TORQUE'],['SET MOTOR MAX TORQUE','IO_ANALOG_INPUT_FUNCTION_SET_MOTOR_MAX_TORQUE'],['SET PID PROCESS FEEDBACK','IO_ANALOG_INPUT_FUNCTION_SET_PID_1_PROCESS_FEEDBACK_VALUE'],['SET PID PROCESS SETPOINT','IO_ANALOG_INPUT_FUNCTION_SET_PID_1_PROCESS_SETPOINT'],['SET PID FORCED OUTPUT','IO_ANALOG_INPUT_FUNCTION_SET_PID_1_FORCED_OUTPUT_VALUE'],['PTC READING','IO_ANALOG_INPUT_FUNCTION_SENSOR_PTC']].map(([l,v])=>({label:l,value:v}));\nconst AI_MODES=[['UNSPECIFIED','IO_ANALOG_INPUT_MODE_UNSPECIFIED'],['4..20mA','IO_ANALOG_INPUT_MODE_4_20_MA'],['0..10V','IO_ANALOG_INPUT_MODE_0_10_V'],['RESISTOR','IO_ANALOG_INPUT_MODE_RESISTOR'],['PTC','IO_ANALOG_INPUT_MODE_PTC']].map(([l,v])=>({label:l,value:v}));\nconst AO_FNS=[['UNSPECIFIED','IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED'],['MOTOR FREQUENCY','IO_ANALOG_OUTPUT_FUNCTION_MOTOR_FREQUENCY'],['MOTOR CURRENT RMS','IO_ANALOG_OUTPUT_FUNCTION_MOTOR_CURRENT_RMS'],['MOTOR VELOCITY','IO_ANALOG_OUTPUT_FUNCTION_MOTOR_VELOCITY'],['DRIVE OUTPUT ACTIVE POWER','IO_ANALOG_OUTPUT_FUNCTION_DRIVE_OUTPUT_ACTIVE_POWER'],['MOTOR TORQUE','IO_ANALOG_OUTPUT_FUNCTION_MOTOR_TORQUE']].map(([l,v])=>({label:l,value:v}));\nconst AO_MODES=[['UNSPECIFIED','IO_ANALOG_OUTPUT_MODE_UNSPECIFIED'],['4..20mA','IO_ANALOG_OUTPUT_MODE_4_20_MA'],['0..10V','IO_ANALOG_OUTPUT_MODE_0_10_V']].map(([l,v])=>({label:l,value:v}));\nconst ALM_ACTIONS=[['NO ACTION (alarm only)','ALARM_ACTION_NO_ACTION'],['STOP \u2014 RAMP DOWN','ALARM_ACTION_STOP_RAMP_DOWN'],['STOP \u2014 FREEWHEEL','ALARM_ACTION_STOP_FREEWHEEL'],['PID 1 \u2014 FORCED OUTPUT','ALARM_ACTION_PID_1_FORCED_OUTPUT'],['PID 1 \u2014 CONSTANT OUTPUT','ALARM_ACTION_PID_1_CONSTANT_OUTPUT']].map(([l,v])=>({label:l,value:v}));\nconst ALM_BOOT=[['NORMAL (inactive on boot)','ALARM_BOOT_STATE_NORMAL'],['RESTORE (persist through reboot)','ALARM_BOOT_STATE_RESTORE']].map(([l,v])=>({label:l,value:v}));\nconst ALM_DEFS={\n  ALARM_CODE_MOTOR_OVERCURRENT:{name:'Motor Overcurrent',sev:'major',grp:'protection',hasTh:true,thLbl:'Trigger threshold (% of nominal current)',hasDly:true,dis:false},\n  ALARM_CODE_MOTOR_OVERLOAD:{name:'Motor Overload',sev:'medium',grp:'protection',hasTh:false,thLbl:'',hasDly:true,dis:false},\n  ALARM_CODE_MOTOR_THERMAL_OVERLOAD:{name:'Motor Thermal Overload',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_MOTOR_OVERTEMP:{name:'Motor Over-Temperature',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_DRIVE_OVERTEMP:{name:'Drive Over-Temperature',sev:'critical',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_DRIVE_HIGH_TEMPERATURE:{name:'Drive High Temperature',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_GROUND_FAULT:{name:'Motor Ground Fault',sev:'critical',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_DC_OVERVOLTAGE:{name:'DC Bus Over-Voltage',sev:'critical',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_DC_UNDERVOLTAGE:{name:'DC Bus Under-Voltage',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:true,dis:false},\n  ALARM_CODE_LINE_VOLTAGE_BALANCE:{name:'Line Voltage Imbalance',sev:'major',grp:'protection',hasTh:true,thLbl:'Max imbalance threshold (%)',hasDly:true,dis:true},\n  ALARM_CODE_LINE_VOLTAGE_SAG:{name:'Line Voltage Sag',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_LINE_VOLTAGE_SWELL:{name:'Line Voltage Swell',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_STALLED:{name:'Motor Stalled',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:true,dis:true},\n  ALARM_CODE_MOTOR_PHASE_LOSS:{name:'Motor Phase Loss',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_MOTOR_DISCONNECTED:{name:'Motor Disconnected',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_OVERSPEED:{name:'Motor Overspeed',sev:'major',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_UNDERSPEED:{name:'Motor Underspeed',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_WRONG_DIRECTION:{name:'Motor Wrong Direction',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MOTOR_APPLICATION_UNDERLOAD:{name:'Motor Application Underload',sev:'minor',grp:'protection',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_EXTERNAL_1:{name:'External Critical Alarm',sev:'critical',grp:'external',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_EXTERNAL_2:{name:'External Major Alarm',sev:'major',grp:'external',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_EXTERNAL_3:{name:'External Medium Alarm',sev:'medium',grp:'external',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_EXTERNAL_4:{name:'External Minor Alarm',sev:'minor',grp:'external',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_MODBUS_COMM_LOSS:{name:'Modbus Communication Loss',sev:'major',grp:'comms',hasTh:false,thLbl:'',hasDly:true,dis:true},\n  ALARM_CODE_BACNET_COMM_LOSS:{name:'BACnet Communication Loss',sev:'major',grp:'comms',hasTh:false,thLbl:'',hasDly:true,dis:true},\n  ALARM_CODE_HMI_COMM_LOSS:{name:'HMI Communication Loss',sev:'major',grp:'comms',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_PID_1_LOW_PROCESS_VALUE:{name:'PID 1 \u2014 Low Process Value',sev:'minor',grp:'pid',hasTh:true,thLbl:'Low threshold (eng. units)',hasDly:true,dis:true},\n  ALARM_CODE_PID_1_HIGH_PROCESS_VALUE:{name:'PID 1 \u2014 High Process Value',sev:'minor',grp:'pid',hasTh:true,thLbl:'High threshold (eng. units)',hasDly:true,dis:true},\n  ALARM_CODE_ANALOG_INPUT_1_SENSOR_LOSS:{name:'Analog Input 1 Sensor Loss',sev:'major',grp:'io',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_ANALOG_INPUT_2_SENSOR_LOSS:{name:'Analog Input 2 Sensor Loss',sev:'major',grp:'io',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_ANALOG_INPUT_3_SENSOR_LOSS:{name:'Analog Input 3 Sensor Loss',sev:'major',grp:'io',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_DAMPER_FEEDBACK_LOSS:{name:'Damper Feedback Loss',sev:'major',grp:'io',hasTh:false,thLbl:'',hasDly:false,dis:true},\n  ALARM_CODE_COOLING_FAN1_FAULT:{name:'Cooling Fan 1 Fault',sev:'major',grp:'cooling',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_COOLING_FAN2_FAULT:{name:'Cooling Fan 2 Fault',sev:'major',grp:'cooling',hasTh:false,thLbl:'',hasDly:false,dis:false},\n  ALARM_CODE_COOLING_FAN_CAPACITY_FAULT:{name:'Cooling Fan Capacity Fault',sev:'critical',grp:'cooling',hasTh:false,thLbl:'',hasDly:false,dis:false},\n};\nconst ALM_GRP_LABELS={protection:'Protection Faults',external:'External Alarms (via Digital Inputs)',comms:'Communication',pid:'PID',io:'I\/O Sensor Loss',cooling:'Cooling System'};\nconst SEV_CLS={critical:'badge-critical',major:'badge-major',medium:'badge-medium',minor:'badge-minor'};\nconst PID_UNITS=[['UNSPECIFIED','PID_ENGINEERING_UNIT_UNSPECIFIED'],['Pa','PID_ENGINEERING_UNIT_PASCAL'],['Bar','PID_ENGINEERING_UNIT_BAR'],['m\u00b3\/s','PID_ENGINEERING_UNIT_CUBIC_METER_PER_SECOND'],['m\u00b3\/h','PID_ENGINEERING_UNIT_CUBIC_METER_PER_HOUR'],['L\/s','PID_ENGINEERING_UNIT_LITER_PER_SECOND'],['m','PID_ENGINEERING_UNIT_METER'],['PSI','PID_ENGINEERING_UNIT_POUND_PER_SQUARE_INCH'],['GPM','PID_ENGINEERING_UNIT_GALLON_PER_MINUTE'],['CFS','PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_SECOND'],['CFM','PID_ENGINEERING_UNIT_CUBIC_FOOT_PER_MINUTE'],['ft','PID_ENGINEERING_UNIT_FOOT'],['Custom','PID_ENGINEERING_UNIT_CUSTOM']].map(([l,v])=>({label:l,value:v}));\n\nconst CH='https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-publication\/';\nconst lnk=(slug,txt)=>`<br><br><a href=\"${CH}${slug}\" target=\"_blank\">User Guide: ${txt}<\/a>`;\n\nconst HELP_GUIDE_FR=\"<br><br><a href=\\\"https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-manuel-d-utilisation\/accueil\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Guide d\u2019utilisation \u2197<\/a>\";\nconst HELP_FR={\"_metadata.deviceModel\": {\"t\": \"Mod\u00e8le de VFD\", \"b\": \"S\u00e9lectionnez le mod\u00e8le de VFD Clean Power. Remplit automatiquement les valeurs par d\u00e9faut de la plaque signal\u00e9tique moteur.\"}, \"configurations.system.driveName\": {\"t\": \"Nom du VFD\", \"b\": \"Nom descriptif de ce VFD ou de cette installation.\"}, \"configurations.system.configName\": {\"t\": \"Nom de configuration\", \"b\": \"Libell\u00e9 libre servant \u00e0 identifier cette configuration (maximum 256 caract\u00e8res).\"}, \"configurations.system.mobilePermissions\": {\"t\": \"Contr\u00f4le moteur par l\u2019application mobile\", \"b\": \"ACTIV\u00c9 : surveillance + contr\u00f4le du moteur. D\u00c9SACTIV\u00c9 : surveillance seulement.\"}, \"configurations.system.controlMode.controlSourceSelector\": {\"t\": \"S\u00e9lecteur de source de contr\u00f4le\", \"b\": \"D\u00e9finit quelle source est autoris\u00e9e \u00e0 passer en mode de contr\u00f4le \u00e0 distance.\"}, \"configurations.system.controlMode.remoteControlSource1\": {\"t\": \"Source distante 1\", \"b\": \"Premi\u00e8re source de contr\u00f4le \u00e0 distance pr\u00e9configur\u00e9e.\"}, \"configurations.system.controlMode.remoteControlSource2\": {\"t\": \"Source distante 2\", \"b\": \"Deuxi\u00e8me source de contr\u00f4le \u00e0 distance pr\u00e9configur\u00e9e.\"}, \"configurations.line.nominalVoltage\": {\"t\": \"Tension du r\u00e9seau d\u2019entr\u00e9e\", \"b\": \"480\/400 V pour la s\u00e9rie SDB-1 ; 600 V pour la s\u00e9rie SDB-2.\"}, \"configurations.line.frequency\": {\"t\": \"Fr\u00e9quence du r\u00e9seau d\u2019entr\u00e9e\", \"b\": \"50 Hz ou 60 Hz.\"}, \"configurations.motor.motorNameplate.nominalVoltage\": {\"t\": \"Tension nominale\", \"b\": \"Tension nominale de la plaque signal\u00e9tique du moteur (V).\"}, \"configurations.motor.motorNameplate.nominalCurrent\": {\"t\": \"Courant nominal\", \"b\": \"Courant \u00e0 pleine charge de la plaque signal\u00e9tique (A).\"}, \"configurations.motor.motorNameplate.nominalPowerUnit\": {\"t\": \"Unit\u00e9 de puissance\", \"b\": \"kW ou HP \u2014 d\u00e9termine l\u2019interpr\u00e9tation de la puissance nominale.\"}, \"configurations.motor.motorNameplate.nominalPower\": {\"t\": \"Puissance nominale\", \"b\": \"Puissance de la plaque signal\u00e9tique dans l\u2019unit\u00e9 choisie.\"}, \"configurations.motor.motorNameplate.nominalInputFrequency\": {\"t\": \"Fr\u00e9quence nominale\", \"b\": \"Fr\u00e9quence nominale de la plaque signal\u00e9tique (Hz).\"}, \"configurations.motor.motorNameplate.minInputFrequency\": {\"t\": \"Fr\u00e9quence minimale\", \"b\": \"Fr\u00e9quence de fonctionnement minimale du moteur (Hz).\"}, \"configurations.motor.motorNameplate.maxInputFrequency\": {\"t\": \"Fr\u00e9quence maximale\", \"b\": \"Fr\u00e9quence de fonctionnement maximale du moteur (Hz).\"}, \"configurations.motor.motorNameplate.nominalVelocity\": {\"t\": \"Vitesse nominale\", \"b\": \"Vitesse de l\u2019arbre de la plaque signal\u00e9tique (tr\/min).\"}, \"configurations.motor.motorNameplate.numberOfPoles\": {\"t\": \"Nombre de p\u00f4les\", \"b\": \"Nombre de p\u00f4les du moteur.\"}, \"configurations.motor.motorNameplate.powerFactor\": {\"t\": \"Facteur de puissance\", \"b\": \"Facteur de puissance de la plaque signal\u00e9tique (0,1\u20131,0).\"}, \"configurations.motor.motorNameplate.phaseInversion\": {\"t\": \"Inversion de phase\", \"b\": \"ACTIV\u00c9 : le VFD permet de changer le sens de rotation sans modifier le c\u00e2blage du moteur.\"}, \"configurations.motor.motorNameplate.timeBetweenStarts\": {\"t\": \"D\u00e9lai de red\u00e9marrage\", \"b\": \"Le d\u00e9lai de red\u00e9marrage automatique est utilis\u00e9 entre les tentatives afin de s\u2019assurer que le moteur est compl\u00e8tement arr\u00eat\u00e9 avant la prochaine tentative de red\u00e9marrage automatique.\"}, \"configurations.motor.thermalProtection.coolingType\": {\"t\": \"Type de refroidissement moteur\", \"b\": \"AUTOVENTIL\u00c9 : ventilateur fix\u00e9 \u00e0 l\u2019arbre du moteur. VENTILATION FORC\u00c9E : ventilateur externe ind\u00e9pendant de la vitesse du moteur.\"}, \"configurations.motor.thermalProtection.temperatureAtPowerUp\": {\"t\": \"Temp\u00e9rature \u00e0 la mise sous tension\", \"b\": \"D\u00c9MARRAGE \u00c0 FROID : le mod\u00e8le thermique est r\u00e9initialis\u00e9 \u00e0 chaque d\u00e9marrage. CONSERVER : restaure la derni\u00e8re temp\u00e9rature connue.\"}, \"configurations.motor.algorithm.controlMode\": {\"t\": \"Mode de contr\u00f4le moteur\", \"b\": \"V\/F FR\u00c9QUENCE : consigne en Hz. V\/F VITESSE : consigne en tr\/min. FOC : tr\/min \u00e0 flux orient\u00e9. COUPLE : % du couple nominal.\"}, \"configurations.motor.algorithm.constantVf.startTorqueBoost.state\": {\"t\": \"Surtension de d\u00e9marrage (boost)\", \"b\": \"Applique une surtension au d\u00e9marrage pour vaincre le frottement statique.\"}, \"configurations.motor.algorithm.constantVf.enableOvermodulation\": {\"t\": \"Surmodulation\", \"b\": \"Active la surmodulation pour tirer un peu plus de tension de l\u2019onduleur \u00e0 haute vitesse.\"}, \"configurations.motor.algorithm.constantVf.start.mode\": {\"t\": \"Mode de d\u00e9marrage V\/F\", \"b\": \"D\u00c9MARRAGE \u00c0 Z\u00c9RO : suppose l\u2019arbre immobile. REPRISE \u00c0 LA VOL\u00c9E : l\u2019arbre peut tourner ; balaye de la fr\u00e9quence max vers le bas pour trouver la vitesse avant de d\u00e9marrer.\"}, \"configurations.motor.options.normalStopBehaviour\": {\"t\": \"Comportement \u00e0 l\u2019arr\u00eat normal\", \"b\": \"RAMPE DE D\u00c9C\u00c9L\u00c9RATION : d\u00e9c\u00e9l\u00e8re jusqu\u2019\u00e0 l\u2019arr\u00eat. ROUE LIBRE : arr\u00eat en roue libre imm\u00e9diat.\"}, \"configurations.motor.options.reverseEnabled\": {\"t\": \"Marche arri\u00e8re autoris\u00e9e\", \"b\": \"ACTIV\u00c9 : les commandes de rotation inverse sont accept\u00e9es.\"}, \"configurations.motor.options.bumplessDirectionChange\": {\"t\": \"Changement de sens sans \u00e0-coup\", \"b\": \"ACTIV\u00c9 : change de sens sans arr\u00eat pr\u00e9alable (n\u00e9cessite Marche arri\u00e8re autoris\u00e9e).\"}, \"configurations.motor.options.spinningDirectionPriority\": {\"t\": \"Priorit\u00e9 de sens\", \"b\": \"Comportement lorsque les commandes AVANT et ARRI\u00c8RE sont actives simultan\u00e9ment.\"}, \"configurations.motor.options.zeroSpeedBehaviour\": {\"t\": \"Comportement \u00e0 vitesse nulle\", \"b\": \"PAUSE MOTEUR : maintien \u00e0 z\u00e9ro, aucun red\u00e9marrage requis. ARR\u00caT EN RAMPE : arr\u00eat complet, red\u00e9marrage requis. MAINTENIR VITESSE MINIMALE : continue \u00e0 la fr\u00e9quence min.\"}, \"configurations.motor.options.lowVoltageRideThrough\": {\"t\": \"Tenue aux creux de tension\", \"b\": \"ACTIV\u00c9 : tente de maintenir le fonctionnement lors de br\u00e8ves baisses de tension.\"}, \"configurations.motor.options.voltageDropCompensations\": {\"t\": \"Compensation de chute de tension\", \"b\": \"ACTIV\u00c9 : augmente la tension de sortie pour compenser les pertes r\u00e9sistives des c\u00e2bles.\"}, \"configurations.motor.autoRestart.mode\": {\"t\": \"Red\u00e9marrage automatique\", \"b\": \"ACTIV\u00c9 : le VFD tentera automatiquement de red\u00e9marrer apr\u00e8s un d\u00e9faut.\"}, \"configurations.motor.autoRestart.maximumRetries\": {\"t\": \"Nombre max de tentatives\", \"b\": \"Nombre de tentatives de red\u00e9marrage avant le verrouillage (1\u201310).\"}, \"configurations.motor.autoRestart.restartDelay\": {\"t\": \"D\u00e9lai de red\u00e9marrage (s)\", \"b\": \"Temps entre les tentatives de red\u00e9marrage (10\u20134000 s).\"}, \"configurations.ios.digitalInputsPowerDirection\": {\"t\": \"Sens d\u2019alimentation des entr\u00e9es TOR\", \"b\": \"SOURCE (PNP) : le VFD fournit le +24 V. SINK INTERNE (NPN) : le VFD fournit la masse. SINK EXTERNE : masse externe.\"}, \"configurations.dateTime.protocol\": {\"t\": \"Synchronisation de l\u2019heure\", \"b\": \"MANUEL : r\u00e9glez l\u2019heure via l\u2019IHM ou l\u2019application. SYNC R\u00c9SEAU (NTP) : synchronisation automatique via un serveur NTP.\"}, \"configurations.dateTime.timezone\": {\"t\": \"Fuseau horaire\", \"b\": \"Nom de la base de fuseaux IANA, p. ex. America\/Toronto.\"}, \"configurations.dateTime.timeServerUri\": {\"t\": \"URI de serveurs NTP\", \"b\": \"URI de serveurs NTP personnalis\u00e9s. Laisser vide pour utiliser le pool par d\u00e9faut. Exemple : pool.ntp.org\"}, \"configurations.dateTime.disableBleSynchronization\": {\"t\": \"D\u00e9sactiver la sync BLE de l\u2019heure\", \"b\": \"Emp\u00eache le Bluetooth de r\u00e9gler automatiquement l\u2019horloge.\"}, \"configurations.network.interfaces.0.method\": {\"t\": \"Attribution d\u2019adresse\", \"b\": \"DHCP : automatique. Manuel (IPv4 statique) : adresse IP fixe.\"}, \"configurations.network.dns.method\": {\"t\": \"M\u00e9thode DNS\", \"b\": \"DHCP : utilise le DNS fourni par le serveur DHCP. Statique : utilise toujours les r\u00e9solveurs list\u00e9s.\"}, \"configurations.network.dns.resolvers\": {\"t\": \"R\u00e9solveurs DNS\", \"b\": \"Adresses des r\u00e9solveurs DNS. Jusqu\u2019\u00e0 4 r\u00e9solveurs.\"}, \"configurations.network.ping.mode\": {\"t\": \"Ping ICMP\", \"b\": \"D\u00c9SACTIV\u00c9 : ignore les requ\u00eates ping. ACTIV\u00c9 EN PERMANENCE : r\u00e9pond aux requ\u00eates ping.\"}, \"configurations.modbus.active\": {\"t\": \"Modbus TCP\", \"b\": \"ACTIV\u00c9 : le VFD accepte les connexions Modbus TCP sur le port 502.\"}, \"configurations.bacnet.active\": {\"t\": \"BACnet IP\", \"b\": \"ACTIV\u00c9 : le VFD expose un serveur BACnet IP.\"}, \"configurations.bacnet.deviceId\": {\"t\": \"ID d\u2019appareil BACnet\", \"b\": \"ID d\u2019appareil BACnet unique (0\u20134194302).\"}, \"configurations.bacnet.deviceName\": {\"t\": \"Nom d\u2019appareil BACnet\", \"b\": \"Nom convivial sur le r\u00e9seau BACnet (max 255 caract\u00e8res).\"}, \"configurations.bacnet.port\": {\"t\": \"Port UDP BACnet\", \"b\": \"Par d\u00e9faut 47808 (0xBAC0).\"}, \"configurations.ethernetIp\": {\"t\": \"EtherNet\/IP\", \"b\": \"ACTIV\u00c9 : le VFD accepte les connexions EtherNet\/IP des automates (API).\"}, \"configurations.ble.advertisingState\": {\"t\": \"Diffusion BLE\", \"b\": \"ACTIV\u00c9 : l\u2019application mobile peut d\u00e9couvrir et se connecter. D\u00c9SACTIV\u00c9 : emp\u00eache les nouvelles connexions BLE.\"}, \"configurations.system.emergencyMode.state\": {\"t\": \"Mode incendie d\u2019urgence\", \"b\": \"ACTIV\u00c9 : le mode peut \u00eatre d\u00e9clench\u00e9 par une entr\u00e9e TOR. D\u00c9SACTIV\u00c9 : fonction d\u00e9sactiv\u00e9e.\"}, \"configurations.system.emergencyMode.action\": {\"t\": \"Action du mode d\u2019urgence\", \"b\": \"FONCTIONNER \u00c0 LA VALEUR PR\u00c9R\u00c9GL\u00c9E : d\u00e9marre aux consignes d\u2019urgence configur\u00e9es. ARR\u00caT EN ROUE LIBRE : arr\u00eat en roue libre. CONSERVER L\u2019OP\u00c9RATION ACTUELLE : aucun changement.\"}, \"configurations.motor.emergencyRun.direction\": {\"t\": \"Sens en urgence\", \"b\": \"Sens de rotation du moteur pendant la marche d\u2019urgence.\"}, \"configurations.motor.emergencyRun.frequency\": {\"t\": \"Fr\u00e9quence d\u2019urgence (Hz)\", \"b\": \"Utilis\u00e9e en mode de contr\u00f4le V\/F Fr\u00e9quence.\"}, \"configurations.motor.emergencyRun.velocity\": {\"t\": \"Vitesse d\u2019urgence (tr\/min)\", \"b\": \"Utilis\u00e9e en modes V\/F Vitesse, FOC ou Couple.\"}};\nconst HELP={\n  '_metadata.deviceModel':{t:'VFD Model',b:`Select the Clean Power VFD model. Auto-fills motor nameplate defaults.${lnk('technical-specifications','Technical Specifications')}`},\n  'configurations.system.driveName':{t:'VFD Name',b:`A descriptive name for this VFD or installation.${lnk('organization-of-the-configurations','Organization')}`},\n  'configurations.system.configName':{t:'Configuration Name',b:`Free-text label used to identify this configuration (maximum 256 characters).${lnk('organization-of-the-configurations','Organization')}`},\n  'configurations.system.mobilePermissions':{t:'Motor Control by Mobile App',b:`ENABLED: monitoring + motor control. DISABLED: monitoring only.${lnk('vfd-control-by-mobile-app','Motor Control by Mobile App')}`},\n  'configurations.system.controlMode.controlSourceSelector':{t:'Control Source Selector',b:`Defines which source is authorised to switch to remote control mode.${lnk('control-source','Control Source')}`},\n  'configurations.system.controlMode.remoteControlSource1':{t:'Remote Source 1',b:`First pre-configured remote control source.${lnk('control-source','Control Source')}`},\n  'configurations.system.controlMode.remoteControlSource2':{t:'Remote Source 2',b:`Second pre-configured remote control source.${lnk('control-source','Control Source')}`},\n  'configurations.line.nominalVoltage':{t:'Input Line Voltage',b:`480\/400 V for SDB-1 series; 600 V for SDB-2 series.${lnk('power-line-parameters','Power Line')}`},\n  'configurations.line.frequency':{t:'Input Line Frequency',b:`50 Hz or 60 Hz.${lnk('power-line-parameters','Power Line')}`},\n  'configurations.motor.motorNameplate.nominalVoltage':{t:'Nominal Voltage',b:`Motor nameplate nominal voltage (V).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.nominalCurrent':{t:'Nominal Current',b:`Motor nameplate full-load current (A).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.nominalPowerUnit':{t:'Power Unit',b:`kW or HP \u2014 affects how Nominal Power is interpreted.${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.nominalPower':{t:'Nominal Power',b:`Motor nameplate power in the selected unit.${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.nominalInputFrequency':{t:'Nominal Frequency',b:`Motor nameplate nominal frequency (Hz).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.minInputFrequency':{t:'Min Frequency',b:`Motor minimum operating frequency (Hz).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.maxInputFrequency':{t:'Max Frequency',b:`Motor maximum operating frequency (Hz).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.nominalVelocity':{t:'Nominal Velocity',b:`Motor nameplate shaft speed (rpm).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.numberOfPoles':{t:'Number of Poles',b:`Motor number of poles.${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.powerFactor':{t:'Power Factor',b:`Motor nameplate power factor (0.1\u20131.0).${lnk('motor-nameplate','Motor Nameplate')}`},\n  'configurations.motor.motorNameplate.phaseInversion':{t:'Phase Reversal',b:`ENABLED: drive allows the user to change the direction of rotation without modifying the wiring of the motor.${lnk('motor-phase-reversal','Phase Reversal')}`},\n  'configurations.motor.motorNameplate.timeBetweenStarts':{t:'Restart delay',b:`The automatic restart delay is used between attempts to ensure that the motor is at a full stop before the next automatic restart attempt.${lnk('automatic-restart','Automatic Restart')}`},\n  'configurations.motor.thermalProtection.coolingType':{t:'Motor Cooling Type',b:`SELF COOLED: fan attached to motor shaft. FORCED COOLED: external fan independent of motor speed.${lnk('motor-thermal-protection','Thermal Protection')}`},\n  'configurations.motor.thermalProtection.temperatureAtPowerUp':{t:'Temperature at Power-Up',b:`COLD START: thermal model resets on every boot. RETAIN: restores last known temperature.${lnk('motor-thermal-protection','Thermal Protection')}`},\n  'configurations.motor.algorithm.controlMode':{t:'Motor Control Mode',b:`V\/F FREQUENCY: Hz setpoint. V\/F VELOCITY: RPM setpoint. FOC: field-oriented RPM. TORQUE: % nominal torque.${lnk('motor-control-mode','Control Mode')}`},\n  'configurations.motor.algorithm.constantVf.startTorqueBoost.state':{t:'Torque Boost',b:`Applies an extra voltage boost at startup to overcome static friction.${lnk('starting-torque-boos','Low Speed Torque Compensation')}`},\n  'configurations.motor.algorithm.constantVf.enableOvermodulation':{t:'Over-Modulation',b:`Enables over-modulation to squeeze slightly more voltage out of the inverter at high speed.${lnk('over-modulation','Over Modulation')}`},\n  'configurations.motor.algorithm.constantVf.start.mode':{t:'V\/F Start Mode',b:`START FROM ZERO: assumes shaft is stationary. SCAN RESTART: shaft may be spinning; scans from max freq down to find speed before starting.${lnk('v-f-start-modes','V\/f Start Mode')}`},\n  'configurations.motor.algorithm.foc.speedControllerResponse':{t:'FOC Speed Response',b:'Controls speed controller bandwidth. FAST = tighter regulation. SLOW = more stable on high-inertia loads.'},\n  'configurations.motor.algorithm.foc.speedSource':{t:'FOC Speed Source',b:'ESTIMATED: uses motor controller algorithm. MEASURED: uses an incremental encoder wired to digital inputs.'},\n  'configurations.motor.algorithm.foc.ecoMode':{t:'FOC Eco Mode',b:'ON WITH MTPA: reduces magnetizing current on light loads, improving efficiency.'},\n  'configurations.motor.options.normalStopBehaviour':{t:'Normal Stop Behaviour',b:`RAMP DOWN: decelerate to stop. FREEWHEEL: coast to stop immediately.${lnk('stopping-the-motor','Stopping the Motor')}`},\n  'configurations.motor.options.reverseEnabled':{t:'Reverse Enabled',b:`ENABLED allows reverse rotation commands to be accepted.${lnk('reverse-rotation-prevention','Reverse Rotation')}`},\n  'configurations.motor.options.bumplessDirectionChange':{t:'Bumpless Direction Change',b:`ENABLED: change direction without stopping first (requires Reverse Enabled).${lnk('change-spinning-direction','Direction Change')}`},\n  'configurations.motor.options.spinningDirectionPriority':{t:'Direction Priority',b:`Behaviour when both FWD and REV commands are simultaneously active.${lnk('spinning-direction-priority','Direction Priority')}`},\n  'configurations.motor.options.zeroSpeedBehaviour':{t:'Zero Speed Behaviour',b:`MOTOR PAUSE: hold at zero, no restart needed. STOP RAMP DOWN: full stop, restart required. MAINTAIN MINIMUM SPEED: keep running at min freq.${lnk('zero-speed-behavior','Zero Speed')}`},\n  'configurations.motor.options.lowVoltageRideThrough':{t:'Low Voltage Ride Through',b:`ENABLED: attempt to maintain operation during brief voltage sags.${lnk('lvrt','LVRT')}`},\n  'configurations.motor.options.voltageDropCompensations':{t:'Voltage Drop Compensation',b:`ENABLED: increase output voltage to compensate for cable resistance losses.${lnk('internal-filter-compensation','Voltage Drop Compensation')}`},\n  'configurations.motor.autoRestart.mode':{t:'Auto-Restart',b:`ENABLED: drive will automatically attempt to restart after a fault.${lnk('automatic-restart','Auto-Restart')}`},\n  'configurations.motor.autoRestart.maximumRetries':{t:'Max Retries',b:`Number of restart attempts before entering lockout (1\u201310).${lnk('automatic-restart','Auto-Restart')}`},\n  'configurations.motor.autoRestart.restartDelay':{t:'Restart Delay (s)',b:`Time between restart attempts (10\u20134000 s).${lnk('automatic-restart','Auto-Restart')}`},\n  'configurations.ios.digitalInputsPowerDirection':{t:'DI Power Direction',b:`SOURCE (PNP): drive provides +24V. SINK INTERNAL (NPN): drive provides GND. SINK EXTERNAL: external GND.${lnk('digital-input-logic-connection','DI Logic Connection')}`},\n  'configurations.dateTime.protocol':{t:'Time Synchronization',b:`MANUAL: set time manually via HMI or app. NETWORK SYNC (NTP): auto-sync via NTP server.${lnk('date-and-time','Date & Time')}`},\n  'configurations.dateTime.timezone':{t:'Timezone',b:`IANA TZ database name, e.g. America\/Toronto.${lnk('date-and-time','Date & Time')}`},\n  'configurations.dateTime.timeServerUri':{t:'NTP Server URIs',b:`Custom NTP server URIs. Leave empty to use default pool. Example: pool.ntp.org${lnk('date-and-time','Date & Time')}`},\n  'configurations.dateTime.disableBleSynchronization':{t:'Disable BLE Time Sync',b:`Prevent Bluetooth from automatically setting the clock.${lnk('date-and-time','Date & Time')}`},\n  'configurations.network.interfaces.0.method':{t:'Address Assignment',b:`DHCP: automatic. Manual (Static IPv4): fixed IP address.${lnk('network-settings','Network')}`},\n  'configurations.network.interfaces.0.ipv4Address':{t:'IPv4 Address',b:'Static IP address, e.g. 192.168.1.100.'},\n  'configurations.network.interfaces.0.ipv4Gateway':{t:'IPv4 Gateway',b:'Gateway IP address, e.g. 192.168.1.1.'},\n  'configurations.network.interfaces.0.ipv4NetworkMask':{t:'IPv4 Network Mask',b:'Subnet mask, e.g. 255.255.255.0.'},\n  'configurations.network.dns.method':{t:'DNS Method',b:`DHCP: use DNS provided by DHCP server. Static: always use the listed resolvers.${lnk('network-settings','Network')}`},\n  'configurations.network.dns.resolvers':{t:'DNS Resolvers',b:`DNS resolver addresses. Up to 4 resolvers.${lnk('network-settings','Network')}`},\n  'configurations.network.ping.mode':{t:'ICMP Ping',b:`DISABLED: drop ping requests. PERMANENT ON: respond to ping requests.${lnk('network-settings','Network')}`},\n  'configurations.modbus.active':{t:'Modbus TCP',b:`ENABLED: drive accepts Modbus TCP connections on port 502.${lnk('modbus-tcp','Modbus TCP')}`},\n  'configurations.bacnet.active':{t:'BACnet IP',b:`ENABLED: drive exposes a BACnet IP server.${lnk('bacnet-ip-configuration','BACnet IP')}`},\n  'configurations.bacnet.deviceId':{t:'BACnet Device ID',b:`Unique BACnet device ID (0\u20134194302).${lnk('bacnet-ip-configuration','BACnet IP')}`},\n  'configurations.bacnet.deviceName':{t:'BACnet Device Name',b:`Friendly name on the BACnet network (max 255 chars).${lnk('bacnet-ip-configuration','BACnet IP')}`},\n  'configurations.bacnet.port':{t:'BACnet UDP Port',b:`Default 47808 (0xBAC0).${lnk('bacnet-ip-configuration','BACnet IP')}`},\n  'configurations.ethernetIp':{t:'EtherNet\/IP',b:`ENABLED: drive accepts EtherNet\/IP connections from PLCs.${lnk('ethernet-ip','EtherNet\/IP')}`},\n  'configurations.ble.advertisingState':{t:'BLE Advertising',b:`ENABLED: mobile app can discover and connect. DISABLED: prevents new BLE connections.${lnk('ble-status','BLE')}`},\n  'configurations.display.screensaverConfiguration.timeout':{t:'Screensaver Timeout (s)',b:'Idle time in seconds before screensaver activates. Set to 0 to disable.'},\n  'configurations.display.screensaverConfiguration.mode':{t:'Screensaver Mode',b:'BACKLIGHT OFF: screen turns off'},\n  'configurations.softwareUpdate.selectedSource':{t:'Update Source',b:'SMARTD: download updates from SmartD servers. LAN: download from a local server. USB: install from USB.'},\n  'configurations.softwareUpdate.lanUrl':{t:'LAN Update URL',b:'URL or IP:port of the local update server. Only ports 80 and 443 are supported.'},\n  'configurations.system.emergencyMode.state':{t:'Emergency Fire Mode',b:`ENABLED: mode can be triggered via a digital input. DISABLED: feature is off.${lnk('emergency-fire-mode','Emergency Fire Mode')}`},\n  'configurations.system.emergencyMode.action':{t:'Emergency Mode Action',b:`RUN AT PRESET VALUE: start at configured emergency setpoints. STOP FREEWHEEL: coast to stop. KEEP CURRENT OPERATION: no change.${lnk('emergency-fire-mode','Emergency Fire Mode')}`},\n  'configurations.motor.emergencyRun.direction':{t:'Emergency Direction',b:`Motor direction during emergency run.${lnk('emergency-fire-mode','Emergency Fire Mode')}`},\n  'configurations.motor.emergencyRun.frequency':{t:'Emergency Frequency (Hz)',b:`Used in V\/F Frequency control mode.${lnk('emergency-fire-mode','Emergency Fire Mode')}`},\n  'configurations.motor.emergencyRun.velocity':{t:'Emergency Velocity (rpm)',b:`Used in V\/F Velocity, FOC, or Torque control modes.${lnk('emergency-fire-mode','Emergency Fire Mode')}`},\n  'alarm.disabled':{t:'Alarm Disabled (OOS)',b:'When disabled the alarm is Out-Of-Service and never triggers.'},\n  'alarm.action':{t:'Alarm Action',b:'NO ACTION: raise alarm only. STOP RAMP DOWN: controlled deceleration. STOP FREEWHEEL: coast immediately.'},\n  'alarm.triggerThreshold':{t:'Trigger Threshold',b:'Value that triggers the alarm. Leave blank to use the drive default.'},\n  'alarm.triggerDelay':{t:'Trigger Delay (s)',b:'Condition must persist this long before alarm fires. Leave blank for immediate trigger.'},\n  'alarm.resetThreshold':{t:'Reset Threshold',b:'Value at which the alarm auto-resets. Leave blank for drive default.'},\n  'alarm.resetDelay':{t:'Reset Delay (s)',b:'Reset condition must persist this long before the alarm clears.'},\n  'alarm.bootState':{t:'Boot State',b:'NORMAL: alarm starts inactive on boot. RESTORE: alarm state is persisted through a reboot.'},\n};\nfor(let i=0;i<6;i++){\n  HELP[`configurations.ios.digitalInputs.${i}.function`]={t:`DI ${i+1} Function`,b:`Function assigned to digital input ${i+1}.${lnk('digital-inputs-setting','Digital Inputs')}`};\n  HELP[`configurations.ios.digitalInputs.${i}.triggerType`]={t:`DI ${i+1} Trigger`,b:`RISING EDGE \/ FALLING EDGE \/ HIGH LEVEL \/ LOW LEVEL.${lnk('digital-inputs-setting','Digital Inputs')}`};\n  HELP[`configurations.ios.digitalInputs.${i}.debounceDelay`]={t:`DI ${i+1} Debounce`,b:`Debounce delay 0\u20135 s.${lnk('digital-inputs-setting','Digital Inputs')}`};\n}\nfor(let i=0;i<3;i++){\n  HELP[`configurations.ios.relays.${i}.function`]={t:`Relay ${i+1} Function`,b:`Function that controls this relay output.${lnk('digital-outputs-setting','Relays')}`};\n  HELP[`configurations.ios.relays.${i}.output`]={t:`Relay ${i+1} Default Coil State`,b:`ENERGIZED by default: coil is on at rest. DE-ENERGIZED by default: coil is off at rest.${lnk('digital-outputs-setting','Relays')}`};\n}\nfor(let i=0;i<3;i++){\n  HELP[`configurations.ios.analogInputs.${i}.function`]={t:`AI ${i+1} Function`,b:`Function assigned to analog input ${i+1}.${lnk('analog-inputs-setting','Analog Inputs')}`};\n  HELP[`configurations.ios.analogInputs.${i}.mode`]={t:`AI ${i+1} Mode`,b:`Signal type: 4\u201320 mA, 0\u201310 V, Resistor, or PTC.${lnk('analog-inputs-setting','Analog Inputs')}`};\n  for(let c=0;c<2;c++){\n    HELP[`configurations.ios.analogInputs.${i}.curvePoints.${c}.x`]={t:`AI ${i+1} CP${c+1} X`,b:`Raw signal value (V or mA).${lnk('analog-inputs-setting','Analog Inputs')}`};\n    HELP[`configurations.ios.analogInputs.${i}.curvePoints.${c}.y`]={t:`AI ${i+1} CP${c+1} Y`,b:`Scaled output value in function units.${lnk('analog-inputs-setting','Analog Inputs')}`};\n  }\n}\nfor(let i=0;i<2;i++){\n  HELP[`configurations.ios.analogOutputs.${i}.function`]={t:`AO ${i+1} Function`,b:`Function assigned to analog output ${i+1}.${lnk('analog-outputs-setting','Analog Outputs')}`};\n  HELP[`configurations.ios.analogOutputs.${i}.mode`]={t:`AO ${i+1} Mode`,b:`Signal type: 4\u201320 mA or 0\u201310 V.${lnk('analog-outputs-setting','Analog Outputs')}`};\n  for(let c=0;c<2;c++){\n    HELP[`configurations.ios.analogOutputs.${i}.curvePoints.${c}.x`]={t:`AO ${i+1} CP${c+1} X`,b:`Function value input.${lnk('analog-outputs-setting','Analog Outputs')}`};\n    HELP[`configurations.ios.analogOutputs.${i}.curvePoints.${c}.y`]={t:`AO ${i+1} CP${c+1} Y`,b:`Output signal value (V or mA).${lnk('analog-outputs-setting','Analog Outputs')}`};\n  }\n}\n\nconst clone = o => JSON.parse(JSON.stringify(o));\nlet DEFAULT_CFG = {}, cfg = {}, activePreset = null, wizSelId = null;\n\nconst almCfgs = () => { let c=get(cfg,'configurations.alarms.configs'); if(!Array.isArray(c)){c=[];set(cfg,'configurations.alarms.configs',c);} return c; };\nconst almByCode = code => almCfgs().find(a=>a.code===code)||null;\nfunction setAlm(code,fn){const cs=almCfgs();let i=cs.findIndex(a=>a.code===code);if(i<0){cs.push({code,disabled:true,triggerThreshold:null,triggerDelay:null,resetThreshold:null,resetDelay:null,triggerActions:[],bootState:'ALARM_BOOT_STATE_NORMAL'});i=cs.length-1;}fn(cs[i]);}\n\nfunction applyModel(sku){const m=MODELS[sku];if(!m)return;set(cfg,'_metadata.deviceModel',sku);set(cfg,'configurations.motor.motorNameplate.nominalVoltage',m.motor.v);set(cfg,'configurations.motor.motorNameplate.nominalCurrent',m.motor.i);set(cfg,'configurations.motor.motorNameplate.nominalPower',m.motor.kw);const lv=get(cfg,'configurations.line.nominalVoltage');if(!m.vo.includes(lv))set(cfg,'configurations.line.nominalVoltage',m.vo[0]);}\nfunction inferModel(){if(get(cfg,'_metadata.deviceModel'))return;applyModel(Number(get(cfg,'configurations.line.nominalVoltage'))>=580?'SDB-2-2220-A':'SDB-1-2185-A');}\nconst isHl = path => !!(activePreset && activePreset.paths.has(path));\n\nfunction tzOpts(){try{if(typeof Intl.supportedValuesOf==='function')return Intl.supportedValuesOf('timeZone').map(z=>({label:z,value:z}));}catch(_){}return['UTC','America\/Toronto','America\/New_York','Europe\/London','Europe\/Paris','Asia\/Seoul','Asia\/Tokyo','Asia\/Dubai'].map(z=>({label:z,value:z}));}\n\nconst SLUG_FR={\"technical-specifications\": \"caracteristiques-techniques\", \"organization-of-the-configurations\": \"organisation-des-configurations\", \"power-line-parameters\": \"parametres-alimentation-electrique\", \"vfd-control-by-mobile-app\": \"commande-vfd-par-application-mobile\", \"control-source\": \"source-de-commande\", \"motor-nameplate\": \"plaque-signaletique-du-moteur\", \"motor-phase-reversal\": \"inversion-des-phases-du-moteur\", \"motor-control-mode\": \"modes-de-controle-du-moteur\", \"v-f-start-modes\": \"modes-de-demarrage-v-f\", \"starting-torque-boos\": \"renforcement-couple-demarrage\", \"over-modulation\": \"sur-modulation\", \"stopping-the-motor\": \"arreter-le-moteur\", \"reverse-rotation-prevention\": \"prevention-de-la-rotation-inverse\", \"change-spinning-direction\": \"changer-le-sens-de-rotation\", \"spinning-direction-priority\": \"priorite-du-sens-de-rotation\", \"zero-speed-behavior\": \"comportement-vitesse-nulle\", \"lvrt\": \"lvrt\", \"internal-filter-compensation\": \"compensation-des-filtres-internes\", \"motor-thermal-protection\": \"protection-thermique-du-moteur\", \"automatic-restart\": \"redemarrage-automatique\", \"acceleration-and-deceleration-ramp-selection\": \"selection-rampe-acceleration-et-deceleration\", \"multi-segment-ramps\": \"rampes-segments-multiples\", \"preset-speeds\": \"vitesses-predefinies\", \"static-auto-tuning\": \"auto-reglage-statique\"};\nfunction hi(path){\n  const s=document.createElement('span');s.className='help-icon';s.textContent='?';\n  s.onclick=e=>{\n    e.stopPropagation();\n    const fr=(typeof LANG!=='undefined'&&LANG==='fr');\n    const en=HELP[path]||{t:'Help',b:'No help for: '+path};\n    const frd=(fr&&HELP_FR[path])?HELP_FR[path]:null;\n    const title=frd?frd.t:en.t;\n    let body;\n    if(frd){\n      let link=HELP_GUIDE_FR;\n      const enb=(HELP[path]&&HELP[path].b)||'';\n      const mm=enb.match(\/clean-power-vfd-publication\\\/([a-z0-9\\-]+)\/i);\n      if(mm){ const fr2=SLUG_FR[mm[1]]; if(fr2){ link='<br><br><a href=\"https:\/\/smartd.clickhelp.co\/articles\/#!clean-power-vfd-manuel-d-utilisation\/'+fr2+'\" target=\"_blank\" rel=\"noopener\">Guide d\\u2019utilisation \\u2197<\/a>'; } }\n      body=frd.b+link;\n    } else { body=en.b; }\n    const m=document.getElementById('alarmHelpModal');\n    if(m){ document.getElementById('alarmHelpTitle').textContent=title; const sub=document.getElementById('alarmHelpSub'); if(sub)sub.textContent=''; document.getElementById('alarmHelpBody').innerHTML=body; m.style.display='flex'; return; }\n    document.getElementById('ceHdTitle').textContent=title;document.getElementById('ceHdContent').innerHTML=body;document.getElementById('ceHelpDrawer').classList.add('open');document.getElementById('ceHdOverlay').classList.add('open');\n  };\n  return s;\n}\n\nconst FIELD_FR={\"Frequency Presets (Hz)\":\"Fr\u00e9quences pr\u00e9d\u00e9finies (Hz)\",\"Velocity Presets (rpm)\":\"Vitesses pr\u00e9d\u00e9finies (tr\/min)\",\"Stator Resistance (\u03a9)\":\"R\u00e9sistance statorique (\u03a9)\",\"Rotor Resistance (\u03a9)\":\"R\u00e9sistance rotorique (\u03a9)\",\"Magnetizing Inductance (H)\":\"Inductance magn\u00e9tisante (H)\",\"Leakage Inductance (H)\":\"Inductance de fuite (H)\",\"Moment of Inertia (kg\u00b7m\u00b2)\":\"Moment d\u2019inertie (kg\u00b7m\u00b2)\",\"Leave blank to use the drive's calculated defaults.\":\"Laisser vide pour utiliser les valeurs par d\u00e9faut calcul\u00e9es par le VFD.\",\"Ramp type\":\"Type de rampe\",\"Disabled \\u2014 enable to configure.\":\"D\\u00e9sactiv\\u00e9 \\u2014 activez pour configurer.\",\"Motor Overload\":\"Surcharge moteur\",\"Motor Thermal Overload\":\"Surcharge thermique moteur\",\"Motor Over-Temperature\":\"Surchauffe moteur\",\"Motor Stalled\":\"Moteur cal\u00e9\",\"Motor Overspeed\":\"Survitesse moteur\",\"Motor Underspeed\":\"Sous-vitesse moteur\",\"Motor Application Underload\":\"Sous-charge applicative moteur\",\"External Critical Alarm\":\"Alarme externe critique\",\"External Major Alarm\":\"Alarme externe majeure\",\"External Medium Alarm\":\"Alarme externe moyenne\",\"External Minor Alarm\":\"Alarme externe mineure\",\"Modbus Communication Loss\":\"Perte de communication Modbus\",\"BACnet Communication Loss\":\"Perte de communication BACnet\",\"HMI Communication Loss\":\"Perte de communication IHM\",\"PID 1 \u2014 Low Process Value\":\"PID 1 \u2014 Valeur de proc\u00e9d\u00e9 basse\",\"PID 1 \u2014 High Process Value\":\"PID 1 \u2014 Valeur de proc\u00e9d\u00e9 haute\",\"Trigger threshold (% of nominal current)\":\"Seuil de d\u00e9clenchement (% du courant nominal)\",\"Trigger threshold (%)\":\"Seuil de d\u00e9clenchement (%)\",\"Trigger threshold (rpm)\":\"Seuil de d\u00e9clenchement (tr\/min)\",\"Trigger threshold (s)\":\"Seuil de d\u00e9clenchement (s)\",\"Trigger threshold\":\"Seuil de d\u00e9clenchement\",\"Trigger delay (s)\":\"D\u00e9lai de d\u00e9clenchement (s)\",\"Reset threshold (%)\":\"Seuil de r\u00e9initialisation (%)\",\"Reset threshold\":\"Seuil de r\u00e9initialisation\",\"Reset delay (s)\":\"D\u00e9lai de r\u00e9initialisation (s)\",\"Memory\":\"M\u00e9moire\",\"Severity\":\"Gravit\u00e9\",\"Action\": \"Action\", \"Address Assignment\": \"Attribution d\u2019adresse\", \"Advertising State\": \"\u00c9tat de diffusion\", \"Analog Inputs\": \"Entr\u00e9es analogiques\", \"Analog Outputs\": \"Sorties analogiques\", \"Auto Restart\": \"Red\u00e9marrage automatique\", \"Avg runtime per start \": \"Dur\u00e9e moyenne par d\u00e9marrage \", \"BACnet IP\": \"BACnet IP\", \"Bluetooth (BLE)\": \"Bluetooth (BLE)\", \"Boost Cutoff (% nominal freq)\": \"Seuil de coupure du boost (% fr\u00e9q. nom.)\", \"Boost Voltage (%)\": \"Tension de boost (%)\", \"Bumpless Direction Change\": \"Changement de sens sans \u00e0-coup\", \"Configuration Name\": \"Nom de configuration\", \"Control Algorithm\": \"Algorithme de contr\u00f4le\", \"Control Mode\": \"Mode de contr\u00f4le\", \"Control Options\": \"Options de contr\u00f4le\", \"Control Source Selector\": \"S\u00e9lecteur de source de contr\u00f4le\", \"Custom Unit Label\": \"Libell\u00e9 d\u2019unit\u00e9 personnalis\u00e9e\", \"DNS\": \"DNS\", \"DNS Method\": \"M\u00e9thode DNS\", \"Days per reboot \": \"Jours par red\u00e9marrage \", \"Deadband\": \"Bande morte\", \"Deadband Delay (s)\": \"D\u00e9lai de bande morte (s)\", \"Deadband Value (\u00b1)\": \"Valeur de bande morte (\u00b1)\", \"Debounce Delay (s)\": \"D\u00e9lai anti-rebond (s)\", \"Default Coil State\": \"\u00c9tat de bobine par d\u00e9faut\", \"Default Setpoint\": \"Consigne par d\u00e9faut\", \"Device ID\": \"ID d\u2019appareil\", \"Device Name\": \"Nom d\u2019appareil\", \"Digital Inputs\": \"Entr\u00e9es\", \"Disable BLE Time Sync\": \"D\u00e9sactiver la sync BLE de l\u2019heure\", \"VFD Name\": \"Nom du VFD\", \"Eco Mode\": \"Mode \u00c9co\", \"Enable PID\": \"Activer le PID\", \"Enable\/Disable\": \"Activer\/D\u00e9sactiver\", \"Engineering Unit\": \"Unit\u00e9 technique\", \"EtherNet\/IP\": \"EtherNet\/IP\", \"FOC Options\": \"Options FOC\", \"Forced Output\": \"Sortie forc\u00e9e\", \"Forced Output Preset (Hz or rpm)\": \"Pr\u00e9r\u00e9glage de sortie forc\u00e9e (Hz ou tr\/min)\", \"Forced Output Type\": \"Type de sortie forc\u00e9e\", \"Frequency (Hz)\": \"Fr\u00e9quence (Hz)\", \"Function\": \"Fonction\", \"General\": \"G\u00e9n\u00e9ral\", \"High Torque Limit (% nominal)\": \"Limite de couple haut (% nominal)\", \"ICMP Ping\": \"Ping ICMP\", \"IP Address\": \"Adresse IP\", \"IPv4 Address\": \"Adresse IPv4\", \"IPv4 Gateway\": \"Passerelle IPv4\", \"IPv4 Network Mask\": \"Masque de sous-r\u00e9seau IPv4\", \"Kp\": \"Kp\", \"Low Torque Limit (%, negative)\": \"Limite de couple bas (%, n\u00e9gatif)\", \"Low Voltage Ride Through\": \"Tenue aux creux de tension\", \"Max Control Output (%)\": \"Sortie de commande max (%)\", \"Max Process Value\": \"Valeur de proc\u00e9d\u00e9 max\", \"Max Velocity (rpm \u2014 0=nameplate)\": \"Vitesse max (tr\/min \u2014 0=plaque)\", \"Max Velocity (rpm)\": \"Vitesse max (tr\/min)\", \"Maximum Frequency (Hz)\": \"Fr\u00e9quence maximale (Hz)\", \"Maximum Retries (1\u201310)\": \"Nombre max de tentatives (1\u201310)\", \"Min Control Output (%)\": \"Sortie de commande min (%)\", \"Min Process Value\": \"Valeur de proc\u00e9d\u00e9 min\", \"Min Velocity (rpm)\": \"Vitesse min (tr\/min)\", \"Minimum Frequency (Hz)\": \"Fr\u00e9quence minimale (Hz)\", \"Modbus TCP\": \"Modbus TCP\", \"Mode\": \"Mode\", \"Model\": \"Mod\u00e8le\", \"Motor Control by Mobile App\": \"Contr\u00f4le moteur par l\u2019application mobile\", \"Motor Cooling Type\": \"Type de refroidissement moteur\", \"Motor Direction\": \"Sens du moteur\", \"Motor Frequency (Hz)\": \"Fr\u00e9quence moteur (Hz)\", \"Motor Velocity (rpm)\": \"Vitesse moteur (tr\/min)\", \"Network\": \"R\u00e9seau\", \"Nominal Current (A)\": \"Courant nominal (A)\", \"Nominal Frequency (Hz)\": \"Fr\u00e9quence nominale (Hz)\", \"Nominal Power\": \"Puissance nominale\", \"Nominal Power Unit\": \"Unit\u00e9 de puissance nominale\", \"Nominal Speed (RPM)\": \"Vitesse nominale (tr\/min)\", \"Nominal Voltage (V)\": \"Tension nominale (V)\", \"Normal Stop Behaviour\": \"Comportement \u00e0 l\u2019arr\u00eat normal\", \"Number of Poles\": \"Nombre de p\u00f4les\", \"Output Compensation (%)\": \"Compensation de sortie (%)\", \"Output Limits\": \"Limites de sortie\", \"Over-Modulation\": \"Surmodulation\", \"PID Gains\": \"Gains PID\", \"Phase Reversal\": \"Inversion de phase\", \"Ping Mode\": \"Mode Ping\", \"Port\": \"Port\", \"Power Direction\": \"Sens d\u2019alimentation\", \"Power Factor\": \"Facteur de puissance\", \"Primary Remote Source (Source 1)\": \"Source distante primaire (Source 1)\", \"Process Ramp Rates\": \"Taux de rampe de proc\u00e9d\u00e9\", \"Ramp Down Rate (units\/s, 0=off)\": \"Taux de d\u00e9c\u00e9l\u00e9ration (unit\u00e9s\/s, 0=d\u00e9sact.)\", \"Ramp Duration (s)\": \"Dur\u00e9e de rampe (s)\", \"Ramp Up Rate (units\/s, 0=off)\": \"Taux d\u2019acc\u00e9l\u00e9ration (unit\u00e9s\/s, 0=d\u00e9sact.)\", \"Relays\": \"Relais\", \"Reset Delay (s, optional)\": \"D\u00e9lai de r\u00e9initialisation (s, optionnel)\", \"Reset Threshold (optional)\": \"Seuil de r\u00e9initialisation (optionnel)\", \"Restart Delay (s)\": \"D\u00e9lai de red\u00e9marrage (s)\", \"Restart success rate \": \"Taux de r\u00e9ussite des red\u00e9marrages \", \"Reverse Enabled\": \"Marche arri\u00e8re autoris\u00e9e\", \"Screensaver\": \"\u00c9conomiseur d\u2019\u00e9cran\", \"Screensaver Timeout (s \u2014 0=off)\": \"D\u00e9lai de l\u2019\u00e9conomiseur (s \u2014 0=d\u00e9sact.)\", \"Secondary Remote Source (Source 2)\": \"Source distante secondaire (Source 2)\", \"Sensor Offset\": \"D\u00e9calage du capteur\", \"Sleep \/ Wake\": \"Veille \/ R\u00e9veil\", \"Sleep Delay (s)\": \"D\u00e9lai de mise en veille (s)\", \"Sleep Threshold\": \"Seuil de mise en veille\", \"Speed Response\": \"R\u00e9ponse en vitesse\", \"Speed Source\": \"Source de vitesse\", \"Spinning Direction Priority\": \"Priorit\u00e9 de sens de rotation\", \"Starts per hour \": \"D\u00e9marrages par heure \", \"Td (s)\": \"Td (s)\", \"Temperature at Power-Up\": \"Temp\u00e9rature \u00e0 la mise sous tension\", \"Thermal Protection\": \"Protection thermique\", \"Ti (s)\": \"Ti (s)\", \"Time Synchronization Protocol\": \"Protocole de synchronisation de l\u2019heure\", \"Timezone\": \"Fuseau horaire\", \"Torque\": \"Couple\", \"Torque Boost\": \"Surtension de d\u00e9marrage (boost)\", \"Trigger Delay (s)\": \"D\u00e9lai de d\u00e9clenchement (s)\", \"Trigger Type\": \"Type de d\u00e9clenchement\", \"Uptime since last boot \": \"Temps de fonctionnement depuis le dernier d\u00e9marrage \", \"V\/F Options\": \"Options V\/F\", \"V\/F Start Mode\": \"Mode de d\u00e9marrage V\/F\", \"Velocity\": \"Vitesse\", \"Voltage (V)\": \"Tension (V)\", \"Voltage Drop Compensation\": \"Compensation de chute de tension\", \"Wakeup Delay (s)\": \"D\u00e9lai de r\u00e9veil (s)\", \"Wakeup Error Threshold\": \"Seuil d\u2019erreur de r\u00e9veil\", \"X\": \"X\", \"Y\": \"Y\", \"Zero Speed Behaviour\": \"Comportement \u00e0 vitesse nulle\"};\nfunction frOf(label){var m;if(FIELD_FR[label]!=null)return FIELD_FR[label];if(m=label.match(\/^Digital Input (\\d+)$\/))return 'DI'+m[1];if(m=label.match(\/^Relay (\\d+)$\/))return 'R'+m[1];if(m=label.match(\/^Analog Output (\\d+)$\/))return 'AQ'+m[1];if(m=label.match(\/^Analog Input (\\d+)$\/))return 'AI'+m[1];if(m=label.match(\/^Curve Point (\\d+)$\/i))return 'Point de courbe '+m[1];return '';}\nfunction fldSpan(label){var sp=document.createElement('span');sp.className='fld-t';var fr=frOf(label);sp.setAttribute('data-fld-en',label);if(fr)sp.setAttribute('data-fld-fr',fr);sp.textContent=(typeof LANG!=='undefined'&&LANG==='fr'&&fr)?fr:label;return sp;}\nfunction field(label, path, type='text', opts={}) {\n  const hl = isHl(path);\n  const wrap = document.createElement('div');\n  wrap.className = 'field' + (hl ? ' hl' : '');\n  const lbl = document.createElement('label');\n  lbl.appendChild(fldSpan(label));\n  lbl.appendChild(hi(path));\n  if (hl) { const dot=document.createElement('span'); dot.className='hl-dot'; dot.title=_isFr()?'D\u00e9fini par le pr\u00e9r\u00e9glage d\u2019application':'Set by application preset'; lbl.appendChild(dot); }\n  wrap.appendChild(lbl);\n  const val = get(cfg, path);\n  let el;\n  if (type==='select') {\n    el = document.createElement('select');\n    (opts.options||[]).forEach(o => { const opt=document.createElement('option'); opt.value=String(o.value??o); opt.textContent=String(o.label??o); if(String(opt.value)===String(val))opt.selected=true; el.appendChild(opt); });\n  } else if (type==='number') {\n    el = Object.assign(document.createElement('input'),{type:'number',step:'any',value:val??''});\n    if(opts.min!=null)el.min=opts.min; if(opts.max!=null)el.max=opts.max;\n  } else if (type==='textarea') {\n    el = document.createElement('textarea'); el.value = val??'';\n  } else {\n    el = Object.assign(document.createElement('input'),{type:'text',value:val??''});\n    if(type==='readonly'){el.readOnly=true;el.classList.add('readonly');}\n    if(opts.maxlength)el.maxLength=opts.maxlength;\n  }\n  if(opts.disabled) el.disabled = true;\n  el.addEventListener('change', () => { const v=type==='number'?Number(el.value):el.value; set(cfg,path,v); opts.onChange?.(v,el); });\n  wrap.appendChild(el);\n  return wrap;\n}\n\nfunction nullNum(label, hkey, value, onChange, opts={}) {\n  const wrap=document.createElement('div');wrap.className='field';\n  const lbl=document.createElement('label');lbl.appendChild(fldSpan(label));lbl.appendChild(hi(hkey));wrap.appendChild(lbl);\n  const el=document.createElement('input');el.type='number';el.step='any';el.placeholder='(drive default)';\n  if(opts.min!=null)el.min=opts.min;\n  el.value=value!=null?value:'';\n  el.addEventListener('change',()=>onChange(el.value===''?null:Number(el.value)));\n  wrap.appendChild(el);return wrap;\n}\n\nfunction durField(label, hkey, value, onChange) {\n  const wrap=document.createElement('div');wrap.className='field';\n  const lbl=document.createElement('label');lbl.appendChild(fldSpan(label));lbl.appendChild(hi(hkey));wrap.appendChild(lbl);\n  const el=document.createElement('input');el.type='number';el.step='0.001';el.min='0';el.placeholder='(drive default, s)';\n  el.value=value!=null?parseFloat(value):'';\n  el.addEventListener('change',()=>onChange(el.value===''?null:`${el.value}s`));\n  wrap.appendChild(el);return wrap;\n}\n\nfunction sh(text){const d=document.createElement('div');d.className='subhead';d.appendChild(fldSpan(text));return d;}\n\nlet ceSecState = {};   \/\/ remembers each section's collapsed state across re-renders\nconst SEC_FR = {\"CLEAN POWER VFD\": \"CLEAN POWER VFD\", \"INPUT LINE\": \"R\u00c9SEAU D\u2019ENTR\u00c9E\", \"LOCAL \/ REMOTE CONTROL\": \"CONTR\u00d4LE LOCAL \/ \u00c0 DISTANCE\", \"MOTOR NAMEPLATE\": \"PLAQUE SIGNAL\u00c9TIQUE MOTEUR\", \"MOTOR CONTROL\": \"CONTR\u00d4LE MOTEUR\", \"MOTOR RAMPS\": \"RAMPES MOTEUR\", \"MOTOR LIMITS\": \"LIMITES MOTEUR\", \"MOTOR SPEED PRESETS\": \"VITESSES PR\u00c9R\u00c9GL\u00c9ES DU MOTEUR\", \"MOTOR ADVANCED PARAMETERS (AUTOTUNE)\": \"PARAM\u00c8TRES AVANC\u00c9S MOTEUR (AUTOTUNE)\", \"I\/Os\": \"E\/S\", \"PID CONTROLLER\": \"R\u00c9GULATEUR PID\", \"DATE & TIME\": \"DATE ET HEURE\", \"NETWORK & COMMUNICATION PROTOCOLS\": \"R\u00c9SEAU ET PROTOCOLES DE COMMUNICATION\", \"DISPLAY\": \"AFFICHAGE\", \"EMERGENCY FIRE MODE\": \"MODE D'INCENDIE\", \"ALARM CONFIGURATION\": \"CONFIGURATION DES ALARMES\"};   \/\/ French names for configuration sections\nfunction grp(title, closed=false){\n  const g=document.createElement('div');\n  const _closed=(title in ceSecState)?ceSecState[title]:closed;\n  g.className='group'+(_closed?' closed':'');\n  const h=document.createElement('div');h.className='head';\n  const _frT=SEC_FR[title]||'';\n  const _disp=(typeof LANG!=='undefined'&&LANG==='fr'&&_frT)?_frT:title;\n  h.innerHTML=`<svg class=\"chev\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"9 18 15 12 9 6\"\/><\/svg><h3 data-sec-en=\"${escapeHtml(title)}\" data-sec-fr=\"${escapeHtml(_frT)}\">${escapeHtml(_disp)}<\/h3>`;\n  h.onclick=()=>{ g.classList.toggle('closed'); ceSecState[title]=g.classList.contains('closed'); };\n  const b=document.createElement('div');b.className='body';g.append(h,b);return{root:g,body:b};\n}\n\nfunction ensureArr(path,len,def={}){let a=get(cfg,path);if(!Array.isArray(a))a=[];while(a.length<len)a.push(clone(def));set(cfg,path,a);return a;}\n\nfunction cpFields(body,base){ensureArr(`${base}.curvePoints`,2,{x:0,y:0});for(let c=0;c<2;c++){body.appendChild(sh(`Curve Point ${c+1}`));body.appendChild(field('X',`${base}.curvePoints.${c}.x`,'number'));body.appendChild(field('Y',`${base}.curvePoints.${c}.y`,'number'));}}\n\nfunction listField(label,hkey,arrPath,ph,type='text'){\n  const wrap=document.createElement('div');\n  const lbl=document.createElement('div');lbl.style.cssText='font-weight:600;margin-bottom:4px;display:flex;align-items:center;gap:4px;';lbl.appendChild(fldSpan(label));lbl.appendChild(hi(hkey));wrap.appendChild(lbl);\n  const ld=document.createElement('div');ld.className='list-builder';\n  const rr=()=>{ld.innerHTML='';const arr=get(cfg,arrPath)||[];arr.forEach((item,idx)=>{const row=document.createElement('div');row.className='list-item';const inp=document.createElement('input');inp.type=type;inp.value=item;inp.placeholder=ph;inp.onchange=()=>{const a=[...get(cfg,arrPath)];a[idx]=type==='number'?Number(inp.value):inp.value;set(cfg,arrPath,a);};const rm=document.createElement('button');rm.textContent='\u2715';rm.className='danger';rm.style.cssText='padding:4px 8px;font-size:12px;';rm.onclick=()=>{const a=[...get(cfg,arrPath)];a.splice(idx,1);set(cfg,arrPath,a);rr();};row.append(inp,rm);ld.appendChild(row);});const add=document.createElement('button');add.textContent='+ Add';add.style.cssText='padding:4px 10px;font-size:12px;color:var(--g);border-color:var(--g);margin-top:2px;';add.onclick=()=>{const a=[...(get(cfg,arrPath)||[])];a.push(type==='number'?0:'');set(cfg,arrPath,a);rr();};ld.appendChild(add);};rr();wrap.appendChild(ld);return wrap;\n}\n\nfunction onDIFn(i,v){const tp=`configurations.ios.digitalInputs.${i}.triggerType`;set(cfg,tp,v==='IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED'?'IO_TRIGGER_TYPE_UNSPECIFIED':DI_NOTRIG.has(v)?'IO_TRIGGER_TYPE_UNSPECIFIED':DI_RISING.has(v)?'IO_TRIGGER_TYPE_RISING_EDGE':'IO_TRIGGER_TYPE_HIGH');render();}\nfunction onRevEn(v){const mo='configurations.motor.options';const en=v===true||v==='true';set(cfg,`${mo}.bumplessDirectionChange`,en?'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_ENABLED':'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_DISABLED');if(!en)set(cfg,`${mo}.spinningDirectionPriority`,'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_RAISE_ALARM');render();}\nfunction onNetMethod(v){const base='configurations.network.interfaces.0';const s=v==='NETWORK_CONFIGURATION_METHOD_STATIC_IPV4';set(cfg,`${base}.ipv4Address`,s?'10.10.10.10':'');set(cfg,`${base}.ipv4Gateway`,s?'10.10.10.1':'');set(cfg,`${base}.ipv4NetworkMask`,s?'255.255.255.0':'');render();}\n\nconst pdur = s => { if(s==null)return null; const n=parseFloat(s); return isNaN(n)?null:n; };\nconst fdur = n => n==null ? null : `${n}s`;\n\nfunction getCurveTotal(cv){\n  if(!cv)return 0;\n  if(cv.linear)return pdur(cv.linear.rampDuration)??0;\n  if(cv.sCurve)return pdur(cv.sCurve.rampDuration)??0;\n  if(cv.linearSegments)return (cv.linearSegments.segments||[]).reduce((s,sg)=>s+(pdur(sg.duration)??0),0);\n  return 0;\n}\n\nfunction rampCard(body, path, idx, label, isUp){\n  const cv = get(cfg, path) || {};\n  const card = document.createElement('div'); card.className = 'card';\n  const ch = document.createElement('div'); ch.className = 'card-header';\n  const t = document.createElement('span'); t.className = 'card-title';\n  t.textContent = `${label} ${idx+1} \u2014 ${cv.linearSegments ? 'Segments' : 'Linear'}`;\n  const totSp = document.createElement('span');\n  totSp.style.cssText = 'font-size:11px;color:var(--m);margin-left:4px;';\n  const tot = getCurveTotal(cv); totSp.textContent = tot > 0 ? `${tot.toFixed(2)} s` : '';\n  const exp = document.createElement('button'); exp.textContent='\u25be'; exp.style.cssText='padding:2px 8px;font-size:12px;border-radius:6px;';\n  exp.onclick = () => card.classList.toggle('closed');\n  ch.append(t, totSp, exp); card.appendChild(ch);\n  const cb = document.createElement('div'); cb.className = 'card-body';\n  const tw = document.createElement('div'); tw.className = 'field';\n  const tl = document.createElement('label'); tl.appendChild(fldSpan('Ramp type'));\n  const ts = document.createElement('select');\n  [['Linear','linear'],['Multiple Segments','linearSegments']].forEach(([n,v])=>{\n    const opt=document.createElement('option'); opt.value=v; opt.textContent=n;\n    const ct = cv.linearSegments ? 'linearSegments' : 'linear'; opt.selected = ct===v;\n    ts.appendChild(opt);\n  });\n  ts.onchange = () => {\n    const nc = {selectable: cv.selectable||'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'};\n    if(ts.value==='linear') nc.linear = {rampDuration:'5s'};\n    else nc.linearSegments = {segments:[{duration:'5s',endValue:null},{duration:'5s',endValue:null}]};\n    set(cfg, path, nc); render();\n  };\n  tw.append(tl, ts); cb.appendChild(tw);\n  if(cv.linear){\n    cb.appendChild(durField('Ramp Duration (s)','ramp.linear.duration', cv.linear.rampDuration, v=>{\n      set(cfg, `${path}.linear.rampDuration`, v);\n      totSp.textContent = `${pdur(v)?.toFixed(2)||0} s`;\n    }));\n  } else if(cv.linearSegments){\n    const tbl = document.createElement('div');\n    tbl.style.cssText = 'display:grid;grid-template-columns:26px 1fr 1fr 30px;gap:4px 8px;align-items:center;font-size:12px;';\n    ['#','Duration (s)','End Value',''].forEach(h=>{\n      const d=document.createElement('div'); d.style.cssText='font-weight:700;color:var(--g);font-size:10px;text-transform:uppercase;'; d.textContent=h; tbl.appendChild(d);\n    });\n    const rrSegs = () => {\n      while(tbl.children.length > 4) tbl.removeChild(tbl.lastChild);\n      const segs = get(cfg, `${path}.linearSegments.segments`) || [];\n      segs.forEach((sg, si) => {\n        const isLast = si === segs.length - 1;\n        const nb = document.createElement('div');\n        nb.style.cssText='width:22px;height:22px;border-radius:50%;background:var(--g);color:#fff;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;';\n        nb.textContent = si+1; tbl.appendChild(nb);\n        const di = document.createElement('input'); di.type='number'; di.step='0.1'; di.min='0.1'; di.placeholder='s'; di.value=pdur(sg.duration)??'';\n        di.onchange = () => {\n          const s2 = [...get(cfg,`${path}.linearSegments.segments`)];\n          s2[si] = {...s2[si], duration: fdur(di.value===''?null:Number(di.value))};\n          set(cfg,`${path}.linearSegments.segments`,s2);\n          totSp.textContent = `${getCurveTotal(get(cfg,path)).toFixed(2)} s`;\n        };\n        tbl.appendChild(di);\n        const ei = document.createElement('input'); ei.type='number'; ei.step='any';\n        if(isLast){\n          ei.placeholder = isUp ? '\u2192 nominal' : '\u2192 0'; ei.disabled=true; ei.style.cssText='background:#f5f5f5;color:#9ca3af;';\n        } else {\n          ei.placeholder='Hz\/rpm'; ei.value=sg.endValue!=null?sg.endValue:'';\n          ei.onchange = () => {\n            const s2=[...get(cfg,`${path}.linearSegments.segments`)];\n            s2[si]={...s2[si],endValue:ei.value===''?null:Number(ei.value)};\n            set(cfg,`${path}.linearSegments.segments`,s2);\n          };\n        }\n        tbl.appendChild(ei);\n        const rb = document.createElement('button');\n        if(segs.length > 1){\n          rb.textContent='\u2715'; rb.className='danger'; rb.style.cssText='padding:2px 6px;font-size:11px;';\n          rb.onclick = () => {\n            const s2=[...get(cfg,`${path}.linearSegments.segments`)]; s2.splice(si,1);\n            set(cfg,`${path}.linearSegments.segments`,s2); rrSegs();\n          };\n        }\n        tbl.appendChild(rb);\n      });\n      if(segs.length < 4){\n        tbl.appendChild(document.createElement('div'));\n        const ab = document.createElement('button'); ab.textContent='+ Add segment';\n        ab.style.cssText='padding:4px 10px;font-size:12px;color:var(--g);border-color:var(--g);grid-column:span 2;';\n        ab.onclick = () => {\n          const s2=[...get(cfg,`${path}.linearSegments.segments`)];\n          s2.splice(s2.length-1,0,{duration:'2s',endValue:null});\n          set(cfg,`${path}.linearSegments.segments`,s2); rrSegs();\n        };\n        tbl.appendChild(ab); tbl.appendChild(document.createElement('div'));\n      }\n    };\n    rrSegs(); cb.appendChild(tbl);\n  }\n  card.appendChild(cb); body.appendChild(card);\n}\n\nfunction rampSection(body, label, curvePath, isUp){\n  const curves = get(cfg, curvePath) || [];\n  const sh2 = document.createElement('div'); sh2.style.cssText='display:flex;align-items:center;gap:8px;margin:6px 0 2px;';\n  const shTxt = document.createElement('div'); shTxt.className='subhead'; shTxt.style.margin='0'; shTxt.textContent=label; sh2.appendChild(shTxt);\n  if(curves.length < 4){\n    const ab = document.createElement('button'); ab.textContent='+ Add Curve'; ab.style.cssText='padding:2px 8px;font-size:12px;color:var(--g);border-color:var(--g);';\n    ab.onclick = () => {\n      const a=[...(get(cfg,curvePath)||[])]; a.push({linear:{rampDuration:'5s'},selectable:'MOTOR_CONTROLLER_CURVE_SELECTABLE_ENABLED'});\n      set(cfg,curvePath,a); render();\n    };\n    sh2.appendChild(ab);\n  }\n  if(curves.length > 1){\n    const rb = document.createElement('button'); rb.textContent='- Remove Last'; rb.style.cssText='padding:2px 8px;font-size:12px;'; rb.className='danger';\n    rb.onclick = () => { const a=[...(get(cfg,curvePath)||[])]; a.pop(); set(cfg,curvePath,a); render(); };\n    sh2.appendChild(rb);\n  }\n  body.appendChild(sh2);\n  curves.forEach((_,i) => rampCard(body, `${curvePath}.${i}`, i, label, isUp));\n}\n\nconst ALM_ACT2=[['STOP \u2014 FREEWHEEL','ALARM_ACTION_STOP_FREEWHEEL'],['STOP \u2014 RAMP DOWN','ALARM_ACTION_STOP_RAMP_DOWN']];\nconst ALM_RULES={\n ALARM_CODE_MOTOR_OVERLOAD:{fields:[{k:'th',label:'Trigger threshold (% of nominal current)',def:110,min:100,max:150},{k:'td',label:'Trigger delay (s)',def:60,min:1,max:600},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_FREEWHEEL'},{k:'rth',label:'Reset threshold (%)',def:100,min:0,max:150},{k:'rd',label:'Reset delay (s)',def:60,min:1,max:600}]},\n ALARM_CODE_MOTOR_APPLICATION_UNDERLOAD:{fields:[{k:'th',label:'Trigger threshold (%)',def:10,min:10,max:90},{k:'td',label:'Trigger delay (s)',def:10,min:1,max:600},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'}]},\n ALARM_CODE_MOTOR_OVERTEMP:{fields:[{k:'mem',label:'Memory',def:'on'},{k:'td',label:'Trigger delay (s)',def:0.2,min:0,max:10},{k:'rd',label:'Reset delay (s)',def:0,min:0,max:10}]},\n ALARM_CODE_MOTOR_UNDERSPEED:{fields:[{k:'th',label:'Trigger threshold (%)',def:100,min:10,max:100},{k:'td',label:'Trigger delay (s)',def:2,min:1,max:10},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'}]},\n ALARM_CODE_MOTOR_OVERSPEED:{fields:[{k:'th',label:'Trigger threshold (%)',def:115,min:100,max:150},{k:'td',label:'Trigger delay (s)',def:2,min:1,max:10},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'},{k:'rth',label:'Reset threshold (%)',def:100,min:0,max:150},{k:'rd',label:'Reset delay (s)',def:2,min:0,max:10}]},\n ALARM_CODE_MOTOR_THERMAL_OVERLOAD:{fields:[{k:'mem',label:'Memory',def:'off'},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'}]},\n ALARM_CODE_MOTOR_STALLED:{noEnable:true,fields:[{k:'th',label:'Trigger threshold (rpm)',def:60,min:60,max:200},{k:'td',label:'Trigger delay (s)',def:10,min:1,max:200}]},\n ALARM_CODE_PID_1_LOW_PROCESS_VALUE:{fields:[{k:'th',label:'Trigger threshold',def:0,min:0,max:100},{k:'td',label:'Trigger delay (s)',def:10,min:0,max:600},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_FREEWHEEL'},{k:'rth',label:'Reset threshold',def:5,min:0,max:100},{k:'rd',label:'Reset delay (s)',def:10,min:0,max:600}]},\n ALARM_CODE_PID_1_HIGH_PROCESS_VALUE:{fields:[{k:'th',label:'Trigger threshold',def:100,min:0,max:100},{k:'td',label:'Trigger delay (s)',def:10,min:0,max:600},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_FREEWHEEL'},{k:'rth',label:'Reset threshold',def:95,min:0,max:100},{k:'rd',label:'Reset delay (s)',def:10,min:0,max:600}]},\n ALARM_CODE_MODBUS_COMM_LOSS:{fields:[{k:'td',label:'Trigger threshold (s)',def:5,min:1,max:60},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'}]},\n ALARM_CODE_BACNET_COMM_LOSS:{fields:[{k:'td',label:'Trigger threshold (s)',def:60,min:10,max:300},{k:'act',label:'Action',def:'ALARM_ACTION_STOP_RAMP_DOWN'}]},\n ALARM_CODE_HMI_COMM_LOSS:{severityOnly:true,fields:[{k:'sev',label:'Severity',def:'ALARM_SEVERITY_CRITICAL'}]},\n ALARM_CODE_EXTERNAL_1:{fields:[{k:'mem',label:'Memory',def:'off'},{k:'td',label:'Trigger delay (s)',def:0.2,min:0,max:120},{k:'rd',label:'Reset delay (s)',def:0.2,min:0,max:120}]},\n ALARM_CODE_EXTERNAL_2:{fields:[{k:'mem',label:'Memory',def:'off'},{k:'td',label:'Trigger delay (s)',def:0.2,min:0,max:120},{k:'rd',label:'Reset delay (s)',def:0.2,min:0,max:120}]},\n ALARM_CODE_EXTERNAL_3:{fields:[{k:'mem',label:'Memory',def:'off'},{k:'td',label:'Trigger delay (s)',def:0.2,min:0,max:120},{k:'rd',label:'Reset delay (s)',def:0.2,min:0,max:120}]},\n ALARM_CODE_EXTERNAL_4:{fields:[{k:'mem',label:'Memory',def:'off'},{k:'td',label:'Trigger delay (s)',def:0.2,min:0,max:120},{k:'rd',label:'Reset delay (s)',def:0.2,min:0,max:120}]},\n};\nfunction almSelField(label,pairs,cur,onChange){var w=document.createElement('div');w.className='field';var l=document.createElement('label');l.appendChild(fldSpan(label));w.appendChild(l);var s=document.createElement('select');pairs.forEach(function(pr){var o=document.createElement('option');o.value=pr[1];o.textContent=pr[0];if(pr[1]===cur)o.selected=true;s.appendChild(o);});s.onchange=function(){onChange(s.value);};w.appendChild(s);return w;}\nfunction almNumField(label,val,min,max,step,onChange){var w=document.createElement('div');w.className='field';var l=document.createElement('label');l.appendChild(fldSpan(label));w.appendChild(l);var i=document.createElement('input');i.type='number';if(min!=null)i.min=min;if(max!=null)i.max=max;i.step=step||'any';i.value=(val!=null?val:'');i.onchange=function(){onChange(i.value);};w.appendChild(i);return w;}\nfunction almField(code,f){\n  var ac=function(){return almByCode(code);};\n  if(f.k==='act'){ var _c0=(ac().triggerActions&&ac().triggerActions[0]&&ac().triggerActions[0].action)||null; if(_c0!=='ALARM_ACTION_STOP_FREEWHEEL'&&_c0!=='ALARM_ACTION_STOP_RAMP_DOWN')setAlm(code,function(a){a.triggerActions=[{action:f.def}];}); var cur=(ac().triggerActions[0]&&ac().triggerActions[0].action); return almSelField(f.label||'Action',ALM_ACT2,cur,function(v){setAlm(code,function(a){a.triggerActions=[{action:v}];});}); }\n  if(f.k==='mem'){ var memDef=(f.def==='on'?'ALARM_BOOT_STATE_RESTORE':'ALARM_BOOT_STATE_NORMAL'); if(ac().bootState==null)setAlm(code,function(a){a.bootState=memDef;}); var mcur=ac().bootState||memDef; return almSelField(f.label||'Memory',[['ENABLED','ALARM_BOOT_STATE_RESTORE'],['DISABLED','ALARM_BOOT_STATE_NORMAL']],mcur,function(v){setAlm(code,function(a){a.bootState=v;});}); }\n  if(f.k==='sev'){ if(ac().severity==null)setAlm(code,function(a){a.severity=f.def;}); var scur=ac().severity||f.def; return almSelField(f.label||'Severity',[['CRITICAL','ALARM_SEVERITY_CRITICAL'],['MINOR','ALARM_SEVERITY_MINOR']],scur,function(v){setAlm(code,function(a){a.severity=v;});}); }\n  var isDelay=(f.k==='td'||f.k==='rd');\n  var prop=f.k==='th'?'triggerThreshold':f.k==='rth'?'resetThreshold':f.k==='td'?'triggerDelay':'resetDelay';\n  if(ac()[prop]==null)setAlm(code,function(a){a[prop]=isDelay?(f.def+'s'):f.def;});\n  var raw=ac()[prop];\n  var val=isDelay?(raw!=null?parseFloat(raw):f.def):(raw!=null?raw:f.def);\n  return almNumField(f.label,val,f.min,f.max,isDelay?0.1:1,function(v){ setAlm(code,function(a){ a[prop]=(v===''||v==null)?null:(isDelay?(v+'s'):Number(v)); }); });\n}\nfunction almRowRule(body,code,def,rule){\n  var ac=function(){return almByCode(code);};\n  var noEnable=!!(rule.noEnable||rule.severityOnly);\n  if(noEnable&&ac().disabled)setAlm(code,function(a){a.disabled=false;});\n  var enabled=noEnable?true:!ac().disabled;\n  var row=document.createElement('div');row.className='card'+((noEnable||enabled)?'':' closed');\n  var hdr=document.createElement('div');hdr.className='card-header';\n  if(!noEnable){var tog=document.createElement('input');tog.type='checkbox';tog.checked=enabled;tog.style.cssText='width:18px;height:18px;cursor:pointer;accent-color:var(--g);margin:0;flex-shrink:0;';tog.onchange=function(){setAlm(code,function(a){a.disabled=!tog.checked;});render();};hdr.appendChild(tog);}\n  var nm=document.createElement('span');nm.className='card-title';nm.appendChild(fldSpan(def.name));hdr.appendChild(nm);\n  var hlp=document.createElement('span');hlp.className='help-icon';hlp.textContent='?';hlp.title='Alarm information & troubleshooting';hlp.style.marginLeft='6px';hlp.style.flexShrink='0';hlp.onclick=function(e){e.stopPropagation();if(typeof hdOpenAlarmHelp==='function')hdOpenAlarmHelp(code);};hdr.appendChild(hlp);\n  var ct=document.createElement('span');ct.style.cssText='font-size:10px;color:var(--m);font-family:monospace;margin-left:auto;';ct.textContent=code.replace('ALARM_CODE_','');hdr.appendChild(ct);\n  var exp=document.createElement('button');exp.textContent='\\u25be';exp.style.cssText='padding:2px 8px;font-size:12px;border-radius:6px;';exp.onclick=function(){row.classList.toggle('closed');};hdr.appendChild(exp);\n  row.appendChild(hdr);\n  var rb=document.createElement('div');rb.className='card-body';\n  if(noEnable||enabled){rule.fields.forEach(function(f){rb.appendChild(almField(code,f));});}\n  else{var note=document.createElement('div');note.style.cssText='font-size:11px;color:var(--m);font-style:italic;';note.appendChild(fldSpan('Disabled \\u2014 enable to configure.'));rb.appendChild(note);}\n  row.appendChild(rb);body.appendChild(row);\n}\nfunction almRow(body,code,def){\n  if(!almByCode(code))almCfgs().push({code,disabled:true,triggerThreshold:null,triggerDelay:null,resetThreshold:null,resetDelay:null,triggerActions:[{action:'ALARM_ACTION_NO_ACTION'}],bootState:'ALARM_BOOT_STATE_NORMAL'});\n  const ac=()=>almByCode(code);\n  const rule=ALM_RULES[code]||null;\n  if(rule){almRowRule(body,code,def,rule);return;}\n  const row=document.createElement('div');row.className='card'+(ac().disabled?' closed':'');\n  const hdr=document.createElement('div');hdr.className='card-header';\n  const tog=document.createElement('input');tog.type='checkbox';tog.checked=!ac().disabled;tog.style.cssText='width:18px;height:18px;cursor:pointer;accent-color:var(--g);margin:0;flex-shrink:0;';\n  if(!def.dis){tog.disabled=true;tog.title='Cannot be disabled';}\n  tog.onchange=()=>{setAlm(code,a=>{a.disabled=!tog.checked;});row.classList.toggle('closed',!tog.checked);};\n  const nm=document.createElement('span');nm.className='card-title';nm.appendChild(fldSpan(def.name));\n  const ct=document.createElement('span');ct.style.cssText='font-size:10px;color:var(--m);font-family:monospace;';ct.textContent=code.replace('ALARM_CODE_','');\n  const exp=document.createElement('button');exp.textContent='\u25be';exp.style.cssText='padding:2px 8px;font-size:12px;border-radius:6px;';exp.onclick=()=>row.classList.toggle('closed');\n  hdr.append(tog,nm,ct,exp);row.appendChild(hdr);\n  const rb=document.createElement('div');rb.className='card-body';\n  if(!def.dis){const note=document.createElement('div');note.style.cssText='font-size:11px;color:var(--m);font-style:italic;';note.textContent='Always active \u2014 cannot be disabled.';rb.appendChild(note);}\n  if(def.hasTh)rb.appendChild(nullNum(def.thLbl,'alarm.triggerThreshold',ac().triggerThreshold,v=>setAlm(code,a=>a.triggerThreshold=v)));\n  if(def.hasDly)rb.appendChild(durField('Trigger Delay (s)','alarm.triggerDelay',ac().triggerDelay,v=>setAlm(code,a=>a.triggerDelay=v)));\n  rb.appendChild(nullNum('Reset Threshold (optional)','alarm.resetThreshold',ac().resetThreshold,v=>setAlm(code,a=>a.resetThreshold=v)));\n  rb.appendChild(durField('Reset Delay (s, optional)','alarm.resetDelay',ac().resetDelay,v=>setAlm(code,a=>a.resetDelay=v)));\n  const asSub=document.createElement('div');asSub.style.cssText='font-size:12px;color:var(--g);font-weight:700;text-transform:uppercase;margin-top:4px;display:flex;align-items:center;gap:6px;';asSub.textContent='Trigger Actions';asSub.appendChild(hi('alarm.action'));rb.appendChild(asSub);\n  const al=document.createElement('div');al.style.cssText='display:grid;gap:4px;margin-left:3ch;';\n  const rdActs=()=>{\n    al.innerHTML='';(ac().triggerActions||[]).forEach((act,idx)=>{\n      const item=document.createElement('div');item.style.cssText='display:flex;align-items:center;gap:6px;';\n      const sel=document.createElement('select');\n      ALM_ACTIONS.forEach(o=>{const opt=document.createElement('option');opt.value=o.value;opt.textContent=o.label;if(o.value===act.action)opt.selected=true;sel.appendChild(opt);});\n      sel.onchange=()=>setAlm(code,a=>{a.triggerActions[idx].action=sel.value;});\n      const rm=document.createElement('button');rm.textContent='\u2715';rm.className='danger';rm.style.cssText='padding:2px 8px;font-size:12px;';\n      rm.onclick=()=>{setAlm(code,a=>{a.triggerActions.splice(idx,1);});rdActs();};\n      item.append(sel,rm);al.appendChild(item);\n    });\n  };\n  rdActs();rb.appendChild(al);\n  const bsSub=document.createElement('div');bsSub.style.cssText='font-size:12px;color:var(--g);font-weight:700;text-transform:uppercase;margin-top:4px;';bsSub.textContent='Boot State';bsSub.appendChild(hi('alarm.bootState'));rb.appendChild(bsSub);\n  const bsF=document.createElement('div');bsF.className='field';const bsL=document.createElement('label');bsL.textContent='State on boot';const bsSel=document.createElement('select');ALM_BOOT.forEach(o=>{const opt=document.createElement('option');opt.value=o.value;opt.textContent=o.label;if((ac().bootState||'ALARM_BOOT_STATE_NORMAL')===o.value)opt.selected=true;bsSel.appendChild(opt);});bsSel.onchange=()=>setAlm(code,a=>{a.bootState=bsSel.value;});bsF.append(bsL,bsSel);rb.appendChild(bsF);\n  row.appendChild(rb);body.appendChild(row);\n}\n\nfunction render(){\n  const ed=document.getElementById('ceEditor');ed.innerHTML='';\n  const rso=[{label:'UNSPECIFIED',value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_UNSPECIFIED'},{label:'I\/O',value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_IO'},{label:'MODBUS TCP',value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_MODBUS'},{label:'BACNET IP',value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_BACNET'},{label:'ETHERNET\/IP',value:'SYSTEM_REMOTE_CONTROL_SOURCE_TYPE_ETHERNET_IP'}];\n  const sku=get(cfg,'_metadata.deviceModel');\n  const cm=get(cfg,'configurations.motor.algorithm.controlMode')||'';\n  const showLimits = cm.includes('FOC') || cm.includes('TORQUE');\n\n  if(activePreset){const ban=document.createElement('div');ban.className='preset-banner';ban.innerHTML=`<span style=\"font-size:18px\">${activePreset.icon}<\/span>`;\n    const n=activePreset.paths.size,txt=document.createElement('div');\n    txt.appendChild(biSpan('','Pr\u00e9r\u00e9glage '));const st=document.createElement('strong');st.appendChild(biSpan(activePreset.label,activePreset.label_fr||activePreset.label));txt.appendChild(st);\n    txt.appendChild(biSpan(` preset active \u2014 ${n} parameter${n!==1?'s':''} highlighted`,` actif \u2014 ${n} param\u00e8tre${n!==1?'s':''} surlign\u00e9${n!==1?'s':''}`));\n    ban.appendChild(txt);\n    const clr=document.createElement('span');clr.className='clr';clr.appendChild(biSpan('Clear highlights','Effacer le surlignage'));clr.onclick=()=>{activePreset=null;render();};ban.appendChild(clr);ed.appendChild(ban);}\n\n  const g1=grp('CLEAN POWER VFD');\n  g1.body.appendChild(field('Model','_metadata.deviceModel','select',{options:Object.entries(MODELS).map(([s,m])=>({label:`${s} \u2014 ${m.motor.kw} kW \/ ${m.motor.hp} HP, Frame ${m.frame}`,value:s})),onChange:s=>{applyModel(s);render();}}));\n  g1.body.appendChild(field('VFD Name','configurations.system.driveName','text'));\n  g1.body.appendChild(field('Configuration Name','configurations.system.configName','text',{maxlength:256}));\n  ed.appendChild(g1.root);\n\n  const g2=grp('INPUT LINE', true);\n  g2.body.appendChild(field('Voltage (V)','configurations.line.nominalVoltage','select',{options:(MODELS[sku]?.vo??[480,400,600]).map(v=>({label:String(v),value:v}))}));\n  g2.body.appendChild(field('Frequency (Hz)','configurations.line.frequency','select',{options:[50,60]}));\n  ed.appendChild(g2.root);\n\n  const g3=grp('LOCAL \/ REMOTE CONTROL');\n  g3.body.appendChild(field('Motor Control by Mobile App','configurations.system.mobilePermissions','select',{options:[{label:'ENABLED',value:'SYSTEM_MOBILE_PERMISSION_FULL_CONTROL'},{label:'DISABLED',value:'SYSTEM_MOBILE_PERMISSION_LIMITED'}]}));\n  g3.body.appendChild(field('Control Source Selector','configurations.system.controlMode.controlSourceSelector','select',{options:[{label:'LOCAL',value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_LOCAL'},{label:'REMOTE SOURCE 1',value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_1'},{label:'REMOTE SOURCE 2',value:'SYSTEM_CONTROL_SOURCE_SELECTOR_SOURCE_2'}]}));\n  g3.body.appendChild(field('Primary Remote Source (Source 1)','configurations.system.controlMode.remoteControlSource1','select',{options:rso}));\n  g3.body.appendChild(field('Secondary Remote Source (Source 2)','configurations.system.controlMode.remoteControlSource2','select',{options:rso}));\n  ed.appendChild(g3.root);\n\n  const gNP=grp('MOTOR NAMEPLATE');const mo='configurations.motor';\n  gNP.body.appendChild(field('Nominal Voltage (V)',`${mo}.motorNameplate.nominalVoltage`,'number'));\n  gNP.body.appendChild(field('Nominal Current (A)',`${mo}.motorNameplate.nominalCurrent`,'number'));\n  gNP.body.appendChild(field('Nominal Power Unit',`${mo}.motorNameplate.nominalPowerUnit`,'select',{options:[{label:'kW',value:'MOTOR_CONTROLLER_POWER_UNIT_W'},{label:'HP',value:'MOTOR_CONTROLLER_POWER_UNIT_HP'}]}));\n  gNP.body.appendChild(field('Nominal Power',`${mo}.motorNameplate.nominalPower`,'number'));\n  gNP.body.appendChild(field('Nominal Frequency (Hz)',`${mo}.motorNameplate.nominalInputFrequency`,'number'));\n  gNP.body.appendChild(field('Minimum Frequency (Hz)',`${mo}.motorNameplate.minInputFrequency`,'number'));\n  gNP.body.appendChild(field('Maximum Frequency (Hz)',`${mo}.motorNameplate.maxInputFrequency`,'number'));\n  gNP.body.appendChild(field('Nominal Speed (RPM)',`${mo}.motorNameplate.nominalVelocity`,'number'));\n  gNP.body.appendChild(field('Number of Poles',`${mo}.motorNameplate.numberOfPoles`,'number'));\n  gNP.body.appendChild(field('Power Factor',`${mo}.motorNameplate.powerFactor`,'number'));\n  gNP.body.appendChild(field('Phase Reversal',`${mo}.motorNameplate.phaseInversion`,'select',{options:[{label:'ENABLED',value:true},{label:'DISABLED',value:false}]}));\n  ed.appendChild(gNP.root);\n\n  const g4=grp('MOTOR CONTROL');\n  g4.body.appendChild(sh('Control Algorithm'));\n  g4.body.appendChild(field('Control Mode',`${mo}.algorithm.controlMode`,'select',{options:[{label:'V\/F FREQUENCY',value:'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_FREQUENCY_V_F'},{label:'V\/F VELOCITY',value:'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_VELOCITY_V_F'},{label:'FOC',value:'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_VELOCITY_FOC'},{label:'TORQUE (FOC)',value:'MOTOR_CONTROLLER_ALGORITHM_CONTROL_MODE_TORQUE_FOC'}],onChange:()=>render()}));\n  if(cm.includes('V_F')){\n    g4.body.appendChild(sh('V\/F Options'));\n    const div=document.createElement('div');div.className='indent';\n    div.appendChild(field('V\/F Start Mode',`${mo}.algorithm.constantVf.start.mode`,'select',{options:[{label:'START FROM ZERO',value:'MOTOR_CONTROLLER_VF_START_MODE_START_FROM_ZERO'},{label:'SCAN RESTART',value:'MOTOR_CONTROLLER_VF_START_MODE_SCAN_RESTART'},{label:'CATCH ON THE FLY',value:'MOTOR_CONTROLLER_VF_START_MODE_CATCH_ON_THE_FLY'}]}));\n    div.appendChild(field('Torque Boost',`${mo}.algorithm.constantVf.startTorqueBoost.state`,'select',{options:[{label:'DISABLED',value:'MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_DISABLED'},{label:'ENABLED',value:'MOTOR_CONTROLLER_START_TORQUE_BOOST_STATE_ENABLED'}],onChange:()=>render()}));\n    if((get(cfg,`${mo}.algorithm.constantVf.startTorqueBoost.state`)||'').includes('ENABLED')){\n      div.appendChild(field('Boost Voltage (%)',`${mo}.algorithm.constantVf.startTorqueBoost.voltagePercentage`,'number',{min:0,max:25}));\n      div.appendChild(field('Boost Cutoff (% nominal freq)',`${mo}.algorithm.constantVf.startTorqueBoost.cutoffFrequencyPercentage`,'number',{min:0,max:100}));\n    }\n    div.appendChild(field('Over-Modulation',`${mo}.algorithm.constantVf.enableOvermodulation`,'select',{options:[{label:'DISABLED',value:false},{label:'ENABLED',value:true}]}));\n    g4.body.appendChild(div);\n  }\n  if(cm.includes('FOC')){\n    g4.body.appendChild(sh('FOC Options'));\n    const div=document.createElement('div');div.className='indent';\n    div.appendChild(field('Speed Response',`${mo}.algorithm.foc.speedControllerResponse`,'select',{options:[{label:'SLOW',value:'MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_SLOW'},{label:'NORMAL',value:'MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_NORMAL'},{label:'FAST',value:'MOTOR_CONTROLLER_FOC_SPEED_RESPONSE_FAST'}]}));\n    div.appendChild(field('Speed Source',`${mo}.algorithm.foc.speedSource`,'select',{options:[{label:'ESTIMATED (no encoder)',value:'MOTOR_CONTROLLER_FOC_SPEED_SOURCE_ESTIMATED'},{label:'MEASURED (encoder)',value:'MOTOR_CONTROLLER_FOC_SPEED_SOURCE_MEASURED'}]}));\n    div.appendChild(field('Eco Mode',`${mo}.algorithm.foc.ecoMode`,'select',{options:[{label:'OFF',value:'MOTOR_CONTROLLER_FOC_ECO_MODE_OFF'},{label:'ON WITH MTPA',value:'MOTOR_CONTROLLER_FOC_ECO_MODE_ON_WITH_MTPA'}]}));\n    div.appendChild(field('Over-Modulation',`${mo}.algorithm.foc.enableOvermodulation`,'select',{options:[{label:'DISABLED',value:false},{label:'ENABLED',value:true}]}));\n    g4.body.appendChild(div);\n  }\n  g4.body.appendChild(sh('Control Options'));\n  g4.body.appendChild(field('Normal Stop Behaviour',`${mo}.options.normalStopBehaviour`,'select',{options:[{label:'RAMP DOWN',value:'MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_RAMP_DOWN'},{label:'FREEWHEEL',value:'MOTOR_CONTROLLER_NORMAL_STOP_BEHAVIOUR_FREEWHEEL'}]}));\n  g4.body.appendChild(field('Reverse Enabled',`${mo}.options.reverseEnabled`,'select',{options:[{label:'ENABLED',value:true},{label:'DISABLED',value:false}],onChange:onRevEn}));\n  g4.body.appendChild(field('Bumpless Direction Change',`${mo}.options.bumplessDirectionChange`,'select',{options:[{label:'DISABLED',value:'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_DISABLED'},{label:'ENABLED',value:'MOTOR_CONTROLLER_BUMPLESS_DIRECTION_CHANGE_ENABLED'}]}));\n  g4.body.appendChild(field('Spinning Direction Priority',`${mo}.options.spinningDirectionPriority`,'select',{options:[{label:'RAISE ALARM',value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_RAISE_ALARM'},{label:'KEEP CURRENT DIRECTION',value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_KEEP_ACTUAL'},{label:'FORWARD priority',value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_FORWARD'},{label:'REVERSE priority',value:'MOTOR_CONTROLLER_SPINNING_DIRECTION_PRIORITY_REVERSE'}]}));\n  g4.body.appendChild(field('Zero Speed Behaviour',`${mo}.options.zeroSpeedBehaviour`,'select',{options:[{label:'MOTOR PAUSE',value:'MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_PAUSE'},{label:'STOP RAMP DOWN',value:'MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_STOP_RAMP_DOWN'},{label:'MAINTAIN MINIMUM SPEED',value:'MOTOR_CONTROLLER_ZERO_SPEED_BEHAVIOUR_MOTOR_MINIMUM_SPEED'}]}));\n  g4.body.appendChild(field('Low Voltage Ride Through',`${mo}.options.lowVoltageRideThrough`,'select',{options:[{label:'ENABLED',value:'MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_ENABLED'},{label:'DISABLED',value:'MOTOR_CONTROLLER_LOW_VOLTAGE_RIDE_THROUGH_DISABLED'}]}));\n  g4.body.appendChild(field('Voltage Drop Compensation',`${mo}.options.voltageDropCompensations`,'select',{options:[{label:'DISABLED',value:'MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_DISABLED'},{label:'ENABLED',value:'MOTOR_CONTROLLER_VOLTAGE_DROP_COMPENSATIONS_ENABLED'}]}));\n  g4.body.appendChild(sh('Thermal Protection'));\n  g4.body.appendChild(field('Motor Cooling Type',`${mo}.thermalProtection.coolingType`,'select',{options:[{label:'SELF COOLED (shaft fan)',value:'MOTOR_CONTROLLER_COOLING_TYPE_SELF_COOLED'},{label:'FORCED COOLED (external fan)',value:'MOTOR_CONTROLLER_COOLING_TYPE_FORCED_COOLED'}]}));\n  g4.body.appendChild(field('Temperature at Power-Up',`${mo}.thermalProtection.temperatureAtPowerUp`,'select',{options:[{label:'COLD START',value:'MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_COLD_START'},{label:'RETAIN LAST VALUE',value:'MOTOR_CONTROLLER_TEMPERATURE_AT_POWER_UP_RETAIN_LAST_VALUE'}]}));\n  g4.body.appendChild(sh('Auto Restart'));\n  g4.body.appendChild(field('Enable\/Disable',`${mo}.autoRestart.mode`,'select',{options:[{label:'DISABLED',value:'MOTOR_CONTROLLER_AUTO_RESTART_MODE_DISABLED'},{label:'ENABLED',value:'MOTOR_CONTROLLER_AUTO_RESTART_MODE_ENABLED'}],onChange:()=>render()}));\n  if((get(cfg,`${mo}.autoRestart.mode`)||'').includes('ENABLED')){\n    g4.body.appendChild(field('Maximum Retries (1\u201310)',`${mo}.autoRestart.maximumRetries`,'number',{min:1,max:10}));\n    if(get(cfg,`${mo}.autoRestart.restartDelay`)==null||get(cfg,`${mo}.autoRestart.restartDelay`)===''){ set(cfg,`${mo}.autoRestart.restartDelay`,'10s'); }\n    g4.body.appendChild(durField('Restart Delay (s)',`${mo}.autoRestart.restartDelay`,get(cfg,`${mo}.autoRestart.restartDelay`),v=>set(cfg,`${mo}.autoRestart.restartDelay`,v!=null?v:'10s')));\n  }\n  ed.appendChild(g4.root);\n\n  if(activePreset){\n    const motorPaths=['configurations.motor.options.reverseEnabled','configurations.motor.options.bumplessDirectionChange','configurations.motor.options.spinningDirectionPriority','configurations.motor.emergencyRun.frequency','configurations.motor.emergencyRun.velocity','configurations.motor.emergencyRun.direction'];\n    if(motorPaths.some(p=>activePreset.paths.has(p)))g4.root.classList.remove('closed');\n  }\n\n  const gR=grp('MOTOR RAMPS');\n  const isFreq=cm.includes('FREQUENCY')||(!cm.includes('VELOCITY')&&!cm.includes('FOC')&&!cm.includes('TORQUE'));\n  const isVel=cm.includes('VELOCITY')||cm.includes('FOC');const isTrq=cm.includes('TORQUE');\n  if(isFreq||!cm){rampSection(gR.body,'Frequency Ramp-Up',`${mo}.ramps.frequencyRampUpCurves`,true);rampSection(gR.body,'Frequency Ramp-Down',`${mo}.ramps.frequencyRampDownCurves`,false);}\n  if(isVel){rampSection(gR.body,'Velocity Ramp-Up',`${mo}.ramps.velocityRampUpCurves`,true);rampSection(gR.body,'Velocity Ramp-Down',`${mo}.ramps.velocityRampDownCurves`,false);}\n  if(isTrq){rampSection(gR.body,'Torque Ramp-Up',`${mo}.ramps.torqueRampUpCurves`,true);rampSection(gR.body,'Torque Ramp-Down',`${mo}.ramps.torqueRampDownCurves`,false);}\n  if(!isFreq&&!isVel&&!isTrq){rampSection(gR.body,'Frequency Ramp-Up',`${mo}.ramps.frequencyRampUpCurves`,true);rampSection(gR.body,'Frequency Ramp-Down',`${mo}.ramps.frequencyRampDownCurves`,false);}\n  ed.appendChild(gR.root);\n\n  if(showLimits){\n    const gL=grp('MOTOR LIMITS',true);\n    const lim=`${mo}.limits`;\n    if(isTrq){\n      gL.body.appendChild(sh('Torque'));\n      gL.body.appendChild(field('High Torque Limit (% nominal)',`${lim}.highTorque.threshold`,'number',{min:0,max:500}));\n      gL.body.appendChild(field('Low Torque Limit (%, negative)',`${lim}.lowTorque.threshold`,'number',{min:-500,max:0}));\n      gL.body.appendChild(sh('Velocity'));\n      gL.body.appendChild(field('Max Velocity (rpm)',`${lim}.maxVelocity.threshold`,'number',{min:0}));\n      gL.body.appendChild(field('Min Velocity (rpm)',`${lim}.minVelocity.threshold`,'number'));\n    } else {\n      gL.body.appendChild(sh('Velocity'));\n      gL.body.appendChild(field('Max Velocity (rpm \u2014 0=nameplate)',`${lim}.maxVelocity.threshold`,'number',{min:0}));\n      gL.body.appendChild(field('Min Velocity (rpm)',`${lim}.minVelocity.threshold`,'number'));\n      gL.body.appendChild(sh('Torque'));\n      gL.body.appendChild(field('High Torque Limit (% nominal)',`${lim}.highTorque.threshold`,'number',{min:0,max:500}));\n      gL.body.appendChild(field('Low Torque Limit (%, negative)',`${lim}.lowTorque.threshold`,'number',{min:-500,max:0}));\n    }\n    ed.appendChild(gL.root);\n  }\n\n  const gP=grp('MOTOR SPEED PRESETS',true);\n  gP.body.appendChild(listField('Frequency Presets (Hz)','configurations.motor.presets.frequencies','configurations.motor.presets.frequencies','e.g. 30','number'));\n  gP.body.appendChild(listField('Velocity Presets (rpm)','configurations.motor.presets.velocities','configurations.motor.presets.velocities','e.g. 900','number'));\n  ed.appendChild(gP.root);\n\n  const gA=grp('MOTOR ADVANCED PARAMETERS (AUTOTUNE)',true);\n  const advN=document.createElement('div');advN.style.cssText='font-size:12px;color:var(--m);margin-bottom:8px;';advN.appendChild(fldSpan(\"Leave blank to use the drive's calculated defaults.\"));gA.body.appendChild(advN);\n  [['Stator Resistance (\u03a9)','statorResistance'],['Rotor Resistance (\u03a9)','rotorResistance'],['Magnetizing Inductance (H)','magnetizingInductance'],['Leakage Inductance (H)','leakageInductance'],['Moment of Inertia (kg\u00b7m\u00b2)','momentOfInertia']].forEach(([lbl,k])=>gA.body.appendChild(nullNum(lbl,`configurations.motor.advancedParameters.${k}`,get(cfg,`${mo}.advancedParameters.${k}`),v=>set(cfg,`${mo}.advancedParameters.${k}`,v))));\n  ed.appendChild(gA.root);\n\n  const g5=grp('I\/Os');\n  if(activePreset){const ioPaths=Array.from(activePreset.paths).filter(p=>p.startsWith('configurations.ios.'));if(ioPaths.length)g5.root.classList.remove('closed');}\n  g5.body.appendChild(sh('Digital Inputs'));\n  g5.body.appendChild(field('Power Direction','configurations.ios.digitalInputsPowerDirection','select',{options:[{label:'SOURCE (PNP)',value:'IO_DIGITAL_INPUTS_POWER_DIRECTION_SOURCE'},{label:'SINK INTERNAL (NPN)',value:'IO_DIGITAL_INPUTS_POWER_DIRECTION_SINK_INTERNAL'},{label:'SINK EXTERNAL (NPN)',value:'IO_DIGITAL_INPUTS_POWER_DIRECTION_SINK_EXTERNAL'}]}));\n  ensureArr('configurations.ios.digitalInputs',6,{function:'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED',triggerType:'IO_TRIGGER_TYPE_UNSPECIFIED',debounceDelay:'0.200'});\n  const trgO=[{label:'UNSPECIFIED',value:'IO_TRIGGER_TYPE_UNSPECIFIED'},{label:'RISING EDGE',value:'IO_TRIGGER_TYPE_RISING_EDGE'},{label:'FALLING EDGE',value:'IO_TRIGGER_TYPE_FALLING_EDGE'},{label:'HIGH LEVEL',value:'IO_TRIGGER_TYPE_HIGH'},{label:'LOW LEVEL',value:'IO_TRIGGER_TYPE_LOW'}];\n  for(let i=0;i<6;i++){\n    const _fn=get(cfg,`configurations.ios.digitalInputs.${i}.function`)||'IO_DIGITAL_INPUT_FUNCTION_UNSPECIFIED';\n    const _tt=trigOptsFor(_fn);\n    const _tp=`configurations.ios.digitalInputs.${i}.triggerType`;\n    if(!_tt.opts.some(o=>o.value===get(cfg,_tp))) set(cfg,_tp,_tt.opts[0].value);\n    g5.body.appendChild(sh(`Digital Input ${i+1}`));\n    g5.body.appendChild(field('Function',`configurations.ios.digitalInputs.${i}.function`,'select',{options:DI_FNS,onChange:v=>onDIFn(i,v)}));\n    g5.body.appendChild(field('Trigger Type',_tp,'select',{options:_tt.opts,disabled:_tt.disabled}));\n    g5.body.appendChild(field('Debounce Delay (s)',`configurations.ios.digitalInputs.${i}.debounceDelay`,'number'));\n  }\n  g5.body.appendChild(sh('Relays'));ensureArr('configurations.ios.relays',3,{});\n  const _isFOC=(get(cfg,'configurations.motor.algorithm.controlMode')||'').includes('FOC');\n  const _relayFOC=[{label:'MOTOR TORQUE',value:'IO_RELAY_FUNCTION_MOTOR_TORQUE'},{label:'ACTIVE CURRENT LIMIT',value:'IO_RELAY_FUNCTION_ACTIVE_SWITCHOVER_LIMIT'}];\n  for(let i=0;i<3;i++){\n    let _opts=_isFOC?RLY_FNS.concat(_relayFOC):RLY_FNS.slice();\n    const _cur=get(cfg,`configurations.ios.relays.${i}.function`);\n    if(_cur && !_opts.some(o=>o.value===_cur)) _opts=_opts.concat([{label:_cur.replace(\/^IO_RELAY_FUNCTION_\/,'').replace(\/_\/g,' '),value:_cur}]);\n    g5.body.appendChild(sh(`Relay ${i+1}`));\n    g5.body.appendChild(field('Function',`configurations.ios.relays.${i}.function`,'select',{options:_opts,onChange:v=>{ if(v==='IO_RELAY_FUNCTION_MOTOR_TORQUE'||v==='IO_RELAY_FUNCTION_ACTIVE_SWITCHOVER_LIMIT'){ set(cfg,`configurations.ios.relays.${i}.output`,'IO_RELAY_OUTPUT_ENERGIZED_BY_DEFAULT'); } render(); }}));\n    g5.body.appendChild(field('Default Coil State',`configurations.ios.relays.${i}.output`,'select',{options:RLY_OUTS}));\n  }\n  g5.body.appendChild(sh('Analog Inputs'));ensureArr('configurations.ios.analogInputs',3,{function:'IO_ANALOG_INPUT_FUNCTION_UNSPECIFIED',mode:'IO_ANALOG_INPUT_MODE_UNSPECIFIED',curvePoints:[{x:0,y:0},{x:0,y:0}]});\n  for(let i=0;i<3;i++){const bp=`configurations.ios.analogInputs.${i}`;g5.body.appendChild(sh(`Analog Input ${i+1}`));g5.body.appendChild(field('Function',`${bp}.function`,'select',{options:AI_FNS}));g5.body.appendChild(field('Mode',`${bp}.mode`,'select',{options:AI_MODES}));cpFields(g5.body,bp);}\n  g5.body.appendChild(sh('Analog Outputs'));ensureArr('configurations.ios.analogOutputs',2,{function:'IO_ANALOG_OUTPUT_FUNCTION_UNSPECIFIED',mode:'IO_ANALOG_OUTPUT_MODE_UNSPECIFIED',curvePoints:[{x:0,y:0},{x:0,y:0}]});\n  for(let i=0;i<2;i++){const bp=`configurations.ios.analogOutputs.${i}`;g5.body.appendChild(sh(`Analog Output ${i+1}`));g5.body.appendChild(field('Function',`${bp}.function`,'select',{options:AO_FNS}));g5.body.appendChild(field('Mode',`${bp}.mode`,'select',{options:AO_MODES}));cpFields(g5.body,bp);}\n  ed.appendChild(g5.root);\n\n  const gPid=grp('PID CONTROLLER', true);\n  ensureArr('configurations.pids.instances',1,{index:'PID_INDEX_1',enable:false,kP:10,tI:1,tD:0,engineeringUnit:'PID_ENGINEERING_UNIT_UNSPECIFIED',engineeringUnitCustom:'',minProcessValue:0,maxProcessValue:100,controlMode:'PID_CONTROL_MODE_REVERSE',minControlOutput:0,maxControlOutput:100,controlOutputCompensation:0,sleepThreshold:0,sleepDelay:'0s',wakeupThreshold:0,wakeupDelay:'0s',forcedOutputPreset:1,forcedOutputType:'PID_FORCED_OUTPUT_TYPE_VALUE',deadbandValue:0,deadbandDelay:'0s',processRampUpRate:0,processRampDownRate:0,wakeupThresholdError:0,defaultSetpoint:80,sensorOffset:0});\n  const pp='configurations.pids.instances.0';\n  gPid.body.appendChild(sh('General'));\n  gPid.body.appendChild(field('Enable PID',`${pp}.enable`,'select',{options:[{label:'DISABLED',value:false},{label:'ENABLED',value:true}]}));\n  gPid.body.appendChild(field('Control Mode',`${pp}.controlMode`,'select',{options:[{label:'REVERSE (output\u2193 when feedback\u2191)',value:'PID_CONTROL_MODE_REVERSE'},{label:'DIRECT (output\u2191 when feedback\u2191)',value:'PID_CONTROL_MODE_DIRECT'}]}));\n  gPid.body.appendChild(field('Engineering Unit',`${pp}.engineeringUnit`,'select',{options:PID_UNITS}));if(get(cfg,`${pp}.engineeringUnit`)==='PID_ENGINEERING_UNIT_CUSTOM')gPid.body.appendChild(field('Custom Unit Label',`${pp}.engineeringUnitCustom`,'text'));\n  gPid.body.appendChild(field('Min Process Value',`${pp}.minProcessValue`,'number'));\n  gPid.body.appendChild(field('Max Process Value',`${pp}.maxProcessValue`,'number'));\n  gPid.body.appendChild(field('Default Setpoint',`${pp}.defaultSetpoint`,'number'));\n  gPid.body.appendChild(field('Sensor Offset',`${pp}.sensorOffset`,'number'));\n  gPid.body.appendChild(sh('PID Gains'));gPid.body.appendChild(field('Kp',`${pp}.kP`,'number',{min:0}));gPid.body.appendChild(field('Ti (s)',`${pp}.tI`,'number',{min:0}));gPid.body.appendChild(field('Td (s)',`${pp}.tD`,'number',{min:0}));\n  gPid.body.appendChild(sh('Output Limits'));\n  gPid.body.appendChild(field('Min Control Output (%)',`${pp}.minControlOutput`,'number',{min:0,max:100}));\n  gPid.body.appendChild(field('Max Control Output (%)',`${pp}.maxControlOutput`,'number',{min:0,max:100}));\n  gPid.body.appendChild(field('Output Compensation (%)',`${pp}.controlOutputCompensation`,'number',{min:-100,max:100}));\n  gPid.body.appendChild(sh('Forced Output'));\n  gPid.body.appendChild(field('Forced Output Type',`${pp}.forcedOutputType`,'select',{options:[{label:'PRESET (fixed value)',value:'PID_FORCED_OUTPUT_TYPE_PRESET'},{label:'VALUE (from setpoints)',value:'PID_FORCED_OUTPUT_TYPE_VALUE'}]}));\n  gPid.body.appendChild(field('Forced Output Preset (Hz or rpm)',`${pp}.forcedOutputPreset`,'number'));\n  gPid.body.appendChild(sh('Sleep \/ Wake'));\n  gPid.body.appendChild(field('Sleep Threshold',`${pp}.sleepThreshold`,'number',{min:0}));\n  gPid.body.appendChild(durField('Sleep Delay (s)','pid.sleepDelay',get(cfg,`${pp}.sleepDelay`),v=>set(cfg,`${pp}.sleepDelay`,v||'0s')));\n  gPid.body.appendChild(field('Wakeup Error Threshold',`${pp}.wakeupThresholdError`,'number',{min:0}));\n  gPid.body.appendChild(durField('Wakeup Delay (s)','pid.wakeupDelay',get(cfg,`${pp}.wakeupDelay`),v=>set(cfg,`${pp}.wakeupDelay`,v||'0s')));\n  gPid.body.appendChild(sh('Deadband'));\n  gPid.body.appendChild(field('Deadband Value (\u00b1)',`${pp}.deadbandValue`,'number',{min:0}));\n  gPid.body.appendChild(durField('Deadband Delay (s)','pid.deadbandDelay',get(cfg,`${pp}.deadbandDelay`),v=>set(cfg,`${pp}.deadbandDelay`,v||'0s')));\n  gPid.body.appendChild(sh('Process Ramp Rates'));\n  gPid.body.appendChild(field('Ramp Up Rate (units\/s, 0=off)',`${pp}.processRampUpRate`,'number',{min:0}));\n  gPid.body.appendChild(field('Ramp Down Rate (units\/s, 0=off)',`${pp}.processRampDownRate`,'number',{min:0}));\n  ed.appendChild(gPid.root);\n\n  const g6=grp('DATE & TIME');\n  g6.body.appendChild(field('Time Synchronization Protocol','configurations.dateTime.protocol','select',{options:[{label:'MANUAL',value:'DATE_TIME_UPDATER_PROTOCOL_NONE'},{label:'NETWORK SYNC (NTP)',value:'DATE_TIME_UPDATER_PROTOCOL_NTP'}]}));\n  g6.body.appendChild(field('Timezone','configurations.dateTime.timezone','select',{options:tzOpts()}));\n  g6.body.appendChild(field('Disable BLE Time Sync','configurations.dateTime.disableBleSynchronization','select',{options:[{label:'DISABLED (BLE may sync clock)',value:false},{label:'ENABLED (prevent BLE time sync)',value:true}]}));\n  ed.appendChild(g6.root);\n\n  const g7=grp('NETWORK & COMMUNICATION PROTOCOLS', true);\n  g7.body.appendChild(sh('Network'));\n  g7.body.appendChild(sh('IP Address'));g7.body.appendChild(field('Address Assignment','configurations.network.interfaces.0.method','select',{options:[{label:'AUTOMATIC (DHCP)',value:'NETWORK_CONFIGURATION_METHOD_DHCP'},{label:'MANUAL (Static IPv4)',value:'NETWORK_CONFIGURATION_METHOD_STATIC_IPV4'}],onChange:onNetMethod}));\n  g7.body.appendChild(field('IPv4 Address','configurations.network.interfaces.0.ipv4Address','text'));\n  g7.body.appendChild(field('IPv4 Gateway','configurations.network.interfaces.0.ipv4Gateway','text'));\n  g7.body.appendChild(field('IPv4 Network Mask','configurations.network.interfaces.0.ipv4NetworkMask','text'));\n  g7.body.appendChild(sh('DNS'));\n  g7.body.appendChild(field('DNS Method','configurations.network.dns.method','select',{options:[{label:'DHCP (auto)',value:'NETWORK_CONFIGURATION_METHOD_DHCP'},{label:'STATIC',value:'NETWORK_CONFIGURATION_METHOD_STATIC_IPV4'}]}));\n  g7.body.appendChild(listField('DNS Resolvers','configurations.network.dns.resolvers','configurations.network.dns.resolvers','e.g. 1.1.1.1'));\n  g7.body.appendChild(sh('ICMP Ping'));\n  g7.body.appendChild(field('Ping Mode','configurations.network.ping.mode','select',{options:[{label:'DISABLED',value:'NETWORK_PING_MODE_DISABLED'},{label:'PERMANENT ON',value:'NETWORK_PING_MODE_PERMANENT_ON'}]}));\n  g7.body.appendChild(sh('Modbus TCP'));g7.body.appendChild(field('Enable\/Disable','configurations.modbus.active','select',{options:[{label:'DISABLED',value:false},{label:'ENABLED',value:true}]}));\n  g7.body.appendChild(sh('BACnet IP'));g7.body.appendChild(field('Enable\/Disable','configurations.bacnet.active','select',{options:[{label:'DISABLED',value:false},{label:'ENABLED',value:true}]}));g7.body.appendChild(field('Device ID','configurations.bacnet.deviceId','number'));g7.body.appendChild(field('Device Name','configurations.bacnet.deviceName','text'));g7.body.appendChild(field('Port','configurations.bacnet.port','number'));\n  g7.body.appendChild(sh('EtherNet\/IP'));const eiW=document.createElement('div');eiW.className='field';const eiL=document.createElement('label');eiL.appendChild(fldSpan('Enable\/Disable'));eiL.appendChild(hi('configurations.ethernetIp'));const eiS=document.createElement('select');[{label:'DISABLED',value:'false'},{label:'ENABLED',value:'true'}].forEach(o=>{const opt=document.createElement('option');opt.value=o.value;opt.textContent=o.label;if((get(cfg,'configurations.ethernetIp')!==null)===(o.value==='true'))opt.selected=true;eiS.appendChild(opt);});eiS.onchange=()=>set(cfg,'configurations.ethernetIp',eiS.value==='true'?{}:null);eiW.append(eiL,eiS);g7.body.appendChild(eiW);\n  g7.body.appendChild(sh('Bluetooth (BLE)'));\n  g7.body.appendChild(field('Advertising State','configurations.ble.advertisingState','select',{options:[{label:'ENABLED',value:'BLE_ADVERTISING_STATE_ENABLED'},{label:'DISABLED',value:'BLE_ADVERTISING_STATE_DISABLED'}]}));\n  ed.appendChild(g7.root);\n\n  const gD=grp('DISPLAY', true);\n  gD.body.appendChild(sh('Screensaver'));\n  gD.body.appendChild(durField('Screensaver Timeout (s \u2014 0=off)','configurations.display.screensaverConfiguration.timeout',get(cfg,'configurations.display.screensaverConfiguration.timeout'),v=>set(cfg,'configurations.display.screensaverConfiguration.timeout',v||'0s')));\n  ed.appendChild(gD.root);\n\n  const g8=grp('EMERGENCY FIRE MODE', true);\n  if(activePreset){const emPaths=['configurations.system.emergencyMode.state','configurations.system.emergencyMode.action','configurations.motor.emergencyRun.direction','configurations.motor.emergencyRun.frequency','configurations.motor.emergencyRun.velocity'];if(emPaths.some(p=>activePreset.paths.has(p)))g8.root.classList.remove('closed');}\n  g8.body.appendChild(field('Enable\/Disable','configurations.system.emergencyMode.state','select',{options:[{label:'DISABLED',value:'SYSTEM_EMERGENCY_MODE_STATE_DISABLED'},{label:'ENABLED',value:'SYSTEM_EMERGENCY_MODE_STATE_ENABLED'}]}));\n  g8.body.appendChild(field('Action','configurations.system.emergencyMode.action','select',{options:[{label:'RUN AT PRESET VALUE',value:'SYSTEM_EMERGENCY_MODE_ACTION_RUN_MOTOR'},{label:'STOP FREEWHEEL',value:'SYSTEM_EMERGENCY_MODE_ACTION_STOP_FREEWHEEL'},{label:'KEEP CURRENT OPERATION',value:'SYSTEM_EMERGENCY_MODE_ACTION_KEEP_SETPOINT'}]}));\n  g8.body.appendChild(field('Motor Direction','configurations.motor.emergencyRun.direction','select',{options:[{label:'FORWARD',value:'MOTOR_CONTROLLER_DIRECTION_FORWARD'},{label:'REVERSE',value:'MOTOR_CONTROLLER_DIRECTION_REVERSE'}]}));\n  g8.body.appendChild(field('Motor Frequency (Hz)','configurations.motor.emergencyRun.frequency','number'));\n  g8.body.appendChild(field('Motor Velocity (rpm)','configurations.motor.emergencyRun.velocity','number'));\n  ed.appendChild(g8.root);\n\n  const g9=grp('ALARM CONFIGURATION', true);\n  const sRef=document.createElement('div');sRef.style.cssText='display:flex;flex-wrap:wrap;gap:6px;margin-bottom:8px;align-items:center;font-size:12px;';sRef.innerHTML='';\n  g9.body.appendChild(sRef);\n  Object.keys(ALM_GRP_LABELS).forEach(grpKey=>{\n    const codes=Object.keys(ALM_DEFS).filter(c=>ALM_DEFS[c].grp===grpKey && ALM_RULES[c]);\n    if(!codes.length)return;\n    g9.body.appendChild(sh(ALM_GRP_LABELS[grpKey]));\n    if(grpKey==='external'){const n=document.createElement('div');n.style.cssText='font-size:12px;color:var(--m);margin:0 0 6px;';n.textContent='Triggered by digital inputs configured as TRIGGER EXTERNAL ALARM 1\u20134.';g9.body.appendChild(n);}\n    codes.forEach(code=>almRow(g9.body,code,ALM_DEFS[code]));\n  });\n  ed.appendChild(g9.root);\n\n  if(activePreset)requestAnimationFrame(()=>{const f=document.querySelector('.field.hl');if(f)f.scrollIntoView({behavior:'smooth',block:'center'});});\n}\n\nconst _isFr=()=>typeof LANG!=='undefined'&&LANG==='fr';\nfunction biSpan(en,fr){const sp=document.createElement('span');sp.className='fld-t';sp.setAttribute('data-fld-en',en);sp.setAttribute('data-fld-fr',fr);sp.textContent=_isFr()?fr:en;return sp;}\nfunction openWizard(){wizSelId=activePreset?.id??null;buildAppGrid();document.getElementById('ceWizardOverlay').classList.add('open');}\nfunction closeWizard(){document.getElementById('ceWizardOverlay').classList.remove('open');}\nfunction buildAppGrid(){\n  const grid=document.getElementById('ceAppGrid');grid.innerHTML='';\n  APP_PRESETS.forEach(p=>{\n    const card=document.createElement('div');card.className='app-card'+(wizSelId===p.id?' selected':'');\n    card.innerHTML=`<div class=\"icon\">${p.icon}<\/div>`;\n    const nm=document.createElement('div');nm.className='name';nm.appendChild(biSpan(p.label,p.label_fr||p.label));\n    const ds=document.createElement('div');ds.className='desc';ds.appendChild(biSpan(p.desc,p.desc_fr||p.desc));\n    card.append(nm,ds);\n    const tag=document.createElement('div');tag.className='tag';\n    const changes=typeof p.changes==='function'?null:p.changes;\n    if(changes&&changes.length===0){tag.style.color='#d97706';tag.appendChild(biSpan('Coming soon','Bient\u00f4t disponible'));}\n    else if(typeof p.changes==='function'){tag.style.color='var(--g)';tag.appendChild(biSpan('Dynamic preset','Pr\u00e9r\u00e9glage dynamique'));}\n    else{tag.style.color='var(--g)';const n=p.changes.length;tag.appendChild(biSpan(`${n} param${n!==1?'s':''} set`,`${n} param\u00e8tre${n!==1?'s':''} d\u00e9fini${n!==1?'s':''}`));}\n    card.appendChild(tag);\n    card.onclick=()=>{wizSelId=p.id;grid.querySelectorAll('.app-card').forEach(c=>c.classList.remove('selected'));card.classList.add('selected');};\n    grid.appendChild(card);\n  });\n}\n\nfunction applyWizard(){\n  if(!wizSelId)return;\n  const p=APP_PRESETS.find(x=>x.id===wizSelId);if(!p)return;\n  const changes=typeof p.changes==='function'?p.changes(cfg):p.changes;\n  if(!changes||!changes.length){alert(_isFr()?`Le pr\u00e9r\u00e9glage \u00ab ${p.label_fr||p.label} \u00bb n\u2019a pas encore de param\u00e8tres d\u00e9finis.`:`\"${p.label}\" preset has no parameters defined yet.`);return;}\n  changes.forEach(({path,value})=>set(cfg,path,value));\n  activePreset={id:p.id,label:p.label,label_fr:p.label_fr||p.label,icon:p.icon,paths:new Set(changes.map(c=>c.path))};\n  closeWizard();render();\n}\n\n\n\/\/ \u2500\u2500 Suite integration: expose SDEditor; seeded from the loaded configuration \u2500\u2500\nlet _ceWired=false;\nfunction ceWire(){\n  if(_ceWired) return; _ceWired=true;\n  document.getElementById('ceSaveBtn').onclick=()=>{\n    const name=(get(cfg,'configurations.system.driveName')||'unnamed').replace(\/\\s+\/g,'_');\n    const ts=new Date().toISOString().replace(\/[:.]\/g,'-').slice(0,19);\n    const a=Object.assign(document.createElement('a'),{download:`smartdrive-config-${name}-${ts}.json`,href:URL.createObjectURL(new Blob([JSON.stringify(cfg,null,2)],{type:'application\/json'}))});a.click();\n  };\n  document.getElementById('ceResetBtn').onclick=()=>{\n    if(!confirm('Reset the editor to the originally loaded configuration?'))return;\n    cfg=clone(_ceSeed||DEFAULT_CFG);activePreset=null;inferModel();render();\n  };\n  document.getElementById('ceWizardBtn').onclick=openWizard;\n  document.getElementById('ceWizardCancel').onclick=closeWizard;\n  document.getElementById('ceWizardApply').onclick=applyWizard;\n  document.getElementById('ceWizardOverlay').onclick=e=>{if(e.target===document.getElementById('ceWizardOverlay'))closeWizard();};\n  document.getElementById('ceHdOverlay').onclick=()=>{document.getElementById('ceHelpDrawer').classList.remove('open');document.getElementById('ceHdOverlay').classList.remove('open');};\n  document.getElementById('ceHdClose').onclick=()=>{document.getElementById('ceHelpDrawer').classList.remove('open');document.getElementById('ceHdOverlay').classList.remove('open');};\n}\nlet _ceSeed=null;\nwindow.SDEditor={\n  init(config){\n    if(!DEFAULT_CFG||!Object.keys(DEFAULT_CFG).length){\n      try{DEFAULT_CFG=JSON.parse(document.getElementById('ceDefaultConfig').textContent.trim());}catch(_){DEFAULT_CFG={};}\n    }\n    _ceSeed = config ? clone(config) : clone(DEFAULT_CFG);\n    cfg = clone(_ceSeed);\n    activePreset=null; inferModel(); ceWire(); render();\n  },\n  getConfig(){ return cfg; },\n  hasConfig(){ return cfg && Object.keys(cfg).length>0; }\n};\n\n})();\n\n<\/script>\n\n\n\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>SD Tools Suite 1.0.10 Reset Create Config Edit Config View Config &amp; Report View Audit Log View Support Package Close EN Summary Configuration Audit Log \u22ef Raw package files \u00d7 \u2014 SmartD Clean Power VFD Tools Suite Access configuration tools, Audit logs or support package. Drag &amp; Drop a file here Supported files: Configuration JSON,&#8230;<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_kad_post_transparent":"disable","_kad_post_title":"hide","_kad_post_layout":"fullwidth","_kad_post_sidebar_id":"","_kad_post_content_style":"unboxed","_kad_post_vertical_padding":"hide","_kad_post_feature":"hide","_kad_post_feature_position":"","_kad_post_header":false,"_kad_post_footer":false,"_kad_post_classname":"","footnotes":""},"class_list":["post-3135","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/pages\/3135","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/comments?post=3135"}],"version-history":[{"count":5,"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/pages\/3135\/revisions"}],"predecessor-version":[{"id":3140,"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/pages\/3135\/revisions\/3140"}],"wp:attachment":[{"href":"https:\/\/services.smartd.co\/fr\/wp-json\/wp\/v2\/media?parent=3135"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}