import { AUTO_SKIP_AT_FIELDS_LS, AUTO_SKIP_VALIDATED_ROWS_LS, CENSUS_AT_SKIP_FIELD_BASES, HIGHLIGHT_FADE_MS, HIGHLIGHT_FLASH_HOLD_MS, HIGHLIGHT_FLASH_MODE_LS, HIGHLIGHT_OFFSET_X, HIGHLIGHT_OFFSET_Y, IMAGE_PAN_KEYBOARD_STEP, IMAGE_ZOOM_KEYBOARD_STEP, ORIG_FOCUS_ZOOM, RECT_AUTO_ZOOM_LS, SNIPPET_CROP_OFFSET_X, SNIPPET_CROP_OFFSET_Y, SNIPPET_CROP_PAD_X, SNIPPET_CROP_PAD_Y, SNIPPET_SIZE_MULTIPLIER_DEFAULT, SNIPPET_SIZE_MULTIPLIER_MAX, SNIPPET_SIZE_SCALE_DOWN_FACTOR, SNIPPET_SIZE_SCALE_UP_FACTOR, SNIPPET_TOGGLE_MODE_LS, ZOOM_MAX, ZOOM_MIN } from '../config/constants.js';
import { $ } from '../core/dom.js';
import { readLs, writeLs } from '../core/storage.js';
import { scheduleUserPreferencesSave } from '../services/preferences.js';
import { getCurrentProjectKind, isOrigInputPersistable, resolveFieldConfigRootKind } from '../services/project-context.js';
import { restoreOrigFromHwr } from '../services/dictionary-autocorrect.js';
import { handleSuggestionKeydown } from '../services/suggestions.js';
import { highlightTimers, state, runtime } from '../state/app-state.js';
import { commitHistoryActionForInput, queueAutosaveForInput } from '../state/autosave.js';
import { fieldConfMeetsRekeyThreshold } from './form/field-table.js';
import { syncBdmPanInputsFromState } from './image-viewer.js';
import { updateVisibleStatusBar } from './status-bar.js';

export function applyImageTransform() {
  const pan = $("imgPan");
  if (!pan) return;
  const z = state.zoom / 100;
  const px = Math.round(state.panX);
  const py = Math.round(state.panY);
  pan.style.transform =
    "translate3d(" + px + "px, " + py + "px, 0) scale(" + z + ")";
  const zr = $("zoomRange");
  if (zr) zr.value = String(state.zoom);
  const disp = $("zoomValueDisplay");
  if (disp && !runtime.zoomValueInputEditing) disp.value = String(state.zoom);
  if (!runtime.bdmPanInputEditing) syncBdmPanInputsFromState();
  updateVisibleStatusBar();
}


export function clearHighlightTimers() {
  if (highlightTimers.hold) {
    clearTimeout(highlightTimers.hold);
    highlightTimers.hold = null;
  }
  if (highlightTimers.fade) {
    clearTimeout(highlightTimers.fade);
    highlightTimers.fade = null;
  }
}


export function setHighlightVisible(hl, isVisible) {
  if (!hl) return;
  hl.classList.remove("is-fading");
  hl.classList.toggle("is-visible", !!isVisible);
}


export function hideFloatingInputOverlay() {
  const fi = $("floatingInput");
  if (!fi) return;
  fi.style.display = "none";
  fi.hidden = true;
}


export function applyHighlightVisibilityMode(hl) {
  if (!hl || hl.style.display === "none") return;
  clearHighlightTimers();
  setHighlightVisible(hl, true);
  if (!state.highlightFlashMode) return;

  highlightTimers.hold = setTimeout(() => {
    highlightTimers.hold = null;
    if (hl.style.display === "none") return;
    hl.classList.add("is-fading");
    hl.classList.remove("is-visible");
    highlightTimers.fade = setTimeout(() => {
      highlightTimers.fade = null;
      if (hl.style.display === "none") return;
      hl.style.display = "none";
      hl.classList.remove("is-fading");
      hideFloatingInputOverlay();
    }, HIGHLIGHT_FADE_MS);
  }, HIGHLIGHT_FLASH_HOLD_MS);
}


export function syncRectFlashToggleUi() {
  const btn = $("btnRectFlash");
  if (!btn) return;
  const on = !!state.highlightFlashMode;
  btn.checked = on;
}


export function syncRectAutoZoomToggleUi() {
  const btn = $("btnRectAutoZoom");
  if (!btn) return;
  btn.checked = !!state.rectAutoZoomEnabled;
  const lab = btn.closest(".rect-auto-zoom");
  if (lab) {
    lab.title = state.rectAutoZoomEnabled
      ? "Auto-zoom et centrage sur le rectangle au focus (Ctrl+Shift +/- pour zoomer, Ctrl+Shift flèches pour défiler)."
      : "Auto-zoom désactivé — le rectangle reste visible sans changer le zoom (Ctrl+Shift +/- pour zoomer, Ctrl+Shift flèches pour défiler).";
  }
}


