const DEFAULT_INSTITUTION_TYPE = "bank"; const QUICK_REQUEST_ENABLED = false; const REGISTRATION_SESSION_KEY = "registrationWorkflowStateV1"; const REGISTRATION_STEPS = ["1", "2", "3", "3b", "4"]; const REGISTRATION_STEP_PATHS = { 1: "/registration/step1", 2: "/registration/step2", 3: "/registration/step3", "3b": "/registration/step3b", 4: "/registration/step4", }; const helperContent = { step1: { text: "Select Bank or Credit Union to begin.", anchorId: "anchor-step1", target: () => els.typeToggle, }, step2: { text: "Type the institution name, add optional city/state hints, then choose the official match.", anchorId: "anchor-step2", target: () => els.subjectSearch, }, step2b: { text: [ "Optional: Add up to three additional institutions for a full comparative credit analysis, including Reg F documentation and supporting credit materials. Click Next when finished.", "0 of 3 additional institutions selected.", ].join("\n\n"), anchorId: "anchor-step2b", target: () => els.peerSearches[0], }, step3: { text: "Select the reporting periods below.", anchorId: "anchor-step3", target: () => els.quarterSelects.find((select) => !select.disabled) || els.quarterSelects[0], }, }; const state = { institutionType: DEFAULT_INSTITUTION_TYPE, subject: null, additional: [null, null, null], validation: { institutionTypeSelected: false, subjectSelected: false, peersSelected: 0, reportFormatSelected: true, reportingPeriodValid: false, userFieldsComplete: false, disclaimerAccepted: false, }, ui: { activeAccordion: "institutionTypePanel", routeStep: "1", reviewAutoCollapsed: false, reviewPrimed: false, submitting: false, submitSucceeded: false, wasSubmitReady: false, pendingFocus: "", isBrandingWindowOpen: false, submissionSuccessOpen: false, institutionTypeChosen: false, }, quickRequest: { open: false, applied: false, activeRow: 0, rows: [], }, logo: { logo_name: "", logo_data_url: "", logo_source_url: "", logo_search_url: "", autoExtract: true, colors: { primaryBg: "#0b3d66", cardBg: "#1f4f7a", textColor: "#f8fafc", accentColor: "#d4af37", }, }, }; const els = { form: document.getElementById("registrationForm"), pageTitle: document.getElementById("pageTitle"), pageSubtext: document.getElementById("pageSubtext"), workflowScroll: document.querySelector(".workflow-scroll"), leftRailStack: document.querySelector(".left-rail-stack"), workflowScrollHint: document.getElementById("workflowScrollHint"), helperPanel: document.getElementById("helperPanel"), helperText: document.getElementById("helperText"), helperArrow: document.getElementById("helperArrow"), typeButtons: Array.from(document.querySelectorAll(".type-option")), typeToggle: document.querySelector(".type-toggle"), typeWarning: document.getElementById("typeWarning"), clearSelectionsButton: document.getElementById("clearSelectionsButton"), subjectSearch: document.getElementById("subjectSearch"), subjectStateHint: document.getElementById("subjectStateHint"), subjectCityHint: document.getElementById("subjectCityHint"), subjectSearchButton: document.getElementById("subjectSearchButton"), subjectNextIndicator: document.getElementById("subjectNextIndicator"), subjectResultsHint: document.getElementById("subjectResultsHint"), subjectResults: document.getElementById("subjectResults"), subjectSelection: document.getElementById("subjectSelection"), logoPanel: document.getElementById("logoPanel"), logoLocked: document.getElementById("logoLocked"), brandingStageShell: document.getElementById("brandingStageShell"), brandingStage: document.getElementById("brandingStage"), logoControls: document.getElementById("logoControls"), logoSearchQuery: document.getElementById("logoSearchQuery"), logoSearchButton: document.getElementById("logoSearchButton"), logoUpload: document.getElementById("logoUpload"), logoPreview: document.getElementById("logoPreview"), logoSelectionStatus: document.getElementById("logoSelectionStatus"), autoColorToggle: document.getElementById("autoColorToggle"), resetBrandingButton: document.getElementById("resetBrandingButton"), presetCards: Array.from(document.querySelectorAll(".preset-card")), brandPreviewInstitution: document.getElementById("brandPreviewInstitution"), brandPreviewTitle: document.getElementById("brandPreviewTitle"), brandPreviewSubtitle: document.getElementById("brandPreviewSubtitle"), brandPreview: document.getElementById("brandPreview"), brandPreviewLogo: document.getElementById("brandPreviewLogo"), brandingCloseContinueButton: document.getElementById("brandingCloseContinueButton"), peerSlots: Array.from(document.querySelectorAll(".peer-slot")), peerSearches: Array.from(document.querySelectorAll(".peer-search")), peerStateHints: Array.from(document.querySelectorAll(".peer-state-hint")), peerCityHints: Array.from(document.querySelectorAll(".peer-city-hint")), peerSearchButtons: Array.from(document.querySelectorAll(".peer-search-button")), peerResults: Array.from(document.querySelectorAll(".peer-results")), peerSelections: Array.from(document.querySelectorAll(".peer-selection")), additionalHint: document.getElementById("additionalHint"), additionalCounter: document.getElementById("additionalCounter"), reportPanel: document.getElementById("reportPanel"), reportFormatChoice: document.getElementById("reportFormatChoice"), reportFormatModes: Array.from(document.querySelectorAll('input[name="reportFormatMode"]')), quarterSelects: Array.from(document.querySelectorAll(".quarter-select")), periodHint: document.getElementById("periodHint"), periodError: document.getElementById("periodError"), submitButton: document.getElementById("submitButton"), submitHelper: document.getElementById("submitHelper"), formStatus: document.getElementById("formStatus"), disclaimerAck: document.getElementById("disclaimerAck"), colorInputs: { primaryBg: document.getElementById("primaryBg"), cardBg: document.getElementById("cardBg"), textColor: document.getElementById("textColor"), accentColor: document.getElementById("accentColor"), }, colorHexInputs: { primaryBg: document.getElementById("primaryBgHex"), cardBg: document.getElementById("cardBgHex"), textColor: document.getElementById("textColorHex"), accentColor: document.getElementById("accentColorHex"), }, accordionPanels: Array.from(document.querySelectorAll(".accordion-panel")), accordionTriggers: Array.from(document.querySelectorAll(".accordion-trigger")), stageTabs: Array.from(document.querySelectorAll(".stage-tab")), stepSections: Array.from(document.querySelectorAll("[data-step-section]")), stepBackButton: document.getElementById("stepBackButton"), stepNextButton: document.getElementById("stepNextButton"), progressFill: document.getElementById("progressFill"), completionPercent: document.getElementById("completionPercent"), statuses: { type: document.getElementById("typeStatus"), subject: document.getElementById("subjectStatus"), additional: document.getElementById("additionalStatus"), period: document.getElementById("periodStatus"), logo: document.getElementById("logoStatus"), review: document.getElementById("reviewStatus"), }, quickRequest: { modal: document.getElementById("quickRequestModal"), summaryCard: document.getElementById("quickRequestSummaryCard"), summaryTitle: document.getElementById("quickRequestSummaryTitle"), summaryMeta: document.getElementById("quickRequestSummaryMeta"), summaryList: document.getElementById("quickRequestSummaryList"), openButton: document.getElementById("quickRequestOpenButton"), editButton: document.getElementById("quickRequestEditButton"), modalStatus: document.getElementById("quickRequestModalStatus"), gridBody: document.getElementById("quickRequestGridBody"), addRowButton: document.getElementById("quickRequestAddRowButton"), cancelButton: document.getElementById("quickRequestCancelButton"), applyButton: document.getElementById("quickRequestApplyButton"), typeInputs: Array.from(document.querySelectorAll('input[name="quickRequestType"]')), idLabels: Array.from(document.querySelectorAll(".quick-id-label")), }, readiness: { type: document.getElementById("readyType"), subject: document.getElementById("readySubject"), additional: document.getElementById("readyAdditional"), period: document.getElementById("readyPeriod"), user: document.getElementById("readyUser"), disclaimer: document.getElementById("readyDisclaimer"), }, submissionSuccess: { modal: document.getElementById("submissionSuccessModal"), closeIcon: document.getElementById("submissionSuccessCloseIcon"), closeButton: document.getElementById("submissionSuccessCloseButton"), }, notesModal: { modal: document.getElementById("notesModal"), toggleButton: document.getElementById("notesToggleButton"), closeButton: document.getElementById("notesModalCloseButton"), inlinePreview: document.getElementById("notesInlinePreview"), }, submitSummary: { subject: document.getElementById("submitSummarySubject"), additional: document.getElementById("submitSummaryAdditional"), period: document.getElementById("submitSummaryPeriod"), logo: document.getElementById("submitSummaryLogo"), colors: document.getElementById("submitSummaryColors"), theme: document.getElementById("submitSummaryTheme"), notes: document.getElementById("submitSummaryNotes"), }, submitReadiness: { type: document.getElementById("submitReadyType"), subject: document.getElementById("submitReadySubject"), additional: document.getElementById("submitReadyAdditional"), period: document.getElementById("submitReadyPeriod"), user: document.getElementById("submitReadyUser"), disclaimer: document.getElementById("submitReadyDisclaimer"), }, selectionSummary: { institutionType: document.getElementById("summaryInstitutionType"), subjectInstitution: document.getElementById("summarySubjectInstitution"), additionalInstitutions: document.getElementById("summaryAdditionalInstitutions"), reportFormat: document.getElementById("summaryReportFormat"), logo: document.getElementById("summaryLogo"), theme: document.getElementById("summaryTheme"), colors: document.getElementById("summaryColors"), notes: document.getElementById("summaryNotes"), logoPreview: document.getElementById("summaryLogoPreview"), }, }; let brandingCloseTimer = null; let logoSearchPopupWindow = null; let logoDropZoneReady = false; let pendingLogoSearchTimer = null; let logoSearchRequestId = 0; const DEFAULT_SUBMIT_BUTTON_TEXT = "Submit Request"; const SUCCESS_SUBMIT_BUTTON_TEXT = "Request Sent"; const MAX_INLINE_LOGO_DATA_URL_LENGTH = 180000; const SLOW_SUBMIT_NOTICE_MS = 20000; const SUBMIT_CONFIRMATION_TIMEOUT_MS = 90000; const LOGO_SEARCH_STABILIZATION_MS = 220; const REQUIRED_USER_FIELD_IDS = ["firstName", "lastName", "email", "phone", "organization"]; const SEARCH_DISPATCH_DEDUPE_WINDOW_MS = 350; let lastSubjectSearchDispatch = { value: "", ts: 0 }; const lastPeerSearchDispatchByIndex = new Map(); let searchRequestCounter = 0; let activeSubjectSearchRequestId = 0; const activePeerSearchRequestByIndex = new Map(); let subjectResultsLocked = false; let helperActiveAnchorId = ""; let helperActiveTargetElement = null; let helperObservedTargetElement = null; let helperPositionFrame = 0; let helperMutationObserver = null; let helperResizeObserver = null; function closeLogoSearchWindow({ aggressive = false } = {}) { const targetWindow = logoSearchPopupWindow; if (!targetWindow || targetWindow.closed) { logoSearchPopupWindow = null; return; } try { targetWindow.close(); } catch (error) { // Ignore browser restrictions; we'll optionally retry in aggressive mode. } if (aggressive && targetWindow && !targetWindow.closed) { // Some browsers can delay close on cross-window drag flows; retry once. setTimeout(() => { try { targetWindow.close(); } catch (error) { // Ignore - nothing else required. } if (logoSearchPopupWindow === targetWindow && targetWindow.closed) { logoSearchPopupWindow = null; } }, 0); } if (targetWindow.closed) { logoSearchPopupWindow = null; } } function isLogoDropZoneInteractive() { return Boolean(state.subject && els.logoControls && !els.logoControls.hidden && els.logoPreview); } function waitForLogoDropZoneReady(callback) { const requestId = logoSearchRequestId; setupLogoDropZone(); if (state.subject) { els.logoControls.hidden = false; setBrandingWindowOpen(true); syncBrandingWindowState(); } requestAnimationFrame(() => { if (requestId !== logoSearchRequestId) return; requestAnimationFrame(() => { if (requestId !== logoSearchRequestId) return; // Force layout after the branding panel expands so drag targets are mounted // before the external browser can take focus. els.logoPreview?.getBoundingClientRect(); pendingLogoSearchTimer = window.setTimeout(() => { if (requestId !== logoSearchRequestId) return; callback(); }, LOGO_SEARCH_STABILIZATION_MS); }); }); } function debounce(fn, delay = 350) { let timer; return (...args) => { clearTimeout(timer); timer = setTimeout(() => fn(...args), delay); }; } function escapeHtml(value) { return String(value || "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } function hasSelections() { return Boolean(state.subject) || selectedAdditional().length > 0; } function selectedAdditional() { return state.additional.filter(Boolean); } function formatTypeLabel(type) { return type === "credit_union" ? "Credit Union" : "Bank"; } function clampStep(step) { const normalized = String(step || "").toLowerCase(); const match = REGISTRATION_STEPS.find((candidate) => candidate === normalized); return match || "1"; } function detectRouteStep() { const match = String(window.location.pathname || "").match(/^\/registration\/step(1|2|3b|3|4)\/?$/i); if (match) return clampStep(match[1]); return "1"; } function persistSessionState() { const snapshot = { state: { institutionType: state.institutionType, subject: state.subject, additional: state.additional, logo: state.logo, ui: { activeAccordion: state.ui.activeAccordion, routeStep: state.ui.routeStep, institutionTypeChosen: state.ui.institutionTypeChosen, }, }, form: { firstName: document.getElementById("firstName")?.value || "", lastName: document.getElementById("lastName")?.value || "", title: document.getElementById("title")?.value || "", email: document.getElementById("email")?.value || "", phone: document.getElementById("phone")?.value || "", organization: document.getElementById("organization")?.value || "", notes: document.getElementById("notes")?.value || "", disclaimerAck: Boolean(els.disclaimerAck?.checked), reportFormatMode: selectedReportFormatMode(), quarters: els.quarterSelects.map((select) => select.value), subjectStateHint: els.subjectStateHint?.value || "", subjectCityHint: els.subjectCityHint?.value || "", peerSearches: els.peerSearches.map((input) => input.value), peerStateHints: els.peerStateHints.map((input) => input.value), peerCityHints: els.peerCityHints.map((input) => input.value), }, }; sessionStorage.setItem(REGISTRATION_SESSION_KEY, JSON.stringify(snapshot)); } function restoreSessionState() { try { const raw = sessionStorage.getItem(REGISTRATION_SESSION_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (parsed?.state) { state.institutionType = parsed.state.institutionType || DEFAULT_INSTITUTION_TYPE; state.subject = parsed.state.subject || null; state.additional = Array.isArray(parsed.state.additional) ? parsed.state.additional.slice(0, 3) : [null, null, null]; while (state.additional.length < 3) state.additional.push(null); if (parsed.state.logo) state.logo = parsed.state.logo; if (parsed.state.ui?.activeAccordion) state.ui.activeAccordion = parsed.state.ui.activeAccordion; state.ui.institutionTypeChosen = Boolean(parsed.state.ui?.institutionTypeChosen || parsed.state.subject); } const form = parsed?.form || {}; ["firstName", "lastName", "title", "email", "phone", "organization", "notes"].forEach((id) => { const input = document.getElementById(id); if (input && typeof form[id] === "string") input.value = form[id]; }); if (els.disclaimerAck) els.disclaimerAck.checked = Boolean(form.disclaimerAck); if (typeof form.subjectStateHint === "string" && els.subjectStateHint) els.subjectStateHint.value = form.subjectStateHint; if (typeof form.subjectCityHint === "string" && els.subjectCityHint) els.subjectCityHint.value = form.subjectCityHint; if (Array.isArray(form.peerSearches)) form.peerSearches.forEach((value, index) => { if (els.peerSearches[index]) els.peerSearches[index].value = value || ""; }); if (Array.isArray(form.peerStateHints)) form.peerStateHints.forEach((value, index) => { if (els.peerStateHints[index]) els.peerStateHints[index].value = value || ""; }); if (Array.isArray(form.peerCityHints)) form.peerCityHints.forEach((value, index) => { if (els.peerCityHints[index]) els.peerCityHints[index].value = value || ""; }); if (Array.isArray(form.quarters)) form.quarters.forEach((value, index) => { if (els.quarterSelects[index] && value) els.quarterSelects[index].value = value; }); if (form.reportFormatMode) { els.reportFormatModes.forEach((modeInput) => { modeInput.checked = modeInput.value === form.reportFormatMode; }); } } catch (error) { // Ignore invalid session payloads and continue with defaults. } } function goToRouteStep(step) { const nextStep = clampStep(step); persistSessionState(); const targetPath = REGISTRATION_STEP_PATHS[nextStep] || REGISTRATION_STEP_PATHS[1]; if (window.location.pathname !== targetPath) { window.location.href = targetPath; } } function stepIndex(stepId) { const index = REGISTRATION_STEPS.indexOf(clampStep(stepId)); return index >= 0 ? index : 0; } function quickRequestType() { const selected = els.quickRequest.typeInputs.find((input) => input.checked); return selected ? selected.value : DEFAULT_INSTITUTION_TYPE; } function createQuickRequestRow(role = "peer", index = 0) { return { role, institutionLabel: role === "subject" ? "Subject" : `Peer ${index}`, name: "", state: "", city: "", }; } function ensureQuickRequestRows() { if (!state.quickRequest.rows.length || state.quickRequest.rows[0]?.role !== "subject") { state.quickRequest.rows = [createQuickRequestRow("subject"), ...state.quickRequest.rows.filter((row) => row.role !== "subject")]; } const peerRows = state.quickRequest.rows.filter((row) => row.role === "peer").slice(0, 3); peerRows.forEach((row, index) => { row.institutionLabel = `Peer ${index + 1}`; }); state.quickRequest.rows = [state.quickRequest.rows[0], ...peerRows]; state.quickRequest.activeRow = Math.max(0, Math.min(state.quickRequest.activeRow || 0, state.quickRequest.rows.length - 1)); } function setQuickRequestStatus(message = "", kind = "") { if (!els.quickRequest.modalStatus) return; els.quickRequest.modalStatus.textContent = message; els.quickRequest.modalStatus.className = `quick-request-modal-status ${kind}`.trim(); } function syncQuickRequestTypeUI(type) { const activeType = type || quickRequestType(); els.quickRequest.typeInputs.forEach((input) => { input.checked = input.value === activeType; }); renderQuickRequestModal(); renderQuickRequestSummary(); } function institutionId(institution) { return institution.cert_or_charter || institution.rssd_id || institution.name; } function institutionIdLabel(institution) { const charter = institution?.cert_or_charter || ""; const rssd = institution?.rssd_id || ""; if (charter && rssd && charter !== rssd) { return `NCUA Charter: ${charter} | RSSD: ${rssd}`; } if (charter) return `NCUA Charter: ${charter}`; if (rssd) return `RSSD: ${rssd}`; return "ID not available"; } function normalizeMatchText(value) { return String(value || "") .toLowerCase() .replaceAll("&", " and ") .replace(/[^a-z0-9]+/g, " ") .replace(/\s+/g, " ") .trim(); } function parseLocationParts(location) { const raw = String(location || "").trim(); if (!raw) return { city: "", state: "" }; const parts = raw.split(",").map((part) => part.trim()).filter(Boolean); if (parts.length >= 2) { return { city: parts.slice(0, -1).join(", "), state: parts[parts.length - 1] }; } return { city: raw, state: "" }; } function buildOfficialSearchQueries(name, city = "", regionState = "") { const cleanName = String(name || "").trim(); const cleanCity = String(city || "").trim(); const cleanState = String(regionState || "").trim(); if (!cleanName) return []; const queries = []; const add = (value) => { const normalized = String(value || "").trim().replace(/\s+/g, " "); if (normalized && !queries.includes(normalized)) { queries.push(normalized); } }; add(cleanName); if (state.institutionType === "bank" && !/\bbank\b/i.test(cleanName)) { add(`${cleanName} Bank`); } if (cleanState) add(`${cleanName} ${cleanState}`); if (cleanCity) add(`${cleanName} ${cleanCity}`); if (cleanCity && cleanState) add(`${cleanName} ${cleanCity} ${cleanState}`); return queries; } function scoreOfficialMatch(item, queryName, queryCity = "", queryState = "") { const normalizedName = normalizeMatchText(queryName); const normalizedCity = normalizeMatchText(queryCity); const normalizedState = normalizeMatchText(queryState); const itemName = normalizeMatchText(item.name); const locationParts = parseLocationParts(item.location); const itemCity = normalizeMatchText(item.city || locationParts.city); const itemState = normalizeMatchText(item.state || locationParts.state); let score = 0; if (itemName === normalizedName) score += 120; else if (itemName.includes(normalizedName)) score += 80; else if (normalizedName.includes(itemName)) score += 55; if (normalizedState) { if (itemState === normalizedState) score += 45; else if (itemState.includes(normalizedState)) score += 20; else score -= 25; } if (normalizedCity) { if (itemCity === normalizedCity) score += 35; else if (itemCity.includes(normalizedCity)) score += 15; } return score; } function prioritizeOfficialResults(results, queryName, queryCity = "", queryState = "") { const deduped = []; const seen = new Set(); results.forEach((item) => { const key = `${(item.rssd_id || "").trim()}|${(item.cert_or_charter || "").trim()}|${normalizeMatchText(item.name)}`; if (seen.has(key)) return; seen.add(key); deduped.push(item); }); return deduped.sort((a, b) => scoreOfficialMatch(b, queryName, queryCity, queryState) - scoreOfficialMatch(a, queryName, queryCity, queryState)); } function setActiveAccordion(panelId) { if (panelId === "reviewPanel") { state.ui.reviewAutoCollapsed = false; state.ui.reviewPrimed = true; } els.accordionPanels.forEach((panel) => { const isOpen = Boolean(panelId) && panel.id === panelId; panel.classList.toggle("open", isOpen); const trigger = panel.querySelector(".accordion-trigger"); if (trigger) trigger.setAttribute("aria-expanded", String(isOpen)); }); state.ui.activeAccordion = panelId || ""; syncOpenStateGlow(); syncSubmitButtonState(state.validation); } function syncBrandingWindowState() { const unlocked = Boolean(state.subject); const isOpen = Boolean(state.ui.isBrandingWindowOpen && unlocked); els.logoControls.hidden = !unlocked; els.brandingStageShell.classList.toggle("is-unlocked", unlocked); els.brandingStageShell.classList.toggle("expanded-active", isOpen); els.logoControls.classList.toggle("expanded-active", isOpen); els.logoControls.setAttribute("aria-hidden", String(!isOpen)); } function setBrandingWindowOpen(isOpen) { const shouldOpen = Boolean(isOpen && state.subject); state.ui.isBrandingWindowOpen = shouldOpen; if (shouldOpen) { els.logoControls.hidden = false; } syncBrandingWindowState(); } function alignBrandingPanelInView() { if (!els.logoPanel) return; const rect = els.logoPanel.getBoundingClientRect(); const targetTop = 10; if (rect.top > targetTop && rect.bottom <= window.innerHeight - 8) return; window.scrollBy({ top: rect.top - targetTop, behavior: "smooth", }); } function closeBrandingAndContinue() { setBrandingWindowOpen(false); openAccordion(""); requestAnimationFrame(() => { if (!els.submitButton.disabled) { els.submitButton.focus(); } }); } function openAccordion(panelId) { if (brandingCloseTimer) { clearTimeout(brandingCloseTimer); brandingCloseTimer = null; } const leavingBranding = state.ui.activeAccordion === "logoPanel" && panelId !== "logoPanel" && state.ui.isBrandingWindowOpen; if (leavingBranding) { setBrandingWindowOpen(false); brandingCloseTimer = setTimeout(() => { setActiveAccordion(panelId); brandingCloseTimer = null; }, 160); return; } setActiveAccordion(panelId); if (panelId === "logoPanel" && state.subject) { els.logoControls.hidden = false; requestAnimationFrame(() => { if (state.ui.activeAccordion === "logoPanel" && state.subject) { setBrandingWindowOpen(true); alignBrandingPanelInView(); } }); return; } setBrandingWindowOpen(false); } function queueFocus(target) { state.ui.pendingFocus = target || ""; } function applyPendingFocus() { if (!state.ui.pendingFocus) return; const pendingTarget = state.ui.pendingFocus; state.ui.pendingFocus = ""; let element = null; if (pendingTarget === "subjectSearch") { element = els.subjectSearch; } else if (pendingTarget === "reportFormatChoice") { element = els.reportFormatModes.find((input) => !input.disabled) || els.reportFormatChoice; } else if (pendingTarget.startsWith("peerSearch:")) { const peerIndex = Number(pendingTarget.split(":")[1]); element = els.peerSearches[peerIndex] || null; } if (!element || element.disabled) return; requestAnimationFrame(() => { const slot = element.closest(".peer-slot"); if (slot) { slot.scrollIntoView({ behavior: "smooth", block: "center", inline: "nearest" }); } else { element.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }); } element.focus(); }); } function syncWorkflowScrollHint() { const container = els.workflowScroll; const hint = els.workflowScrollHint; if (!container || !hint) return; const overflow = (container.scrollHeight - container.clientHeight) > 8; const nearBottom = (container.scrollTop + container.clientHeight) >= (container.scrollHeight - 8); const shouldShow = overflow && !nearBottom; hint.hidden = !shouldShow; hint.setAttribute("aria-hidden", String(!shouldShow)); } function applyStep3bViewportScale() { if (!document.body) return; if (!document.body.classList.contains("step-3b")) { document.body.style.setProperty("--step3b-scale", "1"); document.body.classList.remove("step3b-tight", "step3b-ultra-tight"); return; } const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0); const viewportWidth = Math.max(window.innerWidth || 0, document.documentElement?.clientWidth || 0); const verticalFitBuffer = 30; const horizontalFitBuffer = 8; const fitHeight = Math.max(320, viewportHeight - verticalFitBuffer); const fitWidth = Math.max(320, viewportWidth - horizontalFitBuffer); const shell = document.querySelector(".monitor-shell"); const stepNav = document.querySelector(".step-navigation-panel"); const designHeight = 860; const designWidth = 1500; const heightScale = fitHeight / designHeight; const widthScale = fitWidth / designWidth; const nextScale = Math.min(1, heightScale, widthScale); let boundedScale = Math.max(0.2, nextScale); document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3)); if (shell) { const maxBottom = viewportHeight - 14; const maxRight = viewportWidth - 10; const shellRect = shell.getBoundingClientRect(); const navRect = stepNav ? stepNav.getBoundingClientRect() : shellRect; const bottomEdge = Math.max(shellRect.bottom, navRect.bottom); const rightEdge = Math.max(shellRect.right, navRect.right); const heightRatio = bottomEdge > maxBottom ? maxBottom / bottomEdge : 1; const widthRatio = rightEdge > maxRight ? maxRight / rightEdge : 1; const correction = Math.min(heightRatio, widthRatio, 1); if (correction < 0.999) { boundedScale = Math.max(0.2, boundedScale * correction); document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3)); } } // Final guard rail: keep Step 3b a touch under full size to avoid sub-pixel clipping. boundedScale = Math.max(0.2, boundedScale * 0.985); document.body.style.setProperty("--step3b-scale", boundedScale.toFixed(3)); document.body.classList.toggle("step3b-tight", boundedScale < 0.92); document.body.classList.toggle("step3b-ultra-tight", boundedScale < 0.82); } function renderInstitutionDetails(institution) { return ` ${institution.name}
${institution.location || "Location unavailable"} | ${institutionIdLabel(institution)} | Assets: ${institution.assets || "Not available"}
`; } function setInstitutionType(type, options = {}) { const { advance = true, userInitiated = false } = options; if (state.institutionType && state.institutionType !== type && hasSelections()) { els.typeWarning.hidden = false; return; } state.institutionType = type; if (userInitiated) { state.ui.institutionTypeChosen = true; } els.typeWarning.hidden = true; els.typeButtons.forEach((button) => { const active = button.dataset.type === type; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); els.subjectSearch.disabled = false; if (els.subjectStateHint) els.subjectStateHint.disabled = false; if (els.subjectCityHint) els.subjectCityHint.disabled = false; els.subjectSearchButton.disabled = false; els.subjectSearch.placeholder = `Type ${formatTypeLabel(type)} name and press Search or Enter`; syncQuickRequestTypeUI(type); if (advance) { openAccordion("subjectPanel"); } else { openAccordion("institutionTypePanel"); } render(); } function syncInstitutionTypeUI() { const activeType = effectiveInstitutionType(); els.typeButtons.forEach((button) => { const active = button.dataset.type === activeType; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); const hasType = Boolean(activeType); els.subjectSearch.disabled = !hasType; els.subjectSearchButton.disabled = !hasType; els.subjectSearch.placeholder = hasType ? `Type ${formatTypeLabel(activeType)} name and press Search or Enter` : "Select Bank or Credit Union first"; if (els.subjectStateHint) els.subjectStateHint.disabled = !hasType; if (els.subjectCityHint) els.subjectCityHint.disabled = !hasType; } function clearSelections() { state.subject = null; state.additional = [null, null, null]; state.ui.institutionTypeChosen = false; els.subjectSearch.value = ""; if (els.subjectStateHint) els.subjectStateHint.value = ""; if (els.subjectCityHint) els.subjectCityHint.value = ""; els.peerSearches.forEach((input) => { input.value = ""; }); els.peerStateHints.forEach((input) => { input.value = ""; }); els.peerCityHints.forEach((input) => { input.value = ""; }); els.subjectResults.innerHTML = ""; els.peerResults.forEach((target) => { target.innerHTML = ""; }); els.typeWarning.hidden = true; render(); } function updateQuickRequestRow(index, field, value) { ensureQuickRequestRows(); if (!state.quickRequest.rows[index]) return; state.quickRequest.rows[index][field] = value; } function getQuickRequestValidationClass(value) { const trimmed = String(value || "").trim(); if (!trimmed) return "quick-required"; return "quick-valid"; } function rowDisplayLabel(row) { return row.role === "subject" ? "Subject Institution" : row.institutionLabel; } function renderQuickRequestModal() { if (!els.quickRequest.gridBody) return; ensureQuickRequestRows(); els.quickRequest.gridBody.innerHTML = state.quickRequest.rows.map((row, index) => { const nameClass = getQuickRequestValidationClass(row.name); const stateClass = getQuickRequestValidationClass(row.state); const cityClass = getQuickRequestValidationClass(row.city); const canDelete = row.role === "peer"; const isExpanded = state.quickRequest.activeRow === index; const summary = [row.name || "Institution name pending", row.city && row.state ? `${row.city}, ${row.state}` : "City/state pending"].join(" | "); return `
`; }).join(""); const peerCount = state.quickRequest.rows.filter((row) => row.role === "peer").length; els.quickRequest.addRowButton.disabled = peerCount >= 3; els.quickRequest.addRowButton.hidden = peerCount >= 3; } function quickRequestSummaryData() { ensureQuickRequestRows(); const type = quickRequestType(); return { applied: state.quickRequest.applied, institution_type: type, reporting_period_note: "Quick Request defaults to the last 5 available quarters. Use Status & Controls for different report periods.", subject_institution: { name: state.quickRequest.rows[0]?.name.trim() || "", city: state.quickRequest.rows[0]?.city.trim() || "", state: state.quickRequest.rows[0]?.state.trim() || "", city_state: [state.quickRequest.rows[0]?.city.trim() || "", state.quickRequest.rows[0]?.state.trim() || ""].filter(Boolean).join(", "), }, additional_institutions: state.quickRequest.rows .filter((row) => row.role === "peer") .map((row) => ({ name: row.name.trim(), city: row.city.trim(), state: row.state.trim(), city_state: [row.city.trim(), row.state.trim()].filter(Boolean).join(", "), })), }; } function renderQuickRequestSummary() { if (!els.quickRequest.summaryTitle) return; const summary = quickRequestSummaryData(); const peerCount = summary.additional_institutions.filter((row) => row.name || row.city_state).length; const subjectReady = Boolean(summary.subject_institution.name); if (!state.quickRequest.applied || !subjectReady) { els.quickRequest.summaryTitle.textContent = "No Quick Request institutions added yet."; els.quickRequest.summaryMeta.textContent = "Add the subject institution plus up to 3 peers in a fixed-size modal. The server will resolve RSSIDs or CU identifiers after you submit the request."; els.quickRequest.summaryList.hidden = true; els.quickRequest.summaryList.innerHTML = ""; els.quickRequest.editButton.hidden = true; return; } els.quickRequest.summaryTitle.textContent = `Quick Request: ${summary.subject_institution.name || "Subject added"} + ${peerCount} peer${peerCount === 1 ? "" : "s"} added`; els.quickRequest.summaryMeta.textContent = `${formatTypeLabel(summary.institution_type)} | Last 5 available quarters | Use Status & Controls if you need different report periods.`; els.quickRequest.summaryList.hidden = false; els.quickRequest.editButton.hidden = false; const items = [ { label: "Subject", name: summary.subject_institution.name, meta: `${summary.subject_institution.city_state || "City/State pending"} | Identifier resolved after submit`, }, ...summary.additional_institutions .filter((row) => row.name || row.city_state) .map((row, index) => ({ label: `Peer ${index + 1}`, name: row.name || `Peer ${index + 1}`, meta: `${row.city_state || "City/State pending"} | Identifier resolved after submit`, })), ]; els.quickRequest.summaryList.innerHTML = items.map((item) => `
${item.label}: ${escapeHtml(item.name)}
${escapeHtml(item.meta)}
`).join(""); } function openQuickRequestModal() { ensureQuickRequestRows(); state.quickRequest.open = true; state.quickRequest.activeRow = 0; els.quickRequest.modal.hidden = false; els.quickRequest.modal.setAttribute("aria-hidden", "false"); document.body.classList.add("quick-request-modal-open"); renderQuickRequestModal(); setQuickRequestStatus(""); } function closeQuickRequestModal() { state.quickRequest.open = false; els.quickRequest.modal.hidden = true; els.quickRequest.modal.setAttribute("aria-hidden", "true"); document.body.classList.remove("quick-request-modal-open"); setQuickRequestStatus(""); } function addQuickRequestPeerRow() { ensureQuickRequestRows(); const peerRows = state.quickRequest.rows.filter((row) => row.role === "peer"); if (peerRows.length >= 3) return; state.quickRequest.rows.push(createQuickRequestRow("peer", peerRows.length + 1)); state.quickRequest.activeRow = state.quickRequest.rows.length - 1; renderQuickRequestModal(); } function removeQuickRequestPeerRow(index) { ensureQuickRequestRows(); if (state.quickRequest.rows[index]?.role !== "peer") return; state.quickRequest.rows.splice(index, 1); ensureQuickRequestRows(); state.quickRequest.activeRow = Math.max(0, Math.min(state.quickRequest.activeRow, state.quickRequest.rows.length - 1)); renderQuickRequestModal(); } function validateQuickRequestRows() { ensureQuickRequestRows(); let allValid = true; let firstInvalidMessage = ""; let firstInvalidIndex = -1; state.quickRequest.rows.forEach((row, index) => { const nameOk = Boolean(row.name.trim()); const stateOk = Boolean(row.state.trim()); const cityOk = Boolean(row.city.trim()); const touched = nameOk || stateOk || cityOk; const rowRequired = row.role === "subject" || touched; if (rowRequired && !(nameOk && stateOk && cityOk)) { allValid = false; if (firstInvalidIndex < 0) firstInvalidIndex = index; if (!firstInvalidMessage) { firstInvalidMessage = `${rowDisplayLabel(row)} needs a name, state, and city before you can apply Quick Request.`; } } }); if (firstInvalidIndex >= 0) { state.quickRequest.activeRow = Math.min(firstInvalidIndex, state.quickRequest.rows.length - 1); } renderQuickRequestModal(); return { valid: allValid, message: firstInvalidMessage || "Quick Request is ready." }; } function applyQuickRequestModal() { const validation = validateQuickRequestRows(); if (!validation.valid) { setQuickRequestStatus(validation.message, "error"); return; } state.quickRequest.applied = true; renderQuickRequestSummary(); closeQuickRequestModal(); } function quickRequestAppliedWithRows() { const summary = quickRequestSummaryData(); return Boolean( summary.applied && summary.subject_institution.name ); } function buildInstitutionFromQuickRequest(row, institutionType) { const city = String(row.city || "").trim(); const regionState = String(row.state || "").trim(); return { institution_type: institutionType, name: String(row.name || "").trim(), city, state: regionState, cert_or_charter: "", rssd_id: "", assets: "", location: [city, regionState].filter(Boolean).join(", "), raw: { entry_source: "quick_request", entered_name: String(row.name || "").trim(), entered_city: city, entered_state: regionState, }, }; } function effectiveInstitutionType() { return state.institutionType || (QUICK_REQUEST_ENABLED ? quickRequestType() : "") || DEFAULT_INSTITUTION_TYPE; } function logoSearchTypeQuery() { const subjectTypeHint = String( state.subject?.institution_type || state.subject?.institutionType || state.subject?.type || "" ).toLowerCase(); const subjectNameHint = String(state.subject?.name || "").toLowerCase(); const selectedType = els.typeButtons.find((button) => button.getAttribute("aria-pressed") === "true")?.dataset?.type || ""; const normalizedType = String(selectedType || effectiveInstitutionType() || "").toLowerCase(); const isCreditUnion = subjectTypeHint.includes("credit") || subjectTypeHint.includes("cu") || subjectNameHint.includes("credit union") || /\bfcu\b/i.test(subjectNameHint) || normalizedType === "credit_union"; return isCreditUnion ? "credit union logo" : "bank logo"; } function logoSearchInstitutionQuery() { if (!state.subject) return ""; const locationParts = parseLocationParts(state.subject.location); const city = state.subject.city || locationParts.city || state.subject.raw?.entered_city || ""; const regionState = state.subject.state || locationParts.state || state.subject.raw?.entered_state || ""; return [ state.subject.name, city, regionState, logoSearchTypeQuery(), ].map((part) => String(part || "").trim()).filter(Boolean).join(" "); } function activateOriginalMode() { if (!QUICK_REQUEST_ENABLED) return; if (!state.quickRequest.applied) return; state.quickRequest.applied = false; setQuickRequestStatus(""); renderQuickRequestSummary(); } function effectiveSubjectInstitution() { if (state.subject) return state.subject; if (!QUICK_REQUEST_ENABLED) return null; if (!quickRequestAppliedWithRows()) return null; return buildInstitutionFromQuickRequest(quickRequestSummaryData().subject_institution, effectiveInstitutionType()); } function effectiveAdditionalInstitutions() { const selected = selectedAdditional(); if (selected.length) return selected; if (!QUICK_REQUEST_ENABLED) return []; if (!quickRequestAppliedWithRows()) return []; return quickRequestSummaryData() .additional_institutions .filter((row) => row.name && row.city && row.state) .map((row) => buildInstitutionFromQuickRequest(row, effectiveInstitutionType())); } async function searchInstitutions(query, target, peerIndex = null) { const nameQuery = query.trim(); const hintState = peerIndex === null ? els.subjectStateHint?.value.trim() || "" : els.peerStateHints[peerIndex]?.value.trim() || ""; const hintCity = peerIndex === null ? els.subjectCityHint?.value.trim() || "" : els.peerCityHints[peerIndex]?.value.trim() || ""; const institutionType = effectiveInstitutionType(); if (!institutionType || nameQuery.length < 2) { target.innerHTML = ""; return; } const requestId = ++searchRequestCounter; if (peerIndex === null) { activeSubjectSearchRequestId = requestId; } else { activePeerSearchRequestByIndex.set(peerIndex, requestId); } const isRequestStillActive = () => ( peerIndex === null ? activeSubjectSearchRequestId === requestId : activePeerSearchRequestByIndex.get(peerIndex) === requestId ); target.innerHTML = `
Searching official institution names...
`; try { const aggregatedResults = []; const queries = institutionType === "credit_union" ? [nameQuery] : buildOfficialSearchQueries(nameQuery, hintCity, hintState); for (const candidate of queries) { const url = new URL("/api/institutions/search", window.location.origin); url.searchParams.set("type", institutionType); url.searchParams.set("q", candidate); const response = await fetch(url.toString(), { cache: "no-store" }); if (!isRequestStillActive()) return; const data = await response.json(); if (!isRequestStillActive()) return; if (!response.ok || !data.success) { throw new Error(data.error || "Institution search failed."); } aggregatedResults.push(...(data.results || [])); } if (!isRequestStillActive()) return; renderResults(prioritizeOfficialResults(aggregatedResults, nameQuery, hintCity, hintState), target, peerIndex); } catch (error) { if (!isRequestStillActive()) return; target.innerHTML = `
${error.message}
`; } } function runSubjectSearch() { if ( subjectResultsLocked && state.subject && normalizeMatchText(els.subjectSearch.value || "") === normalizeMatchText(state.subject.name || "") ) { els.subjectResults.innerHTML = ""; els.subjectResults.style.display = "none"; els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); return Promise.resolve(); } activateOriginalMode(); openAccordion("subjectPanel"); return searchInstitutions(els.subjectSearch.value, els.subjectResults); } function runPeerSearch(peerIndex) { activateOriginalMode(); openAccordion("additionalPanel"); return searchInstitutions(els.peerSearches[peerIndex]?.value || "", els.peerResults[peerIndex], peerIndex); } const debouncedSubjectSearch = debounce(() => { const value = (els.subjectSearch.value || "").trim(); if (!value || value.length < 2) return; const now = Date.now(); if ( value === lastSubjectSearchDispatch.value && (now - lastSubjectSearchDispatch.ts) < SEARCH_DISPATCH_DEDUPE_WINDOW_MS ) { return; } lastSubjectSearchDispatch = { value, ts: now }; runSubjectSearch(); }, 150); const debouncedPeerSearch = debounce((peerIndex) => { const value = (els.peerSearches[peerIndex]?.value || "").trim(); if (!value || value.length < 2) return; const now = Date.now(); const last = lastPeerSearchDispatchByIndex.get(peerIndex); if ( last && value === last.value && (now - last.ts) < SEARCH_DISPATCH_DEDUPE_WINDOW_MS ) { return; } lastPeerSearchDispatchByIndex.set(peerIndex, { value, ts: now }); runPeerSearch(peerIndex); }, 150); function renderResults(results, target, peerIndex = null) { if (!results.length) { target.innerHTML = `
No matching institutions found.
`; if (target === els.subjectResults) { els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); syncHelperPanel(); } return; } const isSubject = target === els.subjectResults; target.innerHTML = results .map((institution, index) => `
${institution.name} ${institution.location || "Location unavailable"} | ${institutionIdLabel(institution)}
`) .join(""); if (isSubject) { target.style.display = ""; const firstItem = target.querySelector(".result-item"); firstItem?.classList.add("prefocus"); applySubjectResultsAttentionCue(); syncSubjectResultsGuidance(); syncHelperPanel(); requestAnimationFrame(scrollSubjectResultsIntoViewIfNeeded); } target.querySelectorAll(".result-item").forEach((item) => { const selectInstitution = () => { const institution = results[Number(item.dataset.resultIndex)]; if (isSubject) { state.subject = institution; state.additional = state.additional.map((item) => ( item && institutionId(item) === institutionId(institution) ? null : item )); activeSubjectSearchRequestId = 0; subjectResultsLocked = true; els.subjectSearch.value = institution.name; els.subjectResults.innerHTML = ""; els.subjectResults.style.display = "none"; els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); openAccordion("additionalPanel"); queueFocus("peerSearch:0"); } else { if (Number.isInteger(peerIndex)) { activePeerSearchRequestByIndex.set(peerIndex, 0); } addAdditionalInstitution(institution, peerIndex); } render(); }; item.addEventListener("mousedown", (event) => { // Commit selection before input blur handlers can clear/re-render results. // Mouse selection is handled here to avoid duplicate mousedown+click dispatch. event.preventDefault(); selectInstitution(); }); item.addEventListener("keydown", (event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); selectInstitution(); } }); }); } function addAdditionalInstitution(institution, peerIndex = null) { if (!state.subject) return; if (institutionId(state.subject) === institutionId(institution)) return; if (selectedAdditional().some((item) => institutionId(item) === institutionId(institution))) return; const slotIndex = Number.isInteger(peerIndex) ? peerIndex : state.additional.findIndex((item) => !item); if (slotIndex < 0 || slotIndex > 2) return; state.additional[slotIndex] = institution; els.peerSearches[slotIndex].value = ""; els.peerResults[slotIndex].innerHTML = ""; if (selectedAdditional().length >= 3) { openAccordion("reportPanel"); queueFocus("reportFormatChoice"); } else { openAccordion("additionalPanel"); const nextOpenIndex = state.additional.findIndex((item) => !item); if (nextOpenIndex >= 0) { queueFocus(`peerSearch:${nextOpenIndex}`); } } } function renderAdditional() { const count = selectedAdditional().length; els.peerSlots.forEach((slot, index) => { const institution = state.additional[index]; const previousReady = index === 0 || Boolean(state.additional[index - 1]); const unlocked = Boolean(state.subject) && previousReady; const isActive = unlocked && !institution; slot.classList.toggle("locked", !unlocked); slot.classList.toggle("selected", Boolean(institution)); slot.classList.toggle("active", isActive); const input = els.peerSearches[index]; const stateHint = els.peerStateHints[index]; const cityHint = els.peerCityHints[index]; const button = els.peerSearchButtons[index]; const selection = els.peerSelections[index]; input.disabled = !unlocked || Boolean(institution); stateHint.disabled = !unlocked || Boolean(institution); cityHint.disabled = !unlocked || Boolean(institution); button.disabled = input.disabled; input.placeholder = !state.subject ? "Select a subject institution first" : !previousReady ? `Select additional institution ${index} first` : institution ? "Institution selected" : `Type ${formatTypeLabel(state.institutionType)} name and press Search or Enter`; if (institution) { selection.classList.remove("empty"); selection.innerHTML = `
${renderInstitutionDetails(institution)}
`; } else { selection.classList.add("empty"); selection.innerHTML = unlocked ? "No institution selected." : "Locked until prior selection is complete."; } }); els.peerSelections.forEach((selection) => { selection.querySelectorAll(".remove-button").forEach((button) => { button.addEventListener("click", () => { const index = Number(button.dataset.removeIndex); state.additional[index] = null; for (let next = index + 1; next < state.additional.length; next += 1) { state.additional[next] = null; els.peerSearches[next].value = ""; els.peerStateHints[next].value = ""; els.peerCityHints[next].value = ""; els.peerResults[next].innerHTML = ""; } render(); }); }); }); if (els.additionalCounter) { els.additionalCounter.textContent = count >= 3 ? "3 of 3 additional institutions selected." : `${count} of 3 additional institutions selected.`; } updateAdditionalInstitutionHelper(count); els.reportPanel.classList.toggle("is-prominent", count >= 3); } function renderLogo() { const unlocked = Boolean(state.subject); els.logoPanel.classList.toggle("is-muted", !unlocked); els.logoLocked.hidden = unlocked; els.logoSearchQuery.value = unlocked ? logoSearchInstitutionQuery() : ""; els.logoSearchButton.disabled = !unlocked; els.autoColorToggle.disabled = !unlocked; els.resetBrandingButton.disabled = !unlocked; els.logoSelectionStatus.textContent = state.logo.logo_name ? "Logo selected" : "Awaiting logo"; els.autoColorToggle.classList.toggle("is-active", state.logo.autoExtract); els.autoColorToggle.setAttribute("aria-pressed", String(state.logo.autoExtract)); els.brandPreviewInstitution.textContent = unlocked ? state.subject.name : "Your Institution"; els.brandPreviewTitle.textContent = unlocked ? `${state.subject.name} Analysis` : "Institution Analysis"; els.brandPreviewSubtitle.textContent = unlocked ? `${formatTypeLabel(state.institutionType)} performance and intelligence dashboard` : "Interest Rate Risk & Performance Dashboard"; if (!unlocked) { setBrandingWindowOpen(false); } else if (state.ui.activeAccordion === "logoPanel" && !brandingCloseTimer) { setBrandingWindowOpen(true); } else { syncBrandingWindowState(); } updateBrandPreview(); } function updateBrandPreview() { const colors = state.logo.colors; els.brandPreview.style.background = colors.primaryBg; els.brandPreview.style.color = colors.textColor; const previewMetrics = els.brandPreview.querySelectorAll(".preview-metric-card"); previewMetrics.forEach((card) => { card.style.background = colors.cardBg; card.style.borderColor = mixHex(colors.accentColor, colors.cardBg, 0.52); card.style.color = colors.textColor; }); els.brandPreviewTitle.style.color = colors.textColor; els.brandPreviewSubtitle.style.color = mixHex(colors.textColor, colors.primaryBg, 0.34); els.brandPreviewLogo.innerHTML = state.logo.logo_data_url ? `Selected logo preview` : "HR"; els.brandPreviewLogo.style.background = colors.cardBg; els.brandPreviewLogo.style.color = colors.accentColor; els.brandPreview.querySelectorAll(".preview-metric-card strong").forEach((node) => { node.style.color = colors.textColor; }); els.brandPreview.querySelectorAll(".preview-metric-card small").forEach((node, index) => { node.style.color = index % 2 === 0 ? "#34d399" : "#f87171"; }); const footer = els.brandPreview.querySelector(".brand-preview-footer"); if (footer) footer.style.color = mixHex(colors.textColor, colors.primaryBg, 0.46); } function syncGlowState(element, isActive) { if (!element) return; element.classList.toggle("glow-active", Boolean(isActive)); } function firstEnabledPeerSearch() { return els.peerSearches.find((input) => !input.disabled) || null; } function syncOpenStateGlow() { const typeOpen = state.ui.activeAccordion === "institutionTypePanel"; const subjectOpen = state.ui.activeAccordion === "subjectPanel"; const additionalOpen = state.ui.activeAccordion === "additionalPanel"; const reportOpen = state.ui.activeAccordion === "reportPanel"; const activePeerInput = firstEnabledPeerSearch(); const customMode = selectedReportFormatMode() === "custom"; const activeQuarterSelect = customMode ? els.quarterSelects.find((select) => !select.disabled) || null : null; syncGlowState(els.typeToggle, typeOpen); syncGlowState(els.subjectSearch, subjectOpen && !els.subjectSearch.disabled); els.peerSearches.forEach((input) => { syncGlowState(input, additionalOpen && input === activePeerInput); }); syncGlowState(els.reportFormatChoice, reportOpen); els.quarterSelects.forEach((select) => { syncGlowState(select, reportOpen && customMode && select === activeQuarterSelect); }); } function subjectResultsListHasOptions() { return Boolean(els.subjectResults?.querySelector(".result-item")); } function isSubjectResultsInteractiveOpen() { if (!els.subjectResults || els.subjectResults.style.display === "none") return false; return subjectResultsListHasOptions(); } function scrollSubjectResultsIntoViewIfNeeded() { if (!els.subjectResults || !isSubjectResultsInteractiveOpen()) return; const rect = els.subjectResults.getBoundingClientRect(); const viewportHeight = Math.max(window.innerHeight || 0, document.documentElement?.clientHeight || 0); if (rect.bottom > (viewportHeight - 8) || rect.top < 0) { els.subjectResults.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "nearest" }); } } function applySubjectResultsAttentionCue() { if (!els.subjectResults) return; els.subjectResults.classList.remove("results-attention"); void els.subjectResults.offsetWidth; els.subjectResults.classList.add("results-attention"); els.subjectResults.addEventListener("animationend", () => { els.subjectResults?.classList.remove("results-attention"); }, { once: true }); } function syncSubjectResultsGuidance() { const hint = els.subjectResultsHint; const indicator = els.subjectNextIndicator; if (!hint || !indicator) return; const resultsOpen = isSubjectResultsInteractiveOpen(); document.body.classList.toggle("subject-results-open", resultsOpen); indicator.classList.toggle("visible", resultsOpen); if (resultsOpen) { hint.hidden = false; hint.classList.remove("is-selected"); hint.textContent = "Select your institution from the list below to continue."; return; } if (state.subject?.name) { hint.hidden = false; hint.classList.add("is-selected"); hint.textContent = `Selected: ${state.subject.name}`; return; } hint.hidden = true; hint.classList.remove("is-selected"); hint.textContent = ""; } function stepNextBlockedBySubjectResults(validation) { return clampStep(state.ui.routeStep) === "1" && isSubjectResultsInteractiveOpen() && !validation.subjectSelected; } function helperStepForCurrentState() { const routeStep = clampStep(state.ui.routeStep); if (routeStep === "4") { return null; } if (routeStep === "1") { if (!state.ui.institutionTypeChosen) { return "step1"; } return "step2"; } if (routeStep === "2") { return "step2b"; } if (routeStep === "3") { return "step3"; } return null; } function helperAdditionalSelectedCount() { return selectedAdditional().length; } function helperAdditionalAnchorForCount(countSelected) { if (countSelected <= 0) return "anchor-step2b"; if (countSelected === 1) return "anchor-addl2"; return "anchor-addl3"; } function updateAdditionalInstitutionHelper(countSelected) { if (helperStepForCurrentState() !== "step2b") return; const content = helperContent.step2b; if (els.helperText && !els.helperPanel?.classList.contains("collapsed") && content) { els.helperText.textContent = content.text.replace("0 of 3 additional institutions selected.", `${countSelected} of 3 additional institutions selected.`); } moveHelperTo(helperAdditionalAnchorForCount(countSelected)); const targetIndex = Math.min(countSelected, 2); const targetInput = els.peerSearches[targetIndex] || els.peerSearches[2]; if (targetInput) { positionArrow(targetInput); } } function updateGuidancePosition(targetEl, guidanceEl, arrowEl) { if (!targetEl || !guidanceEl || !arrowEl || guidanceEl.hidden) return; const targetRect = targetEl.getBoundingClientRect(); const container = els.leftRailStack || guidanceEl.offsetParent; if (!container || targetRect.width <= 0 || targetRect.height <= 0 || !targetEl.getClientRects().length) { arrowEl.style.opacity = "0"; return; } const containerRect = container.getBoundingClientRect(); const guidanceHeight = guidanceEl.offsetHeight; if (guidanceHeight <= 0) { arrowEl.style.opacity = "0"; return; } const targetMidY = targetRect.top + (targetRect.height / 2); const desiredViewportTop = targetMidY - (guidanceHeight / 2); const relativeTop = desiredViewportTop - containerRect.top + container.scrollTop; const maxTop = Math.max(0, container.scrollHeight - guidanceHeight); guidanceEl.style.top = `${Math.round(Math.max(0, Math.min(relativeTop, maxTop)))}px`; guidanceEl.style.zIndex = "9999"; const arrowSize = 32; const preferredLeft = targetRect.left - arrowSize - 6; const arrowLeft = Math.max(4, Math.min(preferredLeft, window.innerWidth - arrowSize - 4)); const arrowTop = Math.max(4, targetMidY - (arrowSize / 2)); arrowEl.style.transform = `translate3d(${Math.round(arrowLeft)}px, ${Math.round(arrowTop)}px, 0)`; arrowEl.style.zIndex = "10000"; arrowEl.style.opacity = "1"; } function scheduleGuidancePosition() { if (helperPositionFrame) return; helperPositionFrame = requestAnimationFrame(() => { helperPositionFrame = 0; updateGuidancePosition(helperActiveTargetElement, els.helperPanel, els.helperArrow); }); } function observeGuidanceTarget(targetEl) { if (helperObservedTargetElement === targetEl) return; if (helperResizeObserver && helperObservedTargetElement) { helperResizeObserver.unobserve(helperObservedTargetElement); } helperObservedTargetElement = targetEl || null; if (helperResizeObserver && helperObservedTargetElement) { helperResizeObserver.observe(helperObservedTargetElement); } } function setGuidanceTarget(targetEl) { helperActiveTargetElement = targetEl || null; observeGuidanceTarget(helperActiveTargetElement); if (!helperActiveTargetElement && els.helperArrow) { els.helperArrow.style.opacity = "0"; } scheduleGuidancePosition(); } function initializeGuidancePositioning() { helperResizeObserver = new ResizeObserver(scheduleGuidancePosition); if (els.helperPanel) helperResizeObserver.observe(els.helperPanel); if (els.leftRailStack) helperResizeObserver.observe(els.leftRailStack); helperMutationObserver = new MutationObserver(scheduleGuidancePosition); helperMutationObserver.observe(document.body, { subtree: true, childList: true, attributes: true, attributeFilter: ["class", "hidden", "aria-hidden", "aria-expanded", "disabled"], }); window.addEventListener("resize", scheduleGuidancePosition, { passive: true }); window.addEventListener("scroll", scheduleGuidancePosition, { passive: true, capture: true }); document.addEventListener("focusin", (event) => { if (event.target === helperActiveTargetElement || helperActiveTargetElement?.contains(event.target)) { scheduleGuidancePosition(); } }); } function attachHelperTo(anchorId, targetEl) { if (anchorId) helperActiveAnchorId = anchorId; if (targetEl) setGuidanceTarget(targetEl); else scheduleGuidancePosition(); } // Compatibility shims so existing call sites keep working unchanged. function moveHelperTo(anchorId) { attachHelperTo(anchorId, helperActiveTargetElement); } function positionArrow(targetElement) { attachHelperTo(helperActiveAnchorId, targetElement); } function userInteracted() { scheduleGuidancePosition(); } function syncHelperPanel() { if (!els.helperPanel || !els.helperText || !els.helperArrow) return; const activeHelperStep = helperStepForCurrentState(); const content = activeHelperStep ? helperContent[activeHelperStep] : null; const shouldReserveColumn = ["1", "2", "3"].includes(clampStep(state.ui.routeStep)); const isStep4 = clampStep(state.ui.routeStep) === "4"; document.body.classList.toggle("helper-rail-active", shouldReserveColumn); els.helperPanel.hidden = !shouldReserveColumn; els.helperPanel.classList.toggle("is-visible", Boolean(content)); els.helperPanel.classList.toggle("is-hidden", !content); els.helperPanel.dataset.helperStep = activeHelperStep || ""; if (isStep4) { setGuidanceTarget(null); return; } if (!content) { setGuidanceTarget(null); return; } if (!els.helperPanel.classList.contains("collapsed")) { const additionalCount = helperAdditionalSelectedCount(); const helperText = activeHelperStep === "step2b" ? content.text.replace("0 of 3 additional institutions selected.", `${additionalCount} of 3 additional institutions selected.`) : content.text; els.helperText.textContent = helperText; } const anchorId = activeHelperStep === "step2b" ? helperAdditionalAnchorForCount(helperAdditionalSelectedCount()) : content.anchorId; moveHelperTo(anchorId); const target = typeof content.target === "function" ? content.target() : null; if (target) { positionArrow(target); } else { setGuidanceTarget(null); } } function hasRequiredUserFields() { const requiredValues = getRequiredUserFieldValues(); const emailValue = requiredValues.email; return Boolean( requiredValues.firstName && requiredValues.lastName && emailValue.includes("@") && requiredValues.phone && requiredValues.organization ); } function getRequiredUserFieldValues() { const requesterEmail = document.getElementById("email").value.trim(); return { firstName: document.getElementById("firstName").value.trim(), lastName: document.getElementById("lastName").value.trim(), email: requesterEmail, phone: document.getElementById("phone").value.trim(), organization: document.getElementById("organization").value.trim(), }; } function clearMissingFieldHighlights() { document.querySelectorAll(".input-missing").forEach((node) => { node.classList.remove("input-missing"); }); } function highlightMissingField(fieldId) { const field = document.getElementById(fieldId); field?.classList.add("input-missing"); } function hasDisclaimerAcknowledged() { return Boolean(els.disclaimerAck?.checked); } function computeValidationState() { return { institutionTypeSelected: Boolean(effectiveInstitutionType()), subjectSelected: Boolean(effectiveSubjectInstitution()), peersSelected: effectiveAdditionalInstitutions().length, reportFormatSelected: Boolean(selectedReportFormatMode()), reportingPeriodValid: isValidPeriod(), userFieldsComplete: hasRequiredUserFields(), disclaimerAccepted: hasDisclaimerAcknowledged(), }; } function validationChanged(nextValidation) { return ( state.validation.institutionTypeSelected !== nextValidation.institutionTypeSelected || state.validation.subjectSelected !== nextValidation.subjectSelected || state.validation.peersSelected !== nextValidation.peersSelected || state.validation.reportFormatSelected !== nextValidation.reportFormatSelected || state.validation.reportingPeriodValid !== nextValidation.reportingPeriodValid || state.validation.userFieldsComplete !== nextValidation.userFieldsComplete || state.validation.disclaimerAccepted !== nextValidation.disclaimerAccepted ); } function allRequirementsMet(validation = state.validation) { return Boolean( validation.institutionTypeSelected && validation.subjectSelected && validation.reportFormatSelected && validation.reportingPeriodValid && validation.userFieldsComplete && validation.disclaimerAccepted ); } function missingRequirementMessages(validation = state.validation) { const items = []; if (!validation.institutionTypeSelected) items.push("institution type"); if (!validation.subjectSelected) items.push("subject institution"); if (!validation.reportFormatSelected) items.push("report format"); if (!validation.reportingPeriodValid) items.push("report date selection"); if (!validation.userFieldsComplete) items.push("first name, last name, email, phone, and organization"); if (!validation.disclaimerAccepted) items.push("disclosure acknowledgement"); return items; } function syncReviewAccordion(validation, didValidationChange) { const allValid = allRequirementsMet(validation); if (allValid) { state.ui.reviewAutoCollapsed = true; if (state.ui.activeAccordion === "reviewPanel") { openAccordion(""); } else { syncOpenStateGlow(); } return; } state.ui.reviewAutoCollapsed = false; if (state.ui.reviewPrimed && (didValidationChange || state.ui.activeAccordion === "reviewPanel")) { openAccordion("reviewPanel"); } } function syncSubmitButtonState(validation) { const allValid = allRequirementsMet(validation); if (allValid) { state.ui.reviewAutoCollapsed = true; } const submitReady = allValid; if (submitReady && !state.ui.wasSubmitReady && !state.ui.submitting) { els.submitButton.classList.remove("ready-flash"); void els.submitButton.offsetWidth; els.submitButton.classList.add("ready-flash"); } state.ui.wasSubmitReady = submitReady; els.submitButton.disabled = state.ui.submitting || !submitReady; els.submitButton.classList.toggle("is-submitting", state.ui.submitting); els.submitButton.textContent = state.ui.submitting ? "Submitting Request..." : state.ui.submitSucceeded ? SUCCESS_SUBMIT_BUTTON_TEXT : DEFAULT_SUBMIT_BUTTON_TEXT; syncGlowState(els.submitButton, false); const missingItems = missingRequirementMessages(validation); if (state.ui.submitting) { els.submitHelper.textContent = ""; els.submitHelper.className = "submit-helper"; els.submitButton.title = ""; } else if (submitReady) { els.submitHelper.textContent = "All required items are complete. Submit Request is ready."; els.submitHelper.className = "submit-helper is-ready"; els.submitButton.title = ""; } else { els.submitHelper.textContent = `Submit unlocks after: ${missingItems.join(", ")}.`; els.submitHelper.className = "submit-helper is-blocked"; els.submitButton.title = `Missing: ${missingItems.join(", ")}`; } } function stepNextEnabled(validation) { if (stepNextBlockedBySubjectResults(validation)) return false; const step = clampStep(state.ui.routeStep); if (step === "1") return Boolean(validation.subjectSelected); if (step === "2") return true; if (step === "3") return Boolean(validation.reportFormatSelected); if (step === "3b") return true; return false; } function syncStepVisibilityAndNavigation(validation) { const currentStep = clampStep(state.ui.routeStep); els.stepSections.forEach((section) => { const sectionStep = clampStep(section.dataset.stepSection); section.hidden = sectionStep !== currentStep; }); els.stageTabs.forEach((tab) => { const tabStep = clampStep(tab.dataset.routeStep); tab.classList.toggle("active", tabStep === currentStep); tab.classList.toggle("complete", tabStep < currentStep); }); if (els.stepBackButton) { els.stepBackButton.hidden = currentStep === "1"; els.stepBackButton.disabled = state.ui.submitting; } if (els.stepNextButton) { const isLast = currentStep === "4"; const blockedBySubjectResults = stepNextBlockedBySubjectResults(validation); els.stepNextButton.hidden = isLast; els.stepNextButton.disabled = state.ui.submitting || !stepNextEnabled(validation); els.stepNextButton.title = blockedBySubjectResults ? "Select an institution to continue." : ""; } } function updateDashboardState() { const nextValidation = computeValidationState(); const didValidationChange = validationChanged(nextValidation); state.validation = nextValidation; const checks = { type: nextValidation.institutionTypeSelected, subject: nextValidation.subjectSelected, additional: true, period: nextValidation.reportFormatSelected && nextValidation.reportingPeriodValid, user: nextValidation.userFieldsComplete, disclaimer: nextValidation.disclaimerAccepted, }; const percent = Math.round(((stepIndex(state.ui.routeStep) + 1) / REGISTRATION_STEPS.length) * 100); els.progressFill.style.width = `${percent}%`; els.completionPercent.textContent = `${percent}%`; els.statuses.type.textContent = checks.type ? formatTypeLabel(state.institutionType) : "Required"; document.getElementById("institutionTypePanel").classList.toggle("needs-attention", !checks.type); els.statuses.subject.textContent = checks.subject ? "Selected" : checks.type ? "Ready" : "Locked"; els.statuses.additional.textContent = `${effectiveAdditionalInstitutions().length} selected`; els.statuses.period.textContent = checks.period ? "Ready" : "Invalid"; els.statuses.logo.textContent = state.logo.logo_name ? "Logo added" : checks.subject ? "Optional" : "Locked"; els.statuses.review.textContent = percent === 100 ? "Ready" : "Pending"; if (QUICK_REQUEST_ENABLED) { syncQuickRequestTypeUI(state.institutionType || quickRequestType()); } [els.readiness, els.submitReadiness].forEach((targetGroup) => { Object.entries(checks).forEach(([key, ready]) => { const target = targetGroup[key]; if (!target) return; target.classList.remove("valid-green", "invalid-red"); target.classList.add(ready ? "valid-green" : "invalid-red"); }); }); syncReviewAccordion(nextValidation, didValidationChange); syncSubmitButtonState(nextValidation); syncStepVisibilityAndNavigation(nextValidation); syncHelperPanel(); } function syncSelectionSummary() { if (!els.selectionSummary?.institutionType) return; const additionalList = effectiveAdditionalInstitutions(); const period = selectedPeriod(); const notesValue = document.getElementById("notes")?.value?.trim() || ""; const themeValue = state.logo.autoExtract ? "Standard" : "Manual / Preset"; const subject = effectiveSubjectInstitution(); const locationLabel = (institution) => institution?.location || [institution?.city, institution?.state].filter(Boolean).join(", ") || "Location unavailable"; const logoSource = !state.logo.logo_name ? "No logo selected." : state.logo.logo_source_url ? "Search for Logo" : "Uploaded"; const reportQuarterLines = period.mode === "custom" ? els.quarterSelects.map((select) => select.options[select.selectedIndex]?.text || select.value).filter(Boolean) : ["Last 5 quarters (default)"]; const additionalLines = additionalList.length ? additionalList.map((institution, index) => { return `${index + 1}. ${institution.name || "Unknown"} | ${locationLabel(institution)} | ${institutionIdLabel(institution)}`; }).join("
") : "No additional institutions selected."; const notesPreview = notesValue.length > 80 ? `${notesValue.slice(0, 80)}...` : (notesValue || "None"); const escapedNotesPreview = escapeHtml(notesPreview); const escapedFullNotes = escapeHtml(notesValue || "None"); const colorSummaryHtml = ` ${state.logo.colors.primaryBg.toUpperCase()}
${state.logo.colors.cardBg.toUpperCase()}
${state.logo.colors.accentColor.toUpperCase()} `; els.selectionSummary.institutionType.textContent = `Institution Type: ${formatTypeLabel(effectiveInstitutionType())}`; els.selectionSummary.subjectInstitution.textContent = `Subject Institution: ${effectiveSubjectInstitution()?.name || "Not selected"}`; els.selectionSummary.additionalInstitutions.textContent = `Additional Institutions: ${additionalList.length}`; els.selectionSummary.reportFormat.textContent = `Report Format: ${period.mode === "custom" ? period.label : "Standard (last 5 completed quarters)"}`; els.selectionSummary.logo.textContent = `Logo: ${state.logo.logo_name || "Not selected"}`; els.selectionSummary.theme.textContent = `Theme: ${themeValue}`; els.selectionSummary.colors.innerHTML = `Colors: ${colorSummaryHtml}`; els.selectionSummary.notes.textContent = `Notes: ${notesValue || "None"}`; if (els.selectionSummary.logoPreview) { if (state.logo.logo_data_url) { els.selectionSummary.logoPreview.innerHTML = `Selected logo preview`; } else { els.selectionSummary.logoPreview.textContent = "No logo selected."; } } if (els.notesModal.inlinePreview) { els.notesModal.inlinePreview.textContent = notesValue || "No notes added."; } if (els.submitSummary.subject) { els.submitSummary.subject.innerHTML = subject ? `${escapeHtml(subject.name || "Unknown")}
${escapeHtml(locationLabel(subject))}
${escapeHtml(institutionIdLabel(subject))}
${escapeHtml(formatTypeLabel(effectiveInstitutionType()))}` : "No subject institution selected."; } if (els.submitSummary.additional) { els.submitSummary.additional.innerHTML = additionalLines; } if (els.submitSummary.period) { els.submitSummary.period.innerHTML = reportQuarterLines.map((line) => escapeHtml(line)).join("
"); } if (els.submitSummary.logo) { els.submitSummary.logo.innerHTML = state.logo.logo_data_url ? `Logo preview
Source: ${escapeHtml(logoSource)}
${escapeHtml(state.logo.logo_name || "")}` : "No logo selected."; } if (els.submitSummary.colors) { els.submitSummary.colors.innerHTML = colorSummaryHtml; } if (els.submitSummary.theme) { els.submitSummary.theme.textContent = themeValue; } if (els.submitSummary.notes) { els.submitSummary.notes.innerHTML = `${escapedNotesPreview} View Notes`; const viewNotesLink = document.getElementById("submitSummaryViewNotes"); viewNotesLink?.addEventListener("click", () => { if (!els.notesModal.modal) return; els.notesModal.modal.hidden = false; els.notesModal.modal.setAttribute("aria-hidden", "false"); document.body.classList.add("quick-request-modal-open"); document.getElementById("notes")?.focus(); }, { once: true }); } } function render() { syncInstitutionTypeUI(); els.subjectSelection.classList.toggle("empty", !state.subject); els.subjectSelection.innerHTML = state.subject ? renderInstitutionDetails(state.subject) : "No subject institution selected."; const subjectQueryMatchesSelection = Boolean( state.subject && normalizeMatchText(els.subjectSearch?.value || "") === normalizeMatchText(state.subject.name || "") ); const shouldShowSubjectResults = !state.subject || !subjectQueryMatchesSelection; if (!shouldShowSubjectResults) { els.subjectResults.innerHTML = ""; } els.subjectResults.style.display = shouldShowSubjectResults && !subjectResultsLocked ? "" : "none"; if (els.subjectResults.style.display === "none") { els.subjectResults.classList.remove("results-attention"); } syncSubjectResultsGuidance(); renderQuickRequestSummary(); renderQuickRequestModal(); renderLogo(); renderAdditional(); updateDashboardState(); syncOpenStateGlow(); applyPendingFocus(); requestAnimationFrame(syncWorkflowScrollHint); persistSessionState(); syncSelectionSummary(); } function buildQuarterOptions() { const now = new Date(); const quarterEnds = [ { quarter: "Q1", month: 2, day: 31 }, { quarter: "Q2", month: 5, day: 30 }, { quarter: "Q3", month: 8, day: 30 }, { quarter: "Q4", month: 11, day: 31 }, ]; const options = []; for (let year = now.getFullYear(); year >= now.getFullYear() - 6; year -= 1) { quarterEnds.forEach(({ quarter, month, day }) => { const date = new Date(year, month, day); if (date <= now) { const value = `${String(month + 1).padStart(2, "0")}/${String(day).padStart(2, "0")}/${year}`; options.push({ label: `${quarter} ${year}`, value, date }); } }); } options.sort((a, b) => b.date - a.date); els.quarterSelects.forEach((select, index) => { select.innerHTML = options .map((option) => ``) .join(""); select.selectedIndex = Math.min(index, options.length - 1); select.disabled = true; }); } function selectedPeriod() { const mode = selectedReportFormatMode(); const quarters = els.quarterSelects.map((select) => ({ label: select.options[select.selectedIndex]?.text || "", value: select.value, })); const first = quarters[0] || { label: "", value: "" }; const last = quarters[quarters.length - 1] || { label: "", value: "" }; return { mode, label: `${quarters.map((quarter) => quarter.label).filter(Boolean).join(", ")}`, start_period: last.value, end_period: first.value, quarters, }; } function isValidPeriod() { const period = selectedPeriod(); if (period.quarters.length !== 5) return false; return period.quarters.every((quarter) => quarter.value); } function selectedReportFormatMode() { return els.reportFormatModes.find((input) => input.checked)?.value || "standard"; } function applyReportFormatMode() { const isCustom = selectedReportFormatMode() === "custom"; els.quarterSelects.forEach((select) => { select.disabled = !isCustom; }); els.periodHint.textContent = isCustom ? "Custom mode: choose exactly 5 reporting quarters." : "Standard mode automatically uses the last 5 completed quarters."; } function setStatus(message, kind = "") { els.formStatus.textContent = message; els.formStatus.className = `form-status ${kind}`.trim(); } function hideSubmissionSuccessModal() { if (!els.submissionSuccess.modal) return; els.submissionSuccess.modal.hidden = true; els.submissionSuccess.modal.setAttribute("aria-hidden", "true"); document.body.classList.remove("submission-success-modal-open"); state.ui.submissionSuccessOpen = false; } function openSubmissionSuccessModal() { if (!els.submissionSuccess.modal) return; els.submissionSuccess.modal.hidden = false; els.submissionSuccess.modal.setAttribute("aria-hidden", "false"); document.body.classList.add("submission-success-modal-open"); state.ui.submissionSuccessOpen = true; requestAnimationFrame(() => { els.submissionSuccess.closeButton?.focus(); }); } function closeAndExitAfterSubmission() { sessionStorage.removeItem(REGISTRATION_SESSION_KEY); hideSubmissionSuccessModal(); try { if (window.parent && window.parent !== window) { window.parent.postMessage({ type: "registration-close" }, "*"); } } catch (error) { // Ignore cross-window errors and continue with local navigation fallback. } try { window.close(); } catch (error) { // Some browsers disallow closing non-script-opened tabs. } // Fallback: if the tab cannot be closed, always return to landing. setTimeout(() => { if (!window.closed) { window.location.assign("/"); } }, 80); } function evaluateSubmissionReadiness(validation = computeValidationState()) { const requiredValues = getRequiredUserFieldValues(); if (!validation.institutionTypeSelected) { return { ready: false, error: "Select Bank or Credit Union.", focusFieldId: "subjectSearch" }; } if (!validation.subjectSelected) { return { ready: false, error: "Select a subject institution.", focusFieldId: "subjectSearch" }; } if (!validation.reportFormatSelected) { return { ready: false, error: "Select a report format.", focusFieldId: "reportFormatChoice" }; } if (!validation.reportingPeriodValid) { return { ready: false, error: "Select a valid reporting period.", focusFieldId: "quarter1" }; } if (!requiredValues.firstName) { return { ready: false, error: "First name is required.", focusFieldId: "firstName" }; } if (!requiredValues.lastName) { return { ready: false, error: "Last name is required.", focusFieldId: "lastName" }; } if (!requiredValues.email.includes("@")) { return { ready: false, error: "A valid email is required.", focusFieldId: "email" }; } if (!requiredValues.phone) { return { ready: false, error: "Phone is required.", focusFieldId: "phone" }; } if (!requiredValues.organization) { return { ready: false, error: "Organization is required.", focusFieldId: "organization" }; } if (!validation.disclaimerAccepted) { return { ready: false, error: "Please review and acknowledge the disclosures before submitting.", focusFieldId: "disclaimerAck", }; } return { ready: true, error: "", focusFieldId: "" }; } function validateForm() { state.validation = computeValidationState(); clearMissingFieldHighlights(); const readiness = evaluateSubmissionReadiness(state.validation); if (readiness.focusFieldId === "disclaimerAck") { els.disclaimerAck?.classList.add("input-missing"); } else if (readiness.focusFieldId) { highlightMissingField(readiness.focusFieldId); } return readiness.error; } function payload() { const quickRequest = QUICK_REQUEST_ENABLED ? quickRequestSummaryData() : { applied: false, disabled: true, message: "Quick Request is currently inactive." }; const institutionType = effectiveInstitutionType(); const logoPayload = { ...state.logo, logo_data_url: state.logo.logo_data_url && state.logo.logo_data_url.length <= MAX_INLINE_LOGO_DATA_URL_LENGTH ? state.logo.logo_data_url : "", logo_omitted_reason: state.logo.logo_data_url && state.logo.logo_data_url.length > MAX_INLINE_LOGO_DATA_URL_LENGTH ? "inline_logo_too_large" : "", }; const requesterEmail = document.getElementById("email").value.trim(); return { institution_type: institutionType, subject_institution: effectiveSubjectInstitution(), additional_institutions: effectiveAdditionalInstitutions(), reporting_period: selectedPeriod(), quick_request: quickRequest, logo_colors: logoPayload, disclaimer_acknowledged: hasDisclaimerAcknowledged(), // ⭐ ADD THIS FIELD ⭐ requester_email: document.getElementById("email").value.trim(), user: { first_name: document.getElementById("firstName").value.trim(), last_name: document.getElementById("lastName").value.trim(), title: document.getElementById("title").value.trim(), email: document.getElementById("email").value.trim(), phone: document.getElementById("phone").value.trim(), organization: document.getElementById("organization").value.trim(), notes: document.getElementById("notes").value.trim(), }, timestamp: new Date().toISOString(), }; } function resetRegistrationFlow(statusMessage = "", statusKind = "") { sessionStorage.removeItem(REGISTRATION_SESSION_KEY); closeLogoSearchWindow(); els.form.reset(); state.institutionType = DEFAULT_INSTITUTION_TYPE; state.subject = null; state.additional = [null, null, null]; state.validation = { institutionTypeSelected: false, subjectSelected: false, peersSelected: 0, reportFormatSelected: true, reportingPeriodValid: false, userFieldsComplete: false, disclaimerAccepted: false, }; state.ui.activeAccordion = "institutionTypePanel"; state.ui.reviewAutoCollapsed = false; state.ui.reviewPrimed = false; state.ui.submitting = false; state.ui.submitSucceeded = statusKind === "success"; state.ui.wasSubmitReady = false; state.ui.pendingFocus = ""; state.ui.isBrandingWindowOpen = false; state.ui.submissionSuccessOpen = false; state.quickRequest = { open: false, applied: false, activeRow: 0, rows: [], }; state.logo = { logo_name: "", logo_data_url: "", logo_source_url: "", logo_search_url: "", autoExtract: true, colors: { primaryBg: "#0b3d66", cardBg: "#1f4f7a", textColor: "#f8fafc", accentColor: "#d4af37", }, }; els.typeButtons.forEach((button) => { const active = button.dataset.type === DEFAULT_INSTITUTION_TYPE; button.classList.toggle("active", active); button.setAttribute("aria-pressed", String(active)); }); els.typeWarning.hidden = true; els.subjectSearch.value = ""; els.subjectSearch.disabled = false; els.subjectSearch.placeholder = `Type ${formatTypeLabel(DEFAULT_INSTITUTION_TYPE)} name and press Search or Enter`; if (els.subjectStateHint) { els.subjectStateHint.value = ""; els.subjectStateHint.disabled = false; } if (els.subjectCityHint) { els.subjectCityHint.value = ""; els.subjectCityHint.disabled = false; } els.subjectSearchButton.disabled = false; els.subjectResults.innerHTML = ""; els.peerSearches.forEach((input) => { input.value = ""; }); els.peerStateHints.forEach((input) => { input.value = ""; input.disabled = true; }); els.peerCityHints.forEach((input) => { input.value = ""; input.disabled = true; }); els.peerResults.forEach((target) => { target.innerHTML = ""; }); buildQuarterOptions(); applyReportFormatMode(); syncColorInputs(); if (els.logoPreview) { els.logoPreview.classList.remove("has-logo", "is-dragover"); els.logoPreview.innerHTML = "Drag Selected Logo Here"; } if (els.disclaimerAck) { els.disclaimerAck.checked = false; } hideSubmissionSuccessModal(); closeQuickRequestModal(); setInstitutionType(DEFAULT_INSTITUTION_TYPE, { advance: false }); render(); if (statusMessage) { setStatus(statusMessage, statusKind); } } async function submitForm(event) { event.preventDefault(); hideSubmissionSuccessModal(); const error = validateForm(); const submitPayload = payload(); if (!submitPayload.requester_email || !submitPayload.requester_email.includes("@")) { state.ui.submitSucceeded = false; setStatus("Please enter a valid email address for the reporting package.", "error"); render(); return; } els.periodError.hidden = isValidPeriod(); if (error) { state.ui.submitSucceeded = false; setStatus(error, "error"); render(); return; } const isCreditUnionSubmission = effectiveInstitutionType() === "credit_union"; state.ui.submitting = true; state.ui.submitSucceeded = false; syncGlowState(els.submitButton, false); syncSubmitButtonState(state.validation); setStatus( isCreditUnionSubmission ? "Credit Union request received. Starting the CU workflow; this can take several minutes." : "" ); // CU runs are long-lived cross-service requests. Keep the request open so Cloud Run keeps // processing, but keep the UI visibly alive while the server works. const slowNoticeId = setTimeout(() => { if (state.ui.submitting) { if (isCreditUnionSubmission) { setStatus( "CU workflow is still running. Please keep this page open; the secure-link email will be sent when packaging finishes.", "" ); } else { state.ui.submitting = false; state.ui.submitSucceeded = true; setStatus( "Your request has been received and is being processed. You will receive an email when your report is ready.", "success" ); render(); openSubmissionSuccessModal(); } } }, isCreditUnionSubmission ? 3000 : SLOW_SUBMIT_NOTICE_MS); const submitController = new AbortController(); const submitTimeoutId = isCreditUnionSubmission ? null : setTimeout(() => { submitController.abort("submit-confirmation-timeout"); }, SUBMIT_CONFIRMATION_TIMEOUT_MS); try { const response = await fetch("/api/registration/submit", { method: "POST", headers: { "Content-Type": "application/json" }, signal: submitController.signal, body: JSON.stringify(submitPayload), }); const data = await response.json(); if (!response.ok || !data.success) { throw new Error(data.detail || data.message || "Submission failed."); } const providerMessage = data.email?.provider ? ` Provider: ${data.email.provider}.` : ""; const successMessage = `${data.message} ${data.email?.message || ""}${providerMessage}`.trim(); state.ui.submitting = false; state.ui.submitSucceeded = true; setStatus(successMessage, data.email?.sent ? "success" : ""); render(); openSubmissionSuccessModal(); return; } catch (submitError) { const submitTimedOut = submitError?.name === "AbortError"; state.ui.submitSucceeded = false; if (submitTimedOut) { setStatus( "Submission confirmation timed out. The request may already be queued and processing may have started; please check queue status before re-submitting.", "" ); } else { setStatus(submitError.message || "Submit request failed.", "error"); } } finally { clearTimeout(slowNoticeId); if (submitTimeoutId) { clearTimeout(submitTimeoutId); } if (state.ui.submitting) { state.ui.submitting = false; render(); } } } function setupLogoUpload() { els.logoSearchButton.addEventListener("click", () => { if (!state.subject) return; const query = (els.logoSearchQuery.value || "").trim() || logoSearchInstitutionQuery(); logoSearchRequestId += 1; state.logo.logo_search_url = `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}`; setStatus("Preparing the logo drop zone before opening image search.", ""); render(); if (pendingLogoSearchTimer) { window.clearTimeout(pendingLogoSearchTimer); pendingLogoSearchTimer = null; } waitForLogoDropZoneReady(() => { pendingLogoSearchTimer = null; openLogoSearchWindow(); render(); }); }); els.logoUpload.addEventListener("change", () => { const file = els.logoUpload.files[0]; if (!file) return; closeLogoSearchWindow(); handleLogoFile(file); }); setupLogoDropZone(); } function openLogoSearchWindow() { if (!state.logo.logo_search_url) return; const availWidth = Math.max(window.outerWidth, window.screen?.availWidth || 0); const availHeight = Math.max(window.outerHeight, window.screen?.availHeight || 0); const width = Math.min(900, Math.max(560, Math.round(availWidth * 0.46))); const height = Math.min(900, Math.max(560, Math.round(availHeight * 0.78))); const left = Math.max(0, window.screenX + window.outerWidth - width - 16); const preferredTop = Math.max(0, window.screenY + 24); const maxTop = Math.max(0, window.screenY + availHeight - height - 12); const top = Math.min(preferredTop, maxTop); const windowFeatures = `width=${width},height=${height},left=${left},top=${top},resizable=yes,scrollbars=yes`; // Reuse the existing search window when possible to keep drag/drop behavior stable. if (logoSearchPopupWindow && !logoSearchPopupWindow.closed) { try { logoSearchPopupWindow.location.href = state.logo.logo_search_url; logoSearchPopupWindow.focus(); } catch (error) { closeLogoSearchWindow(); } } if (!logoSearchPopupWindow || logoSearchPopupWindow.closed) { // Open a regular named browser window (not popup mode) for more consistent cross-window dragging. logoSearchPopupWindow = window.open("about:blank", "registrationLogoSearch", windowFeatures); if (logoSearchPopupWindow && !logoSearchPopupWindow.closed) { try { logoSearchPopupWindow.location.href = state.logo.logo_search_url; logoSearchPopupWindow.focus(); } catch (error) { closeLogoSearchWindow(); } } } if (!logoSearchPopupWindow || logoSearchPopupWindow.closed) { logoSearchPopupWindow = window.open(state.logo.logo_search_url, "_blank"); } } function setLogoPreview(src, name, sourceUrl = "") { state.logo.logo_name = name; state.logo.logo_data_url = src; state.logo.logo_source_url = sourceUrl; closeLogoSearchWindow(); els.logoPreview.classList.add("has-logo"); els.logoPreview.innerHTML = ` Selected subject institution logo ${name} `; if (state.logo.autoExtract) { extractLogoColor(src); } render(); } function handleLogoFile(file) { if (!file || !file.type.startsWith("image/")) { setStatus("Drop or upload an image file for the logo.", "error"); return; } const reader = new FileReader(); reader.onload = () => setLogoPreview(reader.result, file.name); reader.readAsDataURL(file); } function handleLogoUrl(url) { const cleanUrl = (url || "").trim(); if (!/^https?:\/\//i.test(cleanUrl) && !cleanUrl.startsWith("data:image/")) { setStatus("Drag an image from the search window or upload a saved logo file.", "error"); return; } setLogoPreview(cleanUrl, "Dragged logo image", cleanUrl); } function setupLogoDropZone() { if (logoDropZoneReady || !els.logoPreview) return; logoDropZoneReady = true; const keepLogoDropEnabled = (event) => { if (!isLogoDropZoneInteractive()) return; event.preventDefault(); }; window.addEventListener("dragenter", keepLogoDropEnabled); window.addEventListener("dragover", keepLogoDropEnabled); ["dragenter", "dragover"].forEach((eventName) => { els.logoPreview.addEventListener(eventName, (event) => { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = "copy"; els.logoPreview.classList.add("is-dragover"); }); }); ["dragleave", "drop"].forEach((eventName) => { els.logoPreview.addEventListener(eventName, (event) => { event.preventDefault(); event.stopPropagation(); els.logoPreview.classList.remove("is-dragover"); }); }); els.logoPreview.addEventListener("drop", (event) => { event.preventDefault(); closeLogoSearchWindow({ aggressive: true }); const file = event.dataTransfer.files[0]; if (file) { handleLogoFile(file); return; } const uri = event.dataTransfer.getData("text/uri-list"); const text = event.dataTransfer.getData("text/plain"); handleLogoUrl(uri || text); }); window.addEventListener("paste", (event) => { if (!isLogoDropZoneInteractive()) return; const imageItem = Array.from(event.clipboardData?.items || []).find((item) => item.type.startsWith("image/")); if (imageItem) { event.preventDefault(); closeLogoSearchWindow(); handleLogoFile(imageItem.getAsFile()); } }); } function extractLogoColor(src) { const image = new Image(); image.crossOrigin = "anonymous"; image.onload = () => { try { const canvas = document.createElement("canvas"); const size = 48; canvas.width = size; canvas.height = size; const ctx = canvas.getContext("2d", { willReadFrequently: true }); ctx.drawImage(image, 0, 0, size, size); const palette = extractPalette(ctx.getImageData(0, 0, size, size).data); if (!palette.length) return; applyExtractedColors(palette); syncColorInputs(); updateBrandPreview(); setStatus("Brand colors extracted from logo. You can adjust them before submitting.", "success"); } catch (error) { // Remote logos can block canvas reads through CORS; keep the preview and let users adjust colors manually. } }; image.onerror = () => { setStatus("Logo preview loaded from a remote source, but color extraction was blocked. You can set colors manually.", ""); }; image.src = src; } function extractPalette(data) { const buckets = new Map(); for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 80) continue; const r = data[i]; const g = data[i + 1]; const b = data[i + 2]; const max = Math.max(r, g, b); const min = Math.min(r, g, b); const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b) / 255; const saturation = max === 0 ? 0 : (max - min) / max; if (luminance > 0.94 || luminance < 0.05 || saturation < 0.06) continue; const key = [r, g, b].map((value) => Math.round(value / 24) * 24).join(","); const bucket = buckets.get(key) || { r: 0, g: 0, b: 0, count: 0, saturation: 0, luminance: 0 }; bucket.r += r; bucket.g += g; bucket.b += b; bucket.count += 1; bucket.saturation += saturation; bucket.luminance += luminance; buckets.set(key, bucket); } return Array.from(buckets.values()) .map((bucket) => ({ r: Math.round(bucket.r / bucket.count), g: Math.round(bucket.g / bucket.count), b: Math.round(bucket.b / bucket.count), count: bucket.count, saturation: bucket.saturation / bucket.count, luminance: bucket.luminance / bucket.count, })) .sort((a, b) => (b.count * (1 + b.saturation)) - (a.count * (1 + a.saturation))); } function rgbToHex({ r, g, b }) { return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`; } function hexToRgb(hex) { const normalized = String(hex || "").trim().replace("#", ""); if (!/^[0-9a-fA-F]{6}$/.test(normalized)) return null; return { r: Number.parseInt(normalized.slice(0, 2), 16), g: Number.parseInt(normalized.slice(2, 4), 16), b: Number.parseInt(normalized.slice(4, 6), 16), }; } function mixHex(colorA, colorB, ratio = 0.5) { const rgbA = hexToRgb(colorA); const rgbB = hexToRgb(colorB); if (!rgbA || !rgbB) return colorA; return rgbToHex({ r: Math.round((rgbA.r * (1 - ratio)) + (rgbB.r * ratio)), g: Math.round((rgbA.g * (1 - ratio)) + (rgbB.g * ratio)), b: Math.round((rgbA.b * (1 - ratio)) + (rgbB.b * ratio)), }); } function normalizeHex(value, fallback) { const normalized = String(value || "").trim().toUpperCase(); if (/^#[0-9A-F]{6}$/.test(normalized)) return normalized; if (/^[0-9A-F]{6}$/.test(normalized)) return `#${normalized}`; return String(fallback || "#000000").toUpperCase(); } function mixColor(color, target, amount) { const mix = (value, targetValue) => Math.round(value + (targetValue - value) * amount); return { r: mix(color.r, target.r), g: mix(color.g, target.g), b: mix(color.b, target.b), }; } function applyExtractedColors(palette) { const primary = palette.find((color) => color.luminance < 0.45) || palette[0]; const accent = palette.find((color) => color.saturation > 0.28 && color.luminance >= 0.22) || palette[1] || primary; const card = mixColor(primary, { r: 255, g: 255, b: 255 }, primary.luminance < 0.35 ? 0.16 : 0.08); const textColor = primary.luminance > 0.58 ? "#0b1220" : "#f8fafc"; state.logo.colors.primaryBg = rgbToHex(primary); state.logo.colors.cardBg = rgbToHex(card); state.logo.colors.textColor = textColor; state.logo.colors.accentColor = rgbToHex(accent); } function syncColorInputs() { Object.entries(els.colorInputs).forEach(([key, input]) => { input.value = state.logo.colors[key]; }); Object.entries(els.colorHexInputs).forEach(([key, input]) => { input.value = state.logo.colors[key].toUpperCase(); }); } function applyPreset(presetName) { const presets = { "corporate-blue": { primaryBg: "#0F1B2D", cardBg: "#1E2D40", textColor: "#E2E8F0", accentColor: "#3B82F6" }, "forest-green": { primaryBg: "#0D2414", cardBg: "#1C3B2A", textColor: "#E8F7EF", accentColor: "#42C781" }, burgundy: { primaryBg: "#2B0E17", cardBg: "#4A1826", textColor: "#F8EAF0", accentColor: "#D94F7A" }, midnight: { primaryBg: "#111426", cardBg: "#1E2448", textColor: "#E7EDFF", accentColor: "#6D7DFF" }, "clean-light": { primaryBg: "#E8EDF3", cardBg: "#C8D5E4", textColor: "#122033", accentColor: "#4B89D8" }, }; const palette = presets[presetName]; if (!palette) return; state.logo.autoExtract = false; Object.assign(state.logo.colors, palette); syncColorInputs(); render(); } function resetBrandingPalette() { Object.assign(state.logo.colors, { primaryBg: "#0b3d66", cardBg: "#1f4f7a", textColor: "#f8fafc", accentColor: "#d4af37", }); syncColorInputs(); render(); } function bindHelperInteractionEvents() { const interactTargets = [ els.subjectSearch, els.subjectSearchButton, ...els.peerSearches, ...els.peerSearchButtons, ...els.reportFormatModes, ...els.quarterSelects, ...els.typeButtons, ].filter(Boolean); interactTargets.forEach((target) => { target.addEventListener("input", userInteracted); target.addEventListener("focus", userInteracted); target.addEventListener("click", userInteracted); target.addEventListener("change", userInteracted); }); } function bindEvents() { bindHelperInteractionEvents(); els.typeButtons.forEach((button) => { button.addEventListener("click", () => setInstitutionType(button.dataset.type)); }); if (QUICK_REQUEST_ENABLED && els.quickRequest.modal && els.quickRequest.openButton && els.quickRequest.gridBody) { els.quickRequest.typeInputs.forEach((input) => { input.addEventListener("change", () => { syncQuickRequestTypeUI(input.value); if (input.checked && (!state.institutionType || !hasSelections())) { setInstitutionType(input.value); } }); }); els.quickRequest.openButton.addEventListener("click", openQuickRequestModal); els.quickRequest.editButton?.addEventListener("click", openQuickRequestModal); els.quickRequest.cancelButton?.addEventListener("click", closeQuickRequestModal); els.quickRequest.applyButton?.addEventListener("click", applyQuickRequestModal); els.quickRequest.addRowButton?.addEventListener("click", addQuickRequestPeerRow); els.quickRequest.modal.addEventListener("click", (event) => { if (event.target.dataset.quickClose === "true") { closeQuickRequestModal(); } }); els.quickRequest.gridBody.addEventListener("input", (event) => { const target = event.target; if (!(target instanceof HTMLInputElement)) return; const rowIndex = Number(target.dataset.quickRowIndex); const field = target.dataset.quickField; if (!Number.isInteger(rowIndex) || !field) return; updateQuickRequestRow(rowIndex, field, target.value); target.classList.remove("quick-required", "quick-valid", "quick-invalid"); target.classList.add(getQuickRequestValidationClass(target.value)); }, true); els.quickRequest.gridBody.addEventListener("click", (event) => { const target = event.target; if (!(target instanceof HTMLElement)) return; if ( target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.closest("label") ) { return; } const accordionButton = target.closest("[data-quick-accordion]"); const deleteButton = target.closest("[data-quick-delete]"); const accordionIndex = accordionButton?.getAttribute("data-quick-accordion"); const deleteIndex = deleteButton?.getAttribute("data-quick-delete"); if (accordionIndex !== null) { state.quickRequest.activeRow = Number(accordionIndex); renderQuickRequestModal(); return; } if (deleteIndex !== null) { removeQuickRequestPeerRow(Number(deleteIndex)); } }); } els.clearSelectionsButton.addEventListener("click", clearSelections); els.subjectSearch.addEventListener("input", () => { if ( !state.subject || normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "") ) { subjectResultsLocked = false; } els.subjectResults.innerHTML = ""; els.subjectResults.style.display = ""; els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); debouncedSubjectSearch(); }); els.subjectSearch.addEventListener("keyup", (event) => { if (event.key === "Enter") return; if ( !state.subject || normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "") ) { subjectResultsLocked = false; } els.subjectResults.innerHTML = ""; els.subjectResults.style.display = ""; els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); debouncedSubjectSearch(); }); els.subjectSearch.addEventListener("change", () => { if ( !state.subject || normalizeMatchText(els.subjectSearch.value || "") !== normalizeMatchText(state.subject.name || "") ) { subjectResultsLocked = false; } els.subjectResults.innerHTML = ""; els.subjectResults.style.display = ""; els.subjectResults.classList.remove("results-attention"); syncSubjectResultsGuidance(); if (!els.subjectSearch.value.trim() || els.subjectSearch.value.trim().length < 2) return; runSubjectSearch(); }); els.subjectSearchButton.addEventListener("click", runSubjectSearch); els.subjectSearch.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); runSubjectSearch(); } }); els.subjectSearch.addEventListener("focus", activateOriginalMode); els.subjectSearchButton.addEventListener("click", activateOriginalMode); els.subjectSearch.addEventListener("focus", () => syncGlowState(els.subjectSearch, true)); els.subjectSearch.addEventListener("blur", syncOpenStateGlow); [els.subjectStateHint, els.subjectCityHint].forEach((input) => { input?.addEventListener("input", () => { els.subjectResults.innerHTML = ""; }); }); els.peerSearches.forEach((input) => { input.addEventListener("input", () => { const peerIndex = Number(input.dataset.peerIndex); els.peerResults[peerIndex].innerHTML = ""; debouncedPeerSearch(peerIndex); }); input.addEventListener("keyup", (event) => { if (event.key === "Enter") return; const peerIndex = Number(input.dataset.peerIndex); els.peerResults[peerIndex].innerHTML = ""; debouncedPeerSearch(peerIndex); }); input.addEventListener("change", () => { const peerIndex = Number(input.dataset.peerIndex); const value = els.peerSearches[peerIndex]?.value || ""; els.peerResults[peerIndex].innerHTML = ""; if (!value.trim() || value.trim().length < 2) return; runPeerSearch(peerIndex); }); input.addEventListener("focus", activateOriginalMode); input.addEventListener("keydown", (event) => { if (event.key === "Enter") { event.preventDefault(); const index = Number(input.dataset.peerIndex); runPeerSearch(index); } }); input.addEventListener("focus", () => { els.peerSearches.forEach((peerInput) => syncGlowState(peerInput, peerInput === input)); }); input.addEventListener("blur", syncOpenStateGlow); }); els.peerStateHints.forEach((input) => { input.addEventListener("input", () => { const index = Number(input.dataset.peerIndex); els.peerResults[index].innerHTML = ""; }); }); els.peerCityHints.forEach((input) => { input.addEventListener("input", () => { const index = Number(input.dataset.peerIndex); els.peerResults[index].innerHTML = ""; }); }); els.peerSearchButtons.forEach((button) => { button.addEventListener("click", () => { const index = Number(button.dataset.peerIndex); runPeerSearch(index); }); }); Object.entries(els.colorInputs).forEach(([key, input]) => { input.addEventListener("input", () => { state.logo.autoExtract = false; state.logo.colors[key] = input.value; els.colorHexInputs[key].value = input.value.toUpperCase(); updateBrandPreview(); renderLogo(); }); }); Object.entries(els.colorHexInputs).forEach(([key, input]) => { input.addEventListener("input", () => { state.logo.autoExtract = false; const normalized = normalizeHex(input.value, state.logo.colors[key]); state.logo.colors[key] = normalized; els.colorInputs[key].value = normalized; input.value = normalized.toUpperCase(); updateBrandPreview(); renderLogo(); }); }); els.autoColorToggle.addEventListener("click", () => { state.logo.autoExtract = !state.logo.autoExtract; if (state.logo.autoExtract && state.logo.logo_data_url) { extractLogoColor(state.logo.logo_data_url); } else { render(); } }); els.resetBrandingButton.addEventListener("click", () => { state.logo.autoExtract = true; resetBrandingPalette(); if (state.logo.logo_data_url) { extractLogoColor(state.logo.logo_data_url); } }); els.presetCards.forEach((button) => { button.addEventListener("click", () => applyPreset(button.dataset.preset)); }); els.reportFormatModes.forEach((input) => { input.addEventListener("change", () => { applyReportFormatMode(); render(); }); input.addEventListener("focus", () => syncGlowState(els.reportFormatChoice, true)); input.addEventListener("blur", syncOpenStateGlow); }); els.quarterSelects.forEach((select) => { select.addEventListener("change", render); select.addEventListener("focus", () => syncGlowState(select, true)); select.addEventListener("blur", syncOpenStateGlow); }); document.querySelectorAll("#firstName, #lastName, #email, #title, #phone, #organization, #notes").forEach((input) => { input.addEventListener("input", () => { input.classList.remove("input-missing"); render(); }); }); els.notesModal.toggleButton?.addEventListener("click", () => { if (!els.notesModal.modal) return; els.notesModal.modal.hidden = false; els.notesModal.modal.setAttribute("aria-hidden", "false"); document.body.classList.add("quick-request-modal-open"); document.getElementById("notes")?.focus(); }); const closeNotesModal = () => { if (!els.notesModal.modal) return; els.notesModal.modal.hidden = true; els.notesModal.modal.setAttribute("aria-hidden", "true"); document.body.classList.remove("quick-request-modal-open"); render(); }; els.notesModal.closeButton?.addEventListener("click", closeNotesModal); els.notesModal.modal?.addEventListener("click", (event) => { if (event.target?.dataset?.notesClose === "true") { closeNotesModal(); } }); els.disclaimerAck?.addEventListener("input", () => { els.disclaimerAck.classList.remove("input-missing"); render(); }); els.accordionTriggers.forEach((trigger) => { trigger.addEventListener("click", () => openAccordion(trigger.closest(".accordion-panel").id)); }); els.stageTabs.forEach((tab) => { tab.addEventListener("click", () => { const step = clampStep(tab.dataset.routeStep); if (stepIndex(step) <= stepIndex(state.ui.routeStep)) { goToRouteStep(step); } }); }); els.stepBackButton?.addEventListener("click", () => { const index = Math.max(0, stepIndex(state.ui.routeStep) - 1); goToRouteStep(REGISTRATION_STEPS[index]); }); els.stepNextButton?.addEventListener("click", () => { if (!stepNextEnabled(state.validation)) return; const index = Math.min(REGISTRATION_STEPS.length - 1, stepIndex(state.ui.routeStep) + 1); goToRouteStep(REGISTRATION_STEPS[index]); }); els.submitButton.addEventListener("animationend", (event) => { if (event.animationName === "submit-ready-flash") { els.submitButton.classList.remove("ready-flash"); } }); els.form.addEventListener("submit", submitForm); els.workflowScroll?.addEventListener("scroll", syncWorkflowScrollHint, { passive: true }); window.addEventListener("resize", () => { syncWorkflowScrollHint(); applyStep3bViewportScale(); syncHelperPanel(); }); els.workflowScrollHint?.addEventListener("click", () => { if (!els.workflowScroll) return; const jump = Math.max(180, Math.round(els.workflowScroll.clientHeight * 0.52)); els.workflowScroll.scrollBy({ top: jump, behavior: "smooth" }); }); els.brandingCloseContinueButton?.addEventListener("click", closeBrandingAndContinue); els.submissionSuccess.closeButton?.addEventListener("click", closeAndExitAfterSubmission); els.submissionSuccess.closeIcon?.addEventListener("click", closeAndExitAfterSubmission); els.submissionSuccess.modal?.addEventListener("click", (event) => { if (event.target?.classList?.contains("submission-success-modal-backdrop")) { closeAndExitAfterSubmission(); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape" && state.ui.submissionSuccessOpen) { closeAndExitAfterSubmission(); } }); setupLogoUpload(); } window.registrationSetInstitutionType = (type) => setInstitutionType(type, { advance: true, userInitiated: true }); window.registrationSearchSubject = () => { return runSubjectSearch(); }; window.registrationSearchPeer = (index) => { const peerIndex = Number(index); return runPeerSearch(peerIndex); }; buildQuarterOptions(); applyReportFormatMode(); syncColorInputs(); state.ui.routeStep = detectRouteStep(); document.body.classList.remove("step-1", "step-2", "step-3", "step-3b", "step-4"); document.body.classList.add(`step-${state.ui.routeStep}`); applyStep3bViewportScale(); if (state.ui.routeStep === "4") { if (els.pageTitle) els.pageTitle.textContent = "Step 4 - Review & Submit"; if (els.pageSubtext) els.pageSubtext.textContent = "Confirm your information and submit your reporting package request."; } else { if (els.pageTitle) els.pageTitle.textContent = "Request Reporting Package"; if (els.pageSubtext) els.pageSubtext.textContent = ""; } restoreSessionState(); applyReportFormatMode(); ensureQuickRequestRows(); if (QUICK_REQUEST_ENABLED) { syncQuickRequestTypeUI(); } bindEvents(); initializeGuidancePositioning(); setInstitutionType(state.institutionType || DEFAULT_INSTITUTION_TYPE, { advance: false }); if (state.ui.routeStep === "1") { openAccordion("institutionTypePanel"); } else if (state.ui.routeStep === "2") { openAccordion("additionalPanel"); } else if (state.ui.routeStep === "3") { openAccordion("reportPanel"); } else if (state.ui.routeStep === "3b") { openAccordion("logoPanel"); } else { openAccordion("reviewPanel"); } syncWorkflowScrollHint();