(function () { "use strict"; let overlayEl = null; let backdropEl = null; let toolbarEl = null; let helpButtonEl = null; let helpPanelEl = null; let compareButtonEl = null; let deckCompareButtonEl = null; let deckComparePanelEl = null; let deckCompareMessageEl = null; let deckCompareDeckListEl = null; let zoomControlEl = null; let zoomSliderEl = null; let zoomValueEl = null; let opacityControlEl = null; let opacitySliderEl = null; let opacityValueEl = null; let stageEl = null; let frameEl = null; let baseLayerEl = null; let overlayLayerEl = null; let compareGridEl = null; let imageEl = null; let overlayImageEl = null; let primaryInfoEl = null; let primaryTitleEl = null; let primaryGroupsEl = null; let primaryHintEl = null; let secondaryInfoEl = null; let secondaryTitleEl = null; let secondaryGroupsEl = null; let secondaryHintEl = null; let compareGridSlots = []; let zoomed = false; let previousFocusedEl = null; const LIGHTBOX_ZOOM_SCALE = 6.66; const LIGHTBOX_ZOOM_STEP = 0.1; const LIGHTBOX_PAN_STEP = 4; const LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY = 0.5; const LIGHTBOX_COMPARE_SEQUENCE_STEP_KEYS = new Set(["ArrowLeft", "ArrowRight"]); const lightboxState = { isOpen: false, compareMode: false, deckCompareMode: false, allowOverlayCompare: false, allowDeckCompare: false, primaryCard: null, secondaryCard: null, activeDeckId: "", activeDeckLabel: "", availableCompareDecks: [], selectedCompareDeckIds: [], deckCompareCards: [], maxCompareDecks: 2, deckComparePickerOpen: false, deckCompareMessage: "", sequenceIds: [], resolveCardById: null, resolveDeckCardById: null, onSelectCardId: null, overlayOpacity: LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY, zoomScale: LIGHTBOX_ZOOM_SCALE, helpOpen: false, primaryRotated: false, overlayRotated: false, zoomOriginX: 50, zoomOriginY: 50 }; function hasSecondaryCard() { return Boolean(lightboxState.secondaryCard?.src); } function clampOverlayOpacity(value) { const numericValue = Number(value); if (!Number.isFinite(numericValue)) { return LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY; } return Math.min(1, Math.max(0.05, numericValue)); } function clampZoomScale(value) { const numericValue = Number(value); if (!Number.isFinite(numericValue)) { return 1; } return Math.min(LIGHTBOX_ZOOM_SCALE, Math.max(1, numericValue)); } function normalizeCompareDetails(compareDetails) { if (!Array.isArray(compareDetails)) { return []; } return compareDetails .map((group) => ({ title: String(group?.title || "").trim(), items: Array.isArray(group?.items) ? [...new Set(group.items.map((item) => String(item || "").trim()).filter(Boolean))] : [] })) .filter((group) => group.title && group.items.length); } function normalizeOpenRequest(srcOrOptions, altText, extraOptions) { if (srcOrOptions && typeof srcOrOptions === "object" && !Array.isArray(srcOrOptions)) { return { ...srcOrOptions }; } return { ...(extraOptions || {}), src: srcOrOptions, altText }; } function normalizeCardRequest(request) { const normalized = normalizeOpenRequest(request); const label = String(normalized.label || normalized.altText || "Tarot card enlarged image").trim() || "Tarot card enlarged image"; return { src: String(normalized.src || "").trim(), altText: String(normalized.altText || label).trim() || label, label, cardId: String(normalized.cardId || "").trim(), deckId: String(normalized.deckId || "").trim(), deckLabel: String(normalized.deckLabel || normalized.deckId || "").trim(), missingReason: String(normalized.missingReason || "").trim(), compareDetails: normalizeCompareDetails(normalized.compareDetails) }; } function normalizeDeckOptions(deckOptions) { if (!Array.isArray(deckOptions)) { return []; } return deckOptions .map((deck) => ({ id: String(deck?.id || "").trim(), label: String(deck?.label || deck?.id || "").trim() })) .filter((deck) => deck.id); } function getDeckLabel(deckId) { const normalizedDeckId = String(deckId || "").trim(); if (!normalizedDeckId) { return ""; } if (normalizedDeckId === lightboxState.activeDeckId && lightboxState.activeDeckLabel) { return lightboxState.activeDeckLabel; } const match = lightboxState.availableCompareDecks.find((deck) => deck.id === normalizedDeckId); return match?.label || normalizedDeckId; } function resolveDeckCardRequest(cardId, deckId) { if (!cardId || !deckId || typeof lightboxState.resolveDeckCardById !== "function") { return null; } const resolved = lightboxState.resolveDeckCardById(cardId, deckId); if (!resolved) { return normalizeCardRequest({ cardId, deckId, deckLabel: getDeckLabel(deckId), label: lightboxState.primaryCard?.label || "Tarot card", altText: lightboxState.primaryCard?.altText || "Tarot card", missingReason: "Card image unavailable for this deck." }); } return normalizeCardRequest({ ...resolved, cardId, deckId, deckLabel: resolved.deckLabel || getDeckLabel(deckId) }); } function syncDeckCompareCards() { if (!lightboxState.deckCompareMode || !lightboxState.primaryCard?.cardId) { lightboxState.deckCompareCards = []; return; } lightboxState.deckCompareCards = lightboxState.selectedCompareDeckIds .slice(0, lightboxState.maxCompareDecks) .map((deckId) => resolveDeckCardRequest(lightboxState.primaryCard.cardId, deckId)) .filter(Boolean); } function hasDeckCompareCards() { return lightboxState.deckCompareMode && lightboxState.deckCompareCards.length > 0; } function restoreLightboxFocus() { if (!overlayEl || !lightboxState.isOpen) { return; } requestAnimationFrame(() => { if (overlayEl && lightboxState.isOpen) { overlayEl.focus({ preventScroll: true }); } }); } function resolveCardRequestById(cardId) { if (!cardId || typeof lightboxState.resolveCardById !== "function") { return null; } const resolved = lightboxState.resolveCardById(cardId); if (!resolved) { return null; } return normalizeCardRequest({ ...resolved, cardId }); } function clearSecondaryCard() { lightboxState.secondaryCard = null; if (overlayImageEl) { overlayImageEl.removeAttribute("src"); overlayImageEl.alt = ""; overlayImageEl.style.display = "none"; } syncComparePanels(); syncOpacityControl(); } function clearDeckCompareState() { lightboxState.deckCompareMode = false; lightboxState.selectedCompareDeckIds = []; lightboxState.deckCompareCards = []; lightboxState.deckComparePickerOpen = false; lightboxState.deckCompareMessage = ""; } function updateDeckCompareMode(deckIds, preservePanel = true) { const uniqueDeckIds = [...new Set((Array.isArray(deckIds) ? deckIds : []).map((deckId) => String(deckId || "").trim()).filter(Boolean))] .filter((deckId) => deckId !== lightboxState.activeDeckId) .slice(0, lightboxState.maxCompareDecks); lightboxState.selectedCompareDeckIds = uniqueDeckIds; lightboxState.deckCompareMode = uniqueDeckIds.length > 0; lightboxState.deckCompareMessage = ""; if (!lightboxState.deckCompareMode) { lightboxState.deckCompareCards = []; if (!preservePanel) { lightboxState.deckComparePickerOpen = false; } return; } lightboxState.compareMode = false; clearSecondaryCard(); syncDeckCompareCards(); } function toggleDeckCompareSelection(deckId) { const normalizedDeckId = String(deckId || "").trim(); if (!normalizedDeckId) { return; } const nextSelection = [...lightboxState.selectedCompareDeckIds]; const existingIndex = nextSelection.indexOf(normalizedDeckId); if (existingIndex >= 0) { nextSelection.splice(existingIndex, 1); updateDeckCompareMode(nextSelection); applyComparePresentation(); return; } if (nextSelection.length >= lightboxState.maxCompareDecks) { lightboxState.deckCompareMessage = `You can compare up to ${lightboxState.maxCompareDecks} decks at once.`; applyComparePresentation(); return; } nextSelection.push(normalizedDeckId); updateDeckCompareMode(nextSelection); applyComparePresentation(); } function toggleDeckComparePanel() { if (!lightboxState.allowDeckCompare) { lightboxState.deckComparePickerOpen = true; lightboxState.deckCompareMessage = "Add another registered deck to use deck compare."; applyComparePresentation(); return; } lightboxState.deckComparePickerOpen = !lightboxState.deckComparePickerOpen; lightboxState.deckCompareMessage = lightboxState.availableCompareDecks.length ? "" : "Add another registered deck to use deck compare."; applyComparePresentation(); } function setOverlayOpacity(value) { const opacity = clampOverlayOpacity(value); lightboxState.overlayOpacity = opacity; if (overlayImageEl) { overlayImageEl.style.opacity = String(opacity); } if (opacitySliderEl) { opacitySliderEl.value = String(Math.round(opacity * 100)); opacitySliderEl.disabled = !lightboxState.compareMode || !hasSecondaryCard(); } if (opacityValueEl) { opacityValueEl.textContent = `${Math.round(opacity * 100)}%`; } } function updateImageCursor() { const nextCursor = zoomed ? "zoom-out" : "zoom-in"; if (lightboxState.deckCompareMode) { compareGridSlots.forEach((slot) => { if (slot?.imageEl) { slot.imageEl.style.cursor = nextCursor; } }); return; } if (!imageEl) { return; } imageEl.style.cursor = nextCursor; } function buildRotationTransform(rotated) { return rotated ? "rotate(180deg)" : "rotate(0deg)"; } function isPrimaryRotationActive() { return !lightboxState.compareMode && lightboxState.primaryRotated; } function isOverlayRotationActive() { return lightboxState.compareMode && hasSecondaryCard() && lightboxState.overlayRotated; } function applyTransformOrigins(originX = lightboxState.zoomOriginX, originY = lightboxState.zoomOriginY) { const nextOrigin = `${originX}% ${originY}%`; if (lightboxState.deckCompareMode) { compareGridSlots.forEach((slot) => { if (slot?.imageEl) { slot.imageEl.style.transformOrigin = nextOrigin; } }); return; } if (baseLayerEl) { baseLayerEl.style.transformOrigin = nextOrigin; } if (overlayLayerEl) { overlayLayerEl.style.transformOrigin = nextOrigin; } } function applyZoomTransform() { const activeZoomScale = zoomed ? lightboxState.zoomScale : 1; const showPrimaryRotation = isPrimaryRotationActive(); const showOverlayRotation = isOverlayRotationActive(); if (lightboxState.deckCompareMode) { compareGridSlots.forEach((slot) => { if (!slot?.imageEl) { return; } slot.imageEl.style.transform = `scale(${activeZoomScale}) ${buildRotationTransform(lightboxState.primaryRotated)}`; }); applyTransformOrigins(); updateImageCursor(); return; } if (baseLayerEl) { baseLayerEl.style.transform = `scale(${activeZoomScale})`; } if (overlayLayerEl) { overlayLayerEl.style.transform = `scale(${activeZoomScale})`; } if (imageEl) { imageEl.style.transform = buildRotationTransform(showPrimaryRotation); } if (overlayImageEl) { overlayImageEl.style.transform = buildRotationTransform(showOverlayRotation); } applyTransformOrigins(); updateImageCursor(); } function setZoomScale(value) { const zoomScale = clampZoomScale(value); lightboxState.zoomScale = zoomScale; if (zoomSliderEl) { zoomSliderEl.value = String(Math.round(zoomScale * 100)); } if (zoomValueEl) { zoomValueEl.textContent = `${Math.round(zoomScale * 100)}%`; } if (lightboxState.isOpen) { applyComparePresentation(); return; } applyZoomTransform(); } function isZoomInKey(event) { return event.key === "+" || event.key === "=" || event.code === "NumpadAdd"; } function isZoomOutKey(event) { return event.key === "-" || event.key === "_" || event.code === "NumpadSubtract"; } function isRotateKey(event) { return event.code === "KeyR" || String(event.key || "").toLowerCase() === "r"; } function stepZoom(direction) { if (!lightboxState.isOpen) { return; } const activeScale = zoomed ? lightboxState.zoomScale : 1; const nextScale = clampZoomScale(activeScale + (direction * LIGHTBOX_ZOOM_STEP)); zoomed = nextScale > 1; setZoomScale(nextScale); if (!zoomed && imageEl) { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; } if (!zoomed && overlayImageEl) { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; } } function isPanUpKey(event) { return event.code === "KeyW" || String(event.key || "").toLowerCase() === "w"; } function isPanLeftKey(event) { return event.code === "KeyA" || String(event.key || "").toLowerCase() === "a"; } function isPanDownKey(event) { return event.code === "KeyS" || String(event.key || "").toLowerCase() === "s"; } function isPanRightKey(event) { return event.code === "KeyD" || String(event.key || "").toLowerCase() === "d"; } function stepPan(deltaX, deltaY) { if (!lightboxState.isOpen || !zoomed || !imageEl) { return; } lightboxState.zoomOriginX = Math.min(100, Math.max(0, lightboxState.zoomOriginX + deltaX)); lightboxState.zoomOriginY = Math.min(100, Math.max(0, lightboxState.zoomOriginY + deltaY)); applyTransformOrigins(); } function toggleRotation() { if (!lightboxState.isOpen) { return; } if (lightboxState.deckCompareMode) { lightboxState.primaryRotated = !lightboxState.primaryRotated; applyZoomTransform(); return; } if (lightboxState.compareMode && hasSecondaryCard()) { lightboxState.overlayRotated = !lightboxState.overlayRotated; } else { lightboxState.primaryRotated = !lightboxState.primaryRotated; } applyZoomTransform(); } function createCompareGroupElement(group) { const sectionEl = document.createElement("section"); sectionEl.style.display = "flex"; sectionEl.style.flexDirection = "column"; sectionEl.style.gap = "5px"; sectionEl.style.paddingTop = "8px"; sectionEl.style.borderTop = "1px solid rgba(148, 163, 184, 0.14)"; const titleEl = document.createElement("div"); titleEl.textContent = group.title; titleEl.style.font = "600 10px/1.2 sans-serif"; titleEl.style.letterSpacing = "0.1em"; titleEl.style.textTransform = "uppercase"; titleEl.style.color = "rgba(148, 163, 184, 0.92)"; const valuesEl = document.createElement("div"); valuesEl.style.display = "flex"; valuesEl.style.flexDirection = "column"; valuesEl.style.gap = "3px"; group.items.forEach((item) => { const itemEl = document.createElement("div"); itemEl.textContent = item; itemEl.style.font = "500 12px/1.35 sans-serif"; itemEl.style.color = "#f8fafc"; valuesEl.appendChild(itemEl); }); sectionEl.append(titleEl, valuesEl); return sectionEl; } function renderComparePanel(panelEl, titleEl, groupsEl, hintEl, cardRequest, roleLabel, hintText, isVisible) { if (!panelEl || !titleEl || !groupsEl || !hintEl) { return; } if (!isVisible || !cardRequest?.label) { panelEl.style.display = "none"; titleEl.textContent = ""; hintEl.textContent = ""; groupsEl.replaceChildren(); return; } panelEl.style.display = "flex"; titleEl.textContent = `${roleLabel}: ${cardRequest.label}`; groupsEl.replaceChildren(); if (Array.isArray(cardRequest.compareDetails) && cardRequest.compareDetails.length) { cardRequest.compareDetails.forEach((group) => { groupsEl.appendChild(createCompareGroupElement(group)); }); } else { const emptyEl = document.createElement("div"); emptyEl.textContent = "No compare metadata available."; emptyEl.style.font = "500 12px/1.35 sans-serif"; emptyEl.style.color = "rgba(226, 232, 240, 0.8)"; groupsEl.appendChild(emptyEl); } hintEl.textContent = hintText; hintEl.style.display = hintText ? "block" : "none"; } function syncComparePanels() { if (lightboxState.deckCompareMode) { renderComparePanel(primaryInfoEl, primaryTitleEl, primaryGroupsEl, primaryHintEl, null, "", "", false); renderComparePanel(secondaryInfoEl, secondaryTitleEl, secondaryGroupsEl, secondaryHintEl, null, "", "", false); return; } const isComparing = lightboxState.compareMode; const overlaySelected = hasSecondaryCard(); const showPrimaryPanel = Boolean(lightboxState.isOpen && lightboxState.allowOverlayCompare && lightboxState.primaryCard?.label && !zoomed); const showSecondaryPanel = Boolean(isComparing && overlaySelected && lightboxState.secondaryCard?.label && !zoomed); renderComparePanel( primaryInfoEl, primaryTitleEl, primaryGroupsEl, primaryHintEl, lightboxState.primaryCard, "Base", isComparing ? (overlaySelected ? "Use Left and Right arrows to move through the overlaid card." : "Click another House card behind the dock, or use Left and Right arrows to pick the first overlay card.") : "Use Left and Right arrows to move through cards. Click Overlay to compare.", showPrimaryPanel ); renderComparePanel( secondaryInfoEl, secondaryTitleEl, secondaryGroupsEl, secondaryHintEl, lightboxState.secondaryCard, "Overlay", overlaySelected ? "Use Left and Right arrows to swap the overlay card." : "", showSecondaryPanel ); } function syncOpacityControl() { if (!opacityControlEl) { return; } if (lightboxState.deckCompareMode) { opacityControlEl.style.display = "none"; return; } opacityControlEl.style.display = lightboxState.compareMode && hasSecondaryCard() && !zoomed ? "flex" : "none"; setOverlayOpacity(lightboxState.overlayOpacity); } function syncDeckComparePicker() { if (!deckCompareButtonEl || !deckComparePanelEl || !deckCompareMessageEl || !deckCompareDeckListEl) { return; } const canShowButton = lightboxState.isOpen && !zoomed && !lightboxState.compareMode; deckCompareButtonEl.style.display = canShowButton && (lightboxState.allowDeckCompare || lightboxState.availableCompareDecks.length === 0) ? "inline-flex" : "none"; deckCompareButtonEl.textContent = lightboxState.selectedCompareDeckIds.length ? `Compare (${lightboxState.selectedCompareDeckIds.length})` : "Compare"; deckCompareButtonEl.setAttribute("aria-pressed", lightboxState.deckComparePickerOpen ? "true" : "false"); if (!lightboxState.deckComparePickerOpen || zoomed || lightboxState.compareMode) { deckComparePanelEl.style.display = "none"; deckCompareDeckListEl.replaceChildren(); return; } deckComparePanelEl.style.display = "flex"; deckCompareMessageEl.textContent = lightboxState.deckCompareMessage || (lightboxState.availableCompareDecks.length ? `Choose up to ${lightboxState.maxCompareDecks} extra decks.` : "Add another registered deck to use deck compare."); deckCompareDeckListEl.replaceChildren(); if (!lightboxState.availableCompareDecks.length) { return; } lightboxState.availableCompareDecks.forEach((deck) => { const isSelected = lightboxState.selectedCompareDeckIds.includes(deck.id); const isDisabled = !isSelected && lightboxState.selectedCompareDeckIds.length >= lightboxState.maxCompareDecks; const deckButtonEl = document.createElement("button"); deckButtonEl.type = "button"; deckButtonEl.textContent = deck.label; deckButtonEl.disabled = isDisabled; deckButtonEl.setAttribute("aria-pressed", isSelected ? "true" : "false"); deckButtonEl.style.padding = "10px 12px"; deckButtonEl.style.borderRadius = "12px"; deckButtonEl.style.border = isSelected ? "1px solid rgba(148, 163, 184, 0.7)" : "1px solid rgba(148, 163, 184, 0.22)"; deckButtonEl.style.background = isSelected ? "rgba(30, 41, 59, 0.92)" : "rgba(15, 23, 42, 0.58)"; deckButtonEl.style.color = isDisabled ? "rgba(148, 163, 184, 0.52)" : "#f8fafc"; deckButtonEl.style.cursor = isDisabled ? "not-allowed" : "pointer"; deckButtonEl.style.font = "600 12px/1.3 sans-serif"; deckButtonEl.addEventListener("click", () => { toggleDeckCompareSelection(deck.id); restoreLightboxFocus(); }); deckCompareDeckListEl.appendChild(deckButtonEl); }); if (lightboxState.selectedCompareDeckIds.length) { const clearButtonEl = document.createElement("button"); clearButtonEl.type = "button"; clearButtonEl.textContent = "Clear Compare"; clearButtonEl.style.marginTop = "4px"; clearButtonEl.style.padding = "9px 12px"; clearButtonEl.style.borderRadius = "12px"; clearButtonEl.style.border = "1px solid rgba(248, 250, 252, 0.16)"; clearButtonEl.style.background = "rgba(15, 23, 42, 0.44)"; clearButtonEl.style.color = "rgba(248, 250, 252, 0.92)"; clearButtonEl.style.cursor = "pointer"; clearButtonEl.style.font = "600 12px/1.3 sans-serif"; clearButtonEl.addEventListener("click", () => { updateDeckCompareMode([]); applyComparePresentation(); restoreLightboxFocus(); }); deckCompareDeckListEl.appendChild(clearButtonEl); } } function renderDeckCompareGrid() { if (!compareGridEl || !compareGridSlots.length) { return; } if (!lightboxState.deckCompareMode) { compareGridEl.style.display = "none"; compareGridSlots.forEach((slot) => { slot.slotEl.style.display = "none"; slot.imageEl.removeAttribute("src"); slot.imageEl.alt = "Tarot compare image"; slot.imageEl.style.display = "none"; slot.fallbackEl.style.display = "none"; }); return; } const visibleCards = [lightboxState.primaryCard, ...lightboxState.deckCompareCards].filter(Boolean); compareGridEl.style.display = "grid"; compareGridEl.style.gridTemplateColumns = `repeat(${Math.max(1, visibleCards.length)}, minmax(0, 1fr))`; compareGridSlots.forEach((slot, index) => { const cardRequest = visibleCards[index] || null; if (!cardRequest) { slot.slotEl.style.display = "none"; slot.imageEl.removeAttribute("src"); slot.imageEl.style.display = "none"; slot.fallbackEl.style.display = "none"; return; } slot.slotEl.style.display = "flex"; slot.badgeEl.textContent = cardRequest.deckLabel || (index === 0 ? "Active Deck" : "Compare Deck"); slot.cardLabelEl.textContent = cardRequest.label || "Tarot card"; if (cardRequest.src) { slot.imageEl.src = cardRequest.src; slot.imageEl.alt = cardRequest.altText || cardRequest.label || "Tarot compare image"; slot.imageEl.style.display = "block"; slot.fallbackEl.style.display = "none"; } else { slot.imageEl.removeAttribute("src"); slot.imageEl.alt = ""; slot.imageEl.style.display = "none"; slot.fallbackEl.textContent = cardRequest.missingReason || "Card image unavailable for this deck."; slot.fallbackEl.style.display = "block"; } }); applyZoomTransform(); } function syncHelpUi() { if (!helpButtonEl || !helpPanelEl) { return; } const canShow = lightboxState.isOpen && !zoomed; helpButtonEl.style.display = canShow ? "inline-flex" : "none"; helpPanelEl.style.display = canShow && lightboxState.helpOpen ? "flex" : "none"; helpButtonEl.textContent = lightboxState.helpOpen ? "Hide Help" : "Help"; } function syncZoomControl() { if (!zoomControlEl) { return; } zoomControlEl.style.display = lightboxState.isOpen && !zoomed ? "flex" : "none"; if (zoomSliderEl) { zoomSliderEl.value = String(Math.round(lightboxState.zoomScale * 100)); } if (zoomValueEl) { zoomValueEl.textContent = `${Math.round(lightboxState.zoomScale * 100)}%`; } } function applyComparePresentation() { if (!overlayEl || !backdropEl || !toolbarEl || !stageEl || !frameEl || !imageEl || !overlayImageEl || !compareButtonEl || !deckCompareButtonEl) { return; } compareButtonEl.hidden = zoomed || lightboxState.deckCompareMode || !lightboxState.allowOverlayCompare || (lightboxState.compareMode && !hasSecondaryCard()); compareButtonEl.textContent = lightboxState.compareMode ? "Done Overlay" : "Overlay"; syncHelpUi(); syncZoomControl(); syncOpacityControl(); syncDeckComparePicker(); if (lightboxState.deckCompareMode) { overlayEl.style.pointerEvents = "none"; backdropEl.style.display = "block"; backdropEl.style.pointerEvents = "auto"; backdropEl.style.background = "rgba(0, 0, 0, 0.88)"; toolbarEl.style.top = "24px"; toolbarEl.style.right = "24px"; toolbarEl.style.left = "auto"; stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.width = "auto"; stageEl.style.height = "auto"; stageEl.style.transform = "none"; stageEl.style.pointerEvents = "auto"; frameEl.style.display = "none"; primaryInfoEl.style.display = "none"; secondaryInfoEl.style.display = "none"; renderDeckCompareGrid(); return; } frameEl.style.display = "block"; compareGridEl.style.display = "none"; if (!lightboxState.compareMode) { overlayEl.style.pointerEvents = "none"; backdropEl.style.display = "block"; backdropEl.style.pointerEvents = "auto"; backdropEl.style.background = "rgba(0, 0, 0, 0.82)"; toolbarEl.style.top = "24px"; toolbarEl.style.right = "24px"; toolbarEl.style.left = "auto"; stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.width = "auto"; stageEl.style.height = "auto"; stageEl.style.transform = "none"; stageEl.style.pointerEvents = "auto"; frameEl.style.position = "relative"; frameEl.style.width = "100%"; frameEl.style.height = "100%"; frameEl.style.maxWidth = "none"; frameEl.style.maxHeight = "none"; frameEl.style.borderRadius = "0"; frameEl.style.background = "transparent"; frameEl.style.boxShadow = "none"; frameEl.style.overflow = "hidden"; primaryInfoEl.style.left = "auto"; primaryInfoEl.style.right = "18px"; primaryInfoEl.style.top = "50%"; primaryInfoEl.style.bottom = "auto"; primaryInfoEl.style.width = "clamp(220px, 20vw, 320px)"; primaryInfoEl.style.transform = "translateY(-50%)"; imageEl.style.width = "100%"; imageEl.style.height = "100%"; imageEl.style.maxWidth = "none"; imageEl.style.maxHeight = "none"; imageEl.style.objectFit = "contain"; overlayImageEl.style.display = "none"; secondaryInfoEl.style.display = "none"; syncComparePanels(); applyZoomTransform(); return; } overlayEl.style.pointerEvents = "none"; backdropEl.style.display = "none"; backdropEl.style.pointerEvents = "none"; toolbarEl.style.top = "18px"; toolbarEl.style.right = "18px"; toolbarEl.style.left = "auto"; stageEl.style.pointerEvents = "auto"; frameEl.style.position = "relative"; frameEl.style.width = "100%"; frameEl.style.height = "100%"; frameEl.style.maxWidth = "none"; frameEl.style.maxHeight = "none"; frameEl.style.overflow = "hidden"; imageEl.style.width = "100%"; imageEl.style.height = "100%"; imageEl.style.maxWidth = "none"; imageEl.style.maxHeight = "none"; imageEl.style.objectFit = "contain"; updateImageCursor(); if (zoomed && hasSecondaryCard()) { stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.width = "auto"; stageEl.style.height = "auto"; stageEl.style.transform = "none"; frameEl.style.borderRadius = "0"; frameEl.style.background = "transparent"; frameEl.style.boxShadow = "none"; } else if (!hasSecondaryCard()) { stageEl.style.top = "auto"; stageEl.style.right = "18px"; stageEl.style.bottom = "18px"; stageEl.style.left = "auto"; stageEl.style.width = "clamp(180px, 18vw, 280px)"; stageEl.style.height = "min(44vh, 520px)"; stageEl.style.transform = "none"; frameEl.style.borderRadius = "22px"; frameEl.style.background = "rgba(13, 13, 20, 0.9)"; frameEl.style.boxShadow = "0 24px 64px rgba(0, 0, 0, 0.5)"; primaryInfoEl.style.left = "auto"; primaryInfoEl.style.right = "calc(100% + 16px)"; primaryInfoEl.style.top = "50%"; primaryInfoEl.style.bottom = "auto"; primaryInfoEl.style.width = "clamp(220px, 22vw, 320px)"; primaryInfoEl.style.transform = "translateY(-50%)"; } else { stageEl.style.top = "50%"; stageEl.style.right = "auto"; stageEl.style.bottom = "auto"; stageEl.style.left = "50%"; stageEl.style.width = "min(44vw, 560px)"; stageEl.style.height = "min(92vh, 1400px)"; stageEl.style.transform = "translate(-50%, -50%)"; frameEl.style.borderRadius = "28px"; frameEl.style.background = "rgba(11, 15, 26, 0.88)"; frameEl.style.boxShadow = "0 30px 90px rgba(0, 0, 0, 0.56)"; primaryInfoEl.style.left = "auto"; primaryInfoEl.style.right = "calc(100% + 10px)"; primaryInfoEl.style.top = "50%"; primaryInfoEl.style.bottom = "auto"; primaryInfoEl.style.width = "clamp(180px, 15vw, 220px)"; primaryInfoEl.style.transform = "translateY(-50%)"; secondaryInfoEl.style.left = "calc(100% + 10px)"; secondaryInfoEl.style.right = "auto"; secondaryInfoEl.style.top = "50%"; secondaryInfoEl.style.bottom = "auto"; secondaryInfoEl.style.width = "clamp(180px, 15vw, 220px)"; secondaryInfoEl.style.transform = "translateY(-50%)"; } if (hasSecondaryCard()) { overlayImageEl.style.display = "block"; } else { overlayImageEl.style.display = "none"; secondaryInfoEl.style.display = "none"; } syncComparePanels(); applyZoomTransform(); setOverlayOpacity(lightboxState.overlayOpacity); } function resetZoom() { if (!imageEl && !overlayImageEl) { return; } lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); zoomed = false; applyZoomTransform(); } function updateZoomOrigin(clientX, clientY, targetImage = imageEl) { if (!zoomed || !targetImage) { return; } const rect = targetImage.getBoundingClientRect(); if (!rect.width || !rect.height) { return; } const x = Math.min(100, Math.max(0, ((clientX - rect.left) / rect.width) * 100)); const y = Math.min(100, Math.max(0, ((clientY - rect.top) / rect.height) * 100)); lightboxState.zoomOriginX = x; lightboxState.zoomOriginY = y; applyTransformOrigins(); } function isPointOnCard(clientX, clientY, targetImage = imageEl) { if (!targetImage) { return false; } const rect = targetImage.getBoundingClientRect(); const naturalWidth = targetImage.naturalWidth; const naturalHeight = targetImage.naturalHeight; if (!rect.width || !rect.height || !naturalWidth || !naturalHeight) { return true; } const frameAspect = rect.width / rect.height; const imageAspect = naturalWidth / naturalHeight; let renderWidth = rect.width; let renderHeight = rect.height; if (imageAspect > frameAspect) { renderHeight = rect.width / imageAspect; } else { renderWidth = rect.height * imageAspect; } const left = rect.left + (rect.width - renderWidth) / 2; const top = rect.top + (rect.height - renderHeight) / 2; const right = left + renderWidth; const bottom = top + renderHeight; return clientX >= left && clientX <= right && clientY >= top && clientY <= bottom; } function ensure() { if (overlayEl && imageEl && overlayImageEl) { return; } overlayEl = document.createElement("div"); overlayEl.setAttribute("aria-hidden", "true"); overlayEl.setAttribute("role", "dialog"); overlayEl.setAttribute("aria-modal", "true"); overlayEl.tabIndex = -1; overlayEl.style.position = "fixed"; overlayEl.style.inset = "0"; overlayEl.style.display = "none"; overlayEl.style.zIndex = "9999"; overlayEl.style.pointerEvents = "none"; helpButtonEl = document.createElement("button"); helpButtonEl.type = "button"; helpButtonEl.textContent = "Help"; helpButtonEl.style.position = "fixed"; helpButtonEl.style.top = "24px"; helpButtonEl.style.left = "24px"; helpButtonEl.style.display = "none"; helpButtonEl.style.alignItems = "center"; helpButtonEl.style.justifyContent = "center"; helpButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; helpButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; helpButtonEl.style.color = "#f8fafc"; helpButtonEl.style.borderRadius = "999px"; helpButtonEl.style.padding = "10px 14px"; helpButtonEl.style.font = "600 13px/1.1 sans-serif"; helpButtonEl.style.cursor = "pointer"; helpButtonEl.style.backdropFilter = "blur(12px)"; helpButtonEl.style.pointerEvents = "auto"; helpButtonEl.style.zIndex = "2"; helpPanelEl = document.createElement("div"); helpPanelEl.style.position = "fixed"; helpPanelEl.style.top = "72px"; helpPanelEl.style.left = "24px"; helpPanelEl.style.display = "none"; helpPanelEl.style.flexDirection = "column"; helpPanelEl.style.gap = "8px"; helpPanelEl.style.width = "min(320px, calc(100vw - 48px))"; helpPanelEl.style.padding = "14px 16px"; helpPanelEl.style.borderRadius = "18px"; helpPanelEl.style.background = "rgba(2, 6, 23, 0.88)"; helpPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)"; helpPanelEl.style.color = "#f8fafc"; helpPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)"; helpPanelEl.style.backdropFilter = "blur(12px)"; helpPanelEl.style.pointerEvents = "auto"; helpPanelEl.style.zIndex = "2"; const helpTitleEl = document.createElement("div"); helpTitleEl.textContent = "Lightbox Shortcuts"; helpTitleEl.style.font = "700 13px/1.3 sans-serif"; const helpListEl = document.createElement("div"); helpListEl.style.display = "flex"; helpListEl.style.flexDirection = "column"; helpListEl.style.gap = "6px"; helpListEl.style.font = "500 12px/1.4 sans-serif"; helpListEl.style.color = "rgba(226, 232, 240, 0.92)"; [ "Click card: toggle zoom at the clicked point", "Left / Right: move cards, or move overlay card in compare mode", "Overlay: pick a second card to compare", "Compare: show the same card from up to two other registered decks", "Space: swap base and overlay cards", "R: rotate base card, or rotate overlay card in compare mode", "+ / -: zoom in or out in steps", "W A S D: pan while zoomed", "Escape or backdrop click: close" ].forEach((line) => { const lineEl = document.createElement("div"); lineEl.textContent = line; helpListEl.appendChild(lineEl); }); helpPanelEl.append(helpTitleEl, helpListEl); backdropEl = document.createElement("button"); backdropEl.type = "button"; backdropEl.setAttribute("aria-label", "Close enlarged tarot card"); backdropEl.style.position = "absolute"; backdropEl.style.inset = "0"; backdropEl.style.border = "none"; backdropEl.style.padding = "0"; backdropEl.style.margin = "0"; backdropEl.style.background = "rgba(0, 0, 0, 0.82)"; backdropEl.style.cursor = "pointer"; toolbarEl = document.createElement("div"); toolbarEl.style.position = "fixed"; toolbarEl.style.top = "24px"; toolbarEl.style.right = "24px"; toolbarEl.style.display = "flex"; toolbarEl.style.flexDirection = "column"; toolbarEl.style.alignItems = "flex-end"; toolbarEl.style.gap = "8px"; toolbarEl.style.pointerEvents = "auto"; toolbarEl.style.zIndex = "2"; compareButtonEl = document.createElement("button"); compareButtonEl.type = "button"; compareButtonEl.textContent = "Overlay"; compareButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; compareButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; compareButtonEl.style.color = "#f8fafc"; compareButtonEl.style.borderRadius = "999px"; compareButtonEl.style.padding = "10px 14px"; compareButtonEl.style.font = "600 13px/1.1 sans-serif"; compareButtonEl.style.cursor = "pointer"; compareButtonEl.style.backdropFilter = "blur(12px)"; deckCompareButtonEl = document.createElement("button"); deckCompareButtonEl.type = "button"; deckCompareButtonEl.textContent = "Compare"; deckCompareButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; deckCompareButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; deckCompareButtonEl.style.color = "#f8fafc"; deckCompareButtonEl.style.borderRadius = "999px"; deckCompareButtonEl.style.padding = "10px 14px"; deckCompareButtonEl.style.font = "600 13px/1.1 sans-serif"; deckCompareButtonEl.style.cursor = "pointer"; deckCompareButtonEl.style.backdropFilter = "blur(12px)"; zoomControlEl = document.createElement("label"); zoomControlEl.style.display = "flex"; zoomControlEl.style.alignItems = "center"; zoomControlEl.style.gap = "8px"; zoomControlEl.style.padding = "10px 14px"; zoomControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; zoomControlEl.style.borderRadius = "999px"; zoomControlEl.style.background = "rgba(15, 23, 42, 0.84)"; zoomControlEl.style.color = "#f8fafc"; zoomControlEl.style.font = "600 12px/1.1 sans-serif"; zoomControlEl.style.backdropFilter = "blur(12px)"; const zoomTextEl = document.createElement("span"); zoomTextEl.textContent = "Zoom"; zoomSliderEl = document.createElement("input"); zoomSliderEl.type = "range"; zoomSliderEl.min = "100"; zoomSliderEl.max = String(Math.round(LIGHTBOX_ZOOM_SCALE * 100)); zoomSliderEl.step = "10"; zoomSliderEl.value = String(Math.round(LIGHTBOX_ZOOM_SCALE * 100)); zoomSliderEl.style.width = "110px"; zoomSliderEl.style.cursor = "pointer"; zoomValueEl = document.createElement("span"); zoomValueEl.textContent = `${Math.round(LIGHTBOX_ZOOM_SCALE * 100)}%`; zoomValueEl.style.minWidth = "42px"; zoomValueEl.style.textAlign = "right"; zoomControlEl.append(zoomTextEl, zoomSliderEl, zoomValueEl); opacityControlEl = document.createElement("label"); opacityControlEl.style.display = "none"; opacityControlEl.style.alignItems = "center"; opacityControlEl.style.gap = "8px"; opacityControlEl.style.padding = "10px 14px"; opacityControlEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; opacityControlEl.style.borderRadius = "999px"; opacityControlEl.style.background = "rgba(15, 23, 42, 0.84)"; opacityControlEl.style.color = "#f8fafc"; opacityControlEl.style.font = "600 12px/1.1 sans-serif"; opacityControlEl.style.backdropFilter = "blur(12px)"; const opacityTextEl = document.createElement("span"); opacityTextEl.textContent = "Overlay"; opacitySliderEl = document.createElement("input"); opacitySliderEl.type = "range"; opacitySliderEl.min = "5"; opacitySliderEl.max = "100"; opacitySliderEl.step = "5"; opacitySliderEl.value = String(Math.round(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY * 100)); opacitySliderEl.style.width = "110px"; opacitySliderEl.style.cursor = "pointer"; opacityValueEl = document.createElement("span"); opacityValueEl.textContent = `${Math.round(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY * 100)}%`; opacityValueEl.style.minWidth = "34px"; opacityValueEl.style.textAlign = "right"; opacityControlEl.append(opacityTextEl, opacitySliderEl, opacityValueEl); deckComparePanelEl = document.createElement("div"); deckComparePanelEl.style.position = "fixed"; deckComparePanelEl.style.top = "24px"; deckComparePanelEl.style.right = "176px"; deckComparePanelEl.style.display = "none"; deckComparePanelEl.style.flexDirection = "column"; deckComparePanelEl.style.gap = "10px"; deckComparePanelEl.style.width = "min(280px, calc(100vw - 48px))"; deckComparePanelEl.style.padding = "14px 16px"; deckComparePanelEl.style.borderRadius = "18px"; deckComparePanelEl.style.background = "rgba(2, 6, 23, 0.88)"; deckComparePanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)"; deckComparePanelEl.style.color = "#f8fafc"; deckComparePanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)"; deckComparePanelEl.style.backdropFilter = "blur(12px)"; deckComparePanelEl.style.pointerEvents = "auto"; deckComparePanelEl.style.zIndex = "2"; const deckCompareTitleEl = document.createElement("div"); deckCompareTitleEl.textContent = "Compare Registered Decks"; deckCompareTitleEl.style.font = "700 13px/1.3 sans-serif"; deckCompareMessageEl = document.createElement("div"); deckCompareMessageEl.style.font = "500 12px/1.4 sans-serif"; deckCompareMessageEl.style.color = "rgba(226, 232, 240, 0.84)"; deckCompareDeckListEl = document.createElement("div"); deckCompareDeckListEl.style.display = "flex"; deckCompareDeckListEl.style.flexDirection = "column"; deckCompareDeckListEl.style.gap = "8px"; deckComparePanelEl.append(deckCompareTitleEl, deckCompareMessageEl, deckCompareDeckListEl); toolbarEl.append(compareButtonEl, deckCompareButtonEl, zoomControlEl, opacityControlEl); stageEl = document.createElement("div"); stageEl.style.position = "fixed"; stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.pointerEvents = "auto"; stageEl.style.overflow = "visible"; stageEl.style.transition = "top 220ms ease, right 220ms ease, bottom 220ms ease, left 220ms ease, width 220ms ease, height 220ms ease, transform 220ms ease"; stageEl.style.transform = "none"; stageEl.style.zIndex = "1"; frameEl = document.createElement("div"); frameEl.style.position = "relative"; frameEl.style.width = "100%"; frameEl.style.height = "100%"; frameEl.style.overflow = "hidden"; frameEl.style.transition = "border-radius 220ms ease, background 220ms ease, box-shadow 220ms ease"; baseLayerEl = document.createElement("div"); baseLayerEl.style.position = "absolute"; baseLayerEl.style.inset = "0"; baseLayerEl.style.transform = "scale(1)"; baseLayerEl.style.transformOrigin = "50% 50%"; baseLayerEl.style.transition = "transform 120ms ease-out"; overlayLayerEl = document.createElement("div"); overlayLayerEl.style.position = "absolute"; overlayLayerEl.style.inset = "0"; overlayLayerEl.style.transform = "scale(1)"; overlayLayerEl.style.transformOrigin = "50% 50%"; overlayLayerEl.style.transition = "transform 120ms ease-out"; overlayLayerEl.style.pointerEvents = "none"; compareGridEl = document.createElement("div"); compareGridEl.style.position = "absolute"; compareGridEl.style.inset = "0"; compareGridEl.style.display = "none"; compareGridEl.style.gridTemplateColumns = "repeat(1, minmax(0, 1fr))"; compareGridEl.style.gap = "14px"; compareGridEl.style.alignItems = "stretch"; compareGridEl.style.padding = "76px 24px 24px"; compareGridEl.style.boxSizing = "border-box"; function createCompareGridSlot() { const slotEl = document.createElement("div"); slotEl.style.display = "none"; slotEl.style.flexDirection = "column"; slotEl.style.minWidth = "0"; slotEl.style.minHeight = "0"; slotEl.style.borderRadius = "22px"; slotEl.style.background = "rgba(11, 15, 26, 0.76)"; slotEl.style.border = "1px solid rgba(148, 163, 184, 0.12)"; slotEl.style.boxShadow = "0 24px 64px rgba(0, 0, 0, 0.36)"; slotEl.style.overflow = "hidden"; const headerEl = document.createElement("div"); headerEl.style.display = "flex"; headerEl.style.alignItems = "center"; headerEl.style.justifyContent = "space-between"; headerEl.style.gap = "10px"; headerEl.style.padding = "10px 12px"; headerEl.style.background = "rgba(15, 23, 42, 0.72)"; headerEl.style.borderBottom = "1px solid rgba(148, 163, 184, 0.1)"; const badgeEl = document.createElement("span"); badgeEl.style.font = "700 11px/1.2 sans-serif"; badgeEl.style.letterSpacing = "0.08em"; badgeEl.style.textTransform = "uppercase"; badgeEl.style.color = "#f8fafc"; const cardLabelEl = document.createElement("span"); cardLabelEl.style.font = "500 11px/1.3 sans-serif"; cardLabelEl.style.color = "rgba(226, 232, 240, 0.84)"; cardLabelEl.style.textAlign = "right"; cardLabelEl.style.whiteSpace = "nowrap"; cardLabelEl.style.overflow = "hidden"; cardLabelEl.style.textOverflow = "ellipsis"; headerEl.append(badgeEl, cardLabelEl); const mediaEl = document.createElement("div"); mediaEl.style.position = "relative"; mediaEl.style.flex = "1 1 auto"; mediaEl.style.minHeight = "0"; mediaEl.style.display = "flex"; mediaEl.style.alignItems = "center"; mediaEl.style.justifyContent = "center"; mediaEl.style.padding = "16px"; mediaEl.style.background = "rgba(2, 6, 23, 0.4)"; mediaEl.style.overflow = "hidden"; const compareImageEl = document.createElement("img"); compareImageEl.alt = "Tarot compare image"; compareImageEl.style.width = "100%"; compareImageEl.style.height = "100%"; compareImageEl.style.objectFit = "contain"; compareImageEl.style.cursor = "zoom-in"; compareImageEl.style.transform = "scale(1) rotate(0deg)"; compareImageEl.style.transformOrigin = "center center"; compareImageEl.style.transition = "transform 120ms ease-out"; compareImageEl.style.userSelect = "none"; const fallbackEl = document.createElement("div"); fallbackEl.style.display = "none"; fallbackEl.style.maxWidth = "260px"; fallbackEl.style.padding = "16px"; fallbackEl.style.textAlign = "center"; fallbackEl.style.font = "600 13px/1.45 sans-serif"; fallbackEl.style.color = "rgba(226, 232, 240, 0.88)"; mediaEl.append(compareImageEl, fallbackEl); slotEl.append(headerEl, mediaEl); return { slotEl, badgeEl, cardLabelEl, imageEl: compareImageEl, fallbackEl }; } compareGridSlots = [createCompareGridSlot(), createCompareGridSlot(), createCompareGridSlot()]; compareGridSlots.forEach((slot) => { compareGridEl.appendChild(slot.slotEl); }); imageEl = document.createElement("img"); imageEl.alt = "Tarot card enlarged image"; imageEl.style.width = "100%"; imageEl.style.height = "100%"; imageEl.style.objectFit = "contain"; imageEl.style.cursor = "zoom-in"; imageEl.style.transform = "rotate(0deg)"; imageEl.style.transformOrigin = "center center"; imageEl.style.transition = "transform 120ms ease-out, opacity 180ms ease"; imageEl.style.userSelect = "none"; overlayImageEl = document.createElement("img"); overlayImageEl.alt = "Tarot card overlay image"; overlayImageEl.style.width = "100%"; overlayImageEl.style.height = "100%"; overlayImageEl.style.objectFit = "contain"; overlayImageEl.style.opacity = String(LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY); overlayImageEl.style.pointerEvents = "none"; overlayImageEl.style.display = "none"; overlayImageEl.style.transform = "rotate(0deg)"; overlayImageEl.style.transformOrigin = "center center"; overlayImageEl.style.transition = "opacity 180ms ease"; function createInfoPanel() { const panelEl = document.createElement("div"); panelEl.style.position = "absolute"; panelEl.style.display = "none"; panelEl.style.flexDirection = "column"; panelEl.style.gap = "10px"; panelEl.style.padding = "14px 16px"; panelEl.style.borderRadius = "18px"; panelEl.style.background = "rgba(2, 6, 23, 0.8)"; panelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)"; panelEl.style.color = "#f8fafc"; panelEl.style.backdropFilter = "blur(12px)"; panelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)"; panelEl.style.transition = "opacity 180ms ease, transform 180ms ease"; panelEl.style.transform = "translateY(-50%)"; panelEl.style.pointerEvents = "none"; panelEl.style.maxHeight = "min(78vh, 760px)"; panelEl.style.overflowY = "auto"; const titleEl = document.createElement("div"); titleEl.style.font = "700 13px/1.3 sans-serif"; titleEl.style.color = "#f8fafc"; const groupsEl = document.createElement("div"); groupsEl.style.display = "flex"; groupsEl.style.flexDirection = "column"; groupsEl.style.gap = "0"; const hintEl = document.createElement("div"); hintEl.style.font = "500 11px/1.35 sans-serif"; hintEl.style.color = "rgba(226, 232, 240, 0.82)"; panelEl.append(titleEl, groupsEl, hintEl); return { panelEl, titleEl, groupsEl, hintEl }; } const primaryPanel = createInfoPanel(); primaryInfoEl = primaryPanel.panelEl; primaryTitleEl = primaryPanel.titleEl; primaryGroupsEl = primaryPanel.groupsEl; primaryHintEl = primaryPanel.hintEl; const secondaryPanel = createInfoPanel(); secondaryInfoEl = secondaryPanel.panelEl; secondaryTitleEl = secondaryPanel.titleEl; secondaryGroupsEl = secondaryPanel.groupsEl; secondaryHintEl = secondaryPanel.hintEl; baseLayerEl.appendChild(imageEl); overlayLayerEl.appendChild(overlayImageEl); frameEl.append(baseLayerEl, overlayLayerEl); stageEl.append(frameEl, compareGridEl, primaryInfoEl, secondaryInfoEl); overlayEl.append(backdropEl, stageEl, toolbarEl, deckComparePanelEl, helpButtonEl, helpPanelEl); const close = () => { if (!overlayEl || !imageEl || !overlayImageEl) { return; } lightboxState.isOpen = false; lightboxState.compareMode = false; lightboxState.deckCompareMode = false; lightboxState.allowOverlayCompare = false; lightboxState.allowDeckCompare = false; lightboxState.primaryCard = null; lightboxState.secondaryCard = null; lightboxState.activeDeckId = ""; lightboxState.activeDeckLabel = ""; lightboxState.availableCompareDecks = []; lightboxState.selectedCompareDeckIds = []; lightboxState.deckCompareCards = []; lightboxState.maxCompareDecks = 2; lightboxState.deckComparePickerOpen = false; lightboxState.deckCompareMessage = ""; lightboxState.sequenceIds = []; lightboxState.resolveCardById = null; lightboxState.resolveDeckCardById = null; lightboxState.onSelectCardId = null; lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY; lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE; lightboxState.helpOpen = false; lightboxState.primaryRotated = false; lightboxState.overlayRotated = false; overlayEl.style.display = "none"; overlayEl.setAttribute("aria-hidden", "true"); imageEl.removeAttribute("src"); imageEl.alt = "Tarot card enlarged image"; overlayImageEl.removeAttribute("src"); overlayImageEl.alt = ""; overlayImageEl.style.display = "none"; resetZoom(); syncHelpUi(); syncComparePanels(); syncOpacityControl(); syncDeckComparePicker(); renderDeckCompareGrid(); if (previousFocusedEl instanceof HTMLElement) { previousFocusedEl.focus({ preventScroll: true }); } previousFocusedEl = null; }; function toggleCompareMode() { if (!lightboxState.allowOverlayCompare || !lightboxState.primaryCard) { return; } lightboxState.compareMode = !lightboxState.compareMode; if (!lightboxState.compareMode) { clearSecondaryCard(); } applyComparePresentation(); } function setSecondaryCard(cardRequest, syncSelection = false) { const normalizedCard = normalizeCardRequest(cardRequest); if (!normalizedCard.src || !normalizedCard.cardId || normalizedCard.cardId === lightboxState.primaryCard?.cardId) { return false; } lightboxState.secondaryCard = normalizedCard; overlayImageEl.src = normalizedCard.src; overlayImageEl.alt = normalizedCard.altText; overlayImageEl.style.display = "block"; overlayImageEl.style.opacity = String(lightboxState.overlayOpacity); if (syncSelection && typeof lightboxState.onSelectCardId === "function") { lightboxState.onSelectCardId(normalizedCard.cardId); } applyComparePresentation(); return true; } function stepSecondaryCard(direction) { const sequence = Array.isArray(lightboxState.sequenceIds) ? lightboxState.sequenceIds : []; if (!lightboxState.compareMode || sequence.length < 2 || typeof lightboxState.resolveCardById !== "function") { return; } const anchorId = lightboxState.secondaryCard?.cardId || lightboxState.primaryCard?.cardId; const startIndex = sequence.indexOf(anchorId); if (startIndex < 0) { return; } for (let offset = 1; offset <= sequence.length; offset += 1) { const nextIndex = (startIndex + direction * offset + sequence.length) % sequence.length; const nextCardId = sequence[nextIndex]; if (!nextCardId || nextCardId === lightboxState.primaryCard?.cardId) { continue; } const nextCard = resolveCardRequestById(nextCardId); if (nextCard && setSecondaryCard(nextCard, true)) { break; } } } function stepPrimaryCard(direction) { const sequence = Array.isArray(lightboxState.sequenceIds) ? lightboxState.sequenceIds : []; if (lightboxState.compareMode || sequence.length < 2 || typeof lightboxState.resolveCardById !== "function") { return; } const startIndex = sequence.indexOf(lightboxState.primaryCard?.cardId); if (startIndex < 0) { return; } const nextIndex = (startIndex + direction + sequence.length) % sequence.length; const nextCardId = sequence[nextIndex]; const nextCard = resolveCardRequestById(nextCardId); if (!nextCard?.src) { return; } lightboxState.primaryCard = nextCard; imageEl.src = nextCard.src; imageEl.alt = nextCard.altText; resetZoom(); if (lightboxState.deckCompareMode) { syncDeckCompareCards(); } else { clearSecondaryCard(); } if (typeof lightboxState.onSelectCardId === "function") { lightboxState.onSelectCardId(nextCard.cardId); } applyComparePresentation(); } function swapCompareCards() { if (!lightboxState.compareMode || !lightboxState.primaryCard?.src || !lightboxState.secondaryCard?.src) { return; } const nextPrimaryCard = lightboxState.secondaryCard; const nextSecondaryCard = lightboxState.primaryCard; lightboxState.primaryCard = nextPrimaryCard; lightboxState.secondaryCard = nextSecondaryCard; imageEl.src = nextPrimaryCard.src; imageEl.alt = nextPrimaryCard.altText; overlayImageEl.src = nextSecondaryCard.src; overlayImageEl.alt = nextSecondaryCard.altText; overlayImageEl.style.display = "block"; overlayImageEl.style.opacity = String(lightboxState.overlayOpacity); if (typeof lightboxState.onSelectCardId === "function") { lightboxState.onSelectCardId(nextPrimaryCard.cardId); } applyComparePresentation(); } function shouldIgnoreGlobalKeydown(event) { const target = event.target; if (!(target instanceof HTMLElement)) { return false; } if (!overlayEl?.contains(target)) { return false; } return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target instanceof HTMLButtonElement; } function restoreLightboxFocus() { if (!overlayEl || !lightboxState.isOpen) { return; } requestAnimationFrame(() => { if (overlayEl && lightboxState.isOpen) { overlayEl.focus({ preventScroll: true }); } }); } backdropEl.addEventListener("click", close); helpButtonEl.addEventListener("click", () => { lightboxState.helpOpen = !lightboxState.helpOpen; syncHelpUi(); restoreLightboxFocus(); }); compareButtonEl.addEventListener("click", () => { toggleCompareMode(); restoreLightboxFocus(); }); deckCompareButtonEl.addEventListener("click", () => { toggleDeckComparePanel(); restoreLightboxFocus(); }); zoomSliderEl.addEventListener("input", () => { setZoomScale(Number(zoomSliderEl.value) / 100); }); zoomSliderEl.addEventListener("change", restoreLightboxFocus); zoomSliderEl.addEventListener("pointerup", restoreLightboxFocus); opacitySliderEl.addEventListener("input", () => { setOverlayOpacity(Number(opacitySliderEl.value) / 100); }); opacitySliderEl.addEventListener("change", restoreLightboxFocus); opacitySliderEl.addEventListener("pointerup", restoreLightboxFocus); imageEl.addEventListener("click", (event) => { event.stopPropagation(); if (!isPointOnCard(event.clientX, event.clientY)) { if (lightboxState.compareMode) { return; } close(); return; } if (!zoomed) { zoomed = true; applyZoomTransform(); updateZoomOrigin(event.clientX, event.clientY); applyComparePresentation(); return; } resetZoom(); applyComparePresentation(); }); imageEl.addEventListener("mousemove", (event) => { updateZoomOrigin(event.clientX, event.clientY); }); imageEl.addEventListener("mouseleave", () => { if (zoomed) { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); } }); compareGridSlots.forEach((slot) => { slot.imageEl.addEventListener("click", (event) => { event.stopPropagation(); if (!isPointOnCard(event.clientX, event.clientY, slot.imageEl)) { close(); return; } if (!zoomed) { zoomed = true; applyZoomTransform(); updateZoomOrigin(event.clientX, event.clientY, slot.imageEl); applyComparePresentation(); return; } resetZoom(); applyComparePresentation(); }); slot.imageEl.addEventListener("mousemove", (event) => { updateZoomOrigin(event.clientX, event.clientY, slot.imageEl); }); slot.imageEl.addEventListener("mouseleave", () => { if (zoomed) { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); } }); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { close(); return; } if (shouldIgnoreGlobalKeydown(event)) { return; } if (lightboxState.isOpen && event.code === "Space" && lightboxState.compareMode && hasSecondaryCard()) { event.preventDefault(); swapCompareCards(); return; } if (lightboxState.isOpen && isRotateKey(event)) { event.preventDefault(); toggleRotation(); return; } if (lightboxState.isOpen && isZoomInKey(event)) { event.preventDefault(); stepZoom(1); return; } if (lightboxState.isOpen && isZoomOutKey(event)) { event.preventDefault(); stepZoom(-1); return; } if (lightboxState.isOpen && zoomed && isPanUpKey(event)) { event.preventDefault(); stepPan(0, -LIGHTBOX_PAN_STEP); return; } if (lightboxState.isOpen && zoomed && isPanLeftKey(event)) { event.preventDefault(); stepPan(-LIGHTBOX_PAN_STEP, 0); return; } if (lightboxState.isOpen && zoomed && isPanDownKey(event)) { event.preventDefault(); stepPan(0, LIGHTBOX_PAN_STEP); return; } if (lightboxState.isOpen && zoomed && isPanRightKey(event)) { event.preventDefault(); stepPan(LIGHTBOX_PAN_STEP, 0); return; } if (!lightboxState.isOpen || !LIGHTBOX_COMPARE_SEQUENCE_STEP_KEYS.has(event.key)) { return; } event.preventDefault(); if (lightboxState.compareMode) { stepSecondaryCard(event.key === "ArrowRight" ? 1 : -1); return; } stepPrimaryCard(event.key === "ArrowRight" ? 1 : -1); }); document.body.appendChild(overlayEl); overlayEl.closeLightbox = close; overlayEl.setSecondaryCard = setSecondaryCard; overlayEl.applyComparePresentation = applyComparePresentation; } function open(srcOrOptions, altText, extraOptions) { const request = normalizeOpenRequest(srcOrOptions, altText, extraOptions); const normalizedPrimary = normalizeCardRequest(request); if (!normalizedPrimary.src) { return; } ensure(); if (!overlayEl || !imageEl || !overlayImageEl) { return; } const canCompare = Boolean( request.allowOverlayCompare && normalizedPrimary.cardId && Array.isArray(request.sequenceIds) && request.sequenceIds.length > 1 && typeof request.resolveCardById === "function" ); const canDeckCompare = Boolean( request.allowDeckCompare && normalizedPrimary.cardId && typeof request.resolveDeckCardById === "function" ); if (lightboxState.isOpen && lightboxState.compareMode && lightboxState.allowOverlayCompare && canCompare && normalizedPrimary.cardId) { if (normalizedPrimary.cardId === lightboxState.primaryCard?.cardId) { return; } overlayEl.setSecondaryCard?.(normalizedPrimary, false); return; } lightboxState.isOpen = true; lightboxState.compareMode = false; lightboxState.deckCompareMode = false; lightboxState.allowOverlayCompare = canCompare; lightboxState.allowDeckCompare = canDeckCompare; lightboxState.primaryCard = normalizedPrimary; lightboxState.activeDeckId = String(request.activeDeckId || normalizedPrimary.deckId || "").trim(); lightboxState.activeDeckLabel = String(request.activeDeckLabel || normalizedPrimary.deckLabel || lightboxState.activeDeckId).trim(); lightboxState.availableCompareDecks = canDeckCompare ? normalizeDeckOptions(request.availableCompareDecks).filter((deck) => deck.id !== lightboxState.activeDeckId) : []; lightboxState.selectedCompareDeckIds = []; lightboxState.deckCompareCards = []; lightboxState.maxCompareDecks = Number.isInteger(Number(request.maxCompareDecks)) && Number(request.maxCompareDecks) > 0 ? Number(request.maxCompareDecks) : 2; lightboxState.deckComparePickerOpen = false; lightboxState.deckCompareMessage = ""; lightboxState.sequenceIds = canCompare ? [...request.sequenceIds] : []; lightboxState.resolveCardById = canCompare ? request.resolveCardById : null; lightboxState.resolveDeckCardById = canDeckCompare ? request.resolveDeckCardById : null; lightboxState.onSelectCardId = canCompare && typeof request.onSelectCardId === "function" ? request.onSelectCardId : null; lightboxState.overlayOpacity = LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY; lightboxState.zoomScale = LIGHTBOX_ZOOM_SCALE; lightboxState.helpOpen = false; lightboxState.primaryRotated = false; lightboxState.overlayRotated = false; imageEl.src = normalizedPrimary.src; imageEl.alt = normalizedPrimary.altText; clearSecondaryCard(); resetZoom(); previousFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null; overlayEl.style.display = "block"; overlayEl.setAttribute("aria-hidden", "false"); overlayEl.applyComparePresentation?.(); overlayEl.focus({ preventScroll: true }); } window.TarotUiLightbox = { ...(window.TarotUiLightbox || {}), open }; })();