export function applyRectAutoZoomMode(enabled) {
  state.rectAutoZoomEnabled = !!enabled;
  writeLs(RECT_AUTO_ZOOM_LS, state.rectAutoZoomEnabled ? "1" : "0");
  scheduleUserPreferencesSave({ rect_auto_zoom: !!state.rectAutoZoomEnabled });
  syncRectAutoZoomToggleUi();
}


export function syncSnippetToggleUi() {
  const btn = $("btnSnippetToggle");
  if (!btn) return;
  const on = !!state.snippetEnabled;
  btn.checked = on;
}


export function applySnippetToggleMode(enabled) {
  state.snippetEnabled = !!enabled;
  writeLs(SNIPPET_TOGGLE_MODE_LS, state.snippetEnabled ? "1" : "0");
  scheduleUserPreferencesSave({ snippet_enabled: !!state.snippetEnabled });
  syncSnippetToggleUi();
  if (!state.snippetEnabled) {
    hideOrigSnippets();
    return;
  }
  refreshActiveOrigSnippet();
}


export function applyRectFlashMode(enabled) {
  state.highlightFlashMode = !!enabled;
  writeLs(HIGHLIGHT_FLASH_MODE_LS, state.highlightFlashMode ? "1" : "0");
  scheduleUserPreferencesSave({ highlight_flash_mode: !!state.highlightFlashMode });
  syncRectFlashToggleUi();

  const activeRow = runtime.activeEditInput ? runtime.activeEditInput.closest(".field-table__row") : null;
  if (activeRow && activeRow.dataset.rect) {
    updateHighlight(activeRow.dataset.rect);
    return;
  }
  const hl = $("highlightRect");
  if (!hl || hl.style.display === "none") return;
  if (state.highlightFlashMode) applyHighlightVisibilityMode(hl);
  else setHighlightVisible(hl, true);
}

/**
 * Positionne le rectangle de surlignage.
 * Format rectStr: "L,T,R,B"
 */

export function updateHighlight(rectStr) {
  const hl = $("highlightRect");
  const img = $("pageImg");
  console.log("updateHighlight called with:", rectStr);
  if (!hl || !img || !rectStr) {
    if (hl) {
      clearHighlightTimers();
      hl.style.display = "none";
      hl.classList.remove("is-visible", "is-fading");
    }
    return;
  }

  const parts = rectStr.split(",").map((s) => parseInt(s.trim(), 10));
  if (parts.length !== 4 || parts.some(isNaN)) {
    hl.style.display = "none";
    return;
  }

  const [l, t, r, b] = parts;
  const shiftedL = l + HIGHLIGHT_OFFSET_X;
  const shiftedT = t + HIGHLIGHT_OFFSET_Y;
  const shiftedB = b + HIGHLIGHT_OFFSET_Y;
  const w = r - l;
  const h = b - t;

  // Positionnement
  hl.style.left = shiftedL + "px";
  hl.style.top = shiftedT + "px";
  hl.style.width = w + "px";
  hl.style.height = h + "px";
  hl.style.display = "block";
  applyHighlightVisibilityMode(hl);

  const fi = $("floatingInput");
  if (fi) {
    fi.style.left = shiftedL + "px";
    // DÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©caler depuis le bas du rectangle aprÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¨s correction d'alignement
    fi.style.top = (shiftedB + 28) + "px";
    // Largeur identique au champ
    fi.style.width = w + "px";
    fi.style.display = "block";
    fi.hidden = false;
  }


  if (!state.rectAutoZoomEnabled) return;

  const cx = shiftedL + w / 2;
  const cy = shiftedT + h / 2;
  const wrap = $("imgWrap");
  if (!wrap) return;

  const vw = wrap.clientWidth;
  const vh = wrap.clientHeight;
  const z = state.zoom / 100;
  state.panX = vw / 2 - cx * z;
  state.panY = vh / 2 - cy * z;
  applyImageTransform();
}


export function shouldForceUppercase(inputElem) {
  if (!inputElem) return false;
  const tr = inputElem.closest("tr");
  if (!tr) return false;
  const base = (tr.dataset.base || "").toLowerCase();
  return ["gender", "sexe", "statut", "status"].some(kw => base.includes(kw));
}


