(function () {
  function createPanelContentRuntime(deps) {
    var tr = deps.tr;
    var escapeHtml = deps.escapeHtml;
    var infoFor = deps.infoFor;
    var versionsData = Array.isArray(deps.versionsData) ? deps.versionsData : [];
    var i18n = deps.i18n;
    var extensionHost = deps.extensionHost;
    var core = window.NetReconNewUICore || {};
    var sharedNet = core.utils ? core.utils.net : null;
    var contentConfig = core.panelContentConfig || {};
    var versionsConfig = contentConfig.versions || {};
    var importToolConfig = contentConfig.importTool || {};
    var resultsIpConfig = contentConfig.resultsIp || {};
    var presetsApi = core.presets || null;
    var macrosApi = core.macros || null;
    var shellcraftCanvasApi = core.shellcraftCanvas || null;
    var pulpitCanvasApi = core.pulpitCanvas || null;
    var pulpitPreviewApi = core.pulpitPreview || null;
    var platformApi = core.platform || null;
    var mailXssTesterApi = core.mailXssTester || null;
    var googleDorkApi = core.googleDork || null;
    var komunikatorApi = core.komunikator || null;
    var agentProfilesApi = core.agentProfiles || null;
    var generalSettingsApi = core.generalSettings || null;

    function trOr(key, fallback) {
      var value = tr(key);
      return value === key ? fallback : value;
    }

    // --- ip-scanner tool keys ---
    // Preset-lookup helpers, used only by renderPresetsTool/renderResultsIp below.
    function getSelectedPresetInfo() {
      var selectEl = document.getElementById("v1PortPreset");
      var selectedId = selectEl ? String(selectEl.value || "").trim() : "";
      var corePresets = core.presets && typeof core.presets.getState === "function"
        ? core.presets.getState()
        : null;
      var presets = corePresets && Array.isArray(corePresets.presets) ? corePresets.presets : [];

      var selected = presets.find(function (item) {
        return String(item && item.id || "") === selectedId;
      });
      if (!selected && corePresets && corePresets.defaultPresetId) {
        selected = presets.find(function (item) {
          return String(item && item.id || "") === String(corePresets.defaultPresetId || "");
        });
      }
      if (!selected) selected = presets[0] || { id: "", emoji: "", name: "" };

      return {
        id: String(selected.id || ""),
        emoji: String(selected.emoji || "").trim(),
        name: String(selected.name || selected.id || "").trim(),
      };
    }

    function getPresetsState() {
      var fallbackState = presetsApi && typeof presetsApi.getDefaultState === "function"
        ? presetsApi.getDefaultState()
        : { defaultPresetId: "all-ports", presets: [] };

      function hasPresetData(state) {
        var presets = state && Array.isArray(state.presets) ? state.presets : [];
        if (!presets.length) return false;
        return presets.some(function (item) {
          if (!item || typeof item !== "object") return false;
          return !!String(item.name || "").trim() || !!String(item.ports || "").trim();
        });
      }

      try {
        if (presetsApi && typeof presetsApi.getState === "function") {
          var state = presetsApi.getState();
          if (hasPresetData(state)) return state;
          if (typeof presetsApi.resetDefaults === "function") {
            var resetState = presetsApi.resetDefaults();
            if (hasPresetData(resetState)) return resetState;
          }
        }
      } catch (_) {
        // ignore presets provider errors
      }

      return fallbackState;
    }

    // --- shell keys ---
    // Generic fallback (also used for extension-contributed tools that only
    // supply title/text/points), plus Help/Options entries that stay in the
    // base shell per FUTURE_PLUGIN_SHELL.md.
    // Addon-declared input fields (contributions.tools[key].fields, e.g. a
    // scan target, or a "technique" picker) - a small, constrained schema
    // (type/name/label/placeholder, or type/name/label/options for a
    // select), not arbitrary markup, matching the security posture of the
    // rest of this static card (see panels-runtime.js's
    // wireExtensionToolActions for how these get read at click time, and
    // for the "dynamic:<fieldName>" commandId convention a select-type
    // technique picker is meant to be used with).
    function renderExtFieldsHtml(fields) {
      if (!Array.isArray(fields) || !fields.length) return "";
      var rows = fields.map(function (field) {
        var name = String((field && field.name) || "");
        if (!name) return "";
        var label = escapeHtml(String((field && field.label) || name));
        var controlHtml;
        if ((field && field.type) === "select") {
          var options = Array.isArray(field.options) ? field.options : [];
          var optionsHtml = options.map(function (opt) {
            var value = escapeHtml(String((opt && opt.value) || ""));
            var optLabel = escapeHtml(String((opt && opt.label) || (opt && opt.value) || ""));
            return "<option value=\"" + value + "\">" + optLabel + "</option>";
          }).join("");
          // name + autocomplete="off": Chrome's DevTools Issues panel flags
          // any form field with neither an id nor a name attribute, and
          // separately suggests an explicit autocomplete value - "off" is
          // also the semantically correct choice here regardless of the
          // lint, since these are addon-specific values (a target IP, a
          // port list) that a browser's address/profile autofill should
          // never try to fill in.
          controlHtml = "<select data-ext-field=\"" + escapeHtml(name) + "\" name=\"" + escapeHtml(name) + "\" autocomplete=\"off\">" + optionsHtml + "</select>";
        } else {
          var type = (field && field.type) === "number" ? "number" : "text";
          var placeholder = escapeHtml(String((field && field.placeholder) || ""));
          // "default" prefills a REAL, editable value (e.g. a ready-to-run
          // ports list the user can tweak) - distinct from "placeholder",
          // which is just a hint that disappears the moment the user types
          // and is never part of the field's actual value.
          var defaultValue = escapeHtml(String((field && field.default) || ""));
          controlHtml = "<input type=\"" + type + "\" data-ext-field=\"" + escapeHtml(name) + "\" name=\"" + escapeHtml(name) + "\" autocomplete=\"off\" placeholder=\"" + placeholder + "\" value=\"" + defaultValue + "\" />";
        }
        return [
          "<label class=\"v1-ext-field-row\">",
          "<span class=\"v1-ext-field-label\">" + label + "</span>",
          controlHtml,
          "</label>"
        ].join("");
      }).join("");
      return "<div class=\"v1-ext-fields\">" + rows + "</div>";
    }

    // Hidden unless "rows" (already-JSON.parsed by the caller, from either a
    // live action result or a stored resultKey value re-rendered fresh - see
    // renderDefaultTool and wireExtensionToolActions in panels-runtime.js,
    // which both parse the same way) is a real array - the existing <pre>
    // output stays in the markup unconditionally as the fallback for non-
    // JSON/error results, so both can exist side by side and only one is
    // ever shown. Pre-populating from "rows" at render time (not just after
    // a live click) matters for the LS/CS split pattern: an action can
    // declare "openTool" to hand its result to a DIFFERENT tool's tab (e.g.
    // a left-panel fields+button tool opening a center-tab results tool) -
    // that other tool only ever gets a fresh renderDefaultTool() call, never
    // a live DOM patch, so it must be able to show the right rows the first
    // time it's rendered, not just after some later click.
    function renderExtResultsTableHtml(resultsTable, rows) {
      if (!resultsTable || !Array.isArray(resultsTable.columns) || !resultsTable.columns.length) return "";
      var headCells = resultsTable.columns.map(function (col) {
        return "<th>" + escapeHtml(String((col && col.label) || (col && col.key) || "")) + "</th>";
      }).join("");
      var bodyHtml = Array.isArray(rows) ? rows.map(function (row) {
        var cells = resultsTable.columns.map(function (col) {
          var value = row && Object.prototype.hasOwnProperty.call(row, col.key) ? row[col.key] : "";
          return "<td>" + escapeHtml(String(value == null ? "" : value)) + "</td>";
        }).join("");
        return "<tr>" + cells + "</tr>";
      }).join("") : "";
      var hiddenAttr = Array.isArray(rows) ? "" : " hidden";
      return [
        "<div class=\"v1-results-table-scroll v1-ext-results-table-scroll\" data-ext-results-table" + hiddenAttr + ">",
        "<table class=\"v1-results-table v1-ext-results-table\">",
        "<thead><tr>" + headCells + "</tr></thead>",
        "<tbody>" + bodyHtml + "</tbody>",
        "</table>",
        "</div>"
      ].join("");
    }

    function renderDefaultTool(tool) {
      var info = infoFor(tool);
      var points = (info.points || []).map(function (p) { return "<li>" + escapeHtml(p) + "</li>"; }).join("");
      var hasActions = Array.isArray(info.actions) && info.actions.length;
      var hasResult = info.resultText != null;
      var fieldsHtml = renderExtFieldsHtml(info.fields);

      var parsedTableRows = null;
      if (info.resultsTable && typeof info.resultText === "string" && info.resultText) {
        try {
          var parsed = JSON.parse(info.resultText);
          if (Array.isArray(parsed)) parsedTableRows = parsed;
        } catch (_) {
          parsedTableRows = null;
        }
      }

      var actionsHtml = "";
      if (hasActions || hasResult) {
        var buttonsHtml = "";
        if (hasActions) {
          var buttons = info.actions.map(function (action, idx) {
            var commandId = String((action && action.commandId) || "");
            if (!commandId) return "";
            var label = escapeHtml(String((action && action.label) || commandId || ("Action " + (idx + 1))));
            return "<button type=\"button\" class=\"v1-ext-action-btn\" data-ext-action-command=\"" + escapeHtml(commandId) + "\">" + label + "</button>";
          }).join(" ");
          buttonsHtml = "<div class=\"v1-ext-actions\">" + buttons + "</div>";
        }
        // Hidden whenever there's genuinely nothing to show yet (no result
        // text at all - e.g. a tool whose action always hands its result to
        // a DIFFERENT tool via openTool, so this <pre> is never populated
        // locally) as well as when a table successfully parsed - otherwise
        // an empty bordered box shows up with nothing in it before the
        // first scan ever runs.
        var preHiddenAttr = (parsedTableRows || !info.resultText) ? " hidden" : "";
        actionsHtml = buttonsHtml + "<pre class=\"v1-ext-action-output\" data-ext-action-output" + preHiddenAttr + ">" + escapeHtml(info.resultText || "") + "</pre>" + renderExtResultsTableHtml(info.resultsTable, parsedTableRows);
      }
      // No <h4>info.title</h4> here on purpose - #v1ToolTitle (panels-
      // runtime.js's refreshActiveUI, an <h3> that sits above this whole
      // card for EVERY active tab) already shows the exact same title once;
      // repeating it here as an <h4> right below it looked like a genuine
      // rendering bug (two near-identical headings stacked with nothing
      // between them) even though it was two intentional-but-redundant
      // pieces of markup, not a duplicate-render defect.
      return "<div>" + escapeHtml(info.text) + "</div><ul>" + points + "</ul>" + fieldsHtml + actionsHtml;
    }

    function renderVersionsTool() {
      if (!versionsData.length) {
        return "<h4>" + escapeHtml(trOr("versionsEmptyTitle", versionsConfig.emptyTitle || "Versions")) + "</h4><div>" + escapeHtml(trOr("versionsEmptyText", versionsConfig.emptyText || "No version entries available.")) + "</div>";
      }

      var chronological = versionsData.slice().reverse();
      var pointsHtml = chronological.map(function (entry, pos) {
        var sourceIndex = versionsData.length - 1 - pos;
        var activeClass = sourceIndex === 0 ? " active" : "";
        return [
          "<button type=\"button\" class=\"v1-version-point" + activeClass + "\" data-version-index=\"" + sourceIndex + "\" aria-label=\"" + escapeHtml(entry.version) + "\">",
          "<span class=\"v1-version-dot is-published\"></span>",
          "<span class=\"v1-version-label\">" + escapeHtml(entry.version) + "</span>",
          "</button>"
        ].join("");
      }).join("");

      var listHtml = versionsData.map(function (entry, idx) {
        var notes = (entry.notes || []).map(function (note) { return "<li>" + escapeHtml(note) + "</li>"; }).join("");
        var activeClass = idx === 0 ? " is-active" : "";
        return "<section class=\"v1-version-entry" + activeClass + "\" id=\"v1VersionEntry-" + idx + "\" data-version-entry-index=\"" + idx + "\"><h4>" + escapeHtml(entry.version) + "</h4><ul>" + notes + "</ul></section>";
      }).join("");

      return [
        "<div class=\"v1-versions-shell\">",
        "<div class=\"v1-versions-timeline-sticky\">",
        "<div class=\"v1-version-track-wrap\">",
        "<button type=\"button\" class=\"v1-version-scroll\" data-version-scroll=\"left\" aria-label=\"" + escapeHtml(trOr("versionsScrollLeftAria", versionsConfig.scrollLeftAria || "Scroll versions left")) + "\">◀</button>",
        "<div class=\"v1-version-track\" id=\"v1VersionTrack\" data-version-role=\"track\" role=\"listbox\" aria-label=\"" + escapeHtml(trOr("versionsTimelineAria", versionsConfig.timelineAria || "Published versions timeline")) + "\">",
        "<div class=\"v1-version-track-inner\">",
        pointsHtml,
        "</div>",
        "</div>",
        "<button type=\"button\" class=\"v1-version-scroll\" data-version-scroll=\"right\" aria-label=\"" + escapeHtml(trOr("versionsScrollRightAria", versionsConfig.scrollRightAria || "Scroll versions right")) + "\">▶</button>",
        "</div>",
        "<div class=\"v1-version-physics\" id=\"v1VersionPhysics\" data-version-role=\"physics\" style=\"--v1-version-progress: 1;\">",
        "<div class=\"v1-version-orb\" aria-hidden=\"true\"></div>",
        "</div>",
        "</div>",
        "<div class=\"v1-versions-list v1-scroll-safe-inline-end\" id=\"v1VersionsList\" data-version-role=\"list\">",
        listHtml,
        "</div>",
        "</div>"
      ].join("");
    }

    function getCurrentVersion() {
      if (!versionsData.length) return "v2.2.0";
      var first = versionsData[0] || {};
      var version = first.version;
      if (!version) return "v2.2.0";
      return String(version);
    }

    function renderAboutTool() {
      var currentVersion = escapeHtml(getCurrentVersion());
      var heading = escapeHtml(tr("aboutHeading")) + " " + currentVersion;
      var contactUrl = "https://" + String(tr("aboutSupportFacebook") || "").trim();
      var projectUrl = "https://" + String(tr("aboutProjectPageUrl") || "").trim();
      var zrzutkaUrl = "https://" + String(tr("aboutZrzutkaUrl") || "").trim();
      var testersUrl = "https://github.com/tBane-Dev";
      return [
        "<div class=\"v1-about\">",
        "<h4>" + heading + "</h4>",
        "<p>" + escapeHtml(tr("aboutByAuthor")) + "</p>",
        "<p>" + escapeHtml(tr("aboutSpecialThanksHeading")) + "</p>",
        "<p>" + escapeHtml(tr("aboutTestersLabel")) + " <a href=\"" + escapeHtml(testersUrl) + "\" target=\"_blank\" rel=\"noopener noreferrer\">GitHub</a></p>",
        "<p>" + escapeHtml(tr("aboutVibecodingNote")) + "</p>",
        "<h4>" + escapeHtml(tr("aboutSupportHeading")) + "</h4>",
        "<p>" + escapeHtml(tr("aboutSupportBody")) + "</p>",
        "<p><strong>" + escapeHtml(tr("aboutSupportQuick")) + "</strong></p>",
        "<p><strong>" + escapeHtml(tr("aboutSupportPhone")) + "</strong></p>",
        "<p>" + escapeHtml(tr("aboutSupportContact")) + " <strong><a href=\"" + escapeHtml(contactUrl) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + escapeHtml(tr("aboutSupportFacebook")) + "</a></strong></p>",
        "<p>" + escapeHtml(tr("aboutProjectPageLabel")) + " <strong><a href=\"" + escapeHtml(projectUrl) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + escapeHtml(tr("aboutProjectPageUrl")) + "</a></strong></p>",
        "<h4>" + escapeHtml(tr("aboutTransferHeading")) + "</h4>",
        "<ul>",
        "<li>" + escapeHtml(tr("aboutTransferName")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTransferCity")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTransferBank")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTransferIban")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTransferTitle")) + "</li>",
        "</ul>",
        "<h4>" + escapeHtml(tr("aboutTotalCostsHeading")) + "</h4>",
        "<ul>",
        "<li>" + escapeHtml(tr("aboutTotalCostClaude")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTotalCostDomains")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTotalCostCopilot")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTotalCostOther")) + "</li>",
        "<li>" + escapeHtml(tr("aboutTotalCostAds")) + "</li>",
        "</ul>",
        "<p>" + escapeHtml(tr("aboutZrzutkaLabel")) + " <strong><a href=\"" + escapeHtml(zrzutkaUrl) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + escapeHtml(zrzutkaUrl) + "</a></strong></p>",
        "</div>"
      ].join("");
    }

    function renderLicenseTool() {
      var licenseText = contentConfig.licenseText || [
        "MIT License",
        "",
        "Copyright (c) Michal Stankiewicz",
        "",
        "Permission is hereby granted, free of charge, to any person obtaining a copy",
        "of this software and associated documentation files (the \"Software\"), to deal",
        "in the Software without restriction, including without limitation the rights",
        "to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
        "copies of the Software, and to permit persons to whom the Software is",
        "furnished to do so, subject to the following conditions:",
        "",
        "The above copyright notice and this permission notice shall be included in all",
        "copies or substantial portions of the Software.",
        "",
        "THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
        "IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
        "FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
        "AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
        "LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
        "OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
        "SOFTWARE."
      ].join("\n");
      return [
        "<div class=\"v1-license\">",
        "<h4>" + escapeHtml(tr("licenseHeading")) + "</h4>",
        "<pre class=\"v1-license-text\">" + escapeHtml(licenseText) + "</pre>",
        "</div>"
      ].join("");
    }

    // shell: throwaway placeholder tool (static lorem ipsum) - a first,
    // deliberately minimal step toward exploring an alternate UI later;
    // gated behind Options -> "Show unfinished tools".
    function renderLoremIpsumTool() {
      var paragraphs = [
        "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
        "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
        "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo.",
        "Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet.",
        "At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident.",
        "Similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga. Et harum quidem rerum facilis est et expedita distinctio. Nam libero tempore, cum soluta nobis est eligendi optio.",
        "Temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae. Itaque earum rerum hic tenetur a sapiente delectus.",
        "Vitae dicta sunt explicabo nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt neque porro quisquam est.",
        "Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse.",
        "Quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur. At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque.",
        "Corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga et harum quidem rerum.",
        "Facilis est et expedita distinctio nam libero tempore, cum soluta nobis est eligendi optio cumque nihil impedit quo minus id quod maxime placeat facere possimus, omnis voluptas assumenda est omnis dolor.",
        "Repellendus temporibus autem quibusdam et aut officiis debitis aut rerum necessitatibus saepe eveniet ut et voluptates repudiandae sint et molestiae non recusandae itaque earum rerum hic tenetur a sapiente.",
        "Delectus ut aut reiciendis voluptatibus maiores alias consequatur aut perferendis doloribus asperiores repellat. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos curabitur.",
      ];
      // shell: STYLELIST.md - live preview of every native <input> type
      // (+ textarea/select/button), pure markup, no data wiring yet ("source
      // of truth" for these is still to be decided, per the STYLELIST.md
      // note) - this tab exists specifically to LOOK at every style at once.
      var styleListItems = [
        { n: 1, type: "text", desc: "Standardowe jednolinijkowe pole tekstowe.", html: "<input type=\"text\" placeholder=\"Tekst...\" />" },
        { n: 2, type: "password", desc: "Pole tekstowe maskujące wpisane znaki kropkami.", html: "<input type=\"password\" placeholder=\"Hasło...\" />" },
        { n: 3, type: "email", desc: "Pole sprawdzające poprawność formatu adresu e-mail.", html: "<input type=\"email\" placeholder=\"nazwa@domena.pl\" />" },
        { n: 4, type: "number", desc: "Pole akceptujące tylko cyfry z suwakiem góra/dół.", html: "<input type=\"number\" placeholder=\"0\" />" },
        { n: 5, type: "checkbox", desc: "Kwadratowe pole wielokrotnego wyboru (zaznacz/odznacz).", html: "<input type=\"checkbox\" />" },
        { n: 6, type: "radio", desc: "Okrągły przycisk jednokrotnego wyboru w ramach jednej grupy.", html: "<label><input type=\"radio\" name=\"v1StyleListRadioDemo\" checked /> A</label> <label><input type=\"radio\" name=\"v1StyleListRadioDemo\" /> B</label>" },
        { n: 7, type: "file", desc: "Przycisk umożliwiający przesłanie pliku z dysku komputera.", html: "<input type=\"file\" />" },
        { n: 8, type: "date", desc: "Kalendarz do wyboru konkretnego dnia, miesiąca i roku.", html: "<input type=\"date\" />" },
        { n: 9, type: "datetime-local", desc: "Pole do wyboru daty oraz godziny bez strefy czasowej.", html: "<input type=\"datetime-local\" />" },
        { n: 10, type: "time", desc: "Pole do wyboru konkretnej godziny (godziny i minuty).", html: "<input type=\"time\" />" },
        { n: 11, type: "month", desc: "Pole ograniczone do wyboru wyłącznie miesiąca i roku.", html: "<input type=\"month\" />" },
        { n: 12, type: "week", desc: "Pole ograniczone do wyboru numeru tygodnia oraz roku.", html: "<input type=\"week\" />" },
        { n: 13, type: "color", desc: "Przycisk otwierający paletę do wyboru koloru.", html: "<input type=\"color\" value=\"#3794ff\" />" },
        { n: 14, type: "range", desc: "Suwak do wyboru przybliżonej wartości numerycznej.", html: "<input type=\"range\" min=\"0\" max=\"100\" value=\"50\" />" },
        { n: 15, type: "search", desc: "Pole tekstowe zoptymalizowane pod kątem wpisywania fraz wyszukiwania.", html: "<input type=\"search\" placeholder=\"Szukaj...\" />" },
        { n: 16, type: "tel", desc: "Pole zoptymalizowane do wprowadzania numerów telefonów.", html: "<input type=\"tel\" placeholder=\"+48 000 000 000\" />" },
        { n: 17, type: "url", desc: "Pole sprawdzające poprawność formatu adresu internetowego.", html: "<input type=\"url\" placeholder=\"https://example.com\" />" },
        { n: 18, type: "hidden", desc: "Ukryte pole przechowujące dane niewidoczne dla użytkownika.", html: "<input type=\"hidden\" value=\"ukryta-wartosc\" /><span class=\"v1-stylelist-hidden-note\">(niewidoczne - wartość: \"ukryta-wartosc\")</span>" },
        { n: 19, type: "button (input)", desc: "Zwykły przycisk aktywowany najczęściej przez JavaScript.", html: "<input type=\"button\" value=\"Button\" />" },
        { n: 20, type: "submit (input)", desc: "Przycisk wysyłający dane z całego formularza na serwer.", html: "<input type=\"submit\" value=\"Submit\" />" },
        { n: 21, type: "reset (input)", desc: "Przycisk przywracający wszystkim polom wartości domyślne.", html: "<input type=\"reset\" value=\"Reset\" />" },
        { n: 22, type: "image (input)", desc: "Przycisk graficzny działający tak samo jak submit.", html: "<input type=\"image\" src=\"zebrus.png\" alt=\"Image submit\" width=\"22\" height=\"22\" />" },
        { n: 23, type: "textarea", desc: "Pole do wprowadzania wielu linijek tekstu (np. komentarze).", html: "<textarea class=\"v1-stylelist-textarea\" rows=\"2\" placeholder=\"Wiele linijek tekstu...\">Linia 1\nLinia 2\nLinia 3\nLinia 4\nLinia 5\nLinia 6</textarea>" },
        { n: 24, type: "select", desc: "Rozwijana lista wyboru zawierająca znaczniki option.", html: "<select><option>Opcja 1</option><option>Opcja 2</option><option>Opcja 3</option></select>" },
        { n: 25, type: "button (tag)", desc: "Znacznik <button> do klikania, mogący zawierać tekst lub grafikę.", html: "<button type=\"button\">🖼️ Przycisk</button>" },
      ];

      // shell: STYLELIST.md "Inne style" section - starts with the custom
      // faux-scrollbar system (vertical/horizontal), since that's what's
      // used app-wide instead of native browser scrollbars (see
      // custom-scrollbar-runtime.js). Both demo boxes are real, functional
      // scroll containers registered in SHELL_SCROLL_TARGETS - not static
      // mockups - so the thumb you see is the actual live mechanism.
      var styleListOtherItems = [
        {
          n: 1,
          type: "pasek pionowy (custom scrollbar)",
          desc: "Wlasny system zamiast natywnego paska przegladarki - .v1-faux-scrollbar / .v1-faux-scrollbar-thumb.",
          html: "<div class=\"v1-stylelist-scroll-v-demo\">" +
            [1, 2, 3, 4, 5, 6, 7, 8].map(function (n) { return "<p>Linia " + n + " przykladowej tresci do przewijania w pionie.</p>"; }).join("") +
            "</div>"
        },
        {
          n: 2,
          type: "pasek poziomy (custom scrollbar)",
          desc: "Ten sam system w wariancie poziomym - .v1-faux-scrollbar-h / .v1-faux-scrollbar-thumb-h.",
          html: "<div class=\"v1-stylelist-scroll-h-demo\"><div class=\"v1-stylelist-scroll-h-track\">" +
            [1, 2, 3, 4, 5, 6, 7, 8].map(function (n) { return "<span>Kolumna " + n + "</span>"; }).join("") +
            "</div></div>"
        },
      ];

      function renderStyleListRows(items) {
        return items.map(function (item) {
          return [
            "<div class=\"v1-stylelist-row\">",
            "<div class=\"v1-stylelist-meta\">",
            "<strong>" + item.n + ". " + escapeHtml(item.type) + "</strong>",
            "<span>" + escapeHtml(item.desc) + "</span>",
            "</div>",
            "<div class=\"v1-stylelist-demo\">" + item.html + "</div>",
            "</div>"
          ].join("");
        }).join("");
      }

      return [
        "<div>",
        "<h4>" + escapeHtml(tr("toolTitle_lorem_ipsum")) + "</h4>",
        paragraphs.map(function (p) { return "<p>" + escapeHtml(p) + "</p>"; }).join(""),
        "</div>",
        "<div class=\"v1-stylelist-columns\">",
        "<div class=\"v1-stylelist\">",
        "<h4>Style List &mdash; Inputy</h4>",
        "<p>Zywy podglad kazdego natywnego typu input + textarea/select/button. Zrodlo prawdy jeszcze nieustalone - patrz STYLELIST.md.</p>",
        renderStyleListRows(styleListItems),
        "</div>",
        "<div class=\"v1-stylelist v1-stylelist-columns-second\">",
        "<h4>Style List &mdash; Inne style</h4>",
        "<p>Pozostale elementy wizualne aplikacji, poza inputami - patrz STYLELIST.md.</p>",
        renderStyleListRows(styleListOtherItems),
        "</div>",
        "</div>"
      ].join("");
    }

    // shell: Language Manager row markup for one installed language - kept
    // as a standalone helper (not just inlined into renderLanguageManagerTool)
    // because panels-runtime.js's wireLanguageManagerButtons() needs the
    // exact same row shape to re-render #v1LangInstalledList after a radio
    // change / local import / catalog install, mirroring how Import Tool's
    // listInstalled()/renderCatalog() independently rebuild their own
    // sub-regions rather than re-running the initial render function.
    function renderLangInstalledRow(item, current) {
      var checked = item.code === current ? " checked" : "";
      var versionHtml = item.version ? " <span>" + escapeHtml(tr("forAppVersionPrefix")) + " " + escapeHtml(item.version) + "</span>" : "";
      return [
        "<label class=\"v1-lang-item\">",
        "<input type=\"radio\" name=\"v1LangActive\" data-lang-radio=\"" + escapeHtml(item.code) + "\"" + checked + " />",
        "<span class=\"v1-lang-flag\" aria-hidden=\"true\">" + escapeHtml(item.flag || "🌐") + "</span>",
        "<strong>" + escapeHtml(item.name || item.code.toUpperCase()) + "</strong>",
        versionHtml,
        "</label>"
      ].join("");
    }

    function renderLanguageManagerTool() {
      var current = document.documentElement.getAttribute("lang") || "en";
      var langDetails = [];
      try {
        langDetails = i18n && i18n.listLanguageDetails ? i18n.listLanguageDetails() : [];
      } catch (_) {
        langDetails = [];
      }
      var installedHtml = langDetails.map(function (item) {
        return renderLangInstalledRow(item, current);
      }).join("");

      return [
        "<div class=\"v1-import-manager\">",
        "<div class=\"v1-import-manager-head\">",
        "<h4 style=\"margin:0 0 4px;\">" + tr("langManagerTitle") + "</h4>",
        "</div>",
        "<h4 style=\"margin:12px 0 4px;\">" + tr("langInstalledHeading") + "</h4>",
        "<div id=\"v1LangInstalledList\" data-lang-role=\"installed-list\">" + installedHtml + "</div>",
        "<div class=\"v1-import-manager-actions\">",
        "<button type=\"button\" data-lang-action=\"import\">" + tr("langImportBtn") + "</button>",
        "</div>",
        "<h4 style=\"margin:12px 0 4px;\">" + tr("langCatalogHeading") + "</h4>",
        "<div id=\"v1LangCatalog\" data-lang-role=\"catalog\" class=\"v1-import-output v1-catalog-list\">" + escapeHtml(tr("langCatalogEmpty")) + "</div>",
        "</div>"
      ].join("");
    }

    // shell: settings screen letting the user pick, per shell-level setting,
    // whether it should be remembered across app restarts (TBM Options ->
    // General). Actual "remember" enforcement lives in bootstrap-runtime.js's
    // applyRememberedSettingsGate() - this function only renders/reads the
    // toggle state.
    function renderGeneralSettingsTool() {
      var settings = generalSettingsApi ? generalSettingsApi.getState() : {};
      var unfinishedVisible = false;
      try {
        unfinishedVisible = localStorage.getItem("netrecon_show_unfinished_tools") === "1";
      } catch (_) {}

      function checkboxRow(key, icon, labelKey, labelFallback) {
        var checked = !!settings[key];
        return [
          "<label class=\"v1-results-columns-item v1-general-settings-item\">",
          "<input type=\"checkbox\" data-general-setting=\"" + escapeHtml(key) + "\"" + (checked ? " checked" : "") + " />",
          "<span class=\"v1-results-columns-icon\" aria-hidden=\"true\">" + escapeHtml(icon) + "</span>",
          "<span>" + escapeHtml(trOr(labelKey, labelFallback)) + "</span>",
          "</label>"
        ].join("");
      }

      function groupHeading(key, fallback) {
        return "<h4 class=\"v1-general-settings-group\">" + escapeHtml(trOr(key, fallback)) + "</h4>";
      }

      // AI Assistant: one self-contained, colon-aligned block per provider
      // (Anthropic/Google) - each with its own model dropdown, API key
      // field, and RAM-vs-localStorage choice, so both can be configured
      // ahead of time; the "Provider" radio (shared name across both
      // blocks) just picks which one is currently active. UI/settings
      // only for now - nothing here calls a real Claude/Google endpoint yet.
      var AI_PROVIDER_META = {
        claude: {
          displayName: "Anthropic",
          keyLink: "https://platform.claude.com/settings/workspaces/default/keys",
          models: [["opus", "aiModelOpus", "Opus"], ["sonnet", "aiModelSonnet", "Sonnet"], ["haiku", "aiModelHaiku", "Haiku"]],
        },
        google: {
          displayName: "Google",
          keyLink: "https://aistudio.google.com/api-keys",
          models: [["pro", "aiModelPro", "Pro"], ["flash", "aiModelFlash", "Flash"]],
        },
      };

      function aiProviderBlock(providerId) {
        var aiConfigApi = window.NetReconNewUICore && window.NetReconNewUICore.aiAssistantConfig;
        var state = aiConfigApi ? aiConfigApi.getState() : { provider: "claude", claude: { model: "sonnet", keyStorage: "localstorage" }, google: { model: "flash", keyStorage: "localstorage" } };
        var apiKey = aiConfigApi ? aiConfigApi.getApiKey(providerId) : "";
        var providerState = state[providerId];
        var meta = AI_PROVIDER_META[providerId];
        var isActive = state.provider === providerId;

        var modelOptions = meta.models.map(function (m) {
          var value = m[0], labelKey = m[1], labelFallback = m[2];
          return "<option value=\"" + value + "\"" + (providerState.model === value ? " selected" : "") + ">" + escapeHtml(trOr(labelKey, labelFallback)) + "</option>";
        }).join("");

        return [
          "<div class=\"v1-ai-provider-block\">",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("aiFieldProvider", "Provider:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<label class=\"v1-ai-provider-radio\">",
          "<input type=\"radio\" name=\"v1AiProvider\" value=\"" + providerId + "\"" + (isActive ? " checked" : "") + " />",
          "<span>" + escapeHtml(meta.displayName) + "</span>",
          "</label>",
          " <a href=\"" + escapeHtml(meta.keyLink) + "\" target=\"_blank\" rel=\"noopener\" class=\"v1-ai-provider-link\">" + escapeHtml(trOr("aiGetKeyLinkText", "get API key")) + "</a>",
          "</span>",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("aiFieldModel", "Model:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<select data-ai-model-select=\"" + providerId + "\">" + modelOptions + "</select>",
          "</span>",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("aiApiKeyLabel", "API key:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<input type=\"text\" data-ai-api-key=\"" + providerId + "\" autocomplete=\"off\" value=\"" + escapeHtml(apiKey) + "\" placeholder=\"" + escapeHtml(trOr("aiApiKeyPlaceholder", "API key")) + "\" />",
          "</span>",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("aiFieldStorage", "Storage:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<label class=\"v1-ai-provider-radio\">",
          "<input type=\"radio\" name=\"v1AiKeyStorage-" + providerId + "\" value=\"localstorage\"" + (providerState.keyStorage === "localstorage" ? " checked" : "") + " />",
          "<span>" + escapeHtml(trOr("aiStorageLocal", "local")) + "</span>",
          "</label>",
          "<label class=\"v1-ai-provider-radio\">",
          "<input type=\"radio\" name=\"v1AiKeyStorage-" + providerId + "\" value=\"ram\"" + (providerState.keyStorage === "ram" ? " checked" : "") + " />",
          "<span>RAM</span>",
          "</label>",
          "</span>",

          "</div>"
        ].join("");
      }

      function aiSystemPromptRow(mode, labelKey, labelFallback) {
        var aiConfigApi = window.NetReconNewUICore && window.NetReconNewUICore.aiAssistantConfig;
        var state = aiConfigApi ? aiConfigApi.getState() : {};
        var field = mode === "ps" ? "systemPromptPs" : "systemPromptUi";
        var value = typeof state[field] === "string" ? state[field] : "";
        return [
          "<div class=\"v1-ai-prompt-block\">",
          "<label for=\"v1AiSystemPrompt-" + mode + "\">" + escapeHtml(trOr(labelKey, labelFallback)) + "</label>",
          "<textarea id=\"v1AiSystemPrompt-" + mode + "\" data-ai-system-prompt=\"" + mode + "\" rows=\"3\">" + escapeHtml(value) + "</textarea>",
          "<button type=\"button\" class=\"v1-ai-prompt-reset\" data-ai-prompt-reset=\"" + mode + "\">" + escapeHtml(trOr("aiSystemPromptRestoreBtn", "Restore default")) + "</button>",
          "</div>"
        ].join("");
      }

      function aiAssistantRow() {
        return aiProviderBlock("claude") + aiProviderBlock("google")
          + aiSystemPromptRow("ui", "aiSystemPromptUiLabel", "System prompt (UI mode)")
          + aiSystemPromptRow("ps", "aiSystemPromptPsLabel", "System prompt (PS mode)");
      }

      // Google Dork API config - just the key/CX fields for now (no
      // consumer yet); reuses the AI provider block's colon-aligned
      // label/value layout since it's a generic shape, not AI-specific.
      function googleDorkApiRow() {
        var api = window.NetReconNewUICore && window.NetReconNewUICore.googleDorkApiConfig;
        var apiKey = api ? api.getApiKey() : "";
        var cx = api ? api.getCx() : "";

        return [
          "<div class=\"v1-ai-provider-block\">",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("googleDorkApiKeyLabel", "API key:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<input type=\"text\" data-google-dork-api-key autocomplete=\"off\" value=\"" + escapeHtml(apiKey) + "\" placeholder=\"" + escapeHtml(trOr("googleDorkApiKeyPlaceholder", "Custom Search API key")) + "\" />",
          " <a href=\"https://console.cloud.google.com/apis/credentials\" target=\"_blank\" rel=\"noopener\" class=\"v1-ai-provider-link\">" + escapeHtml(trOr("aiGetKeyLinkText", "get API key")) + "</a>",
          "</span>",

          "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr("googleDorkApiCxLabel", "Search engine ID:")) + "</span>",
          "<span class=\"v1-ai-provider-field-value\">",
          "<input type=\"text\" data-google-dork-api-cx autocomplete=\"off\" value=\"" + escapeHtml(cx) + "\" placeholder=\"" + escapeHtml(trOr("googleDorkApiCxPlaceholder", "Search engine ID (cx)")) + "\" />",
          " <a href=\"https://programmablesearchengine.google.com/controlpanel/all\" target=\"_blank\" rel=\"noopener\" class=\"v1-ai-provider-link\">" + escapeHtml(trOr("googleDorkApiCxLinkText", "create one")) + "</a>",
          "</span>",

          "</div>",
          "<div class=\"v1-import-manager-note\">" + escapeHtml(trOr("googleDorkApiNote", "Not used yet - saved for a future \"extract results\" feature on Google Dork Finder.")) + "</div>"
        ].join("");
      }

      // Firebase config for Komunikator's Google Sign-In (Phase 1) - just
      // the 3 fields Firebase's own "register a Web App" console screen
      // shows (apiKey/authDomain/projectId). Same block layout as the two
      // rows above.
      function firebaseConfigRow() {
        var api = window.NetReconNewUICore && window.NetReconNewUICore.firebaseConfig;
        var cfg = api ? api.getConfig() : { apiKey: "", authDomain: "", projectId: "" };

        function field(key, labelKey, labelFallback, placeholderKey, placeholderFallback) {
          var inputType = key === "clientSecret" ? "password" : "text";
          return [
            "<span class=\"v1-ai-provider-field-label\">" + escapeHtml(trOr(labelKey, labelFallback)) + "</span>",
            "<span class=\"v1-ai-provider-field-value\">",
            "<input type=\"" + inputType + "\" data-firebase-config-field=\"" + escapeHtml(key) + "\" autocomplete=\"off\" value=\"" + escapeHtml(cfg[key] || "") + "\" placeholder=\"" + escapeHtml(trOr(placeholderKey, placeholderFallback)) + "\" />",
            "</span>"
          ].join("");
        }

        return [
          // <form>, not <div> - clientSecret below is type="password", and
          // Chromium warns in the console about any password input not
          // contained in a <form> (same fix Mail XSS Tester's app-password
          // field already needed). Submit is prevented in
          // wireGeneralSettingsTool since these fields commit individually
          // on "change", not via a submit button.
          "<form class=\"v1-ai-provider-block\" data-firebase-config-form>",
          field("apiKey", "firebaseConfigApiKeyLabel", "API key:", "firebaseConfigApiKeyPlaceholder", "apiKey"),
          field("authDomain", "firebaseConfigAuthDomainLabel", "Auth domain:", "firebaseConfigAuthDomainPlaceholder", "authDomain"),
          field("projectId", "firebaseConfigProjectIdLabel", "Project ID:", "firebaseConfigProjectIdPlaceholder", "projectId"),
          field("oauthClientId", "firebaseConfigOauthClientIdLabel", "OAuth client ID (Desktop app type):", "firebaseConfigOauthClientIdPlaceholder", "...apps.googleusercontent.com"),
          field("clientSecret", "firebaseConfigClientSecretLabel", "OAuth client secret:", "firebaseConfigClientSecretPlaceholder", "GOCSPX-..."),
          "</form>",
          "<div class=\"v1-import-manager-note\">" + escapeHtml(trOr("firebaseConfigNote", "First 3 fields: Firebase Console -> Project settings -> Your apps -> Web app config. OAuth client ID + secret: Google Cloud Console -> Credentials -> Create Credentials -> OAuth client ID -> Desktop app.")) + "</div>"
        ].join("");
      }

      // Small, deliberately non-persisted test: a live "switch UI" toggle,
      // gated behind "Show unfinished tools" like every other unfinished
      // feature. Never written to localStorage on purpose - picking "Test"
      // just navigates to test-ui.html (a placeholder proving the swap
      // works via a plain page load); a normal relaunch always lands back
      // on the default UI (index.html) since nothing records the choice.
      function uiSwitchRow() {
        return [
          "<div class=\"v1-general-settings-ui-switch\" data-general-ui-switch" + (unfinishedVisible ? "" : " hidden") + ">",
          "<span class=\"v1-general-settings-ui-switch-label\">" + escapeHtml(trOr("generalUiSwitchLabel", "UI")) + "</span>",
          "<label class=\"v1-general-settings-ui-switch-option\">",
          "<input type=\"radio\" name=\"v1UiSwitch\" value=\"default\" checked />",
          "<span>" + escapeHtml(trOr("generalUiSwitchDefault", "Default")) + "</span>",
          "</label>",
          "<label class=\"v1-general-settings-ui-switch-option\">",
          "<input type=\"radio\" name=\"v1UiSwitch\" value=\"test\" />",
          "<span>" + escapeHtml(trOr("generalUiSwitchTest", "Test")) + "</span>",
          "</label>",
          "</div>"
        ].join("");
      }

      return [
        "<div class=\"v1-import-manager\">",
        "<div class=\"v1-import-manager-head\">",
        "<h4 style=\"margin:0 0 4px;\">" + escapeHtml(trOr("tipActionGeneral", "General")) + "</h4>",
        "<div class=\"v1-import-manager-note\">" + escapeHtml(trOr("generalIntroNote", "Choose which preferences should be remembered the next time you launch the app.")) + "</div>",
        "</div>",

        groupHeading("generalGroupSession", "Session"),
        checkboxRow("autoLoadLastSession", "🗂", "generalAutoLoadLastSession", "Auto Load last session"),

        groupHeading("generalGroupUpdates", "Updates"),
        checkboxRow("checkForUpdates", "🔔", "generalCheckForUpdates", "Check for updates on startup"),

        groupHeading("generalGroupAppearance", "Appearance"),
        checkboxRow("panelSideRight", "🔀", "generalPanelSideRight", "Swap panel sides (activity bar/LS on the right, RS on the left)"),
        checkboxRow("rememberLanguage", "🌐", "generalRememberLanguage", "Remember UI language"),
        checkboxRow("rememberSkin", "🎨", "generalRememberSkin", "Remember skin / theme"),
        uiSwitchRow(),
        checkboxRow("rememberPanelSizes", "↔️", "generalRememberPanelSizes", "Remember panel sizes and collapsed state"),

        groupHeading("generalGroupPrivacyTools", "Privacy & tools"),
        checkboxRow("rememberBlurIp", "👁", "generalRememberBlurIp", "Remember \"Blur IP addresses\" state"),
        checkboxRow("rememberShowUnfinishedTools", "🚧", "generalRememberShowUnfinishedTools", "Remember \"Show unfinished tools\" state"),

        groupHeading("generalGroupWindows", "Windows"),
        checkboxRow("rememberWindowState", "🖥️", "generalRememberWindowState", "Remember window state (windowed / maximized / fullscreen)"),
        checkboxRow("rememberDetachedWindows", "🗔", "generalRememberDetachedWindows", "Remember detached window layout"),
        checkboxRow("rememberOpenTabs", "📑", "generalRememberOpenTabs", "Remember open tabs (LS/RS/CS)"),

        groupHeading("generalGroupAssistant", "Assistant"),
        checkboxRow("rememberClippyEnabled", "📎", "generalRememberClippyEnabled", "Remember Clippy enabled state"),

        groupHeading("generalGroupAddons", "Addons"),
        checkboxRow("rememberExtensions", "📦", "generalRememberExtensions", "Remember installed addons"),
        "<div class=\"v1-import-manager-note\">" + escapeHtml(trOr("generalExtensionsCautionNote", "If unchecked, installed addons will need to be reinstalled every time the app starts.")) + "</div>",

        groupHeading("generalGroupHistory", "History"),
        checkboxRow("rememberRangeHistory", "🕓", "generalRememberRangeHistory", "Remember IP range history"),

        groupHeading("generalGroupAiAssistant", "AI Assistant"),
        aiAssistantRow(),
        "<div class=\"v1-scanner-actions v1-scanner-actions--spaced\">",
        "<button type=\"button\" data-general-action=\"ai-permissions\">🔐 " + escapeHtml(trOr("aiManagePermissionsBtn", "Manage AI Tools & Permissions...")) + "</button>",
        "</div>",

        groupHeading("generalGroupGoogleDorkApi", "Google Dork API"),
        googleDorkApiRow(),

        // Same "gate behind Show unfinished tools" treatment as
        // uiSwitchRow() above (data-attribute + hidden, so the existing
        // toggle-unfinished-tools handler's unfinishedSelectors list can
        // show/hide it live too, not just on next render) - Communicator
        // works end-to-end but its Firebase setup is real friction most
        // users won't want, see docs/COMMUNICATOR_SETUP.md.
        "<div data-general-firebase-config" + (unfinishedVisible ? "" : " hidden") + ">",
        groupHeading("generalGroupFirebaseConfig", "Firebase (Communicator)"),
        firebaseConfigRow(),
        "</div>",

        "</div>"
      ].join("");
    }

    function renderImportTool() {
      var tools = [];
      try {
        tools = extensionHost && extensionHost.listExtensions ? extensionHost.listExtensions() : [];
      } catch (_) {
        tools = [];
      }

      var listHtml = tools.length
        ? tools.map(function (item) {
            return "<div class=\"v1-import-item\"><strong>" + escapeHtml(item.id) + "</strong> <span>" + escapeHtml(tr("forAppVersionPrefix")) + " " + escapeHtml(item.version) + "</span><div>" + escapeHtml(item.name) + "</div>"
              + "<button type=\"button\" class=\"v1-import-item-uninstall\" data-import-uninstall-id=\"" + escapeHtml(item.id) + "\">" + escapeHtml(tr("importToolUninstallBtn")) + "</button></div>";
          }).join("")
        : "<div class=\"v1-import-empty\">" + escapeHtml(trOr("importToolEmptyText", importToolConfig.emptyText || "No imported tools yet.")) + "</div>";

      var subtitleText = trOr("importToolSubtitle", importToolConfig.subtitle || "Install addons from the GitHub catalog or load a manifest from a local file.");

      return [
        "<div class=\"v1-import-manager\">",
        "<div class=\"v1-import-manager-head\">",
        "<h4 style=\"margin:0 0 4px;\">" + tr("tipActionCustomization") + "</h4>",
        "<div class=\"v1-import-manager-note\">" + escapeHtml(subtitleText) + "</div>",
        "</div>",
        "<h4 style=\"margin:0 0 4px;\">" + escapeHtml(trOr("importToolCatalogHeading", importToolConfig.catalogHeading || "Www addons")) + "</h4>",
        "<div id=\"v1ImportCatalog\" data-import-role=\"catalog\" class=\"v1-import-output v1-catalog-list\">" + escapeHtml(trOr("importToolCatalogEmpty", importToolConfig.catalogEmpty || "Loading...")) + "</div>",
        "<div class=\"v1-import-manager-actions\">",
        "<button type=\"button\" data-import-action=\"load-file\">" + escapeHtml(trOr("importToolLoadFileBtn", importToolConfig.loadFileBtn || "Load from file...")) + "</button>",
        "</div>",
        "<h4 style=\"margin:12px 0 4px;\">" + escapeHtml(tr("extListHeader")) + "</h4>",
        "<div id=\"v1ImportOutput\" data-import-role=\"output\" class=\"v1-import-output\">" + listHtml + "</div>",
        "</div>"
      ].join("");
    }

    var SHELLCRAFT_FUNCTIONAL_BLOCKS = [
      { type: "if", icon: "🔀", labelKey: "shellcraftBlockIfLabel" },
      { type: "repeat-until", icon: "🔁", labelKey: "shellcraftBlockRepeatUntilLabel" },
      { type: "powershell", icon: "⌨", labelKey: "shellcraftBlockPowerShellLabel" },
      { type: "time-trigger", icon: "⏰", labelKey: "shellcraftBlockTimeTriggerLabel" },
    ];

    function shellcraftBlockRowHtml(attrs, icon, label) {
      return [
        "<div class=\"v1-lib-block-row\" draggable=\"true\" " + attrs + ">",
        "<span class=\"v1-lib-block-icon\" aria-hidden=\"true\">" + escapeHtml(icon) + "</span>",
        "<span class=\"v1-lib-block-name\">" + escapeHtml(label) + "</span>",
        "</div>"
      ].join("");
    }

    function renderShellCraftLibrary() {
      var functionalHtml = SHELLCRAFT_FUNCTIONAL_BLOCKS.map(function (block) {
        return shellcraftBlockRowHtml(
          "data-block-type=\"" + block.type + "\" data-block-category=\"functional\"",
          block.icon,
          tr(block.labelKey)
        );
      }).join("");

      var macros = macrosApi ? macrosApi.getMacros() : [];
      var macrosHtml = macros.map(function (macro) {
        return shellcraftBlockRowHtml(
          "data-block-type=\"macro\" data-macro-id=\"" + escapeHtml(macro.id) + "\"",
          macro.iconGlyph,
          tr(macro.nameKey)
        );
      }).join("");

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("shellcraftLibraryFunctionalHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">" + functionalHtml + "</div>",
        "</li>",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("shellcraftLibraryMacrosHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">" + macrosHtml + "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // "fields" (beyond the universal name/host/note every type already
    // has) is a list of {id, label, kind} describing that type's own extra
    // config values, stored in the node's fields{} bag (pulpit-canvas-runtime.js).
    // Adding a whole new device type later is just one more entry here plus
    // matching i18n keys - no other file needs to change.
    var PULPIT_TYPES = [
      { type: "remote", icon: "🌐", labelKey: "pulpitAddRemoteBtn", nameKey: "pulpitDefaultNameRemote", fields: [] },
      { type: "local", icon: "💻", labelKey: "pulpitAddLocalBtn", nameKey: "pulpitDefaultNameLocal", fields: [] },
      { type: "virtual", icon: "🗔", labelKey: "pulpitAddVirtualBtn", nameKey: "pulpitDefaultNameVirtual", fields: [] },
      { type: "own", icon: "⚙", labelKey: "pulpitAddOwnBtn", nameKey: "pulpitDefaultNameOwn", fields: [] },
      { type: "server", icon: "🖧", labelKey: "pulpitAddServerBtn", nameKey: "pulpitDefaultNameServer",
        fields: [{ id: "os", label: "pulpitFieldOsLabel" }, { id: "role", label: "pulpitFieldRoleLabel" }] },
      { type: "switch", icon: "🔀", labelKey: "pulpitAddSwitchBtn", nameKey: "pulpitDefaultNameSwitch",
        fields: [{ id: "ports", label: "pulpitFieldPortsLabel" }, { id: "vlan", label: "pulpitFieldVlanLabel" }] },
      { type: "printer", icon: "🖨", labelKey: "pulpitAddPrinterBtn", nameKey: "pulpitDefaultNamePrinter",
        fields: [{ id: "model", label: "pulpitFieldModelLabel" }, { id: "queue", label: "pulpitFieldQueueLabel" }] },
      { type: "router", icon: "📡", labelKey: "pulpitAddRouterBtn", nameKey: "pulpitDefaultNameRouter",
        fields: [{ id: "wan", label: "pulpitFieldWanLabel" }] },
      { type: "scanner", icon: "🔍", labelKey: "pulpitAddScannerBtn", nameKey: "pulpitDefaultNameScanner", fields: [], isTool: true },
      { type: "sniffer", icon: "🛰", labelKey: "pulpitAddSnifferBtn", nameKey: "pulpitDefaultNameSniffer", fields: [], isTool: true },
    ];

    function pulpitFieldsFor(type) {
      var entry = PULPIT_TYPES.filter(function (t) { return t.type === type; })[0];
      return entry ? entry.fields : [];
    }

    function pulpitIconFor(type) {
      var entry = PULPIT_TYPES.filter(function (t) { return t.type === type; })[0];
      return entry ? entry.icon : "⚙";
    }

    function pulpitTypeLabel(type) {
      return escapeHtml(tr("pulpitType" + type.charAt(0).toUpperCase() + type.slice(1) + "Label"));
    }

    // LS: plain click-to-add buttons (not draggable rows like ShellCraft's
    // library - adding a computer doesn't need a drag gesture, a button is
    // simpler and the pulpitCanvas API already takes an explicit x/y).
    function pulpitAddButtonHtml(entry) {
      return [
        "<button type=\"button\" class=\"v1-pulpit-add-btn\" data-pulpit-add-type=\"" + entry.type + "\">",
        "<span class=\"v1-pulpit-add-icon\" aria-hidden=\"true\">" + escapeHtml(entry.icon) + "</span>",
        "<span class=\"v1-pulpit-add-name\">" + escapeHtml(tr(entry.labelKey)) + "</span>",
        "</button>"
      ].join("");
    }

    function renderPulpitLibrary() {
      var deviceButtonsHtml = PULPIT_TYPES.filter(function (entry) { return !entry.isTool; }).map(pulpitAddButtonHtml).join("");
      var toolButtonsHtml = PULPIT_TYPES.filter(function (entry) { return entry.isTool; }).map(pulpitAddButtonHtml).join("");

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("pulpitLibraryHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-pulpit-auto-discover>" + escapeHtml(tr("pulpitAutoDiscoverBtn")) + "</button>",
        deviceButtonsHtml,
        "</div>",
        "</li>",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("pulpitLibraryToolsHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        toolButtonsHtml,
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // CS: one desktop-style icon (glyph on top, name label underneath) per
    // computer - draggable="true" for repositioning (reuses ShellCraft's
    // native-HTML5-DnD move mechanism, just not its "drag a new one in from
    // the library" mechanism, since adding is button-driven here).
    function renderPulpitNodeHtml(node) {
      var icon = pulpitIconFor(node.type);
      var label = node.name || tr("pulpitDefaultName" + node.type.charAt(0).toUpperCase() + node.type.slice(1));
      return [
        "<div class=\"v1-pulpit-node\" draggable=\"true\" data-node-id=\"" + escapeHtml(node.id) + "\" data-node-type=\"" + escapeHtml(node.type) + "\" style=\"left:" + node.x + "px;top:" + node.y + "px;\">",
        "<button type=\"button\" class=\"v1-pulpit-node-remove\" data-pulpit-node-remove=\"" + escapeHtml(node.id) + "\" aria-label=\"" + escapeHtml(tr("pulpitNodeDeleteBtn")) + "\" title=\"" + escapeHtml(tr("pulpitNodeDeleteBtn")) + "\">&times;</button>",
        "<span class=\"v1-pulpit-node-icon\" aria-hidden=\"true\">" + escapeHtml(icon) + "</span>",
        "<span class=\"v1-pulpit-node-label\">" + escapeHtml(label) + "</span>",
        "<span class=\"v1-pulpit-connector\" data-connector-for=\"" + escapeHtml(node.id) + "\" draggable=\"false\"></span>",
        "</div>"
      ].join("");
    }

    // Resolves which host/port pair is "live" right now based on the
    // node's hypervisor pick - null if no hypervisor is selected yet (VNC
    // has nothing to connect to in that state). Duplicated (not shared via
    // import) in pulpit-preview-runtime.js, same small-logic-duplication
    // convention this codebase already uses between independent runtime
    // modules elsewhere.
    function pulpitResolveVncTarget(node) {
      if (node.hypervisor === "qemu") return { host: node.vncQemuHost, port: node.vncQemuPort };
      if (node.hypervisor === "vb") return { host: node.vncVbHost, port: node.vncVbPort };
      return null;
    }

    // Desktop-only (see pulpit-preview-runtime.js's own comment) - a raw TCP
    // connection to the node's VNC server has to be opened by the native
    // Rust process, no browser (including the www/GitHub Pages build) can
    // do this itself. Rather than let a click silently fail there, the
    // button is replaced by an explanatory note.
    function renderPulpitPreviewToggle(node) {
      if (platformApi && typeof platformApi.isDesktop === "function" && !platformApi.isDesktop()) {
        return "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("pulpitPreviewDesktopOnlyNote")) + "</p>";
      }
      var target = pulpitResolveVncTarget(node);
      var ready = !!(target && target.host && target.port);
      var active = !!(pulpitPreviewApi && pulpitPreviewApi.isActive && pulpitPreviewApi.isActive(node.id));
      var labelKey = active ? "pulpitPreviewStopBtn" : "pulpitPreviewStartBtn";
      return [
        "<button type=\"button\" class=\"v1-pulpit-connect-btn" + (active ? " is-active" : "") + "\" data-pulpit-preview-toggle=\"" + escapeHtml(node.id) + "\"" + (ready ? "" : " disabled") + ">",
        escapeHtml(tr(labelKey)),
        "</button>",
        ready ? "" : "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("pulpitPreviewNeedsHostNote")) + "</p>"
      ].join("");
    }

    // Two independent checkboxes (RDP/VNC - a node can expose either, both,
    // or neither) then a divider then two radio-like checkboxes (QEMU/VB -
    // mutually exclusive, enforced by only ever rendering ONE of them
    // checked based on the single node.hypervisor string - see
    // panel-interactions-runtime.js's change handler for the other half of
    // that). Wired via [data-pulpit-conn-checkbox]/[data-pulpit-hypervisor-
    // checkbox], deliberately NOT data-inspector-field - the existing
    // delegated "input" listener only ever reads target.value, and
    // overloading it to also branch on .checked risks a regression in the
    // text-field path it already handles correctly.
    function renderPulpitConnCheckboxRow(node) {
      function checkboxItem(id, checked, labelKey, extraAttrs) {
        return [
          "<span class=\"v1-pulpit-checkbox-item\">",
          "<input id=\"" + id + "\" type=\"checkbox\"" + extraAttrs + (checked ? " checked" : "") + " />",
          "<label for=\"" + id + "\">" + escapeHtml(tr(labelKey)) + "</label>",
          "</span>"
        ].join("");
      }
      return [
        "<div class=\"v1-pulpit-checkbox-row\">",
        checkboxItem("v1InspectorPulpitConnRdp", node.connRdp, "pulpitConnRdpLabel", " data-pulpit-conn-checkbox=\"connRdp\""),
        checkboxItem("v1InspectorPulpitConnVnc", node.connVnc, "pulpitConnVncLabel", " data-pulpit-conn-checkbox=\"connVnc\""),
        "<span class=\"v1-pulpit-checkbox-divider\" aria-hidden=\"true\"></span>",
        checkboxItem("v1InspectorPulpitHypervisorQemu", node.hypervisor === "qemu", "pulpitHypervisorQemuLabel", " data-pulpit-hypervisor-checkbox data-hypervisor-value=\"qemu\""),
        checkboxItem("v1InspectorPulpitHypervisorVb", node.hypervisor === "vb", "pulpitHypervisorVbLabel", " data-pulpit-hypervisor-checkbox data-hypervisor-value=\"vb\""),
        "</div>"
      ].join("");
    }

    // A QEMU-managed VM and a VirtualBox-managed VM are physically
    // different machines (e.g. QEMU's own loopback-bound native VNC vs. a
    // VirtualBox VM's bridged LAN IP running a guest-side TigerVNC), so
    // which host/port pair is shown - and which one Podgląd actually
    // connects to, see pulpit-preview-runtime.js's resolveVncTarget() -
    // switches with the hypervisor checkbox instead of being one shared
    // field. No hypervisor picked yet -> a note instead of fields, since
    // there'd be no way to know which pair to show/edit.
    function renderPulpitVncFieldsForHypervisor(node) {
      if (!node.hypervisor) {
        return "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("pulpitVncNeedsHypervisorNote")) + "</p>";
      }
      var isQemu = node.hypervisor === "qemu";
      var hostField = isQemu ? "vncQemuHost" : "vncVbHost";
      var portField = isQemu ? "vncQemuPort" : "vncVbPort";
      var hostValue = isQemu ? node.vncQemuHost : node.vncVbHost;
      var portValue = isQemu ? node.vncQemuPort : node.vncVbPort;
      var hintKey = isQemu ? "pulpitHypervisorQemuVncHint" : "pulpitHypervisorVbVncHint";
      return [
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1InspectorPulpitVncHost\">" + escapeHtml(tr("pulpitInspectorHostLabel")) + " (" + escapeHtml(tr(isQemu ? "pulpitHypervisorQemuLabel" : "pulpitHypervisorVbLabel")) + ")</label>",
        "<input id=\"v1InspectorPulpitVncHost\" type=\"text\" data-inspector-field=\"" + hostField + "\" placeholder=\"127.0.0.1\" value=\"" + escapeHtml(hostValue) + "\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1InspectorPulpitVncPort\">" + escapeHtml(tr("pulpitInspectorVncPortLabel")) + "</label>",
        "<input id=\"v1InspectorPulpitVncPort\" type=\"text\" data-inspector-field=\"" + portField + "\" placeholder=\"5900\" value=\"" + escapeHtml(portValue) + "\" />",
        "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr(hintKey)) + "</p>",
        "</div>",
        renderPulpitPreviewToggle(node)
      ].join("");
    }

    // RS: name/host/note are editable (data-inspector-field, same delegated-
    // input-listener convention ShellCraft's inspector already uses); type is
    // fixed at creation, shown read-only.
    function renderPulpitInspector(nodeId) {
      var state = pulpitCanvasApi ? pulpitCanvasApi.getState() : { nodes: [] };
      var node = state.nodes.find(function (n) { return n.id === nodeId; });

      if (!node) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("pulpitInspectorEmptyNote")) + "</div>";
      }

      return [
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label>" + escapeHtml(tr("pulpitInspectorTypeLabel")) + "</label>",
        "<div>" + pulpitTypeLabel(node.type) + "</div>",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1InspectorPulpitName\">" + escapeHtml(tr("pulpitInspectorNameLabel")) + "</label>",
        "<input id=\"v1InspectorPulpitName\" type=\"text\" data-inspector-field=\"name\" value=\"" + escapeHtml(node.name) + "\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1InspectorPulpitHost\">" + escapeHtml(tr("pulpitInspectorHostLabel")) + "</label>",
        "<input id=\"v1InspectorPulpitHost\" type=\"text\" data-inspector-field=\"host\" value=\"" + escapeHtml(node.host) + "\" />",
        "</div>",
        renderPulpitConnCheckboxRow(node),
        node.connRdp
          ? "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-pulpit-rdp-connect=\"" + escapeHtml(node.id) + "\">" + escapeHtml(tr("pulpitRdpConnectBtn")) + "</button>"
          : "",
        node.connVnc ? renderPulpitVncFieldsForHypervisor(node) : "",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1InspectorPulpitNote\">" + escapeHtml(tr("pulpitInspectorNoteLabel")) + "</label>",
        "<textarea id=\"v1InspectorPulpitNote\" rows=\"3\" data-inspector-field=\"note\">" + escapeHtml(node.note) + "</textarea>",
        "</div>",
        pulpitFieldsFor(node.type).map(function (f) {
          var fieldId = "v1InspectorPulpitField_" + f.id;
          var value = (node.fields && node.fields[f.id]) || "";
          return [
            "<div class=\"v1-pulpit-inspector-field\">",
            "<label for=\"" + fieldId + "\">" + escapeHtml(tr(f.label)) + "</label>",
            "<input id=\"" + fieldId + "\" type=\"text\" data-inspector-field=\"fields." + f.id + "\" value=\"" + escapeHtml(value) + "\" />",
            "</div>"
          ].join("");
        }).join(""),
        renderPulpitRemoteRunSection(node)
      ].join("");
    }

    // A deliberately separate form from the fields above: none of these
    // inputs use data-inspector-field, so wirePulpitInspector's delegated
    // "input" listener (which persists every keystroke into the canvas's
    // own localStorage-backed state via updateNodeProperties) never sees
    // them - a remote-install password must never end up in
    // netrecon_pulpit_canvas_v1. Wrapped in <form> (submit handled in JS,
    // never actually navigates) and the password field gets an explicit
    // autocomplete/name/id set so Chromium doesn't warn about an
    // unidentified or formless password field.
    function renderPulpitRemoteRunSection(node) {
      return [
        "<div class=\"v1-pulpit-remote-run\">",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("pulpitRemoteRunHeading")) + "</strong></div>",
        "<form data-pulpit-remote-run-form=\"" + escapeHtml(node.id) + "\">",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1PulpitRemoteHost\">" + escapeHtml(tr("pulpitRemoteHostLabel")) + "</label>",
        "<input id=\"v1PulpitRemoteHost\" name=\"pulpitRemoteHost\" type=\"text\" autocomplete=\"off\" data-remote-run-field=\"computerName\" value=\"" + escapeHtml(node.host) + "\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1PulpitRemoteUsername\">" + escapeHtml(tr("pulpitRemoteUsernameLabel")) + "</label>",
        "<input id=\"v1PulpitRemoteUsername\" name=\"pulpitRemoteUsername\" type=\"text\" autocomplete=\"off\" data-remote-run-field=\"username\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1PulpitRemotePassword\">" + escapeHtml(tr("pulpitRemotePasswordLabel")) + "</label>",
        "<input id=\"v1PulpitRemotePassword\" name=\"pulpitRemotePassword\" type=\"password\" autocomplete=\"off\" data-remote-run-field=\"password\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1PulpitRemoteInstallerUrl\">" + escapeHtml(tr("pulpitRemoteInstallerUrlLabel")) + "</label>",
        "<input id=\"v1PulpitRemoteInstallerUrl\" name=\"pulpitRemoteInstallerUrl\" type=\"text\" autocomplete=\"off\" data-remote-run-field=\"installerUrl\" />",
        "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("pulpitRemoteInstallerUrlHint")) + "</p>",
        "<div class=\"v1-pulpit-remote-quickpicks\">",
        "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-pulpit-remote-quickpick=\"qemu\">" + escapeHtml(tr("pulpitRemoteQemuQuickpickBtn")) + "</button>",
        "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-pulpit-remote-quickpick=\"virtualbox\">" + escapeHtml(tr("pulpitRemoteVirtualboxQuickpickBtn")) + "</button>",
        "</div>",
        "</div>",
        "<button type=\"submit\" class=\"v1-pulpit-remote-run-submit\" data-pulpit-remote-run-submit=\"" + escapeHtml(node.id) + "\">" + escapeHtml(tr("pulpitRemoteRunSubmitBtn")) + "</button>",
        "</form>",
        "<div class=\"v1-pulpit-remote-run-result\" data-pulpit-remote-run-result=\"" + escapeHtml(node.id) + "\" hidden></div>",
        "</div>"
      ].join("");
    }

    // CS: the currently-focused live preview session, big. The actual live
    // surface (noVNC's canvas) is mounted into [data-pulpit-preview-mount]
    // AFTER this string is assigned via innerHTML - by
    // wirePulpitPreviewTool (panel-interactions-runtime.js), via
    // pulpitPreviewApi.mountSurface(), never by this function itself: a
    // live RFB session lives in a real DOM node that must survive being
    // moved around, not be re-created from an HTML string on every render.
    function renderPulpitPreviewTool() {
      // Always wrapped in this one stable element (regardless of which of
      // the 3 states below applies) so wirePulpitPreviewTool
      // (panel-interactions-runtime.js) has something constant to capture
      // at wire-time and check document.body.contains(...) against on
      // later re-renders - the same staleness-guard shape wirePulpitCanvas
      // already uses for its own canvasEl, needed here because this tool's
      // CS content gets fully replaced whenever the user switches to a
      // different tab.
      var inner;
      if (platformApi && typeof platformApi.isDesktop === "function" && !platformApi.isDesktop()) {
        inner = "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("pulpitPreviewDesktopOnlyNote")) + "</div>";
      } else {
        var focusedId = pulpitPreviewApi ? pulpitPreviewApi.getFocusedNodeId() : "";
        inner = focusedId
          ? "<div class=\"v1-pulpit-preview-big\" data-pulpit-preview-mount=\"" + escapeHtml(focusedId) + "\"></div>"
          : "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("pulpitPreviewEmptyNote")) + "</div>";
      }
      return "<div class=\"v1-pulpit-preview-shell\">" + inner + "</div>";
    }

    // RS: every currently-watched node as a small live thumbnail ("like
    // cameras") - same mount-after-render discipline as
    // renderPulpitPreviewTool above.
    function renderPulpitPreviewList() {
      if (platformApi && typeof platformApi.isDesktop === "function" && !platformApi.isDesktop()) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("pulpitPreviewDesktopOnlyNote")) + "</div>";
      }
      var activeIds = pulpitPreviewApi ? pulpitPreviewApi.getActiveNodeIds() : [];
      if (!activeIds.length) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("pulpitPreviewNoneActiveNote")) + "</div>";
      }
      var state = pulpitCanvasApi ? pulpitCanvasApi.getState() : { nodes: [] };
      var nodeById = {};
      state.nodes.forEach(function (n) { nodeById[n.id] = n; });
      var focusedId = pulpitPreviewApi.getFocusedNodeId();

      var thumbsHtml = activeIds.map(function (nodeId) {
        var node = nodeById[nodeId];
        var label = node ? (node.name || node.host || nodeId) : nodeId;
        return [
          "<div class=\"v1-pulpit-preview-thumb" + (nodeId === focusedId ? " is-focused" : "") + "\" data-pulpit-preview-thumb=\"" + escapeHtml(nodeId) + "\">",
          "<div class=\"v1-pulpit-preview-thumb-mount\" data-pulpit-preview-mount=\"" + escapeHtml(nodeId) + "\"></div>",
          "<div class=\"v1-pulpit-preview-thumb-label\">" + escapeHtml(label) + "</div>",
          "<button type=\"button\" class=\"v1-pulpit-preview-thumb-stop\" data-pulpit-preview-stop=\"" + escapeHtml(nodeId) + "\" aria-label=\"" + escapeHtml(tr("pulpitPreviewStopBtn")) + "\" title=\"" + escapeHtml(tr("pulpitPreviewStopBtn")) + "\">&times;</button>",
          "</div>"
        ].join("");
      }).join("");

      return "<div class=\"v1-pulpit-preview-grid\">" + thumbsHtml + "</div>";
    }

    // shell: Globe - CS-only tab (no LS/RS), a lazy-loaded globe.gl WebGL
    // canvas plotting scanned hosts by geolocation. Just an empty, sized
    // shell here - wireGlobeTool (panel-interactions-runtime.js) fills it
    // in async (loading/empty states, then the actual globe) since building
    // it requires the vendored engine to finish loading and geo_lookup
    // calls to resolve, neither of which this synchronous renderer can do.
    function renderGlobeTool() {
      return "<div class=\"v1-globe-shell\" id=\"v1GlobeContainer\"></div>";
    }

    // Mail XSS Tester: desktop-only for the same reason Topology's VNC
    // preview is - the beacon HTTP listener and the cloudflared tunnel
    // process both need Rust's raw TCP/process-spawn capability, neither
    // of which any browser (including the www/GitHub Pages build) has.
    function mailXssTesterIsDesktop() {
      return !(platformApi && typeof platformApi.isDesktop === "function" && !platformApi.isDesktop());
    }

    // LS: payload picker, tunnel start/stop + status, and the send form -
    // password/app-password fields are read directly at submit time by
    // wireMailXssTesterLibrary (panel-interactions-runtime.js), never
    // routed through any persisted-state mechanism, same one-time
    // credential discipline as the remote-install password field.
    function renderMailXssTesterLibrary() {
      // Full form stays visible on www (payload picker, fields, buttons) -
      // only the two actions that need the desktop backend (tunnel,
      // send) are disabled with an inline hint, instead of blocking the
      // whole panel behind a single "desktop only" note.
      var isDesktopMode = mailXssTesterIsDesktop();

      var payloads = mailXssTesterApi ? mailXssTesterApi.getPayloads() : [];
      var selected = mailXssTesterApi ? mailXssTesterApi.getSelectedPayloadIds() : [];
      var status = mailXssTesterApi ? mailXssTesterApi.getTunnelStatus() : "idle";
      var url = mailXssTesterApi ? mailXssTesterApi.getTunnelUrl() : "";
      var error = mailXssTesterApi ? mailXssTesterApi.getTunnelError() : "";

      var payloadsHtml = payloads.map(function (p) {
        var checked = selected.indexOf(p.id) !== -1;
        var fieldId = "v1MailXssPayload_" + p.id;
        return [
          "<span class=\"v1-pulpit-checkbox-item\">",
          "<input id=\"" + fieldId + "\" type=\"checkbox\" data-mail-xss-payload-checkbox=\"" + escapeHtml(p.id) + "\"" + (checked ? " checked" : "") + " />",
          "<label for=\"" + fieldId + "\">" + escapeHtml(tr(p.labelKey)) + "</label>",
          "</span>"
        ].join("");
      }).join("");

      var tunnelStatusHtml = "";
      if (status === "running") {
        tunnelStatusHtml = "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("mailXssTunnelRunningNote")) + " " + escapeHtml(url) + "</p>";
      } else if (status === "starting") {
        tunnelStatusHtml = "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("mailXssTunnelStartingNote")) + "</p>";
      } else if (status === "error") {
        tunnelStatusHtml = [
          "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("mailXssTunnelErrorPrefix")) + " " + escapeHtml(error) + "</p>",
          "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-mail-xss-cloudflared-download>" + escapeHtml(tr("mailXssDownloadCloudflaredBtn")) + "</button>"
        ].join("");
      }

      var sendDisabled = status !== "running" || !isDesktopMode;
      var tunnelBtnDisabled = status === "starting" || !isDesktopMode;
      var desktopOnlyHintHtml = isDesktopMode ? "" : "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("mailXssDesktopOnlyNote")) + "</p>";

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("mailXssPayloadsHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body v1-pulpit-checkbox-row\">",
        payloadsHtml,
        "</div>",
        "</li>",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("mailXssTunnelHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        desktopOnlyHintHtml,
        "<button type=\"button\" class=\"v1-pulpit-connect-btn" + (status === "running" ? " is-active" : "") + "\" data-mail-xss-tunnel-toggle" + (tunnelBtnDisabled ? " disabled" : "") + ">" + escapeHtml(tr(status === "running" ? "mailXssStopTunnelBtn" : "mailXssStartTunnelBtn")) + "</button>",
        tunnelStatusHtml,
        "</div>",
        "</li>",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("mailXssSendHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        "<form data-mail-xss-send-form>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1MailXssGmailAddress\">" + escapeHtml(tr("mailXssGmailAddressLabel")) + "</label>",
        "<input id=\"v1MailXssGmailAddress\" name=\"mailXssGmailAddress\" type=\"email\" autocomplete=\"off\" data-mail-xss-field=\"gmailAddress\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1MailXssAppPassword\">" + escapeHtml(tr("mailXssAppPasswordLabel")) + "</label>",
        "<input id=\"v1MailXssAppPassword\" name=\"mailXssAppPassword\" type=\"password\" autocomplete=\"off\" data-mail-xss-field=\"appPassword\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1MailXssTo\">" + escapeHtml(tr("mailXssToLabel")) + "</label>",
        "<input id=\"v1MailXssTo\" name=\"mailXssTo\" type=\"email\" autocomplete=\"off\" data-mail-xss-field=\"to\" />",
        "</div>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label for=\"v1MailXssSubject\">" + escapeHtml(tr("mailXssSubjectLabel")) + "</label>",
        "<input id=\"v1MailXssSubject\" name=\"mailXssSubject\" type=\"text\" autocomplete=\"off\" data-mail-xss-field=\"subject\" value=\"" + escapeHtml(tr("mailXssDefaultSubject")) + "\" />",
        "</div>",
        "<button type=\"submit\" class=\"v1-pulpit-remote-run-submit\" data-mail-xss-send-submit" + (sendDisabled ? " disabled" : "") + ">" + escapeHtml(tr("mailXssSendBtn")) + "</button>",
        "</form>",
        "<div class=\"v1-pulpit-remote-run-result\" data-mail-xss-send-result hidden></div>",
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // CS: a live results summary - one row per selected payload, updating
    // as newui:mail-xss-tester-changed fires (wireMailXssTesterResults
    // re-renders this same shell, see panel-interactions-runtime.js).
    function renderMailXssTesterTool() {
      var payloads = mailXssTesterApi ? mailXssTesterApi.getPayloads() : [];
      var selected = mailXssTesterApi ? mailXssTesterApi.getSelectedPayloadIds() : [];
      var triggered = mailXssTesterApi ? mailXssTesterApi.getTriggeredPayloadIds() : [];
      var status = mailXssTesterApi ? mailXssTesterApi.getTunnelStatus() : "idle";

      var rowsHtml = payloads.filter(function (p) {
        return selected.indexOf(p.id) !== -1;
      }).map(function (p) {
        var isTriggered = triggered.indexOf(p.id) !== -1;
        return [
          "<tr>",
          "<td>" + escapeHtml(tr(p.labelKey)) + "</td>",
          "<td class=\"" + (isTriggered ? "v1-mail-xss-triggered-yes" : "v1-mail-xss-triggered-no") + "\">" + escapeHtml(tr(isTriggered ? "mailXssTriggeredYes" : "mailXssTriggeredNo")) + "</td>",
          "</tr>"
        ].join("");
      }).join("");

      var inner = [
        !mailXssTesterIsDesktop() ? "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("mailXssDesktopOnlyNote")) + "</p>" : "",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label>" + escapeHtml(tr("mailXssTunnelHeading")) + "</label>",
        "<div>" + escapeHtml(tr("mailXssStatus_" + status)) + "</div>",
        "</div>",
        rowsHtml
          ? "<table class=\"v1-mail-xss-results-table\"><thead><tr><th>" + escapeHtml(tr("mailXssPayloadColumnLabel")) + "</th><th>" + escapeHtml(tr("mailXssTriggeredColumnLabel")) + "</th></tr></thead><tbody>" + rowsHtml + "</tbody></table>"
          : "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("mailXssNoPayloadsSelectedNote")) + "</div>"
      ].join("");
      return "<div class=\"v1-mail-xss-tester-shell\">" + inner + "</div>";
    }

    // RS: raw hit log, newest first - the User-Agent header is useful for
    // identifying which mail client/proxy actually fetched the beacon
    // (e.g. Gmail's own image-proxy UA vs. a desktop client's).
    function renderMailXssTesterResults() {
      var hits = mailXssTesterApi ? mailXssTesterApi.getHits() : [];
      if (!hits.length) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("mailXssNoHitsYetNote")) + "</div>";
      }

      var rowsHtml = hits.slice().reverse().map(function (h) {
        var time = h.timestamp_ms ? new Date(h.timestamp_ms).toLocaleTimeString() : "";
        return [
          "<div class=\"v1-mail-xss-hit-row\">",
          "<div class=\"v1-mail-xss-hit-time\">" + escapeHtml(time) + "</div>",
          "<div class=\"v1-mail-xss-hit-payload\">" + escapeHtml(h.payload_id || "") + "</div>",
          "<div class=\"v1-mail-xss-hit-ua\">" + escapeHtml(h.user_agent || "") + "</div>",
          "<div class=\"v1-mail-xss-hit-addr\">" + escapeHtml(h.remote_addr || "") + "</div>",
          "</div>"
        ].join("");
      }).join("");

      return "<div class=\"v1-mail-xss-hit-log\">" + rowsHtml + "</div>";
    }

    // LS: the 9 builder fields (site:/filetype:/inurl:/intitle:/intext:,
    // exact phrase, +include/-exclude, and a verbatim "raw" field for
    // anything else) - no action button here, "Open in Google" lives on
    // CS next to the live query preview it actually opens.
    function renderGoogleDorkLibrary() {
      var fields = googleDorkApi ? googleDorkApi.getFields() : {};

      function fieldRow(key, labelKey) {
        var id = "v1GoogleDorkField_" + key;
        return [
          "<div class=\"v1-pulpit-inspector-field\">",
          "<label for=\"" + id + "\">" + escapeHtml(tr(labelKey)) + "</label>",
          "<input id=\"" + id + "\" type=\"text\" autocomplete=\"off\" data-google-dork-field=\"" + key + "\" value=\"" + escapeHtml(fields[key] || "") + "\" />",
          "</div>"
        ].join("");
      }

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("googleDorkFieldsHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        fieldRow("target", "googleDorkTargetLabel"),
        fieldRow("filetype", "googleDorkFiletypeLabel"),
        fieldRow("inurl", "googleDorkInurlLabel"),
        fieldRow("intitle", "googleDorkIntitleLabel"),
        fieldRow("intext", "googleDorkIntextLabel"),
        fieldRow("phrase", "googleDorkPhraseLabel"),
        fieldRow("include", "googleDorkIncludeLabel"),
        fieldRow("exclude", "googleDorkExcludeLabel"),
        fieldRow("raw", "googleDorkRawLabel"),
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // CS: live composed query, "Open in Google" (disabled while the query
    // is blank), and the saved query history - use/delete buttons mirror
    // scanner-sidebar-runtime.js's renderRangeHistory() convention (the
    // closest existing analog: a capped, dedupe-on-add local history).
    function renderGoogleDorkTool() {
      var query = googleDorkApi ? googleDorkApi.getComposedQuery() : "";
      var history = googleDorkApi ? googleDorkApi.getHistory() : [];

      var historyHtml = history.length
        ? history.map(function (item, idx) {
            return [
              "<div class=\"v1-google-dork-history-row\">",
              "<span class=\"v1-google-dork-history-text\" title=\"" + escapeHtml(item.query) + "\">" + escapeHtml(item.query) + "</span>",
              "<span class=\"v1-google-dork-history-actions\">",
              "<button type=\"button\" class=\"v1-range-history-btn\" data-google-dork-history-action=\"use\" data-google-dork-history-index=\"" + idx + "\" title=\"" + escapeHtml(tr("googleDorkHistoryUseAria")) + "\" aria-label=\"" + escapeHtml(tr("googleDorkHistoryUseAria")) + "\">&gt;</button>",
              "<button type=\"button\" class=\"v1-range-history-btn\" data-google-dork-history-action=\"delete\" data-google-dork-history-index=\"" + idx + "\" title=\"" + escapeHtml(tr("googleDorkHistoryDeleteAria")) + "\" aria-label=\"" + escapeHtml(tr("googleDorkHistoryDeleteAria")) + "\">&times;</button>",
              "</span>",
              "</div>"
            ].join("");
          }).join("")
        : "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("googleDorkHistoryEmptyNote")) + "</div>";

      return [
        "<div class=\"v1-google-dork-shell\">",
        "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("googleDorkResponsibleUseNote")) + "</p>",
        "<div class=\"v1-pulpit-inspector-field\">",
        "<label>" + escapeHtml(tr("googleDorkQueryHeading")) + "</label>",
        query
          ? "<div class=\"v1-google-dork-query-preview\">" + escapeHtml(query) + "</div>"
          : "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("googleDorkQueryEmptyNote")) + "</div>",
        "</div>",
        "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-google-dork-open-btn" + (query ? "" : " disabled") + ">" + escapeHtml(tr("googleDorkOpenBtn")) + "</button>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("googleDorkHistoryHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        historyHtml,
        "</div>"
      ].join("");
    }

    // RS: categorized preset templates - clicking one fills LS's fields
    // (target/include/exclude untouched, see google-dork-runtime.js's
    // applyTemplate()) rather than opening anything directly.
    function renderGoogleDorkTemplates() {
      var templates = googleDorkApi ? googleDorkApi.getTemplates() : [];
      var categories = googleDorkApi ? googleDorkApi.getTemplateCategories() : [];
      var byCategory = {};
      templates.forEach(function (item) {
        if (!byCategory[item.category]) byCategory[item.category] = [];
        byCategory[item.category].push(item);
      });

      var sectionsHtml = categories.map(function (cat) {
        var items = byCategory[cat] || [];
        if (!items.length) return "";
        var itemsHtml = items.map(function (item) {
          return "<button type=\"button\" class=\"v1-google-dork-template-btn\" data-google-dork-template=\"" + escapeHtml(item.id) + "\">" + escapeHtml(tr(item.labelKey)) + "</button>";
        }).join("");
        return [
          "<li>",
          "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("googleDorkCategory_" + cat)) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
          "<div class=\"v1-section-body v1-google-dork-template-list\">",
          itemsHtml,
          "</div>",
          "</li>"
        ].join("");
      }).join("");

      return "<ul class=\"v1-tool-list\">" + sectionsHtml + "</ul>";
    }

    // Komunikator LS/CS share the same status block (sign-in button /
    // signing-in note / signed-in user card / error), just laid out inside
    // a different wrapper per surface - kept as one shared helper so the
    // two never drift out of sync.
    function komunikatorStatusBlockHtml() {
      var status = komunikatorApi ? komunikatorApi.getStatus() : "signed-out";
      var user = komunikatorApi ? komunikatorApi.getUser() : null;
      var error = komunikatorApi ? komunikatorApi.getError() : "";

      if (status === "signed-in" && user) {
        return [
          "<div class=\"v1-komunikator-user-card\">",
          user.photoURL ? "<img class=\"v1-komunikator-avatar\" src=\"" + escapeHtml(user.photoURL) + "\" alt=\"\" />" : "",
          "<div>",
          "<div>" + escapeHtml(tr("komunikatorSignedInAs")) + " <strong>" + escapeHtml(user.displayName || user.email || "") + "</strong></div>",
          user.email ? "<div class=\"v1-pulpit-remote-hint\">" + escapeHtml(user.email) + "</div>" : "",
          "</div>",
          "</div>",
          "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-komunikator-signout-btn>" + escapeHtml(tr("komunikatorSignOutBtn")) + "</button>"
        ].join("");
      }

      var noteHtml = "";
      if (status === "signing-in") {
        noteHtml = "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("komunikatorSigningInNote")) + "</p>";
      } else if (status === "error") {
        var errText = error === "missing_config" ? tr("komunikatorErrorMissingConfig") : (tr("komunikatorErrorPrefix") + " " + error);
        noteHtml = "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(errText) + "</p>";
      }

      return [
        "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-komunikator-signin-btn" + (status === "signing-in" ? " disabled" : "") + ">" + escapeHtml(tr("komunikatorSignInBtn")) + "</button>",
        noteHtml
      ].join("");
    }

    // LS: account status + sign-in/out action.
    function renderKomunikatorLibrary() {
      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("komunikatorAccountHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        komunikatorStatusBlockHtml(),
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // CS: signed-out/error -> the status block (Phase 1 behavior, unchanged);
    // signed-in but not a room member yet -> a short pointer to RS's "join
    // with code" form; signed-in member -> the actual chat, reusing the AI
    // Assistant's own message-list/bubble/input CSS directly (.v1-ai-chat,
    // .v1-ai-msg, .v1-ai-input, .v1-ai-prompt) rather than duplicating that
    // styling - same reuse convention already used repeatedly this session.
    function renderKomunikatorChatMessageHtml(msg, currentUid) {
      var isOwn = msg.senderUid === currentUid;
      return [
        "<div class=\"v1-ai-msg " + (isOwn ? "user" : "assistant") + "\">",
        isOwn ? "" : "<div class=\"v1-komunikator-msg-sender\">" + escapeHtml(msg.senderName || "") + "</div>",
        "<span class=\"v1-ai-msg-text\">" + escapeHtml(msg.text || "") + "</span>",
        "</div>"
      ].join("");
    }

    function renderKomunikatorTool() {
      var status = komunikatorApi ? komunikatorApi.getStatus() : "signed-out";
      var user = komunikatorApi ? komunikatorApi.getUser() : null;
      var isMember = komunikatorApi ? komunikatorApi.getIsMember() : false;

      var inner;
      if (status !== "signed-in" || !user) {
        inner = [
          "<div class=\"v1-pulpit-inspector-field\">",
          "<label>" + escapeHtml(tr("komunikatorStatusHeading")) + "</label>",
          "<div>" + komunikatorStatusBlockHtml() + "</div>",
          "</div>"
        ].join("");
      } else if (!isMember) {
        inner = "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("komunikatorNotAMemberNote")) + "</div>";
      } else {
        var messages = komunikatorApi ? komunikatorApi.getMessages() : [];
        var actionError = komunikatorApi ? komunikatorApi.getActionError() : "";
        var messagesHtml = messages.length
          ? messages.map(function (m) { return renderKomunikatorChatMessageHtml(m, user.uid); }).join("")
          : "<div class=\"v1-ai-msg assistant\" data-ai-meta=\"true\"><span class=\"v1-ai-msg-text\">" + escapeHtml(tr("komunikatorNoMessagesYetNote")) + "</span></div>";

        inner = [
          "<div class=\"v1-komunikator-chat-shell\">",
          "<div class=\"v1-ai-chat-wrap\">",
          "<div class=\"v1-ai-chat\" data-komunikator-messages>",
          messagesHtml,
          "</div>",
          "</div>",
          actionError ? "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("komunikatorErrorPrefix")) + " " + escapeHtml(actionError) + "</p>" : "",
          "<div class=\"v1-ai-input\">",
          "<textarea class=\"v1-ai-prompt\" rows=\"2\" data-komunikator-message-input placeholder=\"" + escapeHtml(tr("komunikatorMessagePlaceholder")) + "\"></textarea>",
          "</div>",
          "</div>"
        ].join("");
      }

      return "<div class=\"v1-komunikator-shell\">" + inner + "</div>";
    }

    // RS: member list + invite generate/redeem - the two entry points into
    // room membership. Both actions stay visible regardless of current
    // membership (generate is member-only functionally, but the button is
    // simplest kept always-rendered and just no-ops if not a member; join
    // is the only way a non-member can act at all).
    function renderKomunikatorMembers() {
      var user = komunikatorApi ? komunikatorApi.getUser() : null;
      if (!user) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("komunikatorMembersSignInFirstNote")) + "</div>";
      }

      var isMember = komunikatorApi ? komunikatorApi.getIsMember() : false;
      var members = komunikatorApi ? komunikatorApi.getMembers() : [];
      var lastInviteCode = komunikatorApi ? komunikatorApi.getLastInviteCode() : "";
      var actionError = komunikatorApi ? komunikatorApi.getActionError() : "";

      var membersHtml = members.length
        ? members.map(function (m) {
            return [
              "<div class=\"v1-komunikator-member-row\">",
              m.photoURL ? "<img class=\"v1-komunikator-avatar v1-komunikator-avatar--small\" src=\"" + escapeHtml(m.photoURL) + "\" alt=\"\" />" : "",
              "<span>" + escapeHtml(m.displayName || m.email || "?") + "</span>",
              "</div>"
            ].join("");
          }).join("")
        : "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("komunikatorNoMembersYetNote")) + "</div>";

      var inviteSectionHtml = isMember
        ? [
            "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-komunikator-generate-invite-btn>" + escapeHtml(tr("komunikatorGenerateInviteBtn")) + "</button>",
            lastInviteCode ? "<div class=\"v1-komunikator-invite-code-box\" data-komunikator-invite-code>" + escapeHtml(lastInviteCode) + "</div>" : "",
            lastInviteCode ? "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("komunikatorInviteCodeNote")) + "</p>" : ""
          ].join("")
        : [
            "<div class=\"v1-pulpit-inspector-field\">",
            "<label for=\"v1KomunikatorJoinCode\">" + escapeHtml(tr("komunikatorJoinCodeLabel")) + "</label>",
            "<input id=\"v1KomunikatorJoinCode\" type=\"text\" autocomplete=\"off\" data-komunikator-join-code-input />",
            "</div>",
            "<button type=\"button\" class=\"v1-pulpit-connect-btn\" data-komunikator-join-btn>" + escapeHtml(tr("komunikatorJoinBtn")) + "</button>"
          ].join("");

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("komunikatorMembersHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        membersHtml,
        "</div>",
        "</li>",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("komunikatorInviteHeading")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        inviteSectionHtml,
        actionError ? "<p class=\"v1-pulpit-remote-hint\">" + escapeHtml(tr("komunikatorErrorPrefix")) + " " + escapeHtml(actionError) + "</p>" : "",
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // Anchors a connection line on roughly the icon glyph's center, not the
    // node div's raw top-left (x,y) - node.x/node.y are the div's top-left
    // per renderPulpitNodeHtml's own "left:Xpx;top:Ypx" - and not the whole
    // 84px-wide/icon+label block's center either, since the label sits
    // below the icon (see pulpit.css); this offset is a visual
    // approximation, not tied to any exact CSS value.
    function pulpitEdgeAnchor(node) {
      return { x: node.x + 42, y: node.y + 22 };
    }

    // A tap targeting an existing edge (rather than a device) has no single
    // node to anchor to - it points at the wire itself, so it's drawn to
    // the midpoint between that edge's own two endpoints.
    function pulpitEdgeMidpoint(edge, nodeById) {
      var a = nodeById[edge.fromId];
      var b = nodeById[edge.toId];
      if (!a || !b) return null;
      var pa = pulpitEdgeAnchor(a);
      var pb = pulpitEdgeAnchor(b);
      return { x: (pa.x + pb.x) / 2, y: (pa.y + pb.y) / 2 };
    }

    // Rendered as an SVG sibling BEHIND the node divs (see pulpit.css's
    // z-index) inside the same 3000x2000 canvas coordinate space, so line
    // endpoints can be computed directly from each node's x/y - no separate
    // coordinate system to keep in sync. Each edge gets two overlapping
    // lines: a thin visible one and a fatter, normally-invisible "hit" line
    // (wider stroke, transparent) so clicking near-but-not-exactly on the
    // thin line still registers - mirrors the reference mockup's
    // click-to-remove affordance on a thin line.
    function renderPulpitLinksSvg(state) {
      var nodeById = {};
      state.nodes.forEach(function (n) { nodeById[n.id] = n; });
      var edgeById = {};
      state.edges.forEach(function (e) { edgeById[e.id] = e; });

      var lines = state.edges.map(function (edge) {
        var a = nodeById[edge.fromId];
        var b = nodeById[edge.toId];
        if (!a || !b) return "";
        var pa = pulpitEdgeAnchor(a);
        var pb = pulpitEdgeAnchor(b);
        // Hit-line first, visible line second - lets the CSS ":hover + "
        // adjacent-sibling highlight work (only selects a FOLLOWING
        // sibling), so hovering the generous invisible hit target
        // highlights the thin visible line drawn on top of it.
        return [
          "<g data-edge-id=\"" + escapeHtml(edge.id) + "\">",
          "<line class=\"v1-pulpit-link-hit\" data-pulpit-edge-remove=\"" + escapeHtml(edge.id) + "\" x1=\"" + pa.x + "\" y1=\"" + pa.y + "\" x2=\"" + pb.x + "\" y2=\"" + pb.y + "\"></line>",
          "<line class=\"v1-pulpit-link\" x1=\"" + pa.x + "\" y1=\"" + pa.y + "\" x2=\"" + pb.x + "\" y2=\"" + pb.y + "\"></line>",
          "</g>"
        ].join("");
      }).join("");

      // Scanner/sniffer taps: same hit-line + visible-line pattern, but the
      // second endpoint is either another node's anchor (targetKind "node")
      // or an existing edge's midpoint (targetKind "edge") - and the
      // visible line is colored per tool type instead of the dim default,
      // since a tap represents monitoring activity, not a documented
      // physical connection.
      var tapLines = state.taps.map(function (tap) {
        var toolNode = nodeById[tap.toolNodeId];
        if (!toolNode) return "";
        var pa = pulpitEdgeAnchor(toolNode);
        var pb = null;
        if (tap.targetKind === "node") {
          var targetNode = nodeById[tap.targetId];
          pb = targetNode ? pulpitEdgeAnchor(targetNode) : null;
        } else {
          var targetEdge = edgeById[tap.targetId];
          pb = targetEdge ? pulpitEdgeMidpoint(targetEdge, nodeById) : null;
        }
        if (!pb) return "";
        return [
          "<g data-tap-id=\"" + escapeHtml(tap.id) + "\">",
          "<line class=\"v1-pulpit-link-hit\" data-pulpit-tap-remove=\"" + escapeHtml(tap.id) + "\" x1=\"" + pa.x + "\" y1=\"" + pa.y + "\" x2=\"" + pb.x + "\" y2=\"" + pb.y + "\"></line>",
          "<line class=\"v1-pulpit-tap-link v1-pulpit-tap-link--" + escapeHtml(toolNode.type) + "\" x1=\"" + pa.x + "\" y1=\"" + pa.y + "\" x2=\"" + pb.x + "\" y2=\"" + pb.y + "\"></line>",
          "</g>"
        ].join("");
      }).join("");

      return "<svg class=\"v1-pulpit-links\">" + lines + tapLines + "</svg>";
    }

    function renderPulpitCanvasTool() {
      var state = pulpitCanvasApi ? pulpitCanvasApi.getState() : { nodes: [], edges: [], taps: [] };
      var linksHtml = renderPulpitLinksSvg(state);
      var nodesHtml = state.nodes.map(renderPulpitNodeHtml).join("");

      return [
        "<div class=\"v1-pulpit-canvas-shell\">",
        "<div class=\"v1-pulpit-canvas\" id=\"v1PulpitCanvas\">",
        linksHtml,
        nodesHtml,
        "</div>",
        // Sibling of the canvas, not a child of it - canvasEl.innerHTML gets
        // fully replaced on every render() (wirePulpitCanvas), which would
        // destroy this and any listener bound to it if it lived inside.
        "<div class=\"v1-pulpit-context-menu\" data-pulpit-context-menu hidden>",
        "<button type=\"button\" data-pulpit-context-run>" + escapeHtml(tr("pulpitNodeRunBtn")) + "</button>",
        "</div>",
        "</div>"
      ].join("");
    }

    // LS: plain list of profile names (click to select) + "+ Add profile"
    // button. Selection here drives CS's detail card - the inverse of
    // Pulpit's CS-canvas -> RS-inspector relationship, same underlying
    // mechanism (see wireAgentProfileLibrary/wireAgentProfileDetail in
    // panel-interactions-runtime.js).
    function renderAgentProfileLibrary() {
      var state = agentProfilesApi ? agentProfilesApi.getState() : { profiles: [] };
      var rowsHtml = state.profiles.map(function (profile) {
        var label = profile.name || tr("agentProfileDefaultName");
        return [
          "<div class=\"v1-agentprofile-list-row\" data-agentprofile-id=\"" + escapeHtml(profile.id) + "\">",
          "<span class=\"v1-agentprofile-list-name\">" + escapeHtml(label) + "</span>",
          "<button type=\"button\" class=\"v1-agentprofile-list-remove\" data-agentprofile-remove=\"" + escapeHtml(profile.id) + "\" aria-label=\"" + escapeHtml(tr("agentProfileDeleteBtn")) + "\" title=\"" + escapeHtml(tr("agentProfileDeleteBtn")) + "\">&times;</button>",
          "</div>"
        ].join("");
      }).join("");

      return [
        "<ul class=\"v1-tool-list\">",
        "<li>",
        "<div class=\"v1-section-header\"><strong>" + escapeHtml(tr("tabLabel_agent_profiles")) + "</strong><span class=\"v1-collapse-arrow\">▼</span></div>",
        "<div class=\"v1-section-body\">",
        "<button type=\"button\" class=\"v1-agentprofile-add-btn\" data-agentprofile-add>" + escapeHtml(tr("agentProfileAddBtn")) + "</button>",
        "<div class=\"v1-agentprofile-list\">" + rowsHtml + "</div>",
        "</div>",
        "</li>",
        "</ul>"
      ].join("");
    }

    // Right column of the CS card: per-profile "Serwisy społecznościowe" -
    // a repeatable list of services (freeform name, e.g. "Facebook"), each
    // holding a repeatable list of user-named, user-typed fields (label
    // picked freely, type picked at add-time). Service-field value inputs
    // use a DIFFERENT attribute (data-agentprofile-field-part) than the
    // static 6 fields' data-agentprofile-field, since field labels are
    // arbitrary user text and can collide across services ("PIN" in two
    // different services) - copy/password-toggle buttons key off the
    // field's own unique id instead of a name, see
    // wireAgentProfileDetail's generalized lookup.
    function renderAgentProfileServicesColumn(profileId, state) {
      var services = state.services.filter(function (s) { return s.profileId === profileId; });
      var fieldCopyLabel = escapeHtml(tr("agentProfileCopyBtn"));
      var passwordToggleLabel = escapeHtml(tr("agentProfileShowPasswordBtn"));
      var removeFieldLabel = escapeHtml(tr("agentProfileRemoveFieldBtn"));
      var removeServiceLabel = escapeHtml(tr("agentProfileRemoveServiceBtn"));

      var servicesHtml = services.map(function (service) {
        var fields = state.fields.filter(function (f) { return f.serviceId === service.id; });
        var fieldsHtml = fields.map(function (field) {
          var isPassword = field.type === "password";
          var rowClass = isPassword ? "v1-agentprofile-password-row" : "v1-agentprofile-input-row";
          var toggleHtml = isPassword
            ? "<button type=\"button\" class=\"v1-agentprofile-password-toggle\" data-agentprofile-toggle-field-password=\"" + escapeHtml(field.id) + "\" aria-pressed=\"false\" aria-label=\"" + passwordToggleLabel + "\" title=\"" + passwordToggleLabel + "\">👁</button>"
            : "";
          return [
            "<div class=\"v1-agentprofile-service-field-row\" data-agentprofile-field-id=\"" + escapeHtml(field.id) + "\">",
            "<input type=\"text\" class=\"v1-agentprofile-field-label\" data-agentprofile-field-part=\"label\" autocomplete=\"off\" placeholder=\"" + escapeHtml(tr("agentProfileFieldLabelPlaceholder")) + "\" value=\"" + escapeHtml(field.label) + "\" />",
            "<div class=\"" + rowClass + "\">",
            "<input type=\"" + (isPassword ? "password" : "text") + "\" data-agentprofile-field-part=\"value\" autocomplete=\"off\" value=\"" + escapeHtml(field.value) + "\" />",
            toggleHtml,
            "<button type=\"button\" class=\"v1-agentprofile-copy-btn\" data-agentprofile-copy-field=\"" + escapeHtml(field.id) + "\" aria-label=\"" + fieldCopyLabel + "\" title=\"" + fieldCopyLabel + "\">📋</button>",
            "<button type=\"button\" class=\"v1-agentprofile-attachment-remove\" data-agentprofile-remove-field=\"" + escapeHtml(field.id) + "\" aria-label=\"" + removeFieldLabel + "\" title=\"" + removeFieldLabel + "\">&times;</button>",
            "</div>",
            "</div>"
          ].join("");
        }).join("");

        return [
          "<div class=\"v1-agentprofile-service\" data-agentprofile-service-id=\"" + escapeHtml(service.id) + "\">",
          "<div class=\"v1-agentprofile-service-header\">",
          "<input type=\"text\" class=\"v1-agentprofile-service-name\" data-agentprofile-service-field=\"name\" autocomplete=\"off\" placeholder=\"" + escapeHtml(tr("agentProfileServiceNamePlaceholder")) + "\" value=\"" + escapeHtml(service.name) + "\" />",
          "<button type=\"button\" class=\"v1-agentprofile-attachment-remove\" data-agentprofile-remove-service=\"" + escapeHtml(service.id) + "\" aria-label=\"" + removeServiceLabel + "\" title=\"" + removeServiceLabel + "\">&times;</button>",
          "</div>",
          "<div class=\"v1-agentprofile-service-fields\">" + fieldsHtml + "</div>",
          "<div class=\"v1-agentprofile-service-field-actions\">",
          "<button type=\"button\" class=\"v1-agentprofile-add-field-btn\" data-agentprofile-add-field=\"text\" data-agentprofile-add-field-service=\"" + escapeHtml(service.id) + "\">" + escapeHtml(tr("agentProfileAddTextFieldBtn")) + "</button>",
          "<button type=\"button\" class=\"v1-agentprofile-add-field-btn\" data-agentprofile-add-field=\"password\" data-agentprofile-add-field-service=\"" + escapeHtml(service.id) + "\">" + escapeHtml(tr("agentProfileAddPasswordFieldBtn")) + "</button>",
          "</div>",
          "</div>"
        ].join("");
      }).join("");

      return [
        "<div class=\"v1-agentprofile-section-label\">" + escapeHtml(tr("agentProfileServicesLabel")) + "</div>",
        "<div class=\"v1-agentprofile-service-list\">" + servicesHtml + "</div>",
        "<button type=\"button\" class=\"v1-agentprofile-add-btn\" data-agentprofile-add-service>" + escapeHtml(tr("agentProfileAddServiceBtn")) + "</button>"
      ].join("");
    }

    // CS: the detail/edit card for the currently-selected profile. Split
    // into an outer shell (renderAgentProfileDetailTool, the zero-arg
    // function toolRenderers/buildDetailHtml calls on tab activation - an
    // empty mount, no selection known yet) and an inner fields renderer
    // (renderAgentProfileDetailFields(profileId), mirroring
    // renderPulpitInspector's role) that wireAgentProfileDetail calls
    // whenever the LS selection changes.
    function renderAgentProfileDetailFields(profileId) {
      var state = agentProfilesApi ? agentProfilesApi.getState() : { profiles: [], attachments: [] };
      var profile = state.profiles.find(function (p) { return p.id === profileId; });

      if (!profile) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("agentProfileEmptyNote")) + "</div>";
      }

      var attachments = state.attachments.filter(function (a) { return a.profileId === profileId; });
      var photo = attachments.filter(function (a) { return a.role === "photo"; })[0] || null;
      var files = attachments.filter(function (a) { return a.role !== "photo"; });

      var filesHtml = files.map(function (a) {
        return [
          "<div class=\"v1-agentprofile-attachment-row\" data-agentprofile-attachment-id=\"" + escapeHtml(a.id) + "\">",
          "<span class=\"v1-agentprofile-attachment-name\">" + escapeHtml(a.filename) + "</span>",
          "<button type=\"button\" class=\"v1-agentprofile-attachment-remove\" data-agentprofile-remove-attachment=\"" + escapeHtml(a.id) + "\" aria-label=\"" + escapeHtml(tr("agentProfileRemoveAttachmentBtn")) + "\" title=\"" + escapeHtml(tr("agentProfileRemoveAttachmentBtn")) + "\">&times;</button>",
          "</div>"
        ].join("");
      }).join("");

      var copyLabel = escapeHtml(tr("agentProfileCopyBtn"));
      function copyBtn(field) {
        return "<button type=\"button\" class=\"v1-agentprofile-copy-btn\" data-agentprofile-copy=\"" + field + "\" aria-label=\"" + copyLabel + "\" title=\"" + copyLabel + "\">📋</button>";
      }

      return [
        // Wrapped in a <form> (never actually submitted - see
        // wireAgentProfileDetail's delegated submit-preventDefault) purely
        // to stop Chromium logging "[DOM] Password field is not contained
        // in a form" for the password inputs below - the main one AND
        // every per-service one in the right column, all one form now, not
        // two; autocomplete="off" throughout since these are this app's
        // own saved values, not something the browser's address/password
        // manager should offer to fill or save.
        "<form data-agentprofile-form autocomplete=\"off\">",
        "<div class=\"v1-agentprofile-columns\">",
        "<div class=\"v1-agentprofile-column-left\">",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfileName\">" + escapeHtml(tr("agentProfileNameLabel")) + "</label>",
        "<div class=\"v1-agentprofile-input-row\">",
        "<input id=\"v1AgentProfileName\" type=\"text\" name=\"agentProfileName\" autocomplete=\"off\" data-agentprofile-field=\"name\" value=\"" + escapeHtml(profile.name) + "\" />",
        copyBtn("name"),
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfileNickname\">" + escapeHtml(tr("agentProfileNicknameLabel")) + "</label>",
        "<div class=\"v1-agentprofile-input-row\">",
        "<input id=\"v1AgentProfileNickname\" type=\"text\" name=\"agentProfileNickname\" autocomplete=\"off\" data-agentprofile-field=\"nickname\" value=\"" + escapeHtml(profile.nickname) + "\" />",
        copyBtn("nickname"),
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfileEmail\">" + escapeHtml(tr("agentProfileEmailLabel")) + "</label>",
        "<div class=\"v1-agentprofile-input-row\">",
        "<input id=\"v1AgentProfileEmail\" type=\"text\" name=\"agentProfileEmail\" autocomplete=\"off\" data-agentprofile-field=\"email\" value=\"" + escapeHtml(profile.email) + "\" />",
        copyBtn("email"),
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfileLogin\">" + escapeHtml(tr("agentProfileLoginLabel")) + "</label>",
        "<div class=\"v1-agentprofile-input-row\">",
        "<input id=\"v1AgentProfileLogin\" type=\"text\" name=\"agentProfileLogin\" autocomplete=\"off\" data-agentprofile-field=\"login\" value=\"" + escapeHtml(profile.login) + "\" />",
        copyBtn("login"),
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfilePassword\">" + escapeHtml(tr("agentProfilePasswordLabel")) + "</label>",
        "<div class=\"v1-agentprofile-password-row\">",
        "<input id=\"v1AgentProfilePassword\" type=\"password\" name=\"agentProfilePassword\" autocomplete=\"off\" data-agentprofile-field=\"password\" value=\"" + escapeHtml(profile.password) + "\" />",
        "<button type=\"button\" class=\"v1-agentprofile-password-toggle\" data-agentprofile-toggle-password aria-pressed=\"false\" aria-label=\"" + escapeHtml(tr("agentProfileShowPasswordBtn")) + "\" title=\"" + escapeHtml(tr("agentProfileShowPasswordBtn")) + "\">👁</button>",
        copyBtn("password"),
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-field\">",
        "<label for=\"v1AgentProfileNote\">" + escapeHtml(tr("agentProfileNoteLabel")) + "</label>",
        "<div class=\"v1-agentprofile-input-row\">",
        "<textarea id=\"v1AgentProfileNote\" name=\"agentProfileNote\" autocomplete=\"off\" rows=\"3\" data-agentprofile-field=\"note\">" + escapeHtml(profile.note) + "</textarea>",
        copyBtn("note"),
        "</div>",
        "</div>",
        "</div>",
        "<div class=\"v1-agentprofile-column-right\">",
        renderAgentProfileServicesColumn(profileId, state),
        "</div>",
        "</div>",
        "</form>",
        "<div class=\"v1-agentprofile-photo-section\">",
        // A plain heading, not a <label> - there's no single form control it
        // describes (an <img> preview + a button aren't form-associable),
        // and Chromium's DevTools flags any <label> with no matching
        // `for`/nested control as "not associated with a form field".
        "<div class=\"v1-agentprofile-section-label\">" + escapeHtml(tr("agentProfilePhotoLabel")) + "</div>",
        // src is filled in asynchronously by wireAgentProfileDetail (photo
        // bytes live in IndexedDB, not something a pure HTML-string
        // renderer can reach synchronously) - hidden until then.
        "<img class=\"v1-agentprofile-photo-preview\" data-agentprofile-photo-preview alt=\"\"" + (photo ? " data-agentprofile-photo-id=\"" + escapeHtml(photo.id) + "\"" : "") + (photo ? "" : " hidden") + " />",
        "<button type=\"button\" class=\"v1-agentprofile-add-photo-btn\" data-agentprofile-add-photo>" + escapeHtml(tr("agentProfileAddPhotoBtn")) + "</button>",
        "</div>",
        "<div class=\"v1-agentprofile-attachments-section\">",
        "<div class=\"v1-agentprofile-section-label\">" + escapeHtml(tr("agentProfileAttachmentsLabel")) + "</div>",
        "<div class=\"v1-agentprofile-attachments-list\">" + filesHtml + "</div>",
        "<button type=\"button\" class=\"v1-agentprofile-add-attachment-btn\" data-agentprofile-add-attachment>" + escapeHtml(tr("agentProfileAddAttachmentBtn")) + "</button>",
        "</div>"
      ].join("");
    }

    function renderAgentProfileDetailTool() {
      return "<div class=\"v1-agentprofile-detail\" id=\"v1AgentProfileDetail\">"
        + renderAgentProfileDetailFields("")
        + "</div>";
    }

    // --- ip-scanner tool keys ---
    function renderIpLibraryTool() {
      return [
        "<div class=\"v1-import-manager\">",
        "<div class=\"v1-import-manager-head\">",
        "<h4 style=\"margin:0 0 4px;\">" + escapeHtml(tr("ipLibraryTitle")) + "</h4>",
        "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("ipLibraryNote")) + "</div>",
        "</div>",
        "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("ipLibraryCentralHint")) + "</div>",
        "<div class=\"v1-import-manager-note\"><strong>" + escapeHtml(tr("ipLibraryLastUpdateLabel")) + "</strong> <span id=\"v1IpLibraryCenterLastUpdate\" data-iplib-role=\"last-update-center\">-</span></div>",
        "<div id=\"v1IpLibraryCenterStatus\" data-iplib-role=\"status-center\" class=\"v1-import-manager-note\"></div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip\" data-native-hscroll=\"true\">",
        "<table class=\"v1-results-table v1-ip-results-table v1-iplib-table\">",
        "<thead><tr><th class=\"v1-iplib-col-country\">" + escapeHtml(tr("ipLibraryTableCountry")) + "</th><th class=\"v1-iplib-col-address\">" + escapeHtml(tr("ipLibraryTableAddress")) + "</th></tr></thead>",
        "<tbody id=\"v1IpLibraryCenterRows\" data-iplib-role=\"rows\"><tr><td colspan=\"2\" class=\"v1-iplib-empty\">" + escapeHtml(tr("ipLibraryTableEmpty")) + "</td></tr></tbody>",
        "</table>",
        "</div>",
        "</div>"
      ].join("");
    }

    function renderPresetsTool() {
      var state = getPresetsState();
      var presets = Array.isArray(state.presets) ? state.presets : [];
      var selected = presets[0] || { id: "", emoji: "", name: "", ports: "" };

      var rowsHtml = presets.map(function (item) {
        var isDefault = item && item.id === state.defaultPresetId;
        var isSelected = item && item.id === selected.id;
        return [
          '<tr class="v1-presets-row' + (isSelected ? ' is-selected' : '') + '" data-preset-id="' + escapeHtml(item.id || "") + '">',
          '<td class="v1-presets-col-default"><input type="radio" name="v1PresetDefault" data-preset-default="' + escapeHtml(item.id || "") + '" ' + (isDefault ? 'checked' : '') + ' aria-label="' + escapeHtml(trOr("presetsDefaultCol", "Default")) + '" /></td>',
          '<td class="v1-presets-col-emoji"><input type="text" maxlength="4" data-preset-field="emoji" data-preset-id="' + escapeHtml(item.id || "") + '" value="' + escapeHtml(item.emoji || "") + '" placeholder="⭐" /></td>',
          '<td class="v1-presets-col-name"><input type="text" data-preset-field="name" data-preset-id="' + escapeHtml(item.id || "") + '" value="' + escapeHtml(item.name || "") + '" placeholder="' + escapeHtml(trOr("presetsNameLabel", "Name")) + '" /></td>',
          '<td class="v1-presets-col-ports"><input type="text" data-preset-field="ports" data-preset-id="' + escapeHtml(item.id || "") + '" value="' + escapeHtml(item.ports || "") + '" placeholder="80,443,8080" /></td>',
          '</tr>'
        ].join("");
      }).join("");

      return [
        "<div class=\"v1-presets-shell\">",
        "<div class=\"v1-presets-actions\">",
        "<button type=\"button\" data-preset-action=\"add\">" + escapeHtml(trOr("presetsAddBtn", "+ Add")) + "</button>",
        "<button type=\"button\" data-preset-action=\"delete\">" + escapeHtml(trOr("presetsDeleteBtn", "Delete")) + "</button>",
        "<button type=\"button\" data-preset-action=\"move-up\">" + escapeHtml(trOr("presetsMoveUpBtn", "Move Up")) + "</button>",
        "<button type=\"button\" data-preset-action=\"move-down\">" + escapeHtml(trOr("presetsMoveDownBtn", "Move Down")) + "</button>",
        "</div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip v1-presets-table-wrap\">",
        "<table class=\"v1-results-table v1-presets-table\" role=\"grid\" aria-label=\"" + escapeHtml(trOr("presetsListAria", "Port presets")) + "\">",
        "<thead><tr><th class=\"v1-presets-col-default\">" + escapeHtml(trOr("presetsDefaultCol", "Default")) + "</th><th class=\"v1-presets-col-emoji\">" + escapeHtml(trOr("presetsEmojiLabel", "Emoji")) + "</th><th class=\"v1-presets-col-name\">" + escapeHtml(trOr("presetsNameLabel", "Name")) + "</th><th class=\"v1-presets-col-ports\">" + escapeHtml(trOr("presetsPortsLabel", "Ports")) + "</th></tr></thead>",
        "<tbody>",
        rowsHtml,
        "</tbody>",
        "</table>",
        "</div>",
        "<p class=\"v1-presets-hint\">" + escapeHtml(trOr("presetsHint", "Enter port numbers separated by commas, e.g. 80, 443, 8080, 554")) + "</p>",
        "</div>"
      ].join("");
    }

    function renderResultsIp() {
      function readPersistedScanRows() {
        var STORAGE_KEY = "netrecon_scan_results_v1";
        try {
          var raw = window.localStorage ? window.localStorage.getItem(STORAGE_KEY) : "";
          if (!raw) return [];
          var parsed = JSON.parse(raw);
          if (!Array.isArray(parsed)) return [];

          return parsed
            .filter(function (row) {
              return row && typeof row === "object" && String(row.ip || "").trim();
            })
            .map(function (row) {
              var ports = Array.isArray(row.ports) ? row.ports : [];
              return {
                ip: String(row.ip || "").trim(),
                ping: String(row.ping || "-").trim() || "-",
                hostname: String(row.hostname || "-").trim() || "-",
                flag: String(row.flag || "-").trim() || "-",
                isp: String(row.isp || "-").trim() || "-",
                as: String(row.as || "").trim(),
                deviceIdentification: String(row.deviceIdentification || "").trim(),
                city: String(row.city || "").trim(),
                countryCode: String(row.countryCode || "").trim(),
                lat: typeof row.lat === "number" ? row.lat : null,
                lon: typeof row.lon === "number" ? row.lon : null,
                status: String(row.status || "active").trim() || "active",
                statusClass: String(row.statusClass || "is-up").trim() || "is-up",
                ports: ports,
              };
            });
        } catch (_) {
          return [];
        }
      }

      // Demo Data mode gates the sample rows shown while there's no real
      // scan yet - a pure visibility filter (no functional side effects),
      // never mixed with real persisted rows once those exist. See
      // project memory "Demo Data mode" for the constraints this must
      // keep following.
      var demoDataModeOn = document.body.classList.contains("v1-demo-data-on");
      var persistedRows = readPersistedScanRows();
      var rows = persistedRows.length
        ? persistedRows
        : (demoDataModeOn && Array.isArray(resultsIpConfig.sampleRows) ? resultsIpConfig.sampleRows : []);
      var selectedPreset = getSelectedPresetInfo();
      var selectedPresetEmoji = selectedPreset.emoji || "🔎";
      var selectedPresetLabel = selectedPreset.name || selectedPreset.id || "";
      // Menu order here deliberately mirrors the RS Config tab's Detect
      // grouping (Service Probing = port-level, then Host Enrichment =
      // host/IP-level), with a visual separator between the two - NOT the
      // actual table column order below, which stays as-is (hostname/flag/
      // isp/as/device/http/access/banner/sslCert) so existing layouts don't
      // shift.
      var columnItems = [
        { key: "banner", icon: "📡", label: trOr("resultsIpColumnBanner", "Banner Grabbing"), defaultVisible: true },
        { key: "http", icon: "📄", label: trOr("resultsIpColumnHttpPageTitle", "HTTP Page Title"), defaultVisible: true },
        { key: "access", icon: "🔑", label: trOr("resultsIpColumnAccessSnapshot", "Access / Snapshot"), defaultVisible: true },
        { key: "sslCert", icon: "🔒", label: trOr("resultsIpColumnSslCert", "SSL/TLS Certificate Info"), defaultVisible: true },
        { key: "hostname", icon: "🧭", label: trOr("resultsIpColumnHostname", "Hostname"), defaultVisible: true, groupStart: true },
        { key: "flag", icon: "🌎", label: trOr("resultsIpColumnCountryFlag", "Country Flag"), defaultVisible: true },
        { key: "isp", icon: "🏢", label: trOr("resultsIpColumnIsp", "ISP"), defaultVisible: true },
        { key: "as", icon: "🕷", label: trOr("resultsIpColumnAs", "AS"), defaultVisible: true },
        { key: "device", icon: "📱", label: trOr("resultsIpColumnDeviceIdentification", "Device Identification"), defaultVisible: true },
        { key: "location", icon: "📍", label: trOr("resultsIpColumnLocation", "Location"), defaultVisible: true }
      ];
      var filterGroups = [
        {
          key: "type",
          buttonLabel: trOr("resultsIpFilterTypeButton", "IP / Ports"),
          items: [
            { key: "ip", icon: "🖧", label: trOr("resultsIpFilterTypeIp", "IP"), defaultChecked: true },
            { key: "ports", icon: "🔌", label: trOr("resultsIpFilterTypePorts", "Ports"), defaultChecked: true }
          ]
        },
        {
          key: "marks",
          buttonLabel: trOr("resultsIpFilterMarksButton", "Favorites / Checked"),
          items: [
            { key: "favorite", icon: "★", label: trOr("resultsIpFilterMarksFavorite", "Favorites"), defaultChecked: false },
            { key: "check", icon: "✓", label: trOr("resultsIpFilterMarksChecked", "Checked"), defaultChecked: false }
          ]
        },
        {
          key: "status",
          buttonLabel: trOr("resultsIpFilterStatusButton", "Status"),
          items: [
            { key: "active", icon: "🟢", label: trOr("resultsIpFilterStatusActive", "Active"), defaultChecked: true },
            { key: "unknown", icon: "⚪", label: trOr("resultsIpFilterStatusUnknown", "Unknown"), defaultChecked: true },
            { key: "dead", icon: "🔴", label: trOr("resultsIpFilterStatusDead", "Dead"), defaultChecked: true }
          ]
        }
      ];

      function resolvePortEntry(port) {
        if (port && typeof port === "object") {
          var rawPort = port.port != null ? port.port : (port.value != null ? port.value : "");
          return {
            portLabel: String(rawPort || "").replace(/^:/, "").trim(),
            httpPageTitle: String(port.httpPageTitle || port.pageTitle || port.title || "").trim(),
            accessSnapshot: String(port.accessSnapshot || port.access || port.snapshot || port.url || "").trim(),
            banner: String(port.banner || "").trim(),
            sslCertInfo: String(port.sslCertInfo || port.sslCert || port.certInfo || "").trim(),
            protocol: String(port.protocol || "TCP").trim().toUpperCase(),
            // "open" (confirmed) or "open_filtered" (UDP only - no response
            // at all, which for UDP can mean open OR silently filtered;
            // there's no way to tell those apart, see probe_port_udp in
            // main.rs). TCP/ICMP results are always "open" - a completed
            // handshake or echo reply leaves no ambiguity.
            status: String(port.status || "open").trim().toLowerCase(),
            service: String(port.service || "").trim(),
            ping: String(port.ping || "-").trim() || "-"
          };
        }

        var legacyLabel = String(port || "").replace(/^:/, "").trim();
        return {
          portLabel: legacyLabel,
          httpPageTitle: "",
          accessSnapshot: "",
          banner: "",
          sslCertInfo: "",
          protocol: "TCP",
          status: "open",
          service: sharedNet && typeof sharedNet.lookupPortService === "function"
            ? sharedNet.lookupPortService(legacyLabel)
            : "",
          ping: "-"
        };
      }

      function resolveStatusKey(row) {
        var explicit = String((row && (row.status || row.state || row.health)) || "").toLowerCase().trim();
        if (explicit === "active" || explicit === "up" || explicit === "alive") return "active";
        if (explicit === "dead" || explicit === "down") return "dead";
        if (explicit === "unknown") return "unknown";

        var cls = String((row && row.statusClass) || "").toLowerCase();
        if (cls.indexOf("is-up") >= 0 || cls.indexOf("active") >= 0) return "active";
        if (cls.indexOf("is-down") >= 0 || cls.indexOf("dead") >= 0) return "dead";
        return "unknown";
      }

      // "City, CC" when both resolved, whichever one alone if only one did,
      // else "-" - same optional-field fallback convention as hostname's
      // own "-" default elsewhere in this table.
      function resolveLocationLabel(row) {
        var city = String((row && row.city) || "").trim();
        var countryCode = String((row && row.countryCode) || "").trim();
        if (city && countryCode) return city + ", " + countryCode;
        if (city) return city;
        if (countryCode) return countryCode;
        return "-";
      }

      var totalPorts = rows.reduce(function (sum, row) {
        return sum + ((row.ports && row.ports.length) || 0);
      }, 0);

      var bodyHtml = rows.map(function (row, idx) {
        var statusKey = resolveStatusKey(row);
        var resultKey = String(row.ip || "").trim();
        var portList = Array.isArray(row.ports) ? row.ports : [];
        var portsHtml = portList.length ? portList.map(function (port, portIdx) {
          var portEntry = resolvePortEntry(port);
          var portLabel = portEntry.portLabel;
          var portKey = String(row.ip || "").trim() + "|" + portLabel;
          var portHttpTitle = portEntry.httpPageTitle || "-";
          var portAccess = portEntry.accessSnapshot || "-";
          var portBanner = portEntry.banner || "-";
          var portSslCert = portEntry.sslCertInfo || "-";
          var portProtocolBadge = "<span class=\"v1-ip-port-badge v1-ip-port-badge--protocol is-" + escapeHtml(portEntry.protocol.toLowerCase()) + "\">" + escapeHtml(portEntry.protocol) + "</span>";
          // Only ever "open_filtered" for UDP (see resolvePortEntry) - a
          // confirmed-open port (TCP, or UDP with a real reply) gets no
          // extra badge at all.
          var portStatusBadge = portEntry.status === "open_filtered"
            ? "<span class=\"v1-ip-port-badge v1-ip-port-badge--status is-open-filtered\" title=\"" + escapeHtml(trOr("portStatusOpenFilteredTitle", "No response - port may be open or silently filtered, can't tell which")) + "\">" + escapeHtml(trOr("portStatusOpenFiltered", "open?")) + "</span>"
            : "";
          var portServiceBadge = portEntry.service ? "<span class=\"v1-ip-port-badge v1-ip-port-badge--service\">" + escapeHtml(portEntry.service) + "</span>" : "";
          return [
            "<tr class=\"v1-ip-port-row\" data-ports-row=\"" + idx + "\" data-port-index=\"" + portIdx + "\" data-port-key=\"" + escapeHtml(portKey) + "\" data-status=\"" + escapeHtml(statusKey) + "\" hidden>",
            "<td class=\"v1-ip-col-check\"><button type=\"button\" class=\"v1-ip-port-action-btn\" data-port-action=\"check\" data-port-key=\"" + escapeHtml(portKey) + "\" aria-pressed=\"false\" aria-label=\"Mark port\">✓</button></td>",
            "<td class=\"v1-ip-col-star\"><button type=\"button\" class=\"v1-ip-port-action-btn\" data-port-action=\"favorite\" data-port-key=\"" + escapeHtml(portKey) + "\" aria-pressed=\"false\" aria-label=\"Add port to favorites\">★</button></td>",
            "<td class=\"v1-ip-col-status\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-ip\"><span class=\"v1-ip-port-line\"><span class=\"v1-ip-port-line-start\"><span class=\"v1-ip-port-chip-emoji\" aria-hidden=\"true\" title=\"" + escapeHtml(selectedPresetLabel) + "\">" + escapeHtml(selectedPresetEmoji) + "</span><span class=\"v1-ip-port-value\">" + escapeHtml(portLabel) + "</span></span>" + portProtocolBadge + portStatusBadge + "</span></td>",
            "<td class=\"v1-ip-col-expand\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-ping\">" + escapeHtml(portEntry.ping) + "</td>",
            "<td class=\"v1-ip-col-host\" data-col=\"hostname\">" + portServiceBadge + "</td>",
            "<td class=\"v1-ip-col-flag\" data-col=\"flag\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-isp\" data-col=\"isp\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-as\" data-col=\"as\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-device\" data-col=\"device\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-location\" data-col=\"location\" aria-hidden=\"true\"></td>",
            "<td class=\"v1-ip-col-http\" data-col=\"http\">" + escapeHtml(portHttpTitle) + "</td>",
            "<td class=\"v1-ip-col-access\" data-col=\"access\"><span class=\"v1-ip-port-link\">" + escapeHtml(portAccess) + "</span></td>",
            "<td class=\"v1-ip-col-banner\" data-col=\"banner\">" + escapeHtml(portBanner) + "</td>",
            "<td class=\"v1-ip-col-sslCert\" data-col=\"sslCert\">" + escapeHtml(portSslCert) + "</td>",
            "</tr>"
          ].join("");
        }).join("") : [
          "<tr class=\"v1-ip-port-row v1-ip-port-row--empty\" data-ports-row=\"" + idx + "\" data-status=\"" + escapeHtml(statusKey) + "\" hidden>",
          "<td class=\"v1-ip-col-check\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-star\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-status\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-ip\"><span class=\"v1-ip-port-empty\">" + escapeHtml(trOr("resultsIpNoOpenPorts", resultsIpConfig.noOpenPorts || "No open ports")) + "</span></td>",
          "<td class=\"v1-ip-col-expand\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-ping\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-host\" data-col=\"hostname\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-flag\" data-col=\"flag\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-isp\" data-col=\"isp\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-as\" data-col=\"as\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-device\" data-col=\"device\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-location\" data-col=\"location\" aria-hidden=\"true\"></td>",
          "<td class=\"v1-ip-col-http\" data-col=\"http\">-</td>",
          "<td class=\"v1-ip-col-access\" data-col=\"access\">-</td>",
          "<td class=\"v1-ip-col-banner\" data-col=\"banner\">-</td>",
          "<td class=\"v1-ip-col-sslCert\" data-col=\"sslCert\">-</td>",
          "</tr>"
        ].join("");

        var rowAs = String(row.as || row.autonomousSystem || "").trim() || "-";
        var rowDevice = String(row.deviceIdentification || row.device || "").trim() || "-";
        var rowLocation = resolveLocationLabel(row);

        return [
          "<tr class=\"v1-ip-result-row\" data-row-index=\"" + idx + "\" data-result-key=\"" + escapeHtml(resultKey) + "\" data-status=\"" + escapeHtml(statusKey) + "\">",
          "<td class=\"v1-ip-col-check\"><button type=\"button\" class=\"v1-ip-port-action-btn\" data-result-action=\"check\" data-result-key=\"" + escapeHtml(resultKey) + "\" aria-pressed=\"false\" aria-label=\"Mark IP\">✓</button></td>",
          "<td class=\"v1-ip-col-star\"><button type=\"button\" class=\"v1-ip-port-action-btn\" data-result-action=\"favorite\" data-result-key=\"" + escapeHtml(resultKey) + "\" aria-pressed=\"false\" aria-label=\"Add IP to favorites\">★</button></td>",
          "<td class=\"v1-ip-col-status\"><span class=\"v1-ip-status-dot " + escapeHtml(row.statusClass || "") + "\"></span></td>",
          "<td class=\"v1-ip-col-ip\">" + escapeHtml(row.ip) + "</td>",
          "<td class=\"v1-ip-col-expand\"><button type=\"button\" class=\"v1-ip-expand-btn\" data-open-ports=\"" + idx + "\" aria-expanded=\"false\">+</button></td>",
          "<td class=\"v1-ip-col-ping\">" + escapeHtml(row.ping) + "</td>",
          "<td class=\"v1-ip-col-host\" data-col=\"hostname\">" + escapeHtml(row.hostname) + "</td>",
          "<td class=\"v1-ip-col-flag\" data-col=\"flag\">" + escapeHtml(row.flag) + "</td>",
          "<td class=\"v1-ip-col-isp\" data-col=\"isp\">" + escapeHtml(row.isp) + "</td>",
          "<td class=\"v1-ip-col-as\" data-col=\"as\">" + escapeHtml(rowAs) + "</td>",
          "<td class=\"v1-ip-col-device\" data-col=\"device\">" + escapeHtml(rowDevice) + "</td>",
          "<td class=\"v1-ip-col-location\" data-col=\"location\">" + escapeHtml(rowLocation) + "</td>",
          "<td class=\"v1-ip-col-http\" data-col=\"http\">-</td>",
          "<td class=\"v1-ip-col-access\" data-col=\"access\">-</td>",
          "<td class=\"v1-ip-col-banner\" data-col=\"banner\">-</td>",
          "<td class=\"v1-ip-col-sslCert\" data-col=\"sslCert\">-</td>",
          "</tr>",
          portsHtml
        ].join("");
      }).join("");

      var headers = resultsIpConfig.headers || {};
      var columnsMenuHtml = columnItems.map(function (item) {
        var separator = item.groupStart ? "<div class=\"v1-results-columns-separator\" role=\"separator\"></div>" : "";
        return separator + [
          "<label class=\"v1-results-columns-item\">",
          "<input type=\"checkbox\" data-column-key=\"" + escapeHtml(item.key) + "\"" + (item.defaultVisible ? " checked" : "") + " />",
          "<span class=\"v1-results-columns-icon\" aria-hidden=\"true\">" + escapeHtml(item.icon) + "</span>",
          "<span>" + escapeHtml(item.label) + "</span>",
          "</label>"
        ].join("");
      }).join("");
      var filtersHtml = filterGroups.map(function (group) {
        var itemsHtml = group.items.map(function (item) {
          return [
            "<label class=\"v1-results-columns-item\">",
            "<input type=\"checkbox\" data-filter-group=\"" + escapeHtml(group.key) + "\" data-filter-key=\"" + escapeHtml(item.key) + "\"" + (item.defaultChecked ? " checked" : "") + " />",
            "<span class=\"v1-results-columns-icon\" aria-hidden=\"true\">" + escapeHtml(item.icon) + "</span>",
            "<span>" + escapeHtml(item.label) + "</span>",
            "</label>"
          ].join("");
        }).join("");

        return [
          "<div class=\"v1-results-columns v1-results-filter\" data-results-filter data-filter-group=\"" + escapeHtml(group.key) + "\">",
          "<button type=\"button\" class=\"v1-results-columns-btn\" data-filter-toggle=\"" + escapeHtml(group.key) + "\" data-filter-label=\"" + escapeHtml(group.buttonLabel) + "\" aria-expanded=\"false\">" + escapeHtml(group.buttonLabel) + " ▾</button>",
          "<div class=\"v1-results-columns-menu\" data-filter-menu=\"" + escapeHtml(group.key) + "\" hidden>" + itemsHtml + "</div>",
          "</div>"
        ].join("");
      }).join("");

      return [
        "<div class=\"v1-results-meta-row\">",
        "<span>" + escapeHtml(trOr("resultsIpHostsLabel", resultsIpConfig.hostsLabel || "Hosty")) + ": <b id=\"resIpHostCount\">" + rows.length + "</b></span>",
        "<span>" + escapeHtml(trOr("resultsIpOpenPortsLabel", resultsIpConfig.openPortsLabel || "Otwarte porty")) + ": <b id=\"resIpPortCount\">" + totalPorts + "</b></span>",
        "<div class=\"v1-results-controls\">",
        filtersHtml,
        "<button type=\"button\" class=\"v1-results-columns-btn\" data-reset-filters>" + escapeHtml(trOr("resultsIpResetFilters", "Reset filters")) + "</button>",
        "<div class=\"v1-results-columns\" data-results-columns>",
        "<button type=\"button\" class=\"v1-results-columns-btn\" data-columns-toggle aria-expanded=\"false\">" + escapeHtml(trOr("resultsIpColumnsButton", "Columns")) + " ▾</button>",
        "<div class=\"v1-results-columns-menu\" data-columns-menu hidden>" + columnsMenuHtml + "</div>",
        "</div>",
        "</div>",
        "</div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip\">",
        "<table class=\"v1-results-table v1-ip-results-table\">",
        "<thead><tr><th class=\"v1-ip-col-check\">✓</th><th class=\"v1-ip-col-star\">★</th><th class=\"v1-ip-col-status\">●</th><th class=\"v1-ip-col-ip\">" + escapeHtml(trOr("resultsIpHeaderIpAddressPort", headers.ipAddressPort || "IP Adress / Port")) + "</th><th class=\"v1-ip-col-expand\">+</th><th class=\"v1-ip-col-ping\">" + escapeHtml(trOr("resultsIpHeaderPing", headers.ping || "Ping")) + "</th><th class=\"v1-ip-col-host\" data-col=\"hostname\">" + escapeHtml(trOr("resultsIpHeaderHostname", headers.hostname || "Hostname")) + "</th><th class=\"v1-ip-col-flag\" data-col=\"flag\">" + escapeHtml(trOr("resultsIpHeaderFlag", headers.flag || "Flag")) + "</th><th class=\"v1-ip-col-isp\" data-col=\"isp\">" + escapeHtml(trOr("resultsIpHeaderIsp", headers.isp || "ISP")) + "</th><th class=\"v1-ip-col-as\" data-col=\"as\">" + escapeHtml(trOr("resultsIpHeaderAs", headers.as || "AS")) + "</th><th class=\"v1-ip-col-device\" data-col=\"device\">" + escapeHtml(trOr("resultsIpHeaderDeviceIdentification", headers.deviceIdentification || "Device Identification")) + "</th><th class=\"v1-ip-col-location\" data-col=\"location\">" + escapeHtml(trOr("resultsIpHeaderLocation", headers.location || "Location")) + "</th><th class=\"v1-ip-col-http\" data-col=\"http\">" + escapeHtml(trOr("resultsIpHeaderHttpPageTitle", headers.httpPageTitle || "HTTP Page Title")) + "</th><th class=\"v1-ip-col-access\" data-col=\"access\">" + escapeHtml(trOr("resultsIpHeaderAccessSnapshot", headers.accessSnapshot || "Access / Snapshot")) + "</th><th class=\"v1-ip-col-banner\" data-col=\"banner\">" + escapeHtml(trOr("resultsIpHeaderBanner", headers.banner || "Banner Grabbing")) + "</th><th class=\"v1-ip-col-sslCert\" data-col=\"sslCert\">" + escapeHtml(trOr("resultsIpHeaderSslCert", headers.sslCert || "SSL/TLS Certificate Info")) + "</th></tr></thead>",
        "<tbody>" + bodyHtml + "</tbody>",
        "</table>",
        "</div>"
      ].join("");
    }

    function renderCanvasBlockHtml(block) {
      var runnable = block.type === "macro";
      var iconGlyph = "⚙";
      var titleText = block.type;
      var bodyHtml = "";

      if (block.type === "macro") {
        var macro = macrosApi ? macrosApi.getMacro(block.properties.macroId) : null;
        iconGlyph = macro ? macro.iconGlyph : "🌐";
        titleText = macro ? tr(macro.nameKey) : block.properties.macroId;
        bodyHtml = "<button type=\"button\" class=\"v1-canvas-block-run-btn\" data-canvas-macro-run=\"" + escapeHtml(block.properties.macroId) + "\">" + escapeHtml(tr("macroRunBtn")) + "</button>";
      } else {
        if (block.type === "if") { iconGlyph = "🔀"; titleText = tr("shellcraftBlockIfLabel"); }
        else if (block.type === "repeat-until") { iconGlyph = "🔁"; titleText = tr("shellcraftBlockRepeatUntilLabel"); }
        else if (block.type === "powershell") { iconGlyph = "⌨"; titleText = tr("shellcraftBlockPowerShellLabel"); }
        else if (block.type === "time-trigger") { iconGlyph = "⏰"; titleText = tr("shellcraftBlockTimeTriggerLabel"); }
        bodyHtml = "<div class=\"v1-canvas-block-not-runnable-note\">" + escapeHtml(tr("shellcraftBlockNotRunnableNote")) + "</div>";
      }

      return [
        "<div class=\"v1-canvas-block\" draggable=\"true\" data-block-id=\"" + escapeHtml(block.id) + "\" data-block-type=\"" + escapeHtml(block.type) + "\"" + (runnable ? "" : " data-block-not-runnable=\"true\"") + " style=\"left:" + block.x + "px;top:" + block.y + "px;\">",
        "<div class=\"v1-canvas-block-head\"><span class=\"v1-canvas-block-icon\" aria-hidden=\"true\">" + escapeHtml(iconGlyph) + "</span><span class=\"v1-canvas-block-title\">" + escapeHtml(titleText) + "</span><button type=\"button\" class=\"v1-canvas-block-remove\" data-canvas-block-remove=\"" + escapeHtml(block.id) + "\" aria-label=\"" + escapeHtml(tr("shellcraftBlockDeleteBtn")) + "\" title=\"" + escapeHtml(tr("shellcraftBlockDeleteBtn")) + "\">&times;</button></div>",
        bodyHtml,
        "</div>"
      ].join("");
    }

    function renderShellCraftInspector(blockId) {
      var state = shellcraftCanvasApi ? shellcraftCanvasApi.getState() : { blocks: [] };
      var block = state.blocks.find(function (b) { return b.id === blockId; });

      if (!block) {
        return "<div class=\"v1-import-manager-note\">" + escapeHtml(tr("shellcraftInspectorEmptyNote")) + "</div>";
      }

      if (block.type === "macro") {
        var macro = macrosApi ? macrosApi.getMacro(block.properties.macroId) : null;
        return [
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label>" + escapeHtml(tr("shellcraftInspectorMacroNameLabel")) + "</label>",
          "<div>" + escapeHtml(macro ? tr(macro.nameKey) : block.properties.macroId) + "</div>",
          "</div>",
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label>" + escapeHtml(tr("shellcraftInspectorMacroActionLabel")) + "</label>",
          "<div>" + escapeHtml(macro ? macro.scannerAction : "") + "</div>",
          "</div>",
          "<button type=\"button\" class=\"v1-canvas-block-run-btn\" data-canvas-macro-run=\"" + escapeHtml(block.properties.macroId) + "\">" + escapeHtml(tr("shellcraftInspectorRunBtn")) + "</button>"
        ].join("");
      }

      if (block.type === "if") {
        return [
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorCondition\">" + escapeHtml(tr("shellcraftInspectorConditionLabel")) + "</label>",
          "<textarea id=\"v1InspectorCondition\" rows=\"3\" data-inspector-field=\"condition\">" + escapeHtml(block.properties.condition) + "</textarea>",
          "</div>"
        ].join("");
      }

      if (block.type === "repeat-until") {
        return [
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorCondition\">" + escapeHtml(tr("shellcraftInspectorConditionLabel")) + "</label>",
          "<textarea id=\"v1InspectorCondition\" rows=\"3\" data-inspector-field=\"condition\">" + escapeHtml(block.properties.condition) + "</textarea>",
          "</div>",
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorMaxIterations\">" + escapeHtml(tr("shellcraftInspectorMaxIterationsLabel")) + "</label>",
          "<input id=\"v1InspectorMaxIterations\" type=\"number\" min=\"1\" data-inspector-field=\"maxIterations\" value=\"" + escapeHtml(String(block.properties.maxIterations)) + "\" />",
          "</div>"
        ].join("");
      }

      if (block.type === "powershell") {
        return [
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorCommand\">" + escapeHtml(tr("shellcraftInspectorCommandLabel")) + "</label>",
          "<textarea id=\"v1InspectorCommand\" rows=\"3\" data-inspector-field=\"command\">" + escapeHtml(block.properties.command) + "</textarea>",
          "</div>"
        ].join("");
      }

      if (block.type === "time-trigger") {
        return [
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorTime\">" + escapeHtml(tr("shellcraftInspectorTimeLabel")) + "</label>",
          "<input id=\"v1InspectorTime\" type=\"text\" placeholder=\"HH:MM\" data-inspector-field=\"time\" value=\"" + escapeHtml(block.properties.time) + "\" />",
          "</div>",
          "<div class=\"v1-shellcraft-inspector-field\">",
          "<label for=\"v1InspectorIntervalMinutes\">" + escapeHtml(tr("shellcraftInspectorIntervalMinutesLabel")) + "</label>",
          "<input id=\"v1InspectorIntervalMinutes\" type=\"number\" min=\"0\" data-inspector-field=\"intervalMinutes\" value=\"" + escapeHtml(String(block.properties.intervalMinutes)) + "\" />",
          "</div>"
        ].join("");
      }

      return "";
    }

    var SHELLCRAFT_VIEWS = [
      { id: "flow", labelKey: "shellcraftViewFlowLabel", enabled: true },
      { id: "timeline", labelKey: "shellcraftViewTimelineLabel", enabled: false },
      { id: "tree", labelKey: "shellcraftViewTreeLabel", enabled: false },
      { id: "layered", labelKey: "shellcraftViewLayeredLabel", enabled: false },
    ];

    function renderShellCraftViewSwitcher() {
      var buttonsHtml = SHELLCRAFT_VIEWS.map(function (view) {
        var attrs = "type=\"button\" class=\"v1-shellcraft-view-btn" + (view.id === "flow" ? " is-active" : "") + "\" data-shellcraft-view=\"" + view.id + "\"";
        if (!view.enabled) {
          attrs += " disabled title=\"" + escapeHtml(tr("shellcraftViewNotImplementedNote")) + "\"";
        }
        return "<button " + attrs + ">" + escapeHtml(tr(view.labelKey)) + "</button>";
      }).join("");

      return "<div class=\"v1-shellcraft-view-switcher\">" + buttonsHtml + "</div>";
    }

    function renderShellCraftCanvasTool() {
      var state = shellcraftCanvasApi ? shellcraftCanvasApi.getState() : { blocks: [] };
      var blocksHtml = state.blocks.map(renderCanvasBlockHtml).join("");

      return [
        renderShellCraftViewSwitcher(),
        "<div class=\"v1-shellcraft-canvas-shell\">",
        "<div class=\"v1-shellcraft-canvas\" id=\"v1ShellCraftCanvas\">",
        blocksHtml,
        "</div>",
        "</div>"
      ].join("");
    }

    // TCP connection-state -> status-dot modifier class, matching the
    // existing .v1-ip-status-dot.is-up precedent instead of new badge markup.
    var netMonStateClass = {
      ESTABLISHED: "is-established",
      LISTEN: "is-listening",
      TIME_WAIT: "is-time-wait",
      CLOSE_WAIT: "is-close-wait",
      SYN_SENT: "is-pending",
      SYN_RCVD: "is-pending",
      FIN_WAIT1: "is-closing",
      FIN_WAIT2: "is-closing",
      CLOSING: "is-closing",
      LAST_ACK: "is-closing",
      CLOSED: "is-closed",
      DELETE_TCB: "is-closed"
    };

    function vendorForMac(mac) {
      var table = (window.NetReconNewUICore && window.NetReconNewUICore.ouiVendorData) || [];
      var prefix = String(mac || "").slice(0, 8).toUpperCase();
      for (var i = 0; i < table.length; i++) {
        if (table[i].prefix === prefix) return table[i].vendor;
      }
      return "-";
    }

    function netMonSortTh(colKey, labelKey) {
      return "<th data-netmon-sort-col=\"" + colKey + "\">" + escapeHtml(tr(labelKey)) +
        "<span class=\"v1-netmon-sort-arrow\" data-netmon-sort-arrow=\"" + colKey + "\"></span></th>";
    }

    function netMonViewSelect(kind, optionPairs) {
      var options = optionPairs.map(function (pair) {
        return "<option value=\"" + pair[0] + "\">" + escapeHtml(tr(pair[1])) + "</option>";
      }).join("");
      return "<select class=\"v1-netmon-view-select\" data-netmon-view-select=\"" + kind + "\">" + options + "</select>";
    }

    function renderNetworkMonitorTool() {
      // Order-agnostic: always emitted [connections, lan] here - actual
      // display order is a pure DOM swap applied right after wiring
      // (panel-interactions-runtime.js's applyNetMonOrder(), driven by
      // the same tool-content-runtime.js netMonState the LS panel reads),
      // keeping this renderer itself simple and stateless. Sort/view/
      // visibility state and history events live entirely in panel-
      // interactions-runtime.js module state - this function always emits
      // the same "neutral" markup, then wireNetworkMonitorTool() re-applies
      // all of it right after (and again on every refresh/redock).
      return [
        "<div class=\"v1-netmon-shell\">",
        "<div class=\"v1-netmon-toolbar\">",
        "<label class=\"v1-netmon-toolbar-check\"><input type=\"checkbox\" data-netmon-visibility=\"connections\" checked /> " + escapeHtml(tr("netMonConnectionsTitle")) + "</label>",
        "<label class=\"v1-netmon-toolbar-check\"><input type=\"checkbox\" data-netmon-visibility=\"lan\" checked /> " + escapeHtml(tr("netMonLanDevicesTitle")) + "</label>",
        "<label class=\"v1-netmon-toolbar-check\"><input type=\"checkbox\" data-netmon-keep-marks /> " + escapeHtml(tr("netMonKeepChangesLabel")) + "</label>",
        "<div class=\"v1-netmon-toolbar-radios\" role=\"radiogroup\">",
        "<label><input type=\"radio\" name=\"netmon-display-mode\" value=\"actual\" data-netmon-display-mode /> " + escapeHtml(tr("netMonModeActual")) + "</label>",
        "<label><input type=\"radio\" name=\"netmon-display-mode\" value=\"all\" data-netmon-display-mode checked /> " + escapeHtml(tr("netMonModeAll")) + "</label>",
        "<label><input type=\"radio\" name=\"netmon-display-mode\" value=\"changes\" data-netmon-display-mode /> " + escapeHtml(tr("netMonModeChanges")) + "</label>",
        "</div>",
        "</div>",
        "<div class=\"v1-netmon-section\" data-netmon-section=\"connections\" data-netmon-view=\"flat\">",
        "<div class=\"v1-netmon-section-head\">",
        "<h4 style=\"margin:0;\">" + escapeHtml(tr("netMonConnectionsTitle")) + "</h4>",
        netMonViewSelect("connections", [
          ["flat", "netMonViewFlat"],
          ["process", "netMonViewGroupProcess"],
          ["pid", "netMonViewGroupPid"],
          ["protocol", "netMonViewGroupProtocol"],
          ["local", "netMonViewGroupLocal"],
          ["remote", "netMonViewGroupRemote"],
          ["state", "netMonViewGroupState"]
        ]),
        "</div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip\" data-native-hscroll=\"true\">",
        "<table class=\"v1-results-table v1-netmon-table\">",
        "<thead><tr>",
        netMonSortTh("protocol", "netMonColProtocol"),
        netMonSortTh("local", "netMonColLocalAddr"),
        netMonSortTh("remote", "netMonColRemoteAddr"),
        netMonSortTh("state", "netMonColState"),
        netMonSortTh("pid", "netMonColPid"),
        netMonSortTh("process", "netMonColProcess"),
        "</tr></thead>",
        "<tbody id=\"v1NetMonConnectionsRows\" data-netmon-role=\"connections-rows\"><tr><td colspan=\"6\" class=\"v1-iplib-empty\">" + escapeHtml(tr("netMonEmptyConnections")) + "</td></tr></tbody>",
        "</table>",
        "</div>",
        "</div>",
        "<div class=\"v1-netmon-section\" data-netmon-section=\"lan\" data-netmon-view=\"flat\">",
        "<div class=\"v1-netmon-section-head\">",
        "<h4 style=\"margin:0;\">" + escapeHtml(tr("netMonLanDevicesTitle")) + "</h4>",
        netMonViewSelect("lan", [
          ["flat", "netMonViewFlat"],
          ["vendor", "netMonViewGroupVendor"],
          ["interface", "netMonViewGroupInterface"],
          ["ip", "netMonViewGroupIp"],
          ["mac", "netMonViewGroupMac"]
        ]),
        "</div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip\" data-native-hscroll=\"true\">",
        "<table class=\"v1-results-table v1-netmon-table\">",
        "<thead><tr>",
        netMonSortTh("ip", "netMonColIp"),
        netMonSortTh("mac", "netMonColMac"),
        netMonSortTh("vendor", "netMonColVendor"),
        netMonSortTh("interface", "netMonColInterface"),
        "</tr></thead>",
        "<tbody id=\"v1NetMonArpRows\" data-netmon-role=\"arp-rows\"><tr><td colspan=\"4\" class=\"v1-iplib-empty\">" + escapeHtml(tr("netMonEmptyArp")) + "</td></tr></tbody>",
        "</table>",
        "</div>",
        "</div>",
        "</div>"
      ].join("");
    }

    // Recently appeared/disappeared rows aren't a separate log anymore -
    // they're mixed straight into the live table (flat or grouped alike),
    // marked with a leading +/- badge and a row class, for as long as
    // panel-interactions-runtime.js's grace window keeps them tagged
    // (row.__netmonGone/__netmonNew, set there right before rendering).
    function netMonRowMark(row) {
      if (row.__netmonGone) return "<span class=\"v1-netmon-row-mark is-gone\" title=\"" + escapeHtml(tr("netMonHistoryDisappeared")) + "\">−</span> ";
      if (row.__netmonNew) return "<span class=\"v1-netmon-row-mark is-new\" title=\"" + escapeHtml(tr("netMonHistoryAppeared")) + "\">+</span> ";
      return "";
    }

    function netMonRowClass(row, extraClass) {
      var cls = row.__netmonGone ? "v1-netmon-row-gone" : (row.__netmonNew ? "v1-netmon-row-new" : "");
      if (extraClass) cls = cls ? cls + " " + extraClass : extraClass;
      return cls ? " class=\"" + cls + "\"" : "";
    }

    function netMonConnectionRowHtml(row) {
      var protocolBadge = "<span class=\"v1-ip-port-badge v1-ip-port-badge--protocol is-" + escapeHtml(String(row.protocol || "").toLowerCase()) + "\">" + escapeHtml(row.protocol) + "</span>";
      var stateHtml = row.state
        ? "<span class=\"v1-ip-status-dot " + (netMonStateClass[row.state] || "") + "\"></span>" + escapeHtml(row.state)
        : "-";
      return [
        "<td>" + netMonRowMark(row) + protocolBadge + "</td>",
        "<td>" + escapeHtml(row.local_addr) + ":" + escapeHtml(String(row.local_port)) + "</td>",
        "<td>" + (row.remote_addr ? escapeHtml(row.remote_addr) + ":" + escapeHtml(String(row.remote_port)) : "-") + "</td>",
        "<td>" + stateHtml + "</td>",
        "<td>" + escapeHtml(String(row.pid)) + "</td>",
        "<td>" + escapeHtml(row.process_name || "-") + "</td>"
      ].join("");
    }

    function netMonArpRowHtml(row) {
      return [
        "<td>" + netMonRowMark(row) + escapeHtml(row.ip) + "</td>",
        "<td>" + escapeHtml(row.mac) + "</td>",
        "<td>" + escapeHtml(vendorForMac(row.mac)) + "</td>",
        "<td>" + escapeHtml(row.interface) + "</td>"
      ].join("");
    }

    // Shared grouping engine for both tables: buckets rows by a display
    // label (the group key itself, e.g. a process name or a MAC vendor),
    // sorted alphabetically, each group collapsed to a single summary row
    // (name + count) unless its key is present in expandedKeys - in which
    // case its individual rows render underneath (optionally sorted by
    // sortCompareFn first - column sorting works the same whether flat or
    // grouped, it just sorts within each group instead of the whole table),
    // using the exact same per-row cell markup the flat view uses (rowHtmlFn).
    function netMonGroupedRowsHtml(rows, groupKeyFn, rowHtmlFn, colCount, expandedKeys, emptyKey, sortCompareFn) {
      if (!rows || !rows.length) {
        return "<tr><td colspan=\"" + colCount + "\" class=\"v1-iplib-empty\">" + escapeHtml(tr(emptyKey)) + "</td></tr>";
      }
      var groups = {};
      var order = [];
      rows.forEach(function (row) {
        var label = groupKeyFn(row) || "-";
        if (!groups[label]) { groups[label] = []; order.push(label); }
        groups[label].push(row);
      });
      order.sort(function (a, b) { return String(a).localeCompare(String(b)); });
      return order.map(function (label) {
        var groupRows = groups[label];
        var isExpanded = !!(expandedKeys && expandedKeys[label]);
        var arrow = isExpanded ? "▼" : "▶";
        var headerRow = "<tr class=\"v1-netmon-group-row\" data-netmon-group-key=\"" + escapeHtml(label) + "\">" +
          "<td colspan=\"" + colCount + "\"><span class=\"v1-collapse-arrow\">" + arrow + "</span> " +
          "<strong>" + escapeHtml(label) + "</strong> (" + groupRows.length + ")</td></tr>";
        var childRows = isExpanded
          ? (sortCompareFn ? groupRows.slice().sort(sortCompareFn) : groupRows)
            .map(function (r) { return "<tr" + netMonRowClass(r, "v1-netmon-group-child") + ">" + rowHtmlFn(r) + "</tr>"; }).join("")
          : "";
        return headerRow + childRows;
      }).join("");
    }

    function renderNetworkMonitorConnectionsRows(rows) {
      if (!rows || !rows.length) {
        return "<tr><td colspan=\"6\" class=\"v1-iplib-empty\">" + escapeHtml(tr("netMonEmptyConnections")) + "</td></tr>";
      }
      return rows.map(function (row) { return "<tr" + netMonRowClass(row) + ">" + netMonConnectionRowHtml(row) + "</tr>"; }).join("");
    }

    function renderNetworkMonitorConnectionsGrouped(rows, groupBy, expandedKeys, sortCompareFn) {
      var keyFn;
      if (groupBy === "pid") keyFn = function (r) { return String(r.pid); };
      else if (groupBy === "protocol") keyFn = function (r) { return r.protocol || "-"; };
      else if (groupBy === "local") keyFn = function (r) { return r.local_addr || "-"; };
      else if (groupBy === "remote") keyFn = function (r) { return r.remote_addr || "-"; };
      else if (groupBy === "state") keyFn = function (r) { return r.state || "-"; };
      else keyFn = function (r) { return r.process_name || "-"; };
      return netMonGroupedRowsHtml(rows, keyFn, netMonConnectionRowHtml, 6, expandedKeys, "netMonEmptyConnections", sortCompareFn);
    }

    function renderNetworkMonitorArpRows(rows) {
      if (!rows || !rows.length) {
        return "<tr><td colspan=\"4\" class=\"v1-iplib-empty\">" + escapeHtml(tr("netMonEmptyArp")) + "</td></tr>";
      }
      return rows.map(function (row) { return "<tr" + netMonRowClass(row) + ">" + netMonArpRowHtml(row) + "</tr>"; }).join("");
    }

    function renderNetworkMonitorArpGrouped(rows, groupBy, expandedKeys, sortCompareFn) {
      var keyFn;
      if (groupBy === "vendor") keyFn = function (r) { return vendorForMac(r.mac); };
      else if (groupBy === "interface") keyFn = function (r) { return r.interface || "-"; };
      else if (groupBy === "ip") keyFn = function (r) { return r.ip || "-"; };
      else keyFn = function (r) { return r.mac || "-"; };
      return netMonGroupedRowsHtml(rows, keyFn, netMonArpRowHtml, 4, expandedKeys, "netMonEmptyArp", sortCompareFn);
    }

    // Email Recon (CS results table) - source/status keys are what the
    // email_recon_lookup Rust command returns; map each to its i18n label.
    var emailReconSourceLabelKey = {
      emailrep: "emailReconSrcEmailrep",
      gravatar: "emailReconSrcGravatar",
      github: "emailReconSrcGithub",
      hibp_breaches: "emailReconSrcHibpBreaches",
      hibp_pastes: "emailReconSrcHibpPastes",
      xposedornot: "emailReconSrcXposedornot",
      leakcheck: "emailReconSrcLeakcheck"
    };
    var emailReconStatusLabelKey = {
      found: "emailReconStatusFound",
      not_found: "emailReconStatusNotFound",
      error: "emailReconStatusError",
      skipped_no_key: "emailReconStatusSkippedNoKey",
      skipped_disabled: "emailReconStatusSkippedDisabled"
    };
    // Reuses the .v1-ip-status-dot family (see cards.css) rather than
    // inventing a parallel badge component - same convention as Network
    // Monitor's TCP-state dots.
    var emailReconStatusClass = {
      found: "is-found",
      not_found: "is-not-found",
      error: "is-error",
      skipped_no_key: "is-skipped",
      skipped_disabled: "is-skipped"
    };

    function renderEmailReconTool() {
      return [
        "<div class=\"v1-emailrecon-shell\">",
        "<div class=\"v1-card v1-emailrecon-summary\" data-emailrecon-role=\"summary\">",
        "<span class=\"v1-emailrecon-summary-exists\" data-emailrecon-role=\"exists-badge\"></span>",
        "<span class=\"v1-emailrecon-summary-count\" data-emailrecon-role=\"hit-count\"></span>",
        "</div>",
        "<div class=\"v1-results-table-scroll v1-results-table-scroll--ip\" data-native-hscroll=\"true\">",
        "<table class=\"v1-results-table v1-emailrecon-table\">",
        "<thead><tr>",
        "<th>" + escapeHtml(tr("emailReconColSource")) + "</th>",
        "<th>" + escapeHtml(tr("emailReconColStatus")) + "</th>",
        "<th>" + escapeHtml(tr("emailReconColSummary")) + "</th>",
        "<th>" + escapeHtml(tr("emailReconColDetail")) + "</th>",
        "</tr></thead>",
        "<tbody id=\"v1EmailReconRows\" data-emailrecon-role=\"results-rows\"><tr><td colspan=\"4\" class=\"v1-iplib-empty\">" + escapeHtml(tr("emailReconEmptyResults")) + "</td></tr></tbody>",
        "</table>",
        "</div>",
        // LeakCheck's free public API's only usage condition (per their
        // docs): a visible "Powered by LeakCheck" link wherever results
        // appear - this satisfies that, not just a courtesy credit.
        "<div class=\"v1-emailrecon-attribution\">" + escapeHtml(tr("emailReconAttributionPrefix")) + " <a href=\"https://leakcheck.io\" target=\"_blank\" rel=\"noopener\">LeakCheck</a></div>",
        "</div>"
      ].join("");
    }

    function renderEmailReconRows(sources) {
      if (!sources || !sources.length) {
        return "<tr><td colspan=\"4\" class=\"v1-iplib-empty\">" + escapeHtml(tr("emailReconEmptyResults")) + "</td></tr>";
      }
      return sources.map(function (row) {
        var sourceLabel = tr(emailReconSourceLabelKey[row.source] || row.source);
        var statusLabel = tr(emailReconStatusLabelKey[row.status] || row.status);
        var statusClass = emailReconStatusClass[row.status] || "";
        return [
          "<tr>",
          "<td>" + escapeHtml(sourceLabel) + "</td>",
          "<td><span class=\"v1-ip-status-dot " + statusClass + "\"></span>" + escapeHtml(statusLabel) + "</td>",
          "<td>" + escapeHtml(row.summary || "-") + "</td>",
          "<td>" + escapeHtml(row.detail || "-") + "</td>",
          "</tr>"
        ].join("");
      }).join("");
    }

    function renderEmailReconSummary(result) {
      if (!result) return { exists: "", count: "" };
      var existsKey = result.exists_hint === "yes" ? "emailReconSummaryExistsYes"
        : result.exists_hint === "no" ? "emailReconSummaryExistsNo"
        : "emailReconSummaryExistsUnknown";
      var total = (result.sources || []).length;
      var countText = tr("emailReconHitCount")
        .replace("{n}", String(result.hit_count || 0))
        .replace("{total}", String(total));
      return { exists: tr(existsKey), count: countText };
    }

    // AI Tools & Permissions (CS tab, opened from General settings). UI/
    // settings only - see ai-permissions-runtime.js's file-top comment.
    function aiPermTreeRow(node, depth, settingsLocked, groupUnavailable) {
      var api = window.NetReconNewUICore && window.NetReconNewUICore.aiPermissions;
      if (!api) return "";
      var unavailable = !!groupUnavailable || !!node.unavailable;
      var value = api.computeDisplayLevel(node);
      var isLocked = !!node.locked;
      var options = ["off", "auto", "ask"].map(function (opt) {
        return "<option value=\"" + opt + "\"" + (value === opt ? " selected" : "") + ">" + escapeHtml(trOr("aiPermLevel_" + opt, opt)) + "</option>";
      }).join("");
      if (value === "mixed") {
        options += "<option value=\"mixed\" selected disabled>" + escapeHtml(trOr("aiPermLevelMixed", "Mixed")) + "</option>";
      }
      var rowHtml = [
        "<div class=\"v1-ai-perm-row" + (unavailable ? " v1-ai-perm-row--unavailable" : "") + "\" style=\"padding-left:" + (depth * 18) + "px;\"" + (unavailable ? " title=\"" + escapeHtml(trOr("aiPermUnavailableNote", "Jeszcze niepodpiete pod realne wywolania AI")) + "\"" : "") + ">",
        "<span class=\"v1-ai-perm-label\">" + escapeHtml(trOr(node.labelKey, node.fallback)) + "</span>",
        "<select class=\"v1-ai-perm-select\" data-ai-perm-select=\"" + escapeHtml(node.id) + "\"" + (isLocked || settingsLocked || unavailable ? " disabled" : "") + ">" + options + "</select>",
        isLocked ? "<span class=\"v1-ai-perm-lock\" title=\"" + escapeHtml(trOr("aiPermLockedNote", "Locked - always requires confirmation")) + "\">🔒</span>" : "",
        "</div>"
      ].join("");
      var childrenHtml = (node.children || []).map(function (child) { return aiPermTreeRow(child, depth + 1, settingsLocked, unavailable); }).join("");
      return rowHtml + childrenHtml;
    }

    function renderAiPermLogHtml(log) {
      return log.length
        ? log.map(function (entry) { return "<div class=\"v1-ai-perm-log-item\">" + escapeHtml(JSON.stringify(entry)) + "</div>"; }).join("")
        : "<div class=\"v1-iplib-empty\">" + escapeHtml(trOr("aiPermLogEmpty", "No actions logged yet.")) + "</div>";
    }

    function renderAiPermissionsTool() {
      var api = window.NetReconNewUICore && window.NetReconNewUICore.aiPermissions;
      if (!api) return "";
      var state = api.getState();
      var log = api.loadAuditLog();

      var locked = state.lockSettings;

      function profileRadio(value, labelKey, labelFallback) {
        return [
          "<label class=\"v1-general-settings-ui-switch-option\">",
          "<input type=\"radio\" name=\"v1AiPermProfile\" value=\"" + value + "\"" + (state.profile === value ? " checked" : "") + (value === "custom" || locked ? " disabled" : "") + " />",
          "<span>" + escapeHtml(trOr(labelKey, labelFallback)) + "</span>",
          "</label>"
        ].join("");
      }

      return [
        "<div class=\"v1-import-manager\">",
        "<div class=\"v1-import-manager-head\">",
        "<h4 style=\"margin:0 0 4px;\">" + escapeHtml(trOr("aiPermTitle", "AI Tools & Permissions")) + "</h4>",
        "<div class=\"v1-import-manager-note\">" + escapeHtml(trOr("aiPermIntroNote", "UI only for now - these settings don't affect the assistant's behavior yet.")) + "</div>",
        "</div>",

        "<h4 class=\"v1-general-settings-group\" style=\"margin-top:0;\">" + escapeHtml(trOr("aiPermGroupProfile", "Profile")) + "</h4>",
        "<div class=\"v1-general-settings-ui-switch v1-general-settings-ui-switch--stacked\">",
        profileRadio("readonly", "aiPermProfileReadonly", "Read-only"),
        profileRadio("assisted", "aiPermProfileAssisted", "Assisted"),
        profileRadio("autonomous", "aiPermProfileAutonomous", "Autonomous"),
        profileRadio("custom", "aiPermProfileCustom", "Custom"),
        "</div>",

        "<h4 class=\"v1-general-settings-group\">" + escapeHtml(trOr("aiPermGroupAccess", "Tool Access")) + "</h4>",
        "<div class=\"v1-config-field-row\">",
        "<label for=\"v1AiPermPriority\">" + escapeHtml(trOr("aiPermPriorityLabel", "Priorytet przy konflikcie (drzewko vs. makro)")) + "</label>",
        "<select id=\"v1AiPermPriority\" class=\"v1-ai-perm-select\" disabled>",
        "<option>" + escapeHtml(trOr("aiPermPriorityRestrictive", "Wygrywa bardziej restrykcyjne (zalecane)")) + "</option>",
        "<option>" + escapeHtml(trOr("aiPermPriorityTool", "Priorytet: narzędzie")) + "</option>",
        "<option>" + escapeHtml(trOr("aiPermPriorityMacro", "Priorytet: makro")) + "</option>",
        "</select>",
        "</div>",
        "<div class=\"v1-ai-perm-tree\">" + (window.NetReconNewUICore.aiPermissions.TREE || []).map(function (node) { return aiPermTreeRow(node, 0, locked); }).join("") + "</div>",

        "<h4 class=\"v1-general-settings-group\">" + escapeHtml(trOr("aiPermGroupGuardrails", "Guardrails")) + "</h4>",
        "<div class=\"v1-config-field-row\">",
        "<label for=\"v1AiPermMaxActions\">" + escapeHtml(trOr("aiPermMaxActionsLabel", "Max actions per conversation")) + "</label>",
        "<input id=\"v1AiPermMaxActions\" type=\"number\" min=\"1\" max=\"100\" value=\"" + state.maxActionsPerConversation + "\"" + (locked ? " disabled" : "") + " />",
        "</div>",
        "<label class=\"v1-config-checkbox-row\">",
        "<input type=\"checkbox\" id=\"v1AiPermLockSettings\"" + (state.lockSettings ? " checked" : "") + " />",
        "<span>🔒 " + escapeHtml(trOr("aiPermLockSettingsLabel", "Block further changes")) + "</span>",
        "</label>",

        "<h4 class=\"v1-general-settings-group\">" + escapeHtml(trOr("aiPermGroupAuditLog", "Audit Log")) + "</h4>",
        "<div class=\"v1-scanner-actions v1-scanner-actions--spaced\">",
        "<button type=\"button\" data-ai-perm-action=\"clear-log\"" + (locked ? " disabled" : "") + ">" + escapeHtml(trOr("aiPermClearLogBtn", "Clear log")) + "</button>",
        "<button type=\"button\" data-ai-perm-action=\"export-log\">" + escapeHtml(trOr("aiPermExportLogBtn", "Export log")) + "</button>",
        "</div>",
        "<div class=\"v1-ai-perm-log\" id=\"v1AiPermLog\">",
        renderAiPermLogHtml(log),
        "</div>",

        "</div>"
      ].join("");
    }

    var toolRenderers = {
      // --- shell keys ---
      versions: renderVersionsTool,
      about: renderAboutTool,
      license: renderLicenseTool,
      "lorem-ipsum": renderLoremIpsumTool,
      general: renderGeneralSettingsTool,
      "import-tool": renderImportTool,
      "language-manager": renderLanguageManagerTool,
      shellcraft: renderShellCraftCanvasTool,
      pulpit: renderPulpitCanvasTool,
      "pulpit-preview": renderPulpitPreviewTool,
      "mail-xss-tester": renderMailXssTesterTool,
      "google-dork": renderGoogleDorkTool,
      komunikator: renderKomunikatorTool,
      globe: renderGlobeTool,
      "agent-profiles": renderAgentProfileDetailTool,

      // --- ip-scanner tool keys ---
      "ip-library": renderIpLibraryTool,
      presets: renderPresetsTool,
      "results-ip": renderResultsIp,
      "network-monitor": renderNetworkMonitorTool,
      "email-recon": renderEmailReconTool,
      "ai-permissions": renderAiPermissionsTool,
    };

    function buildDetailHtml(tool) {
      var renderer = toolRenderers[tool] || function () { return renderDefaultTool(tool); };
      return renderer();
    }

    return {
      buildDetailHtml: buildDetailHtml,
      renderShellCraftLibrary: renderShellCraftLibrary,
      renderCanvasBlockHtml: renderCanvasBlockHtml,
      renderShellCraftInspector: renderShellCraftInspector,
      renderPulpitLibrary: renderPulpitLibrary,
      renderPulpitNodeHtml: renderPulpitNodeHtml,
      renderPulpitLinksSvg: renderPulpitLinksSvg,
      renderPulpitInspector: renderPulpitInspector,
      renderPulpitPreviewList: renderPulpitPreviewList,
      renderPulpitPreviewTool: renderPulpitPreviewTool,
      renderMailXssTesterLibrary: renderMailXssTesterLibrary,
      renderMailXssTesterTool: renderMailXssTesterTool,
      renderMailXssTesterResults: renderMailXssTesterResults,
      renderGoogleDorkLibrary: renderGoogleDorkLibrary,
      renderGoogleDorkTool: renderGoogleDorkTool,
      renderGoogleDorkTemplates: renderGoogleDorkTemplates,
      renderKomunikatorLibrary: renderKomunikatorLibrary,
      renderKomunikatorTool: renderKomunikatorTool,
      renderKomunikatorMembers: renderKomunikatorMembers,
      pulpitEdgeAnchor: pulpitEdgeAnchor,
      renderAgentProfileLibrary: renderAgentProfileLibrary,
      renderAgentProfileDetailFields: renderAgentProfileDetailFields,
      renderNetworkMonitorConnectionsRows: renderNetworkMonitorConnectionsRows,
      renderNetworkMonitorArpRows: renderNetworkMonitorArpRows,
      renderNetworkMonitorConnectionsGrouped: renderNetworkMonitorConnectionsGrouped,
      renderNetworkMonitorArpGrouped: renderNetworkMonitorArpGrouped,
      netMonVendorForMac: vendorForMac,
      renderEmailReconRows: renderEmailReconRows,
      renderEmailReconSummary: renderEmailReconSummary,
      renderAiPermissionsTool: renderAiPermissionsTool,
      renderAiPermLogHtml: renderAiPermLogHtml,
    };
  }

  window.NetReconNewUICore = window.NetReconNewUICore || {};
  window.NetReconNewUICore.newUiRuntimes = window.NetReconNewUICore.newUiRuntimes || {};
  window.NetReconNewUICore.newUiRuntimes.createPanelContentRuntime = createPanelContentRuntime;
})();
