fluidPage(
  withMathJax(),
  theme = nord_light,
  tags$head(
    tags$link(rel = "icon", type = "image/png", href = "favicon.png"),
    tags$script(type = "text/x-mathjax-config", HTML("
      MathJax.Hub.Config({
        'HTML-CSS': { preferredFont: 'TeX', scale: 90 }
      });
    ")),
    tags$style(HTML("
    .container-fluid { padding-left: 8px; padding-right: 8px; }
    .dataTable td, .dataTable th { padding: 4px 8px !important; }
    .dataTables_wrapper { font-size: 0.9em; overflow-x: auto; }
    .dataTable td { max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
    #eui-cell-popup { display: none; }
    .eui-popup-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.3); z-index: 9998; }
    .eui-popup-content { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: var(--bs-body-bg, #eceff4); color: var(--bs-body-color, #2e3440); border-radius: 8px; padding: 20px; max-width: 80vw; max-height: 70vh; overflow: auto; z-index: 9999; box-shadow: 0 4px 20px rgba(0,0,0,0.3); }
    .eui-popup-content pre { white-space: pre-wrap; word-wrap: break-word; margin-bottom: 12px; font-size: 0.9em; }
    .eui-popup-close { float: right; }
    .MathJax_Display { text-align: left !important; margin-left: 1em !important; }
    .eui-param-help { position: absolute; top: 0; right: 0; width: 18px; height: 18px; border-radius: 50%; background: #88c0d0; color: #fff; font-size: 11px; font-weight: bold; text-align: center; line-height: 18px; cursor: pointer; z-index: 10; }
    .eui-param-help:hover { background: #5e81ac; }
    .eui-section-info { position: absolute; right: 0; top: 50%; transform: translateY(-50%); display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; border-radius: 50%; background: #88c0d0; color: #fff; font-size: 11px; font-weight: bold; line-height: 18px; cursor: pointer; }
    .eui-section-info:hover { background: #5e81ac; }
    details.eui-section > summary { position: relative; padding-right: 28px; }
    .eui-section-hdr { position: relative; padding-right: 28px; }
    #eui-theme-toggle { width: 38px; height: 38px; border-radius: 50%; border: 2px solid var(--bs-border-color); background: var(--bs-body-bg); color: var(--bs-body-color); font-size: 18px; cursor: pointer; display: flex; align-items: center; justify-content: center; box-shadow: 0 2px 6px rgba(0,0,0,0.15); transition: all 0.3s; }
    #eui-theme-toggle:hover { box-shadow: 0 2px 10px rgba(0,0,0,0.25); }
    .eui-navbar { display: flex; align-items: center; padding: 10px 15px; gap: 8px; flex-wrap: wrap; }
    .eui-navbar .eui-brand { font-size: 1.3em; font-weight: bold; margin-right: 8px; white-space: nowrap; }
    .eui-navbar .eui-brand small { font-size: 0.55em; color: var(--bs-secondary-color); font-weight: normal; }
    .eui-navbar .eui-brand img { height: 26px; margin-right: 6px; vertical-align: middle; }
    .eui-navbar .dropdown { position: relative; }
    .eui-navbar .eui-menu-btn { background: none; border: 1px solid var(--bs-border-color); color: var(--bs-body-color); font-size: 0.9em; padding: 6px 12px; cursor: pointer; border-radius: 4px; }
    .eui-navbar .eui-menu-btn:hover { background: var(--bs-tertiary-bg); }
    .eui-navbar .eui-dropdown-menu { display: none; position: absolute; top: 100%; left: 0; background: var(--bs-body-bg); border: 1px solid var(--bs-border-color); border-radius: 6px; padding: 12px 16px; min-width: 280px; z-index: 10001; box-shadow: 0 4px 16px rgba(0,0,0,0.2); }
    .eui-navbar .dropdown.open .eui-dropdown-menu { display: block; }
    .eui-navbar .eui-spacer { flex: 1; }
    [data-eui-theme='dark'] .eui-popup-content { background: #3b4252; color: #d8dee9; }
    [data-eui-theme='dark'] details > summary { color: #d8dee9 !important; }
    [data-eui-theme='dark'] .nav-tabs .nav-link.active { color: #d8dee9 !important; background-color: #2e3440 !important; border-color: #434c5e #434c5e #2e3440 !important; }
    [data-eui-theme='dark'] .nav-tabs .nav-link { color: #81a1c1; }
    [data-eui-theme='dark'] .nav-tabs .nav-link:hover { color: #d8dee9; border-color: #434c5e; }
    #earth_output { font-family: 'Roboto Condensed', sans-serif; font-size: 0.9em; line-height: 1.5; }
    .eui-matrix-header th { position: sticky; top: 0; z-index: 2; background: var(--bs-body-bg, #fff); }
    .eui-matrix-header th:first-child { position: sticky; left: 0; z-index: 3; }
    .eui-matrix-rowlabel { position: sticky; left: 0; z-index: 1; background: var(--bs-body-bg, #fff); }
    .eui-type-select { appearance: auto; -webkit-appearance: auto; }
    [data-eui-theme='dark'] .eui-type-select { background: #3b4252 !important; color: #d8dee9 !important; border-color: #434c5e !important; }
    .eui-special-select { appearance: auto; -webkit-appearance: auto; }
    [data-eui-theme='dark'] .eui-special-select { background: #3b4252 !important; color: #d8dee9 !important; border-color: #434c5e !important; }
    .col-sm-4 { min-width: 500px; }
    #shiny-notification-panel { width: 450px; }
    .shiny-notification { font-size: 0.85em; word-wrap: break-word; overflow-wrap: anywhere; }
    details.eui-section > summary { cursor: pointer; list-style: none; }
    details.eui-section > summary::-webkit-details-marker { display: none; }
    details.eui-section > summary h4 { display: inline; }
    details.eui-section > summary::before { content: '\\25B6'; margin-right: 6px; font-size: 0.75em; transition: transform 0.2s; display: inline-block; }
    details.eui-section[open] > summary::before { transform: rotate(90deg); }
    .eui-locale-row .form-group { margin-bottom: 0; }
    .radio-inline { margin-right: 16px; }
    .shiny-input-radiogroup input[type='radio'] { appearance: none; -webkit-appearance: none; width: 18px; height: 18px; border: 3px solid #2e3440; border-radius: 50%; margin-right: 5px; vertical-align: middle; cursor: pointer; position: relative; }
    .shiny-input-radiogroup input[type='radio']:checked { border-color: #2e3440; }
    .shiny-input-radiogroup input[type='radio']:checked::after { content: ''; position: absolute; top: 2px; left: 2px; width: 8px; height: 8px; border-radius: 50%; background: #2e3440; }
    [data-eui-theme='dark'] .shiny-input-radiogroup input[type='radio'] { border-color: #d8dee9; }
    [data-eui-theme='dark'] .shiny-input-radiogroup input[type='radio']:checked { border-color: #d8dee9; }
    [data-eui-theme='dark'] .shiny-input-radiogroup input[type='radio']:checked::after { background: #d8dee9; }
    [data-eui-theme='dark'] .modal .btn-outline-primary { color: #88c0d0; border-color: #88c0d0; }
    [data-eui-theme='dark'] .modal .btn-outline-primary:hover { background: #88c0d0; color: #2e3440; }
    [data-eui-theme='dark'] .modal .btn-outline-secondary { color: #81a1c1; border-color: #81a1c1; }
    [data-eui-theme='dark'] .modal .btn-outline-secondary:hover { background: #81a1c1; color: #2e3440; }
    [data-eui-theme='dark'] .modal .btn-outline-danger { color: #bf616a; border-color: #bf616a; }
    [data-eui-theme='dark'] .modal .btn-outline-danger:hover { background: #bf616a; color: #eceff4; }
    .eui-tab-elapsed { color: #81a1c1; font-size: 0.95em; }
    .eui-spinner-dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; background: #81a1c1; animation: eui-pulse 1.2s ease-in-out infinite; vertical-align: middle; }
    @keyframes eui-pulse { 0%, 100% { opacity: 0.3; transform: scale(0.8); } 50% { opacity: 1; transform: scale(1.2); } }
  "))),
  tags$script(HTML("
    $(document).on('shiny:connected', function() {
      function initPopovers() {
        var els = document.querySelectorAll('[data-bs-toggle=\"popover\"]');
        els.forEach(function(el) {
          if (!bootstrap.Popover.getInstance(el)) {
            new bootstrap.Popover(el, { html: false, container: 'body' });
          }
        });
      }
      initPopovers();
      var obs = new MutationObserver(function() { setTimeout(initPopovers, 200); });
      obs.observe(document.body, { childList: true, subtree: true });
    });
  ")),
  # --- Top Menu Bar ---
  tags$nav(class = "eui-navbar",
    tags$span(class = "eui-brand",
      tags$img(src = "logo.png"),
      "earthUI",
      tags$small(" - Interactive Earth Model Builder")
    ),
    tags$div(class = "dropdown", id = "eui-settings-dropdown",
      tags$button(class = "eui-menu-btn", onclick = "toggleDropdown('eui-settings-dropdown')",
                  HTML("&#9881; Settings")),
      tags$div(class = "eui-dropdown-menu",
        selectInput("locale_country", "Country",
                    choices = earthUI:::locale_country_choices_(),
                    selected = "us", width = "100%"),
        selectInput("locale_paper", "Paper",
                    choices = c("Letter" = "letter", "A4" = "a4"),
                    selected = "letter", width = "100%"),
        actionLink("locale_save_default", "Save as my default",
                   style = "font-size: 0.85em; color: #5e81ac; display: block; margin-top: 4px;"),

        tags$hr(style = "margin: 8px 0;"),
        tags$div(style = "font-weight: bold; font-size: 0.9em; margin-bottom: 4px;",
                 "regProj Root Folder"),
        tags$div(class = "small text-muted",
                 style = "margin-bottom: 6px;",
                 "Where all projects are stored. Pick a drive with enough space."),
        tags$div(style = "display:flex; gap:6px; align-items:flex-end;",
          tags$div(style = "flex:1;",
            textInput("regproj_root", NULL, value = "", width = "100%")),
          shinyFiles::shinyDirButton("regproj_root_browse",
                                     "Browse…", "Select regProj Root Folder",
                                     class = "btn btn-outline-secondary btn-sm",
                                     style = "margin-bottom:15px;")
        ),
        actionLink("regproj_root_save", "Save",
                   style = "font-size: 0.85em; color: #5e81ac; display: block; margin-top: 4px;")
      )
    ),
    tags$div(class = "eui-spacer"),
    tags$button(id = "eui-theme-toggle", onclick = "toggleTheme()", HTML("&#9790;"))
  ),
  tags$script(HTML("
    var euiCurrentMode = 'light';

    function toggleDropdown(id) {
      var el = document.getElementById(id);
      if (el) el.classList.toggle('open');
    }
    // Close dropdown when clicking outside
    document.addEventListener('click', function(e) {
      var dropdowns = document.querySelectorAll('.eui-navbar .dropdown');
      dropdowns.forEach(function(dd) {
        if (!dd.contains(e.target)) dd.classList.remove('open');
      });
    });

    function toggleTheme() {
      euiCurrentMode = (euiCurrentMode === 'dark') ? 'light' : 'dark';
      document.body.setAttribute('data-eui-theme', euiCurrentMode);
      Shiny.setInputValue('dark_mode', euiCurrentMode, {priority: 'event'});
      var btn = document.getElementById('eui-theme-toggle');
      if (btn) btn.innerHTML = (euiCurrentMode === 'dark') ? '\\u2600' : '\\u263E';
      try { localStorage.setItem('earthUI_theme', euiCurrentMode); } catch(e) {}
    }

    $(document).on('shiny:connected', function() {
      var saved = null;
      try { saved = localStorage.getItem('earthUI_theme'); } catch(e) {}
      if (saved === 'dark') {
        euiCurrentMode = 'dark';
        document.body.setAttribute('data-eui-theme', 'dark');
        var btn = document.getElementById('eui-theme-toggle');
        if (btn) btn.innerHTML = '\\u2600';
        Shiny.setInputValue('dark_mode', 'dark', {priority: 'event'});
      } else {
        document.body.setAttribute('data-eui-theme', 'light');
        Shiny.setInputValue('dark_mode', 'light', {priority: 'event'});
      }

      // --- Global purpose tracking ---
      window.euiCurrentPurpose = 'general';
      window.euiPurposeSwitching = false;

      // Migrate old localStorage keys (without purpose suffix) to new format
      (function() {
        try {
          var prefixes = ['earthUI_settings_', 'earthUI_vars_', 'earthUI_interactions_',
                          'earthUI_wp_', 'earthUI_blk1_'];
          var purposes = ['general', 'appraisal', 'market'];
          var keys = [];
          for (var i = 0; i < localStorage.length; i++) keys.push(localStorage.key(i));

          // First pass: build a map of filename -> purpose from settings keys
          var filePurpose = {};
          keys.forEach(function(key) {
            if (!key || key.indexOf('earthUI_settings_') !== 0) return;
            var rest = key.substring('earthUI_settings_'.length);
            var hasPurpose = false;
            for (var q = 0; q < purposes.length; q++) {
              if (rest.match(new RegExp('_' + purposes[q] + '$'))) { hasPurpose = true; break; }
            }
            if (hasPurpose) return;
            // Old-format settings key: detect purpose from JSON
            try {
              var s = JSON.parse(localStorage.getItem(key));
              if (s && s.purpose) filePurpose[rest] = s.purpose;
            } catch(e2) {}
          });

          // Second pass: migrate all old keys using the detected purpose
          keys.forEach(function(key) {
            if (!key) return;
            for (var p = 0; p < prefixes.length; p++) {
              var prefix = prefixes[p];
              if (key.indexOf(prefix) !== 0) continue;
              var rest = key.substring(prefix.length);
              // Skip if already has a purpose suffix
              var hasPurpose = false;
              for (var q = 0; q < purposes.length; q++) {
                if (rest.match(new RegExp('_' + purposes[q] + '$'))) { hasPurpose = true; break; }
              }
              if (hasPurpose) break;
              // Use purpose detected from companion settings key, or 'general'
              var purpose = filePurpose[rest] || 'general';
              var newKey = key + '_' + purpose;
              if (!localStorage.getItem(newKey)) {
                localStorage.setItem(newKey, localStorage.getItem(key));
              }
              break;
            }
          });
        } catch(e) {}
      })();

      // Restore last-used purpose
      var lastPurpose = null;
      try { lastPurpose = localStorage.getItem('earthUI_last_purpose'); } catch(e) {}
      if (lastPurpose) {
        window.euiCurrentPurpose = lastPurpose;
        var radio = $(\"input[name='purpose'][value='\" + lastPurpose + \"']\");
        if (radio.length) {
          radio.prop('checked', true).trigger('change');
        }
      }
    });
  ")),
  tags$script(HTML("
    // --- Per-purpose localStorage key helper ---
    // All file-specific keys use this to append _<purpose>
    // Always derive purpose from the live radio (matches server input$purpose);
    // window.euiCurrentPurpose can go stale when a project sets the radio
    // programmatically, which broke per-purpose settings save/restore.
    window.euiActivePurpose = function() {
      var r = $(\"input[name='purpose']:checked\").val();
      return r || window.euiCurrentPurpose || 'general';
    };
    window.euiPurposeKey = function(prefix, filename) {
      return prefix + filename + '_' + window.euiActivePurpose();
    };

    // --- Checkmark helper ---
    function addCheck(btnId) {
      var $btn = $('#' + btnId);
      if ($btn.length && !$btn.find('.eui-check').length) {
        $btn.append(' <span class=\"eui-check\" style=\"color:#fff;\">&#10003;</span>');
      }
    }
    function addCheckMulti(btnId) {
      var $btn = $('#' + btnId);
      if ($btn.length) {
        $btn.append('<span class=\"eui-check\" style=\"color:#fff;\"> &#10003;</span>');
      }
    }
    function removeCheck(btnId) {
      $('#' + btnId + ' .eui-check').remove();
    }
    function removeAllChecks() {
      $('.eui-check').remove();
    }

    // Purpose switch orchestration
    $(document).on('change', 'input[name=purpose]', function() {
      var newPurpose = $(this).val();
      var oldPurpose = window.euiCurrentPurpose || 'general';
      if (newPurpose === oldPurpose) {
        // Still remove checks on same-purpose trigger (e.g. restore on connect)
        removeAllChecks();
        return;
      }

      window.euiPurposeSwitching = true;
      removeAllChecks();

      // Update global purpose
      window.euiCurrentPurpose = newPurpose;
      try { localStorage.setItem('earthUI_last_purpose', newPurpose); } catch(e) {}

      window.euiPurposeSwitching = false;
    });

    Shiny.addCustomMessageHandler('fitting_start', function(msg) {
      // Remove any previous overlays and checkmarks
      $('#eui-fitting-modal, #eui-fitting-backdrop').remove();
      clearInterval(window.euiTimerInterval);
      clearInterval(window.euiFittingTabsPoll);
      removeCheck('run_model');
      removeCheck('export_data');
      removeCheck('export_data_nonadj');
      removeCheck('rca_output_btn');
      removeCheck('sales_grid_btn');
      removeCheck('export_report_btn');

      var start = Date.now();
      var modal = $(
        '<div id=\"eui-fitting-modal\" style=\"position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:10001;' +
        'background:#2e3440;color:#d8dee9;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,0.5);' +
        'width:520px;max-width:90vw;font-family:monospace;overflow:hidden;\">' +
          '<div style=\"background:#3b4252;padding:10px 16px;display:flex;justify-content:space-between;align-items:center;\">' +
            '<span style=\"font-size:0.95em;font-weight:bold;\">Fitting Earth Model</span>' +
            '<span id=\"eui-timer\" style=\"font-size:0.85em;color:#81a1c1;\">0s</span>' +
            '<button id=\"eui-fitting-abort\" style=\"margin-left:12px;padding:2px 12px;font-size:0.8em;' +
              'background:#bf616a;color:#fff;border:none;border-radius:4px;cursor:pointer;\">Abort</button>' +
            '<span id=\"eui-fitting-close\" style=\"display:none;color:#81a1c1;cursor:pointer;' +
              'font-size:1.3em;line-height:1;margin-left:12px;\">&times;</span>' +
          '</div>' +
          '<div id=\"eui-trace-log\" style=\"padding:8px 12px;height:300px;overflow-y:auto;font-size:0.78em;line-height:1.5;\"></div>' +
        '</div>'
      );
      // Add semi-transparent backdrop
      $('<div id=\"eui-fitting-backdrop\" style=\"position:fixed;top:0;left:0;width:100%;height:100%;' +
        'background:rgba(0,0,0,0.4);z-index:10000;\"></div>').appendTo('body');
      modal.appendTo('body');

      // Abort button handler
      $('#eui-fitting-abort').on('click', function() {
        $(this).prop('disabled', true).text('Aborting...');
        Shiny.setInputValue('abort_fit', Date.now(), {priority: 'event'});
        setTimeout(function() {
          $('#eui-fitting-modal, #eui-fitting-backdrop').fadeOut(300, function(){ $(this).remove(); });
        }, 500);
      });

      // Close X handler
      $('#eui-fitting-close').on('click', function() {
        clearInterval(window.euiTimerInterval);
        clearInterval(window.euiFittingTabsPoll);
        window.euiTimerInterval = null;
        window.euiFittingTabsPoll = null;
        $('#eui-fitting-modal, #eui-fitting-backdrop').fadeOut(300, function(){ $(this).remove(); });
      });

      // Add initial message
      $('#eui-trace-log').append($('<div style=\"color:#88c0d0;\">').text('Starting model fit...'));

      window.euiTimerInterval = setInterval(function() {
        var s = Math.floor((Date.now() - start) / 1000);
        var m = Math.floor(s / 60);
        var label = m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';
        $('#eui-timer').text(label);
      }, 1000);
    });

    Shiny.addCustomMessageHandler('trace_line', function(msg) {
      var $log = $('#eui-trace-log');
      if ($log.length) {
        var color = '#a3be8c';
        if (msg.text.match(/^Preparing results:/)) color = '#88c0d0';
        else if (msg.text.match(/^\\s*(CV|cross)/i)) color = '#ebcb8b';
        else if (msg.text.match(/error|fail/i)) color = '#bf616a';
        var $line = $('<div style=\"color:' + color + ';\">').text(msg.text);
        $log.append($line);
        $log.scrollTop($log[0].scrollHeight);
      }
    });

    Shiny.addCustomMessageHandler('fitting_done', function(msg) {
      $('#eui-fitting-abort').hide();
      var $log = $('#eui-trace-log');
      var hasError = msg.text === 'Error' || $log.find('div').filter(function() {
        return $(this).text().match(/error|fail/i);
      }).length > 0;
      var color = hasError ? '#bf616a' : '#a3be8c';
      $log.append($('<div style=\"color:' + color + ';font-weight:bold;margin-top:4px;\">').text(msg.text));
      $log.scrollTop($log[0].scrollHeight);
      if (hasError) {
        clearInterval(window.euiTimerInterval);
        $('#eui-fitting-close').show();
        $('#eui-fitting-backdrop').fadeOut(300, function(){ $(this).remove(); });
      } else {
        addCheck('run_model');
        clearInterval(window.euiTimerInterval);
        window.euiTimerInterval = null;
        var $log = $('#eui-trace-log');
        $log.append($('<div style=\"color:#88c0d0;margin-top:4px;\">').text('You may close this dialog. Tabs will render as you click them.'));
        $log.scrollTop($log[0].scrollHeight);
        // Show close button and remove backdrop immediately — tabs render on demand
        $('#eui-fitting-close').show();
        $('#eui-fitting-backdrop').fadeOut(300, function(){ $(this).remove(); });
        // Auto-dismiss modal once all outputs finish rendering
        var dismissPoll = setInterval(function() {
          if ($('.recalculating').length === 0) {
            clearInterval(dismissPoll);
            setTimeout(function() {
              $('#eui-fitting-modal, #eui-fitting-backdrop').fadeOut(300, function(){ $(this).remove(); });
            }, 500);
          }
        }, 500);
      }
    });

    // Check download buttons on completion
    Shiny.addCustomMessageHandler('download_check', function(msg) {
      addCheck(msg.id);
    });
    Shiny.addCustomMessageHandler('download_check_multi', function(msg) {
      addCheckMulti(msg.id);
    });

    // Toggle tab waiting messages vs content.
    // State tracked in window.euiModelReady so the watchdog can enforce it.
    // When ready=false: show waiting (static text), hide content, hide timer.
    // When ready=true: show content + animated timer while outputs recalculate,
    //   then hide waiting. Timer shows elapsed seconds so users know it's alive.
    window.euiModelReady = false;
    window.euiTabsReadyPoll = null;
    window.euiTabsTimer = null;
    window.euiTabsTimerStart = null;

    function euiUpdateTimerText() {
      if (!window.euiTabsTimerStart) return;
      var s = Math.floor((Date.now() - window.euiTabsTimerStart) / 1000);
      var m = Math.floor(s / 60);
      var label = m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';
      $('.eui-tab-timer-text').text('Processing... ' + label);
    }

    function euiResetToWaiting() {
      // Restore all tabs to the pre-fit waiting state
      $(document).off('shown.bs.tab.eui');
      clearInterval(window.euiTabsReadyPoll);
      clearInterval(window.euiTabsTimer);
      window.euiTabsReadyPoll = null;
      window.euiTabsTimer = null;
      window.euiTabsTimerStart = null;
      $('.eui-tab-waiting').show();
      $('.eui-tab-content').hide();
      $('.eui-tab-elapsed').hide();
      $('.eui-tab-waiting-msg').text('Waiting for processing to complete.');
    }

    Shiny.addCustomMessageHandler('eui_tabs_ready', function(msg) {
      window.euiModelReady = msg.ready;
      if (msg.ready) {
        // --- Model is ready: show content, start timer ---
        clearInterval(window.euiTabsReadyPoll);
        clearInterval(window.euiTabsTimer);

        // Show content in active tab so Shiny renders it on demand
        var $active = $('.tab-pane.active');
        $active.find('.eui-tab-content').show();
        $active.find('.eui-tab-elapsed').show();

        // Start elapsed timer
        window.euiTabsTimerStart = Date.now();
        euiUpdateTimerText();
        window.euiTabsTimer = setInterval(euiUpdateTimerText, 1000);

        // Continuous poll: checks active tab each cycle.
        // Hides waiting only when that tab finishes rendering.
        // Runs until euiResetToWaiting() is called (new fit or purpose switch).
        window.euiTabsReadyPoll = setInterval(function() {
          var $cur = $('.tab-pane.active');
          var still = $cur.find('.eui-tab-content .recalculating').length;
          if (still === 0) {
            $cur.find('.eui-tab-waiting').hide();
          }
        }, 300);

        // When user clicks a different tab, show its content for Shiny rendering.
        // If still recalculating, keep waiting + timer visible until poll clears it.
        $(document).off('shown.bs.tab.eui').on('shown.bs.tab.eui', function(e) {
          var $pane = $($(e.target).attr('href') || $(e.target).data('bs-target'));
          if ($pane.length) {
            $pane.find('.eui-tab-content').show();
            // Check if this tab is still rendering
            var still = $pane.find('.eui-tab-content .recalculating').length;
            if (still > 0) {
              // Still rendering: show waiting + reset timer for this tab
              $pane.find('.eui-tab-waiting').show();
              $pane.find('.eui-tab-elapsed').show();
              window.euiTabsTimerStart = Date.now();
              euiUpdateTimerText();
              if (!window.euiTabsTimer) {
                window.euiTabsTimer = setInterval(euiUpdateTimerText, 1000);
              }
            } else {
              // Already rendered: hide waiting immediately
              $pane.find('.eui-tab-waiting').hide();
            }
          }
        });
      } else {
        // --- No model: reset to static waiting state ---
        euiResetToWaiting();
      }
    });

    // RCA tab has its own toggle (independent of general tabs)
    Shiny.addCustomMessageHandler('eui_rca_ready', function(msg) {
      if (msg.ready) {
        $('.eui-rca-waiting').hide();
        $('.eui-rca-content').show();
      } else {
        $('.eui-rca-waiting').show();
        $('.eui-rca-content').hide();
      }
    });

    // Watchdog: every 2 seconds, enforce correct visibility state.
    // This is a safety net that catches any edge case where messages
    // get hidden incorrectly (e.g. race conditions, JS timing issues).
    setInterval(function() {
      if (!window.euiModelReady) {
        // No model — all waiting must be visible, all content hidden
        var $waiting = $('.eui-tab-waiting');
        var $content = $('.eui-tab-content');
        $waiting.each(function() {
          if ($(this).css('display') === 'none') {
            $(this).show();
          }
        });
        $content.each(function() {
          if ($(this).css('display') !== 'none') {
            $(this).hide();
          }
        });
        // Timer should be hidden when no model
        $('.eui-tab-elapsed').hide();
      }
    }, 2000);


    // --- Report rendering modal ---
    Shiny.addCustomMessageHandler('report_start', function(msg) {
      $('#eui-report-modal').remove();
      var start = Date.now();
      var modal = $(
        '<div id=\"eui-report-modal\" style=\"position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:10001;' +
        'background:#2e3440;color:#d8dee9;border-radius:8px;box-shadow:0 4px 24px rgba(0,0,0,0.5);' +
        'width:420px;max-width:90vw;font-family:monospace;overflow:hidden;\">' +
          '<div style=\"background:#3b4252;padding:10px 16px;display:flex;justify-content:space-between;align-items:center;\">' +
            '<span style=\"font-size:0.95em;font-weight:bold;\">Rendering Report</span>' +
            '<span id=\"eui-report-timer\" style=\"font-size:0.85em;color:#81a1c1;margin-right:30px;\">0s</span>' +
          '</div>' +
          '<div id=\"eui-report-log\" style=\"padding:12px 16px;max-height:200px;overflow-y:auto;font-size:0.85em;line-height:1.6;\">' +
            '<div style=\"color:#88c0d0;\">' + msg.text + '</div>' +
          '</div>' +
        '</div>'
      );
      $('<div id=\"eui-report-backdrop\" style=\"position:fixed;top:0;left:0;width:100%;height:100%;' +
        'background:rgba(0,0,0,0.4);z-index:10000;\"></div>').appendTo('body');
      modal.appendTo('body');
      window.euiReportTimer = setInterval(function() {
        var s = Math.floor((Date.now() - start) / 1000);
        var m = Math.floor(s / 60);
        var label = m > 0 ? m + 'm ' + (s % 60) + 's' : s + 's';
        $('#eui-report-timer').text(label);
      }, 1000);
    });

    Shiny.addCustomMessageHandler('report_line', function(msg) {
      var $log = $('#eui-report-log');
      if ($log.length) {
        var color = msg.error ? '#bf616a' : '#a3be8c';
        $log.append($('<div style=\"color:' + color + ';\">').text(msg.text));
        $log.scrollTop($log[0].scrollHeight);
      }
    });

    Shiny.addCustomMessageHandler('report_done', function(msg) {
      clearInterval(window.euiReportTimer);
      var $log = $('#eui-report-log');
      var color = msg.error ? '#bf616a' : '#a3be8c';
      $log.append($('<div style=\"color:' + color + ';font-weight:bold;margin-top:4px;\">').text(msg.text));
      $log.scrollTop($log[0].scrollHeight);
      var $btn = $('<span id=\"eui-report-close\" style=\"position:absolute;top:8px;right:12px;' +
        'color:#81a1c1;cursor:pointer;font-size:1.3em;line-height:1;\">&times;</span>');
      $btn.on('click', function() {
        $('#eui-report-modal, #eui-report-backdrop').fadeOut(300, function(){ $(this).remove(); });
      });
      $('#eui-report-modal').append($btn);
    });

    // --- wp display, button state, and persistence ---
    Shiny.addCustomMessageHandler('update_wp_display', function(msg) {
      $('#wp_display').text(msg.text);
    });
    Shiny.addCustomMessageHandler('wp_btn_state', function(msg) {
      if (msg.disabled) {
        $('#wp_set_btn').prop('disabled', true).addClass('disabled');
      } else {
        $('#wp_set_btn').prop('disabled', false).removeClass('disabled');
      }
    });
    // newvar.penalty is incompatible with case weights in earth. Disable
    // the input and show a note whenever a weight column is designated.
    Shiny.addCustomMessageHandler('newvar_penalty_state', function(msg) {
      if (msg.disabled) {
        $('#newvar_penalty').val(0).prop('disabled', true)
          .trigger('change');
        $('#newvar_penalty_note').show();
      } else {
        $('#newvar_penalty').prop('disabled', false);
        $('#newvar_penalty_note').hide();
      }
    });
    Shiny.addCustomMessageHandler('save_wp_weights', function(msg) {
      try {
        localStorage.setItem(window.euiPurposeKey('earthUI_wp_', msg.filename), JSON.stringify(msg.weights));
      } catch(e) {}
    });
    // Restore wp weights when target selectize changes
    $(document).on('change', '#target + .selectize-control', function() {
      setTimeout(function() {
        var fn = window.euiCurrentFilename || 'default';
        try {
          var saved = JSON.parse(localStorage.getItem(window.euiPurposeKey('earthUI_wp_', fn)));
          if (saved && Object.keys(saved).length > 0) {
            Shiny.setInputValue('wp_weights_restored', saved, {priority: 'event'});
          }
        } catch(e) {}
      }, 500);
    });

    // --- SQLite settings bridge ---

    // Restore settings from SQLite into localStorage (and optionally apply to DOM)
    Shiny.addCustomMessageHandler('restore_all_settings', function(msg) {
      var fn = msg.filename;
      window.euiCurrentFilename = fn;
      var purpose = msg.purpose || window.euiCurrentPurpose || 'general';
      // msg.settings/variables/interactions are ALREADY JSON strings (R sends
      // the DB column text). Store them VERBATIM. Do NOT JSON.stringify() —
      // that double-encodes, and euiSaveToServer would then write the garbage
      // back into the DB, corrupting the row on the next save.
      var storeRaw = function(key, val) {
        if (val && val !== '{}' && val !== 'null') {
          try { localStorage.setItem(key, val); } catch(e) {}
        }
      };
      storeRaw('earthUI_settings_'     + fn + '_' + purpose, msg.settings);
      storeRaw('earthUI_vars_'         + fn + '_' + purpose, msg.variables);
      storeRaw('earthUI_interactions_' + fn + '_' + purpose, msg.interactions);

      // If apply flag set, push settings directly into Shiny inputs
      if (msg.apply) {
        setTimeout(function() {
          // Apply model parameters
          if (msg.settings) {
            var s = msg.settings;
            // Selectize inputs
            ['target','locale_country','locale_paper','locale_import',
             'degree','pmethod','glm_family','trace','varmod_method'].forEach(function(id) {
              if (s[id] !== undefined) {
                var el = document.getElementById(id);
                if (el && el.selectize) {
                  if (id === 'target' && Array.isArray(s[id])) {
                    var valid = s[id].filter(function(v) { return el.selectize.options[v]; });
                    if (valid.length > 0) el.selectize.setValue(valid, true);
                  } else {
                    el.selectize.setValue(s[id], true);
                  }
                }
              }
            });
            // Numeric inputs (output_folder is owned by the active project
            // and intentionally excluded so a stale SQLite snapshot doesn't
            // override the project-derived path)
            ['nprune','thresh','penalty','minspan','endspan','fast_k','nfold_override',
             'nk','newvar_penalty','fast_beta','ncross','varmod_exponent','varmod_conv',
             'varmod_clamp','varmod_minspan','adjust_endspan','exhaustive_tol',
             'random_seed'].forEach(function(id) {
              if (s[id] !== undefined) $('#' + id).val(s[id]).trigger('change');
            });
            // Checkbox inputs
            ['stratify','keepxy','scale_y','auto_linpreds','use_beta_cache',
             'force_xtx_prune','get_leverages','force_weights',
             'skip_subject_row'].forEach(function(id) {
              if (s[id] !== undefined) $('#' + id).prop('checked', s[id]).trigger('change');
            });
            // Date inputs (Shiny dateInput wraps <input> in a <div>)
            ['effective_date'].forEach(function(id) {
              if (s[id] !== undefined && s[id] !== null) {
                var $inp = $('#' + id + ' input');
                if ($inp.length) { $inp.val(s[id]).trigger('change'); }
                else { $('#' + id).val(s[id]).trigger('change'); }
              }
            });
          }
          // Apply variable checkboxes
          if (msg.variables) {
            var v = msg.variables;
            var cols = [];
            $('[id^=eui_inc_]').each(function() { cols.push(this.id.replace('eui_inc_','')); });
            // Get column names from the table
            var $rows = $('#variable_table .eui-var-cb');
            // Simpler: trigger restore by re-reading localStorage
            var varsKey = window.euiPurposeKey('earthUI_vars_', fn);
            var saved = null;
            try { saved = JSON.parse(localStorage.getItem(varsKey)); } catch(e) {}
            if (saved) {
              var n = $('[id^=eui_inc_]').length;
              for (var i = 1; i <= n; i++) {
                var colName = $('#eui_inc_' + i).closest('tr').find('td:first').text().trim();
                if (!colName) continue;
                var sv = saved[colName];
                if (sv) {
                  $('#eui_inc_' + i).prop('checked', sv.inc);
                  $('#eui_fac_' + i).prop('checked', sv.fac);
                  $('#eui_lin_' + i).prop('checked', sv.lin);
                  if (sv.type) {
                    $('#eui_type_' + i).val(sv.type);
                  }
                  if (sv.special) {
                    $('#eui_special_' + i).val(sv.special);
                  }
                }
              }
              $('.eui-var-cb').first().trigger('change');
              if (typeof window.euiUpdateBadges === 'function') window.euiUpdateBadges();
            }
          }
          // Apply interaction matrix
          if (msg.interactions) {
            var interKey = window.euiPurposeKey('earthUI_interactions_', fn);
            var saved2 = null;
            try { saved2 = JSON.parse(localStorage.getItem(interKey)); } catch(e) {}
            if (saved2) {
              for (var key in saved2) {
                var $cb = $('#allowed_' + key);
                if ($cb.length) $cb.prop('checked', saved2[key]);
              }
              $('.eui-interaction-cb').first().trigger('change');
            }
          }
        }, 300);
      }
    });

    // Save-to-server: collect localStorage and send to R/SQLite.
    // euiSaveToServer() debounces (2s) for noisy per-keystroke / per-toggle
    // changes. euiSaveToServerNow() flushes immediately for explicit,
    // user-driven advance points (opening section 4, clicking Fit) so the
    // write can't be lost to the debounce window if the user reloads or
    // switches right after.
    window.euiSaveTimer = null;
    window.euiBuildSavePayload = function(fn) {
      var purpose = window.euiActivePurpose ? window.euiActivePurpose() : (window.euiCurrentPurpose || 'general');
      var payload = { filename: fn, purpose: purpose, settings: null, variables: null, interactions: null };
      try { payload.settings     = localStorage.getItem(window.euiPurposeKey('earthUI_settings_', fn)); } catch(e) {}
      try { payload.variables    = localStorage.getItem(window.euiPurposeKey('earthUI_vars_', fn)); } catch(e) {}
      try { payload.interactions = localStorage.getItem(window.euiPurposeKey('earthUI_interactions_', fn)); } catch(e) {}
      return payload;
    };
    window.euiSaveToServerNow = function(fn) {
      if (window.euiPurposeSwitching) return;
      clearTimeout(window.euiSaveTimer);
      Shiny.setInputValue('eui_save_trigger', window.euiBuildSavePayload(fn), {priority: 'event'});
    };
    window.euiSaveToServer = function(fn) {
      if (window.euiPurposeSwitching) return;
      clearTimeout(window.euiSaveTimer);
      window.euiSaveTimer = setTimeout(function() {
        Shiny.setInputValue('eui_save_trigger', window.euiBuildSavePayload(fn), {priority: 'deferred'});
      }, 2000);
    };

    // No section-advance / Fit save triggers. Saving is button-only by design:
    // the section 3 Save-current-as-default button saves target + predictors,
    // and the section 4 button saves the earth params + interactions — both
    // server-side. Opening a section or clicking Fit does NOT persist anything.

    // Apply earth() factory defaults to all parameter inputs
    Shiny.addCustomMessageHandler('apply_earth_defaults', function(msg) {
      // Selectize inputs (includes new params 1-4)
      var selects = {
        weights_col: 'null',
        locale_country: 'us', locale_paper: 'letter', locale_import: 'us',
        degree: '1', pmethod: 'backward', glm_family: 'none',
        trace: '0', varmod_method: 'lm'
      };
      for (var id in selects) {
        var el = document.getElementById(id);
        if (el && el.selectize) el.selectize.setValue(selects[id], true);
      }
      // Numeric inputs ('' = NA/auto)
      var nInc = $('[id^=eui_inc_]:checked').length || 1;
      var nkVal = Math.max(1, 3 * nInc);
      var numerics = {
        penalty: 2, nk: nkVal, thresh: 0.001, minspan: 0, endspan: 0,
        newvar_penalty: 0, fast_k: 20, fast_beta: 1,
        nprune: '', nfold_override: 10, ncross: 20,
        varmod_exponent: 1, varmod_conv: 1, varmod_clamp: 0.1, varmod_minspan: -3,
        adjust_endspan: 2, exhaustive_tol: 1e-10,
        subset_arg: '',
        random_seed: String(Math.floor(Math.random() * 2147483647) + 1)
      };
      for (var id in numerics) {
        $('#' + id).val(numerics[id]).trigger('change');
      }
      // Checkbox inputs
      var checks = {
        keepxy: false, stratify: true, scale_y: true, auto_linpreds: true,
        use_beta_cache: true, force_xtx_prune: false, get_leverages: true,
        force_weights: false, skip_subject_row: false
      };
      for (var id in checks) {
        $('#' + id).prop('checked', checks[id]).trigger('change');
      }
      // Reset linpreds: uncheck all Linear checkboxes
      $('[id^=eui_lin_]').prop('checked', false);
      // Reset type dropdowns to auto-detected values
      if (window.euiDetectedTypes && window.euiCols) {
        for (var i = 0; i < window.euiCols.length; i++) {
          var dt = window.euiDetectedTypes[window.euiCols[i]];
          if (dt) {
            $('#eui_type_' + (i + 1)).val(dt);
          }
        }
      }
      // Reset special dropdowns to 'no'
      $('[id^=eui_special_]').val('no');
      // Check all interaction checkboxes (allowed = NULL)
      $('.eui-interaction-cb').prop('checked', true);
      // Trigger change events
      if ($('.eui-var-cb').length) $('.eui-var-cb').first().trigger('change');
      if ($('.eui-type-select').length) $('.eui-type-select').first().trigger('change');
      if ($('.eui-interaction-cb').length) $('.eui-interaction-cb').first().trigger('change');
    });

    // Pre-seed sale_age as included in localStorage when computed
    Shiny.addCustomMessageHandler('sale_age_added', function(msg) {
      var fn = msg.filename || 'default';
      var varsKey = window.euiPurposeKey('earthUI_vars_', fn);
      var saved = {};
      try { saved = JSON.parse(localStorage.getItem(varsKey)) || {}; } catch(e) {}
      saved['sale_age'] = { inc: true, fac: false, lin: false, type: 'integer', special: 'no' };
      try { localStorage.setItem(varsKey, JSON.stringify(saved)); } catch(e) {}
    });

    // Collect current settings from localStorage and save as defaults
    Shiny.addCustomMessageHandler('collect_and_save_defaults', function(msg) {
      var fn = msg.filename;
      var payload = { filename: '__defaults__', settings: null, variables: null, interactions: null };
      try { payload.settings     = localStorage.getItem(window.euiPurposeKey('earthUI_settings_', fn)); } catch(e) {}
      try { payload.variables    = localStorage.getItem(window.euiPurposeKey('earthUI_vars_', fn)); } catch(e) {}
      try { payload.interactions = localStorage.getItem(window.euiPurposeKey('earthUI_interactions_', fn)); } catch(e) {}
      Shiny.setInputValue('eui_save_trigger', payload, {priority: 'deferred'});
    });

  ")),

  sidebarLayout(
    sidebarPanel(
      width = 4,

      # --- Project (Phase B: top-of-sidebar selector) ---
      tags$details(class = "eui-section", open = NA,
        tags$summary(h4("1. Project"),
          tags$span(class = "eui-section-info",
                    `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                    `data-bs-content` = "Active project sets the location (purpose, country, state, county, city). Pick from existing or create a new one. Once a project is active its location is locked; click Close Project to switch.",
                    `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                    "?")),
        uiOutput("regproj_project_ui")
      ),
      hr(),

      # --- Purpose (hidden — driven by active project; kept in DOM so
      # existing input$purpose-based logic continues to work until Phase E) ---
      conditionalPanel(condition = "false",
        tags$div(
          radioButtons("purpose", "Purpose:",
                       choices = c("General" = "general",
                                   "For Appraisal" = "appraisal",
                                   "Market Area Analysis" = "market"),
                       selected = "general", inline = TRUE)
        ),
        checkboxInput("skip_subject_row", "Skip first row", value = FALSE)
      ),

      # --- Data Import (only when an active project exists) ---
      conditionalPanel(condition = "output.has_active_project",
        tags$details(class = "eui-section", open = NA,
          tags$summary(h4("2. Import Data"),
          tags$span(class = "eui-section-info",
                    `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                    `data-bs-content` = "Pick a CSV or Excel file from the active project's in/ folder. Add files manually to <project>/<os>/in/ via Finder/Explorer; click Refresh to rescan. Locale (decimal/field separators) comes from the project's country.",
                    `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                    "?")),
        tags$label(class = "control-label", "Pick a file from the project's in/"),
        uiOutput("regproj_file_picker_ui"),
        actionLink("regproj_files_refresh", "Refresh file list",
                   style = "font-size: 0.85em; color: #5e81ac; display: block; margin-top: 4px;"),
        uiOutput("sheet_selector"),
        uiOutput("data_preview_info")
        )
      ),
      hr(),

      # output_folder is now driven entirely by the active project \u2014 kept
      # hidden in the DOM so existing input$output_folder readers continue
      # to work. The active-project observer in server.R writes the
      # canonical <project>/<os>/out/earth path on project switch.
      conditionalPanel(condition = "false",
        textInput("output_folder", NULL, value = "")
      ),

      # --- Variable Configuration ---
      conditionalPanel(
        condition = "output.data_loaded",
        tags$details(class = "eui-section",
          tags$summary(h4("3. Variable Configuration"),
            tags$span(class = "eui-section-info",
                      `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                      `data-bs-content` = "Select target variable(s), then configure predictors. Include = use as predictor, Factor = treat as categorical, Linear = linear only (no hinges). Special assigns column roles for appraisal workflows (e.g. contract_date, living_area).",
                      `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                      "?")),
          uiOutput("target_selector"),
          # Effective Date applies to every purpose — general/CMA and market
          # values have an effective date too, not just USPAP/IVS appraisals.
          # (It is only USED to compute sale_age in appraisal/market modes; in
          # general it is simply recorded.)
          dateInput("effective_date", "Effective Date", value = Sys.Date()),
          h5("Predictor Settings"),
          uiOutput("predictor_hint_text"),
          div(style = "max-height: 400px; overflow-y: auto; border: 1px solid var(--bs-border-color, #ddd); border-radius: 4px;",
              uiOutput("variable_table")),
          tags$div(style = "margin-top: 8px;",
            actionButton("eui_save_varconfig", "Save current as default",
                         class = "btn-outline-secondary btn-sm"))
        ),
        hr(),

        # --- Earth Call Parameters ---
        tags$details(class = "eui-section", id = "eui_section_earthparams",
          tags$summary(h4("4. Earth Call Parameters"),
            tags$span(class = "eui-section-info",
                      `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                      `data-bs-content` = "Fine-tune earth() model parameters. Hover the blue ? icons for individual parameter details. Use 'Earth defaults' to reset all, or 'Saved defaults' to apply your saved configuration.",
                      `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                      "?")),
        tags$div(
          style = "margin-bottom: 4px; font-size: 0.85em;",
          radioButtons("eui_defaults_action", NULL,
                       choices = c("Use last settings for input file" = "last",
                                   "Use default settings" = "use_default",
                                   "Earth defaults" = "earth_defaults"),
                       selected = "last", inline = TRUE)
        ),
        actionButton("param_info", "Parameter Info",
                     class = "btn-info btn-sm",
                     style = "margin-bottom: 10px; width: 100%;"),

        # 1. subset
        param_with_help(
          tagList(
            textInput("subset_arg", "subset", value = "",
                      placeholder = "e.g. sale_age < 1000 & age > 20"),
            actionButton("subset_builder_btn", "Build filter...",
                         class = "btn-outline-secondary btn-sm",
                         style = "margin-top: -10px; margin-bottom: 8px; width: 100%;")
          ),
          "Row filter expression using column names. Leave blank to use all rows. Examples: sale_age < 1000, area_id != 460, sale_price > 100000 & age < 50"),

        # 2. weights
        param_with_help(
          selectInput("weights_col", "weights",
                      choices = c("NULL (none)" = "null"),
                      selected = "null"),
          "Case weights. Select a numeric column or NULL for no weighting."),

        # 3. wp (response weights — per-target numeric, not a column)
        param_with_help(
          tagList(
            tags$label("wp (response weights)", style = "font-weight: bold; font-size: 0.85em;"),
            tags$div(id = "wp_display",
                     style = "font-size: 0.85em; color: var(--bs-body-color, #666); margin-bottom: 4px;",
                     "NULL (equal weights)"),
            actionButton("wp_set_btn", "Set weights...",
                         class = "btn-outline-secondary btn-sm",
                         style = "margin-bottom: 8px; width: 100%;")
          ),
          "Response weights — one numeric weight per target variable. Only for multi-target models. Default NULL (equal weights)."),

        # 4. keepxy (na.action removed — always na.fail)
        param_with_help(
          checkboxInput("keepxy", "Keep x,y in model (keepxy)", value = FALSE),
          "Default FALSE. Retain x, y, subset, weights in model object. Required for some CV statistics. Makes CV slower."),

        # 6. trace
        param_with_help(
          selectInput("trace", "Trace level (trace)",
                      choices = c("0" = "0", "0.3" = "0.3", "0.5" = "0.5",
                                  "1" = "1", "2" = "2", "3" = "3",
                                  "4" = "4", "5" = "5"),
                      selected = "0"),
          "0=none, 0.3=variance model, 0.5=CV, 1=overview, 2=forward pass, 3=pruning, 4=model mats/pruning details, 5=full details."),

        # 7. glm
        param_with_help(
          selectInput("glm_family", "GLM family (glm)",
                      choices = c("none", "gaussian", "binomial", "poisson"),
                      selected = "none"),
          "Optional GLM family applied to earth basis functions. Example: 'binomial' for binary outcomes."),

        # 8. degree
        param_with_help(
          selectInput("degree", "Max interaction order (degree)",
                      choices = 1:4, selected = 1),
          "Maximum degree of interaction. 1 = no interactions. When >= 2, cross-validation is automatically enabled."),

        # 9. penalty
        param_with_help(
          numericInput("penalty", "GCV penalty per knot (penalty)", value = 2, min = -1, step = 0.5),
          "GCV penalty per knot. Default is 3 if degree>1, else 2. Use 0 to penalize only terms. Use -1 for no penalty (GCV = RSS/n)."),
        uiOutput("rec_penalty"),

        # 10. nk
        param_with_help(
          numericInput("nk", "Max terms before pruning (nk)", value = NA, min = 1, step = 1),
          "Maximum number of model terms before pruning. Default = 3 x number of selected predictors."),
        uiOutput("rec_nk"),

        # 11. thresh
        param_with_help(
          numericInput("thresh", "Forward step threshold (thresh)", value = 0.001, min = 0, step = 0.0001),
          "Forward pass terminates if adding a term changes RSq by less than this value. Default 0.001."),

        # 12. minspan
        param_with_help(
          numericInput("minspan", "Min span (minspan)", value = 0, step = 1),
          "Minimum observations between knots. 0 = auto-calculated. Negative values specify max knots per predictor (e.g., -3 = three evenly spaced knots)."),
        uiOutput("rec_minspan"),

        # 13. endspan
        param_with_help(
          numericInput("endspan", "End span (endspan)", value = 0, min = 0, step = 1),
          "Minimum observations before first and after last knot. 0 = auto-calculated. Be wary of reducing this for predictions near data limits."),
        uiOutput("rec_endspan"),

        # 14. newvar.penalty
        param_with_help(
          numericInput("newvar_penalty", "New variable penalty (newvar.penalty)", value = 0, min = 0, step = 0.01),
          "Penalty for adding a new variable (Friedman's gamma). Default 0. Non-zero values (0.01-0.2) prefer reusing existing variables, simplifying interpretation."),
        tags$div(id = "newvar_penalty_note", style = "display:none; margin-top:-6px; margin-bottom:8px;",
          tags$small(style = "color: var(--bs-warning-text-emphasis, #b48ead);",
            "Must be disabled (0) while a weight column is in use — earth does not support new variable penalty with case weights.")),

        # 15. fast.k
        param_with_help(
          numericInput("fast_k", "fast.k", value = 20, min = 0, step = 1),
          "Max parent terms per forward step (Fast MARS). Default 20. Set 0 to disable. Lower = faster, higher = potentially better model."),

        # 16. fast.beta
        param_with_help(
          numericInput("fast_beta", "fast.beta", value = 1, min = 0, step = 0.1),
          "Fast MARS ageing coefficient. Default 1. A value of 0 sometimes gives better results."),

        # 17. linpreds
        tags$div(
          style = "position: relative; margin-bottom: 10px;",
          tags$label("linpreds", style = "font-weight: bold; font-size: 0.85em;"),
          tags$p(class = "text-muted", style = "font-size: 0.8em; margin: 0;",
                 "Controlled by the Linear column in Variable Configuration above. Default FALSE (no forced linear predictors).")
        ),

        # 18. allowed
        conditionalPanel(
          condition = "input.degree >= 2",
          div(
            class = "alert alert-warning",
            style = "font-size: 0.85em;",
            strong("Note:"),
            "Interaction terms increase the risk of overfitting.",
            "Cross-validation has been enabled (10-fold)."
          ),
          h6("allowed (Interaction Matrix)", style = "border-bottom: 1px solid #ccc;"),
          p("Uncheck pairs to disallow specific interactions. Default NULL (all allowed).",
            class = "text-muted", style = "font-size: 0.85em;"),
          div(style = "margin-bottom: 6px;",
            tags$label(style = "font-size: 0.85em; margin-right: 12px; cursor: pointer;",
              tags$input(type = "checkbox", id = "eui_allow_all",
                         class = "eui-interaction-toggle", checked = "checked",
                         style = "margin-right: 4px;"),
              "Allow All"),
            tags$label(style = "font-size: 0.85em; cursor: pointer;",
              tags$input(type = "checkbox", id = "eui_clear_all",
                         class = "eui-interaction-toggle",
                         style = "margin-right: 4px;"),
              "Clear All")
          ),
          tags$script(HTML("
            $(document).on('change', '#eui_allow_all', function() {
              if ($(this).is(':checked')) {
                $('#eui_clear_all').prop('checked', false);
                $('.eui-interaction-cb').prop('checked', true).trigger('change');
              }
            });
            $(document).on('change', '#eui_clear_all', function() {
              if ($(this).is(':checked')) {
                $('#eui_allow_all').prop('checked', false);
                $('.eui-interaction-cb').prop('checked', false).trigger('change');
              }
            });
            $(document).on('change', '.eui-interaction-cb', function() {
              var all = $('.eui-interaction-cb').length;
              var checked = $('.eui-interaction-cb:checked').length;
              $('#eui_allow_all').prop('checked', checked === all);
              $('#eui_clear_all').prop('checked', checked === 0);
            });
          ")),
          uiOutput("allowed_matrix_ui")
        ),

        # --- For Pruning Pass (collapsible) ---
        tags$details(
          tags$summary(style = "cursor: pointer; font-weight: bold; margin-top: 8px;",
                       class = "text-body", "For Pruning Pass"),
          div(style = "padding-top: 6px;",
            # 19. pmethod
            param_with_help(
              selectInput("pmethod", "Pruning method (pmethod)",
                          choices = c("backward", "none", "exhaustive", "forward", "seqrep", "cv"),
                          selected = "backward"),
              "Pruning method. Default 'backward'. Use 'cv' with nfold to select terms by cross-validation. Use 'none' to retain all forward pass terms."),
            uiOutput("rec_pmethod"),
            # 20. nprune
            param_with_help(
              numericInput("nprune", "Max terms after pruning (nprune)", value = NA, min = 1, step = 1),
              "Maximum terms (including intercept) in pruned model. Default NULL = all terms from forward pass, after pruning."),
            uiOutput("rec_nprune")
          )
        ),

        # --- For Cross Validation (collapsible) ---
        tags$details(
          tags$summary(style = "cursor: pointer; font-weight: bold; margin-top: 8px;",
                       class = "text-body", "For Cross Validation"),
          div(style = "padding-top: 6px;",
            # 21. nfold
            param_with_help(
              numericInput("nfold_override", "CV folds (nfold)", value = 10, min = 0, step = 1),
              "Number of CV folds. Default 10. Auto-set to 10 when degree >= 2. Use trace=0.5 to trace CV."),
            uiOutput("rec_nfold"),
            # 22. ncross
            param_with_help(
              numericInput("ncross", "ncross", value = 20, min = 1, step = 1),
              "Number of cross-validations (each has nfold folds). Default 20. Use higher values with variance models."),
            uiOutput("rec_ncross"),
            # 23. stratify
            param_with_help(
              checkboxInput("stratify", "Stratify CV samples (stratify)", value = TRUE),
              "Default TRUE. Stratify CV samples so each fold has approximately equal response distribution.")
          )
        ),

        # --- For Variance Models (collapsible) ---
        tags$details(
          tags$summary(style = "cursor: pointer; font-weight: bold; margin-top: 8px;",
                       class = "text-body", "For Variance Models"),
          div(style = "padding-top: 6px;",
            # 24. varmod.method
            param_with_help(
              selectInput("varmod_method", "varmod.method",
                          choices = c("none", "const", "lm", "rlm", "earth", "gam",
                                      "power", "power0", "x.lm", "x.rlm", "x.earth", "x.gam"),
                          selected = "lm"),
              "Variance model method. Requires nfold and ncross. Use trace=0.3 to trace. 'lm','rlm','earth','gam' regress on predicted response; 'x.*' variants regress on predictors."),
            uiOutput("rec_varmod"),
            # 25. varmod.exponent
            param_with_help(
              numericInput("varmod_exponent", "varmod.exponent", value = 1, min = 0, step = 0.1),
              "Power transform for residual regression. Default 1. Use 0.5 if std dev increases with square root of response."),
            # 26. varmod.conv
            param_with_help(
              numericInput("varmod_conv", "varmod.conv", value = 1, step = 0.1),
              "Convergence criterion (%) for IRLS in variance model. Default 1. Negative values force that many iterations."),
            # 27. varmod.clamp
            param_with_help(
              numericInput("varmod_clamp", "varmod.clamp", value = 0.1, min = 0, step = 0.01),
              "Min estimated std dev = varmod.clamp * mean(sd(residuals)). Default 0.1. Prevents negative or tiny std dev estimates."),
            # 28. varmod.minspan
            param_with_help(
              numericInput("varmod_minspan", "varmod.minspan", value = -3, step = 1),
              "minspan for internal earth call in variance model. Default -3 (three evenly spaced knots per predictor).")
          )
        ),

        # --- Advanced Use (collapsible) ---
        tags$details(
          tags$summary(style = "cursor: pointer; font-weight: bold; margin-top: 8px;",
                       class = "text-body", "Advanced Use"),
          div(style = "padding-top: 6px;",
            # 29. Scale.y
            param_with_help(
              checkboxInput("scale_y", "Scale response (Scale.y)", value = TRUE),
              "Default TRUE. Scale response internally (subtract mean, divide by sd). Provides better numeric stability."),
            # 30. Adjust.endspan
            param_with_help(
              numericInput("adjust_endspan", "Adjust.endspan", value = 2, min = 1, step = 0.5),
              "In interaction terms, endspan is multiplied by this value. Default 2. Reduces overfitting at data boundaries."),
            # 31. Auto.linpreds
            param_with_help(
              checkboxInput("auto_linpreds", "Auto.linpreds", value = TRUE),
              "Default TRUE. If the best knot is at the predictor minimum, add the predictor linearly (no hinge). Only affects predictions outside training data range."),
            # 32. Force.weights
            param_with_help(
              checkboxInput("force_weights", "Force.weights", value = FALSE),
              "Default FALSE. For testing: force weighted code path even without weights."),
            # 33. Use.beta.cache
            param_with_help(
              checkboxInput("use_beta_cache", "Use.beta.cache", value = TRUE),
              "Default TRUE. Cache regression coefficients in forward pass for 20%+ speed improvement. Uses more memory."),
            # 34. Force.xtx.prune
            param_with_help(
              checkboxInput("force_xtx_prune", "Force.xtx.prune", value = FALSE),
              "Default FALSE. Force X'X-based subset evaluation in pruning instead of QR-based leaps. Advanced use only."),
            # 35. Get.leverages
            param_with_help(
              checkboxInput("get_leverages", "Get.leverages", value = TRUE),
              "Default TRUE (unless >100k cases). Calculate diagonal hat values for diagnostics."),
            # 36. Exhaustive.tol
            param_with_help(
              numericInput("exhaustive_tol", "Exhaustive.tol", value = 1e-10, min = 0, step = 1e-11),
              "Default 1e-10. If reciprocal condition number < this, forces pmethod='backward'. Only applies with pmethod='exhaustive'.")
          )
        ),
        tags$div(style = "margin-top: 8px;",
          actionButton("eui_save_defaults", "Save current as default",
                       class = "btn-dark btn-sm",
                       style = "padding: 2px 8px; font-size: 0.85em;"))
        ),
        hr(),

        # --- 5. Fit ---
        tags$details(class = "eui-section", open = NA, id = "eui_section_fit",
          tags$summary(h4("5. Fit Earth Model")),
          div(style = "position: relative;",
            textInput("random_seed", "Random seed",
                      value = as.character(sample.int(.Machine$integer.max, 1L))),
            uiOutput("seed_history_ui"),
            tags$span(class = "eui-param-help",
                      `data-bs-toggle` = "popover",
                      `data-bs-trigger` = "hover focus",
                      `data-bs-content` = "Integer seed for reproducible cross-validation folds. Default 42. Change to get different fold assignments. Dropdown shows the last 10 seeds used. The seed used is shown in Earth Output after fitting.",
                      `data-bs-placement` = "left",
                      "?")
          ),
          actionButton("run_model", "Fit Earth Model",
                       class = "btn-primary btn-lg",
                       style = "width: 100%; margin-top: 10px;")
        ),

        # --- 6. Download Estimated ... & Residuals ---
        conditionalPanel(
          condition = "output.data_loaded",
          hr(),
          tags$details(class = "eui-section",
            tags$summary(uiOutput("download_heading", inline = TRUE),
              tags$span(class = "eui-section-info",
                        `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                        `data-bs-content` = "Downloads an Excel file with model predictions, residuals, CQA scores, per-variable contributions, and basis values appended to your data.",
                        `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                        "?")),
            conditionalPanel(
              condition = "input.purpose === 'appraisal'",
              actionButton("export_data", "Download Intermediate Output (Excel)",
                           class = "btn-primary",
                           style = "width: 100%;")
            ),
            conditionalPanel(
              condition = "input.purpose !== 'appraisal'",
              actionButton("export_data_nonadj", "Download Output (Excel)",
                           class = "btn-primary",
                           style = "width: 100%;")
            )
          )
        ),

        # --- 7. Calculate RCA Adjustments (Appraisal only) ---
        conditionalPanel(
          condition = "output.model_fitted && input.purpose === 'appraisal'",
          hr(),
          tags$details(class = "eui-section",
            tags$summary(h4("7. Calculate RCA Adjustments & Download"),
              tags$span(class = "eui-section-info",
                        `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                        `data-bs-content` = "Calculates Residential Cost Approach adjustments. Enter the subject's CQA score to interpolate its residual, then computes per-variable and net adjustments for each comparable sale.",
                        `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                        "?")),
            actionButton("rca_output_btn", "Calculate RCA Adjustments & Download",
                         class = "btn-primary",
                         style = "width: 100%;")
          )
        ),

        # --- 8. Generate Sales Grid & Download (Appraisal only) ---
        conditionalPanel(
          condition = "output.model_fitted && input.purpose === 'appraisal'",
          hr(),
          tags$details(class = "eui-section",
            tags$summary(h4("8. Generate Sales Grid & Download"),
              tags$span(class = "eui-section-info",
                        `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                        `data-bs-content` = "Generates a formatted Excel sales comparison grid. Recommends comps with gross adjustments under 25%, sorted by sale age. Each sheet shows the subject and up to 3 comps with factual values, contributions, and adjustments.",
                        `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                        "?")),
            actionButton("sales_grid_btn",
                         "Generate Sales Grid & Download",
                         class = "btn-primary",
                         style = "width: 100%;")
          )
        ),

        # --- 9. Generate Quarto Report (writes .qmd + assets, no rendering) ---
        conditionalPanel(
          condition = "output.model_fitted",
          hr(),
          tags$details(class = "eui-section",
            tags$summary(uiOutput("report_heading", inline = TRUE),
              tags$span(class = "eui-section-info",
                        `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                        `data-bs-content` = "Writes a self-contained Quarto report bundle (.qmd source + plots + data + reference.docx) to the project's output folder. The bundle can be edited or combined with other Quarto sources before being rendered via Section 10.",
                        `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                        "?")),
            actionButton("generate_qmd_btn", "Generate Quarto Report",
                         class = "btn-primary",
                         style = "width: 100%;")
          )
        ),

        # --- 10. Convert Quarto Report (any .qmd) -> HTML/Word/PDF ---
        conditionalPanel(
          condition = "output.has_active_project",
          hr(),
          tags$details(class = "eui-section",
            tags$summary(h4("10. Convert Quarto Report"),
              tags$span(class = "eui-section-info",
                        `data-bs-toggle` = "popover", `data-bs-trigger` = "hover focus",
                        `data-bs-content` = "Renders any Quarto .qmd file to HTML / Word / PDF. Defaults to the current project's most recently generated bundle, but you can browse to any .qmd — useful for converting hand-authored or combined reports.",
                        `data-bs-placement` = "left", onclick = "event.stopPropagation();",
                        "?")),
            tags$div(style = "display:flex; gap:6px; align-items:flex-end;",
              tags$div(style = "flex:1;",
                textInput("convert_qmd_path", "Quarto source (.qmd)", value = "")),
              shinyFiles::shinyFilesButton("convert_qmd_browse", "Browse…",
                                            "Choose a Quarto file",
                                            multiple = FALSE,
                                            class = "btn btn-outline-secondary btn-sm",
                                            style = "margin-bottom:15px;")
            ),
            checkboxGroupInput("convert_formats", "Output formats",
                               choices = {
                                 fmts <- c("HTML" = "html", "Word" = "docx")
                                 if (earthUI:::has_latex_())
                                   fmts <- c(fmts, "PDF" = "pdf")
                                 fmts
                               },
                               selected = "html", inline = TRUE),
            if (!earthUI:::has_latex_()) {
              tags$p(style = "font-size: 0.78em; color: #81A1C1; margin-top: -8px; margin-bottom: 6px;",
                "PDF requires LaTeX. Install with: ",
                tags$code("tinytex::install_tinytex()"))
            },
            actionButton("convert_qmd_btn", "Convert",
                         class = "btn-primary",
                         style = "width: 100%;")
          )
        )
      )
    ),

    mainPanel(
      width = 8,
      conditionalPanel(
        condition = "!output.data_loaded",
        div(
          class = "text-muted",
          style = "text-align: center; padding: 80px 20px;",
          h3("Welcome to earthUI"),
          p("Upload a CSV or Excel file to get started."),
          p("Build and explore Earth (MARS-style) models interactively.")
        )
      ),
      conditionalPanel(
        condition = "output.data_loaded",
        tabsetPanel(
          id = "results_tabs",
          tabPanel(
            "Data",
            br(),
            conditionalPanel(
              condition = "input.purpose === 'appraisal'",
              h5("Subject Property"),
              DT::dataTableOutput("data_subjects"),
              h5("Comparable Sales"),
              DT::dataTableOutput("data_comps")
            ),
            conditionalPanel(
              condition = "input.purpose !== 'appraisal'",
              DT::dataTableOutput("data_preview")
            )
          ),
          # REQUIREMENT: Each tab has a static "Waiting..." div and a content
          # div. JS toggles visibility via 'eui_tabs_ready' message from server.
          # Do NOT use uiOutput wrappers (suspended in inactive tabs) or
          # conditionalPanel (renders all tabs simultaneously). The RCA tab
          # has a specific message about step 7.
          tabPanel("Equation", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                div(style = "overflow-x: auto; padding: 10px 10px 10px 0;",
                    uiOutput("model_equation")))
          ),
          tabPanel("Summary", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                uiOutput("summary_metrics"),
                h5("Coefficients & Basis Functions"),
                DT::dataTableOutput("summary_table"))
          ),
          tabPanel("Variable Importance", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                plotOutput("importance_plot", height = "400px"),
                br(),
                DT::dataTableOutput("importance_table"))
          ),
          tabPanel("Contribution", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                uiOutput("response_selector_contrib"),
                uiOutput("contrib_g_selector"),
                uiOutput("contrib_plot_container"))
          ),
          tabPanel("Correlation", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                uiOutput("correlation_plot_ui"))
          ),
          tabPanel("Diagnostics", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                uiOutput("response_selector_diag"),
                fluidRow(
                  column(6, plotOutput("residuals_plot", height = "350px")),
                  column(6, plotOutput("qq_plot", height = "350px"))
                ),
                br(),
                plotOutput("actual_vs_predicted_plot", height = "400px"))
          ),
          tabPanel("RCA Adjustments", br(),
            div(class = "eui-tab-waiting eui-rca-waiting",
                style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted",
                   "7. Calculate RCA Adjustments & Download must first be initiated and completed."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content eui-rca-content", style = "display:none;",
                uiOutput("rca_plots_ui"))
          ),
          tabPanel("ANOVA", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                DT::dataTableOutput("anova_table"))
          ),
          tabPanel("Earth Output", br(),
            div(class = "eui-tab-waiting", style = "text-align: center; padding: 60px 20px;",
                h4(class = "text-muted eui-tab-waiting-msg", "Waiting for processing to complete."),
                div(class = "eui-tab-elapsed", style = "display:none; margin-top:12px;",
                    tags$span(class = "eui-spinner-dot"), " ",
                    tags$span(class = "eui-tab-timer-text", "Processing..."))),
            div(class = "eui-tab-content", style = "display:none;",
                div(style = "overflow-x: auto;",
                    verbatimTextOutput("earth_output")))
          )
        )
      )
    )
  ),

  # ── AGPL-3 legal footer ──────────────────────────────────────────────
  tags$hr(style = "margin-top: 30px; margin-bottom: 10px;"),
  tags$footer(
    style = paste(
      "text-align: center; padding: 10px 15px 15px;",
      "font-size: 0.8em; color: #7f8c8d;"
    ),
    tags$p(
      style = "margin: 2px 0;",
      HTML(paste0(
        "earthUI v", utils::packageVersion("earthUI"),
        " &mdash; Copyright &copy; 2026 Pacific Vista Net / William Craytor"
      ))
    ),
    tags$p(
      style = "margin: 2px 0;",
      "Licensed under the ",
      tags$a(
        href = "https://www.gnu.org/licenses/agpl-3.0.html",
        target = "_blank",
        "GNU Affero General Public License v3.0"
      ),
      " or later (AGPL-3)."
    ),
    tags$p(
      style = "margin: 2px 0;",
      HTML("This software is provided &ldquo;as is&rdquo;, without warranty of any kind.")
    ),
    tags$p(
      style = "margin: 2px 0;",
      "Source code: ",
      tags$a(
        href = "https://github.com/wcraytor/earthUI",
        target = "_blank",
        "github.com/wcraytor/earthUI"
      )
    )
  )
)