export function adjustFloatingInputFontSize() {
  const fl = $("floatingInput");
  if (!fl || fl.style.display === "none") return;
  fl.style.fontSize = "20pt";
  let size = 20;
  while (fl.scrollWidth > fl.clientWidth && size > 8) {
    size -= 1;
    fl.style.fontSize = size + "pt";
  }
}


export function syncFloatingInputTo(input) {
  const fl = $("floatingInput");
  if (!fl || !input) return;
  fl.value = input.value;
  adjustFloatingInputFontSize();
}


export function getFloatingOrigSnippetEl() {
  let root = $("floatingOrigSnippet");
  if (root) return root;
  root = document.createElement("div");
  root.id = "floatingOrigSnippet";
  root.className = "floating-orig-snippet";
  root.hidden = true;
  const img = document.createElement("img");
  img.id = "floatingOrigSnippetImg";
  img.className = "floating-orig-snippet__img";
  img.alt = "Extrait du champ actif";
  root.appendChild(img);
  document.body.appendChild(root);
  return root;
}


export function hideOrigSnippets() {
  const root = $("floatingOrigSnippet");
  if (!root) return;
  root.hidden = true;
  root.style.left = "";
  root.style.top = "";
  root.style.width = "";
  root.style.height = "";
  const img = $("floatingOrigSnippetImg");
  if (img) img.removeAttribute("src");
}


export function parseRectParts(rectStr) {
  if (!rectStr) return null;
  const parts = String(rectStr).split(",").map((s) => parseInt(s.trim(), 10));
  if (parts.length !== 4 || parts.some((n) => Number.isNaN(n))) return null;
  const [l, t, r, b] = parts;
  if (r <= l || b <= t) return null;
  return { l, t, r, b };
}


export function updateOrigSnippetForInput(input, rectStr) {
  if (!state.snippetEnabled) {
    hideOrigSnippets();
    return;
  }
  if (!input) {
    hideOrigSnippets();
    return;
  }
  const rect = parseRectParts(rectStr);
  const img = $("pageImg");
  if (!rect || !img || !img.complete) {
    hideOrigSnippets();
    return;
  }

  const sourceW = img.naturalWidth || img.width;
  const sourceH = img.naturalHeight || img.height;
  if (!sourceW || !sourceH) {
    hideOrigSnippets();
    return;
  }

  const srcLeft = rect.l + SNIPPET_CROP_OFFSET_X - SNIPPET_CROP_PAD_X;
  const srcTop = rect.t + SNIPPET_CROP_OFFSET_Y - SNIPPET_CROP_PAD_Y;
  const srcRight = rect.r + SNIPPET_CROP_OFFSET_X + SNIPPET_CROP_PAD_X;
  const srcBottom = rect.b + SNIPPET_CROP_OFFSET_Y + SNIPPET_CROP_PAD_Y;
  const sx = Math.max(0, Math.min(sourceW - 1, srcLeft));
  const sy = Math.max(0, Math.min(sourceH - 1, srcTop));
  const boundedRight = Math.max(sx + 1, Math.min(sourceW, srcRight));
  const boundedBottom = Math.max(sy + 1, Math.min(sourceH, srcBottom));
  const sw = Math.max(1, boundedRight - sx);
  const sh = Math.max(1, boundedBottom - sy);

  const canvas = document.createElement("canvas");
  canvas.width = sw;
  canvas.height = sh;
  const ctx = canvas.getContext("2d");
  if (!ctx) {
    hideOrigSnippets();
    return;
  }
  ctx.drawImage(img, sx, sy, sw, sh, 0, 0, sw, sh);
  const root = getFloatingOrigSnippetEl();
  const snippetImg = $("floatingOrigSnippetImg");
  if (!snippetImg) {
    hideOrigSnippets();
    return;
  }

  const zoom = ORIG_FOCUS_ZOOM / 100;
  const snippetScale = Number.isFinite(state.snippetSizeMultiplier)
    ? state.snippetSizeMultiplier
    : SNIPPET_SIZE_MULTIPLIER_DEFAULT;
  const width = Math.max(72, Math.round(sw * zoom * snippetScale));
  const height = Math.max(26, Math.round(sh * zoom * snippetScale));
  const viewportPad = 8;
  const margin = 6;
  const inputRect = input.getBoundingClientRect();
  let left = inputRect.left;
  let top = inputRect.top - height - margin;
  const maxLeft = window.innerWidth - width - viewportPad;
  if (left > maxLeft) left = maxLeft;
  if (left < viewportPad) left = viewportPad;
  if (top < viewportPad) top = Math.min(window.innerHeight - height - viewportPad, inputRect.bottom + margin);

  snippetImg.src = canvas.toDataURL("image/png");
  root.style.left = Math.round(left) + "px";
  root.style.top = Math.round(top) + "px";
  root.style.width = width + "px";
  root.style.height = height + "px";
  root.hidden = false;
}


export function refreshActiveOrigSnippet() {
  if (!state.snippetEnabled) {
    hideOrigSnippets();
    return;
  }
  if (!runtime.activeEditInput) {
    hideOrigSnippets();
    return;
  }
  const row = runtime.activeEditInput.closest(".field-table__row");
  if (!row || !row.dataset.rect) {
    hideOrigSnippets();
    return;
  }
  updateOrigSnippetForInput(runtime.activeEditInput, row.dataset.rect);
}

// DÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©tection de la prÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©fÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©rence utilisateur (souris ou frappe manuelle)
document.addEventListener("mousedown", (e) => {
  if (e.target.closest && e.target.closest(".field-table__row")) {
    const row = e.target.closest(".field-table__row");
    // On n'enregistre le choix "table" QUE si l'ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©tiquette ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©tait disponible (prÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©sence de coordonnÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©es)
    if (row && row.dataset.rect) {
      runtime.userFocusPreference = "table";
    }
  }
});

document.addEventListener("keydown", (e) => {
  // ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â°vite de traiter `Enter` et `Tab` ici car ils gÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¨rent la progression naturelle
  if (e.key === "Enter" || e.key === "Tab") return; 
  
  if (e.target.classList && e.target.classList.contains("field-table__input")) {
    const row = e.target.closest(".field-table__row");
    // On n'enregistre le choix "table" QUE si l'ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©tiquette ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©tait disponible
    if (row && row.dataset.rect) {
      runtime.userFocusPreference = "table";
    }
  }
});

// ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â°vÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©nements de focus pour le surlignage et le champ flottant
document.addEventListener("focusin", (e) => {
  const fi = $("floatingInput");
  
  // Si c'est le champ flottant qui prend le focus, on enregistre la prÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©fÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©rence et on garde la ligne active
// Retirer le style actif partout
  document.querySelectorAll(".field-table__row.row-active").forEach(r => r.classList.remove("row-active"));

  const isEditInput = e.target.classList && e.target.classList.contains("field-table__input") && !e.target.readOnly;
  const row = e.target.closest(".field-table__row");

  if (row && isEditInput) {
    if (isOrigEditorTarget(e.target) && isOrigInputSkippable(e.target)) {
      const next = findNextNonSkippableOrigInput(e.target, 1);
      if (next && next !== e.target) {
        requestAnimationFrame(() => next.focus());
        return;
      }
    }
    runtime.activeEditInput = e.target;
    row.classList.add("row-active");
    // Scroll doux pour centrer la ligne dans le tableau
    row.scrollIntoView({ behavior: 'smooth', block: 'center' });

    if (row.dataset.rect) {
      if (state.rectAutoZoomEnabled) {
        state.zoom = ORIG_FOCUS_ZOOM;
      }
      updateHighlight(row.dataset.rect);
      syncFloatingInputTo(runtime.activeEditInput);
      updateOrigSnippetForInput(runtime.activeEditInput, row.dataset.rect);
      // Vole le focus pour le mettre dans le champ flottant si des coordonnÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©es existent et si c'est la prÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©fÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©rence
} else {
      updateHighlight(null);
      if (fi) {
        fi.style.display = "none";
        fi.hidden = true;
      }
      hideOrigSnippets();
    }
  } else {
    runtime.activeEditInput = null;
    updateHighlight(null);
    if (fi) {
      fi.style.display = "none";
      fi.hidden = true;
    }
    hideOrigSnippets();
  }
});

document.addEventListener("focusout", (e) => {
  const fi = $("floatingInput");
  // Ignorer si on bascule entre l'ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©dit et le flottant
  if (e.relatedTarget === fi || (e.relatedTarget && e.relatedTarget.classList && e.relatedTarget.classList.contains("field-table__input") && !e.relatedTarget.readOnly)) {
    return;
  }
  setTimeout(() => {
    if (!document.activeElement.closest(".field-table__row") && document.activeElement !== fi) {
      updateHighlight(null);
      if (fi) {
        fi.style.display = "none";
        fi.hidden = true;
      }
      hideOrigSnippets();
      document.querySelectorAll(".field-table__row.row-active").forEach(r => r.classList.remove("row-active"));
      runtime.activeEditInput = null;
    }
  }, 50);
});

// Synchro depuis le champ flottant vers le champ ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©dit

export function getNavigableOrigInputs() {
  return Array.from(
    document.querySelectorAll('.field-table__input[data-col$="_orig"]')
  ).filter((inp) => {
    if (inp.readOnly) return false;
    const tr = inp.closest("tr");
    // On ignore les lignes masquÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©es (ex: filtre "masquer les champs vides").
    if (tr && tr.hidden) return false;
    return true;
  });
}


export function isOrigInputValidated(input) {
  if (!input || !input.closest) return false;
  const row = input.closest("tr.field-table__row");
  if (!row || row.dataset.hasThreshold !== "1") return false;
  const base = String(row.dataset.base || "");
  const confInput = row.querySelector('input.field-table__input[data-col$="_conf"]');
  const confVal =
    row.dataset.confRaw != null && row.dataset.confRaw !== ""
      ? row.dataset.confRaw
      : confInput
        ? confInput.value
        : "";
  const meta = {
    threshold:
      row.dataset.rekeyThreshold != null && row.dataset.rekeyThreshold !== ""
        ? row.dataset.rekeyThreshold
        : null,
  };
  return fieldConfMeetsRekeyThreshold(base, confVal, meta);
}


export function isOrigInputAtSkipValue(input) {
  if (!input || !input.closest) return false;
  if (resolveFieldConfigRootKind(getCurrentProjectKind()) !== "Census") return false;
  const row = input.closest("tr.field-table__row");
  if (!row) return false;
  const base = String(row.dataset.base || "");
  if (!CENSUS_AT_SKIP_FIELD_BASES.has(base)) return false;
  return String(input.value || "").trim() === "@";
}


/** Validated rows and census « @ » markers must not be overwritten by auto-fill. */
export function isOrigInputProtectedFromAutofill(input) {
  return isOrigInputValidated(input) || isOrigInputAtSkipValue(input);
}


export function shouldAutoSkipOrigInput(input) {
  if (!state.autoSkipValidatedRows) return false;
  return isOrigInputValidated(input);
}


export function shouldAutoSkipAtOrigInput(input) {
  if (!state.autoSkipAtFields) return false;
  return isOrigInputAtSkipValue(input);
}


export function isOrigInputSkippable(input) {
  return shouldAutoSkipOrigInput(input) || shouldAutoSkipAtOrigInput(input);
}


export function findNextNonSkippableOrigInput(currentInput, step = 1) {
  const allInputs = getNavigableOrigInputs();
  let idx = allInputs.indexOf(currentInput);
  if (idx === -1) return null;
  let nextIdx = idx + step;
  while (nextIdx >= 0 && nextIdx < allInputs.length) {
    const candidate = allInputs[nextIdx];
    if (!isOrigInputSkippable(candidate)) return candidate;
    nextIdx += step;
  }
  return null;
}


export function getOrigInputGroupKey(input) {
  if (!input || !input.closest) return null;
  const row = input.closest(".field-table__row");
  if (!row) return null;
  if (row.dataset.recordId) return "record:" + row.dataset.recordId;
  if (row.dataset.headerId) return "header:" + row.dataset.headerId;
  return "header";
}


export function getOrderedOrigInputGroups() {
  const inputs = getNavigableOrigInputs();
  const seen = new Set();
  const groups = [];
  inputs.forEach((inp) => {
    const key = getOrigInputGroupKey(inp);
    if (!key || seen.has(key)) return;
    seen.add(key);
    groups.push({ key, firstInput: inp });
  });
  return groups;
}


export function focusFirstHeaderOrigInput() {
  const firstHeaderOrig = document.querySelector(
    '#headerFields .field-table__input[data-col$="_orig"]:not([readonly])'
  );
  if (!firstHeaderOrig || typeof firstHeaderOrig.focus !== "function") return false;
  firstHeaderOrig.focus();
  return true;
}


/** Focus the first editable orig. field of a record card (after insert / undo-redo). */
export function focusFirstRecordOrigInput(recordId) {
  if (recordId == null) return false;
  const card = document.querySelector(
    `.record-card[data-record-id="${String(recordId)}"]`
  );
  if (!card) return false;
  const inputs = card.querySelectorAll(
    '.field-table__input[data-col$="_orig"]:not([readonly])'
  );
  for (const inp of inputs) {
    const tr = inp.closest("tr");
    if (tr && tr.hidden) continue;
    if (isOrigInputSkippable(inp)) continue;
    inp.focus();
    return true;
  }
  for (const inp of inputs) {
    const tr = inp.closest("tr");
    if (tr && tr.hidden) continue;
    inp.focus();
    return true;
  }
  if (typeof card.scrollIntoView === "function") {
    card.scrollIntoView({ behavior: "smooth", block: "nearest" });
  }
  return false;
}


export function isOrigEditorTarget(target) {
  return !!(
    target &&
    target.classList &&
    target.classList.contains("field-table__input") &&
    !target.readOnly &&
    String(target.dataset.col || "").endsWith("_orig")
  );
}

/** Persist orig. field edits to history + server (immediate flush); used on blur and Enter. */

export function commitOrigFieldImmediately(input) {
  if (!isOrigEditorTarget(input) || !isOrigInputPersistable(input)) return;
  commitHistoryActionForInput(input);
  queueAutosaveForInput(input, true);
}

document.addEventListener("keydown", (e) => {
  handleSuggestionKeydown(e);
});

document.addEventListener("keydown", (e) => {
  if (!isOrigEditorTarget(e.target)) return;
  if (!e.ctrlKey || e.altKey || e.shiftKey || e.metaKey) return;
  if (e.code !== "KeyH" && String(e.key || "").toLowerCase() !== "h") return;
  if (!restoreOrigFromHwr(e.target)) return;
  e.preventDefault();
  syncFloatingInputTo(e.target);
  commitOrigFieldImmediately(e.target);
});


export function adjustActiveSnippetSize(direction) {
  if (!runtime.activeEditInput || (direction !== "grow" && direction !== "shrink")) return false;
  const row = runtime.activeEditInput.closest(".field-table__row");
  if (!row || !row.dataset.rect) return false;

  const current = Number.isFinite(state.snippetSizeMultiplier)
    ? state.snippetSizeMultiplier
    : SNIPPET_SIZE_MULTIPLIER_DEFAULT;
  const nextRaw = direction === "grow"
    ? current * SNIPPET_SIZE_SCALE_UP_FACTOR
    : current * SNIPPET_SIZE_SCALE_DOWN_FACTOR;
  const next = direction === "grow"
    ? Math.min(SNIPPET_SIZE_MULTIPLIER_MAX, nextRaw)
    : nextRaw;
  if (Math.abs(next - current) < 1e-6) return true;
  state.snippetSizeMultiplier = Math.round(next * 100) / 100;
  updateOrigSnippetForInput(runtime.activeEditInput, row.dataset.rect);
  return true;
}

// Navigation fluide avec Enter/Tab/Ctrl+Arrow: orig. -> orig.
document.addEventListener("keydown", (e) => {
  const key = String(e.key || "");
  const lowerKey = key.toLowerCase();
  const isEnterOrTab = key === "Enter" || key === "Tab";
  const isCtrlArrowNav =
    e.ctrlKey &&
    !e.altKey &&
    !e.shiftKey &&
    (key === "ArrowUp" || key === "ArrowDown");
  const isCtrlArrowGroupNav =
    e.ctrlKey &&
    !e.altKey &&
    !e.shiftKey &&
    (key === "ArrowLeft" || key === "ArrowRight");
  const isSnippetGrowShortcut =
    e.ctrlKey &&
    !e.altKey &&
    (key === "+" || key === "=" || lowerKey === "add" || key === "NumpadAdd");
  const isSnippetShrinkShortcut =
    e.ctrlKey &&
    !e.altKey &&
    !e.shiftKey &&
    (key === "-" || lowerKey === "subtract" || key === "NumpadSubtract");
  if (!isEnterOrTab && !isCtrlArrowNav && !isCtrlArrowGroupNav && !isSnippetGrowShortcut && !isSnippetShrinkShortcut) return;

  if (!isOrigEditorTarget(e.target)) return;

  if (isSnippetGrowShortcut || isSnippetShrinkShortcut) {
    if (adjustActiveSnippetSize(isSnippetGrowShortcut ? "grow" : "shrink")) {
      e.preventDefault();
    }
    return;
  }

  if (isCtrlArrowGroupNav) {
    const groups = getOrderedOrigInputGroups();
    if (!groups.length || !runtime.activeEditInput) return;
    const currentKey = getOrigInputGroupKey(runtime.activeEditInput);
    const idx = groups.findIndex((g) => g.key === currentKey);
    if (idx === -1) return;
    const nextIdx = idx + (key === "ArrowRight" ? 1 : -1);
    if (nextIdx >= 0 && nextIdx < groups.length) {
      e.preventDefault();
      const groupKey = groups[nextIdx].key;
      const groupInputs = getNavigableOrigInputs().filter((inp) => getOrigInputGroupKey(inp) === groupKey);
      const firstEditable = groupInputs.find((inp) => !isOrigInputSkippable(inp));
      (firstEditable || groups[nextIdx].firstInput).focus();
    }
    return;
  }

  if (!runtime.activeEditInput) return;
  const currentInput = runtime.activeEditInput;
  const allInputs = getNavigableOrigInputs();
  const idx = allInputs.indexOf(currentInput);
  if (idx === -1) return;

  e.preventDefault();
  const step = isCtrlArrowNav
    ? (key === "ArrowUp" ? -1 : 1)
    : (key === "Tab" && e.shiftKey ? -1 : 1);
  let nextIdx = idx + step;
  while (nextIdx >= 0 && nextIdx < allInputs.length) {
    const candidate = allInputs[nextIdx];
    if (!isOrigInputSkippable(candidate)) break;
    nextIdx += step;
  }
  if (key === "Enter") {
    commitOrigFieldImmediately(currentInput);
  }
  if (nextIdx >= 0 && nextIdx < allInputs.length) {
    // Ceci dÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©clenche focusin() et la logique de surlignage automatiquement.
    allInputs[nextIdx].focus();
  }
});

/**
 * Zoom (%) en conservant le point sous (clientX, clientY).
 * Centre de rÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©fÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©rence : image affichÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©e (#pageImg) si dispo, sinon #imgPan.
 */

export function zoomTowardClientPoint(newZoomPct, clientX, clientY) {
  const wrap = $("imgWrap");
  const panEl = $("imgPan");
  if (!wrap || !panEl || panEl.hidden) return;

  const oldZ = state.zoom / 100;
  const nz = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, newZoomPct));
  const newZ = nz / 100;
  if (Math.abs(newZ - oldZ) < 1e-9) return;

  const wrapRect = wrap.getBoundingClientRect();
  // Point relatif au viewport (le point sous la souris)
  const mouseX = clientX - wrapRect.left;
  const mouseY = clientY - wrapRect.top;

  // Formule pour zoomer vers un point fixe (mouseX, mouseY) avec origin 0,0 :
  // nouveau_pan = mouse - (mouse - ancien_pan) * (nouveau_zoom / ancien_zoom)
  const factor = newZ / oldZ;
  state.panX = mouseX - (mouseX - state.panX) * factor;
  state.panY = mouseY - (mouseY - state.panY) * factor;
  state.zoom = nz;

  applyImageTransform();
}

/**
 * Mesure la largeur intrinsÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¨que (ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©vite la boucle : largeur colonne ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¾Ãƒâ€šÃ‚Â¢ retours ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â  la ligne ÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â ÃƒÆ’Ã†â€™Ãƒâ€šÃ‚Â¢ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡Ãƒâ€šÃ‚Â¬ÃƒÆ’Ã‚Â¢ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¾Ãƒâ€šÃ‚Â¢ scrollWidth qui gonfle).
 */

export function adjustImageZoom(direction) {
  const panEl = $("imgPan");
  const wrap = $("imgWrap");
  if (!panEl || panEl.hidden || !wrap) return false;
  const step = IMAGE_ZOOM_KEYBOARD_STEP * (direction >= 0 ? 1 : -1);
  const r = wrap.getBoundingClientRect();
  zoomTowardClientPoint(state.zoom + step, r.left + r.width / 2, r.top + r.height / 2);
  return true;
}


export function panImage(deltaX, deltaY) {
  const panEl = $("imgPan");
  if (!panEl || panEl.hidden) return false;
  state.panX += IMAGE_PAN_KEYBOARD_STEP * deltaX;
  state.panY += IMAGE_PAN_KEYBOARD_STEP * deltaY;
  applyImageTransform();
  return true;
}


export function fitImageToViewAfterLoad() {
  const wrap = $("imgWrap");
  const img = $("pageImg");
  if (!wrap || !img || !img.naturalWidth) return;
  img.style.width = img.naturalWidth + "px";
  img.style.height = img.naturalHeight + "px";
  img.style.maxWidth = "none";
  img.style.maxHeight = "none";
  const cs = window.getComputedStyle(wrap);
  const px =
    parseFloat(cs.paddingLeft || "0") + parseFloat(cs.paddingRight || "0");
  const py =
    parseFloat(cs.paddingTop || "0") + parseFloat(cs.paddingBottom || "0");
  const ww = Math.max(8, wrap.clientWidth - px);
  const wh = Math.max(8, wrap.clientHeight - py);
  const nw = img.naturalWidth;
  const nh = img.naturalHeight;
  const fit = Math.min(ww / nw, wh / nh);
  let z = Math.round(Math.min(fit, 1) * 100);
  z = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, z));
  state.zoom = z;
  const scale = z / 100;
  state.panX = (ww - nw * scale) / 2; // Centrer horizontalement au dÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©part
  state.panY = (wh - nh * scale) / 2; // Centrer verticalement au dÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©part
  applyImageTransform();
}


export function fitImageToViewPreservingZoom() {
  const wrap = $("imgWrap");
  const img = $("pageImg");
  if (!wrap || !img || !img.naturalWidth) return;
  img.style.width = img.naturalWidth + "px";
  img.style.height = img.naturalHeight + "px";
  
  // Si on a dÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â©jÃƒÆ’Ã†â€™Ãƒâ€ Ã¢â‚¬â„¢ÃƒÆ’Ã¢â‚¬Â ÃƒÂ¢Ã¢â€šÂ¬Ã¢â€žÂ¢ÃƒÆ’Ã†â€™ÃƒÂ¢Ã¢â€šÂ¬Ã…Â¡ÃƒÆ’Ã¢â‚¬Å¡Ãƒâ€šÃ‚Â  un zoom (pas le premier chargement), on garde le zoom actuel.
  // On se contente de recentrer l'image.
  const cs = window.getComputedStyle(wrap);
  const px = parseFloat(cs.paddingLeft || "0") + parseFloat(cs.paddingRight || "0");
  const py = parseFloat(cs.paddingTop || "0") + parseFloat(cs.paddingBottom || "0");
  const ww = Math.max(8, wrap.clientWidth - px);
  const wh = Math.max(8, wrap.clientHeight - py);
  const z = state.zoom / 100;
  
  state.panX = (ww - img.naturalWidth * z) / 2;
  state.panY = (wh - img.naturalHeight * z) / 2;
  applyImageTransform();
}


export function initImagePanZoom() {
  const wrap = $("imgWrap");
  const panEl = $("imgPan");
  if (!wrap || !panEl) return;

  wrap.addEventListener(
    "wheel",
    (e) => {
      if (panEl.hidden) return;
      e.preventDefault();
      const dy = e.deltaY;
      const dir = dy > 0 ? -1 : 1;
      const mag = Math.min(120, Math.abs(dy));
      const step = Math.max(5, Math.min(14, Math.round(5 + mag / 10)));
      const next = Math.max(
        ZOOM_MIN,
        Math.min(ZOOM_MAX, state.zoom + dir * step)
      );
      zoomTowardClientPoint(next, e.clientX, e.clientY);
    },
    { passive: false }
  );

  wrap.addEventListener("mousedown", (e) => {
    if (panEl.hidden || e.button !== 0) return;
    if (e.target.closest("input, button, a")) return;
    e.preventDefault();
    state.panDrag = {
      x: e.clientX,
      y: e.clientY,
      panX: state.panX,
      panY: state.panY,
    };
    wrap.classList.add("is-panning");
  });

  document.addEventListener("mousemove", (e) => {
    if (!state.panDrag) return;
    state.panX = state.panDrag.panX + (e.clientX - state.panDrag.x);
    state.panY = state.panDrag.panY + (e.clientY - state.panDrag.y);
    applyImageTransform();
  });

  document.addEventListener("mouseup", () => {
    if (state.panDrag) {
      state.panDrag = null;
      wrap.classList.remove("is-panning");
    }
  });
}


export function commitZoomFromInput() {
  const el = $("zoomValueDisplay");
  if (!el) return;
  const raw = String(el.value || "")
    .replace(/%/g, "")
    .trim();
  const n = parseInt(raw, 10);
  const zv = String(state.zoom);
  if (!Number.isFinite(n)) {
    el.value = zv;
    return;
  }
  const next = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, n));
  const wrap = $("imgWrap");
  const panEl = $("imgPan");
  if (wrap && panEl && !panEl.hidden) {
    const r = wrap.getBoundingClientRect();
    zoomTowardClientPoint(next, r.left + r.width / 2, r.top + r.height / 2);
  } else {
    state.zoom = next;
    applyImageTransform();
  }
}


export function initZoomControls() {
  const zr = $("zoomRange");
  if (zr) {
    zr.min = String(ZOOM_MIN);
    zr.max = String(ZOOM_MAX);
  }
  const zmin = $("zoomLabelMin");
  const zmax = $("zoomLabelMax");
  if (zmin) zmin.textContent = ZOOM_MIN + "%";
  if (zmax) zmax.textContent = ZOOM_MAX + "%";
  const disp = $("zoomValueDisplay");
  if (disp) disp.value = String(state.zoom);
  if (disp) {
    disp.addEventListener("focus", () => {
      runtime.zoomValueInputEditing = true;
    });
    disp.addEventListener("blur", () => {
      runtime.zoomValueInputEditing = false;
      commitZoomFromInput();
    });
    disp.addEventListener("keydown", (e) => {
      if (e.key === "Enter") {
        e.preventDefault();
        disp.blur();
      }
      if (e.key === "Escape") {
        e.preventDefault();
        runtime.zoomValueInputEditing = false;
        disp.value = String(state.zoom);
        disp.blur();
      }
    });
  }
}
