(function () { "use strict"; let overlayEl = null; let backdropEl = null; let toolbarEl = null; let settingsButtonEl = null; let settingsPanelEl = null; let helpButtonEl = null; let helpPanelEl = null; let compareButtonEl = null; let deckCompareButtonEl = null; let mobileInfoButtonEl = null; let mobileInfoPrimaryTabEl = null; let mobileInfoSecondaryTabEl = 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 exportButtonEl = 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 mobileInfoPanelEl = null; let mobileInfoTitleEl = null; let mobileInfoGroupsEl = null; let mobileInfoHintEl = null; let mobilePrevButtonEl = null; let mobileNextButtonEl = null; let compareGridSlots = []; let zoomed = false; let previousFocusedEl = null; let activePointerId = null; let activePointerStartX = 0; let activePointerStartY = 0; let activePointerMoved = false; let activePinchGesture = null; let suppressNextCardClick = false; let suppressDeckCompareToggleUntil = 0; 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 LIGHTBOX_EXPORT_MIME_TYPE = "image/webp"; const LIGHTBOX_EXPORT_QUALITY = 0.96; const LIGHTBOX_INFO_VISIBLE_STORAGE_KEY = "tarot-lightbox-info-visible-v1"; 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, settingsMenuOpen: false, helpOpen: false, primaryRotated: false, overlayRotated: false, mobileInfoOpen: false, mobileInfoView: "primary", zoomOriginX: 50, zoomOriginY: 50, exportInProgress: false }; function hasSecondaryCard() { return Boolean(lightboxState.secondaryCard?.src); } function isCompactLightboxLayout() { if (typeof window === "undefined") { return false; } if (typeof window.matchMedia === "function") { return window.matchMedia("(max-width: 900px)").matches; } return Number(window.innerWidth) <= 900; } function hasSequenceNavigation() { return Array.isArray(lightboxState.sequenceIds) && lightboxState.sequenceIds.length > 1 && typeof lightboxState.resolveCardById === "function"; } function getActiveMobileInfoView() { return lightboxState.compareMode && hasSecondaryCard() && lightboxState.mobileInfoView === "overlay" ? "overlay" : "primary"; } function getEffectiveMaxCompareDecks() { return isCompactLightboxLayout() ? Math.min(1, lightboxState.maxCompareDecks) : lightboxState.maxCompareDecks; } function getCompareDeckLimitMessage() { const compareDeckLimit = getEffectiveMaxCompareDecks(); if (compareDeckLimit === 1 && isCompactLightboxLayout()) { return "Choose 1 extra deck on mobile."; } return `Choose up to ${compareDeckLimit} extra decks.`; } function shouldHandleCompactPointerGesture(event) { return Boolean( lightboxState.isOpen && isCompactLightboxLayout() && event?.pointerType && event.pointerType !== "mouse" ); } function clearActivePointerGesture() { activePointerId = null; activePointerStartX = 0; activePointerStartY = 0; activePointerMoved = false; } function clearActivePinchGesture() { activePinchGesture = null; } function getTouchMidpoint(touches) { if (!touches || touches.length < 2) { return null; } const first = touches[0]; const second = touches[1]; if (!first || !second) { return null; } return { x: (Number(first.clientX) + Number(second.clientX)) / 2, y: (Number(first.clientY) + Number(second.clientY)) / 2 }; } function getTouchDistance(touches) { if (!touches || touches.length < 2) { return 0; } const first = touches[0]; const second = touches[1]; if (!first || !second) { return 0; } return Math.hypot(Number(first.clientX) - Number(second.clientX), Number(first.clientY) - Number(second.clientY)); } function consumeSuppressedCardClick() { if (!suppressNextCardClick) { return false; } suppressNextCardClick = false; return true; } 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 readStorageValue(key) { try { return window.localStorage?.getItem?.(key) ?? ""; } catch (_error) { return ""; } } function writeStorageValue(key, value) { try { window.localStorage?.setItem?.(key, value); return true; } catch (_error) { return false; } } function getPersistedInfoPanelVisibility() { return String(readStorageValue(LIGHTBOX_INFO_VISIBLE_STORAGE_KEY) || "") === "1"; } function setInfoPanelOpen(nextOpen, options = {}) { const persist = options.persist !== false; lightboxState.mobileInfoOpen = Boolean(nextOpen); if (persist) { writeStorageValue(LIGHTBOX_INFO_VISIBLE_STORAGE_KEY, lightboxState.mobileInfoOpen ? "1" : "0"); } } function sanitizeExportToken(value, fallback = "tarot") { const normalized = String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "-") .replace(/(^-|-$)/g, ""); return normalized || fallback; } function canvasToBlobByFormat(canvas, mimeType, quality) { return new Promise((resolve, reject) => { canvas.toBlob((blob) => { if (blob) { resolve(blob); return; } reject(new Error("Canvas export failed.")); }, mimeType, quality); }); } function getVisibleElementRect(element) { if (!(element instanceof HTMLElement)) { return null; } const computedStyle = window.getComputedStyle(element); if (computedStyle.display === "none" || computedStyle.visibility === "hidden") { return null; } const rect = element.getBoundingClientRect(); if (!rect.width || !rect.height) { return null; } return { left: rect.left, top: rect.top, right: rect.right, bottom: rect.bottom, width: rect.width, height: rect.height }; } function getCssPixelNumber(value, fallback = 0) { const numericValue = Number.parseFloat(String(value || "")); return Number.isFinite(numericValue) ? numericValue : fallback; } function drawRoundedRectPath(context, x, y, width, height, radius) { const safeRadius = Math.max(0, Math.min(radius, width / 2, height / 2)); context.beginPath(); context.moveTo(x + safeRadius, y); context.arcTo(x + width, y, x + width, y + height, safeRadius); context.arcTo(x + width, y + height, x, y + height, safeRadius); context.arcTo(x, y + height, x, y, safeRadius); context.arcTo(x, y, x + width, y, safeRadius); context.closePath(); } function wrapCanvasText(context, text, maxWidth) { const normalized = String(text || "").replace(/\s+/g, " ").trim(); if (!normalized) { return []; } const words = normalized.split(" "); const lines = []; let currentLine = words.shift() || ""; words.forEach((word) => { const nextLine = currentLine ? `${currentLine} ${word}` : word; if (context.measureText(nextLine).width <= maxWidth || !currentLine) { currentLine = nextLine; return; } lines.push(currentLine); currentLine = word; }); if (currentLine) { lines.push(currentLine); } return lines; } function extractPanelSections(panelEl) { if (!(panelEl instanceof HTMLElement)) { return null; } const title = String(panelEl.children[0]?.textContent || "").trim(); const groupsRoot = panelEl.children[1] instanceof HTMLElement ? panelEl.children[1] : null; const hint = String(panelEl.children[2]?.textContent || "").trim(); const groups = groupsRoot ? Array.from(groupsRoot.children).map((sectionEl) => { const titleEl = sectionEl.children[0]; const valuesEl = sectionEl.children[1]; return { title: String(titleEl?.textContent || "").trim(), items: valuesEl instanceof HTMLElement ? Array.from(valuesEl.children).map((itemEl) => String(itemEl.textContent || "").trim()).filter(Boolean) : [] }; }).filter((group) => group.title && group.items.length) : []; return { title, hint, groups }; } async function loadExportImageAsset(source, cache) { const normalizedSource = String(source || "").trim(); if (!normalizedSource) { return null; } if (cache.has(normalizedSource)) { return cache.get(normalizedSource); } const pending = (async () => { const response = await fetch(normalizedSource); if (!response.ok) { throw new Error(`Failed to load export image: ${normalizedSource}`); } const blob = await response.blob(); if (typeof createImageBitmap === "function") { try { return await createImageBitmap(blob); } catch (_error) { } } const blobUrl = URL.createObjectURL(blob); try { return await new Promise((resolve, reject) => { const image = new Image(); image.decoding = "async"; image.onload = () => resolve(image); image.onerror = () => reject(new Error(`Failed to decode export image: ${normalizedSource}`)); image.src = blobUrl; }); } finally { URL.revokeObjectURL(blobUrl); } })(); cache.set(normalizedSource, pending); return pending; } function buildLightboxExportLayout() { const items = []; const pushPanel = (panelEl) => { const rect = getVisibleElementRect(panelEl); if (!rect) { return; } const sections = extractPanelSections(panelEl); if (!sections?.title) { return; } const computedStyle = window.getComputedStyle(panelEl); items.push({ type: "panel", rect, title: sections.title, hint: sections.hint, groups: sections.groups, backgroundColor: computedStyle.backgroundColor || "rgba(2, 6, 23, 0.86)", borderColor: computedStyle.borderColor || "rgba(148, 163, 184, 0.16)", borderRadius: getCssPixelNumber(computedStyle.borderTopLeftRadius, 18) }); }; if (lightboxState.deckCompareMode) { const visibleCards = [lightboxState.primaryCard, ...lightboxState.deckCompareCards].filter(Boolean); compareGridSlots.forEach((slot, index) => { const cardRequest = visibleCards[index] || null; const rect = getVisibleElementRect(slot?.slotEl); const headerRect = getVisibleElementRect(slot?.headerEl); const mediaRect = getVisibleElementRect(slot?.mediaEl); if (!cardRequest || !rect || !headerRect || !mediaRect) { return; } items.push({ type: "deck-card", rect, headerRect, mediaRect, badge: String(slot.badgeEl?.textContent || cardRequest.deckLabel || "Deck").trim(), label: String(slot.cardLabelEl?.textContent || cardRequest.label || "Tarot card").trim(), src: String(cardRequest.src || "").trim(), missingReason: String(cardRequest.missingReason || slot.fallbackEl?.textContent || "Card image unavailable.").trim(), rotated: Boolean(lightboxState.primaryRotated) }); }); } else { const rect = getVisibleElementRect(frameEl); if (rect) { const computedStyle = window.getComputedStyle(frameEl); items.push({ type: "frame", rect, backgroundColor: computedStyle.backgroundColor || "transparent", borderRadius: getCssPixelNumber(computedStyle.borderTopLeftRadius, 0), primarySrc: String(lightboxState.primaryCard?.src || "").trim(), primaryMissingReason: String(lightboxState.primaryCard?.missingReason || "Card image unavailable.").trim(), overlaySrc: hasSecondaryCard() && window.getComputedStyle(overlayImageEl).display !== "none" ? String(lightboxState.secondaryCard?.src || "").trim() : "", overlayMissingReason: String(lightboxState.secondaryCard?.missingReason || "Overlay image unavailable.").trim(), primaryRotated: Boolean(isPrimaryRotationActive()), overlayRotated: Boolean(isOverlayRotationActive()), overlayOpacity: Number(lightboxState.overlayOpacity) || LIGHTBOX_COMPARE_DEFAULT_OVERLAY_OPACITY }); } } pushPanel(primaryInfoEl); pushPanel(secondaryInfoEl); pushPanel(mobileInfoPanelEl); if (!items.length) { return null; } const padding = 16; const minLeft = Math.min(...items.map((item) => item.rect.left)); const minTop = Math.min(...items.map((item) => item.rect.top)); const maxRight = Math.max(...items.map((item) => item.rect.right)); const maxBottom = Math.max(...items.map((item) => item.rect.bottom)); return { padding, minLeft, minTop, width: Math.max(1, Math.ceil((maxRight - minLeft) + (padding * 2))), height: Math.max(1, Math.ceil((maxBottom - minTop) + (padding * 2))), items }; } function toExportRect(layout, rect) { return { x: Math.round((rect.left - layout.minLeft) + layout.padding), y: Math.round((rect.top - layout.minTop) + layout.padding), width: Math.round(rect.width), height: Math.round(rect.height) }; } function drawContainedVisual(context, asset, rect, options = {}) { const inset = Number(options.inset) || 0; const opacity = Number.isFinite(Number(options.opacity)) ? Number(options.opacity) : 1; const rotation = options.rotation === 180 ? Math.PI : 0; const innerWidth = Math.max(1, rect.width - (inset * 2)); const innerHeight = Math.max(1, rect.height - (inset * 2)); const sourceWidth = asset?.width || asset?.naturalWidth || 0; const sourceHeight = asset?.height || asset?.naturalHeight || 0; if (!sourceWidth || !sourceHeight) { return; } const scale = Math.min(innerWidth / sourceWidth, innerHeight / sourceHeight); const drawWidth = sourceWidth * scale; const drawHeight = sourceHeight * scale; const drawX = rect.x + inset + ((innerWidth - drawWidth) / 2); const drawY = rect.y + inset + ((innerHeight - drawHeight) / 2); context.save(); context.globalAlpha = opacity; context.translate(drawX + (drawWidth / 2), drawY + (drawHeight / 2)); if (rotation) { context.rotate(rotation); } context.drawImage(asset, -drawWidth / 2, -drawHeight / 2, drawWidth, drawHeight); context.restore(); } function drawFallbackText(context, rect, text) { context.save(); drawRoundedRectPath(context, rect.x, rect.y, rect.width, rect.height, 16); context.fillStyle = "rgba(15, 23, 42, 0.82)"; context.fill(); context.fillStyle = "rgba(226, 232, 240, 0.9)"; context.font = "600 14px sans-serif"; context.textAlign = "center"; context.textBaseline = "middle"; const lines = wrapCanvasText(context, text, Math.max(80, rect.width - 32)); const lineHeight = 18; const startY = rect.y + (rect.height / 2) - (((lines.length - 1) * lineHeight) / 2); lines.forEach((line, index) => { context.fillText(line, rect.x + (rect.width / 2), startY + (index * lineHeight)); }); context.restore(); } function drawPanel(context, item, layout) { const rect = toExportRect(layout, item.rect); context.save(); drawRoundedRectPath(context, rect.x, rect.y, rect.width, rect.height, item.borderRadius || 18); context.fillStyle = item.backgroundColor || "rgba(2, 6, 23, 0.86)"; context.fill(); context.lineWidth = 1; context.strokeStyle = item.borderColor || "rgba(148, 163, 184, 0.16)"; context.stroke(); const contentX = rect.x + 16; const contentWidth = Math.max(80, rect.width - 32); let cursorY = rect.y + 18; context.fillStyle = "#f8fafc"; context.font = "700 13px sans-serif"; context.textBaseline = "top"; wrapCanvasText(context, item.title, contentWidth).forEach((line) => { context.fillText(line, contentX, cursorY); cursorY += 16; }); cursorY += 6; item.groups.forEach((group, groupIndex) => { if (groupIndex > 0) { context.strokeStyle = "rgba(148, 163, 184, 0.14)"; context.lineWidth = 1; context.beginPath(); context.moveTo(contentX, cursorY + 2); context.lineTo(contentX + contentWidth, cursorY + 2); context.stroke(); cursorY += 10; } context.fillStyle = "rgba(148, 163, 184, 0.92)"; context.font = "600 10px sans-serif"; wrapCanvasText(context, String(group.title || "").toUpperCase(), contentWidth).forEach((line) => { context.fillText(line, contentX, cursorY); cursorY += 12; }); cursorY += 4; context.fillStyle = "#f8fafc"; context.font = "500 12px sans-serif"; group.items.forEach((entry) => { wrapCanvasText(context, entry, contentWidth).forEach((line) => { context.fillText(line, contentX, cursorY); cursorY += 16; }); }); cursorY += 2; }); if (item.hint) { cursorY += 4; context.fillStyle = "rgba(226, 232, 240, 0.82)"; context.font = "500 11px sans-serif"; wrapCanvasText(context, item.hint, contentWidth).forEach((line) => { context.fillText(line, contentX, cursorY); cursorY += 14; }); } context.restore(); } function drawDeckCompareCard(context, item, layout, asset) { const slotRect = toExportRect(layout, item.rect); const headerRect = toExportRect(layout, item.headerRect); const mediaRect = toExportRect(layout, item.mediaRect); context.save(); drawRoundedRectPath(context, slotRect.x, slotRect.y, slotRect.width, slotRect.height, 22); context.fillStyle = "rgba(11, 15, 26, 0.76)"; context.fill(); drawRoundedRectPath(context, headerRect.x, headerRect.y, headerRect.width, headerRect.height, 0); context.fillStyle = "rgba(15, 23, 42, 0.72)"; context.fill(); context.fillStyle = "#f8fafc"; context.font = "700 11px sans-serif"; context.textBaseline = "top"; context.fillText(item.badge, headerRect.x + 12, headerRect.y + 10); context.fillStyle = "rgba(226, 232, 240, 0.84)"; context.font = "500 11px sans-serif"; const labelLines = wrapCanvasText(context, item.label, Math.max(80, headerRect.width - 24)); const labelText = labelLines.slice(0, 2).join(" "); context.fillText(labelText, headerRect.x + 12, headerRect.y + 26); if (asset) { drawContainedVisual(context, asset, mediaRect, { inset: 16, rotation: item.rotated ? 180 : 0, opacity: 1 }); } else { drawFallbackText(context, { x: mediaRect.x + 16, y: mediaRect.y + 16, width: Math.max(1, mediaRect.width - 32), height: Math.max(1, mediaRect.height - 32) }, item.missingReason); } context.restore(); } function drawFrameVisual(context, item, layout, primaryAsset, overlayAsset) { const rect = toExportRect(layout, item.rect); context.save(); if (item.backgroundColor && item.backgroundColor !== "rgba(0, 0, 0, 0)" && item.backgroundColor !== "transparent") { drawRoundedRectPath(context, rect.x, rect.y, rect.width, rect.height, item.borderRadius || 0); context.fillStyle = item.backgroundColor; context.fill(); } if (primaryAsset) { drawContainedVisual(context, primaryAsset, rect, { inset: 0, rotation: item.primaryRotated ? 180 : 0, opacity: 1 }); } else { drawFallbackText(context, rect, item.primaryMissingReason); } if (overlayAsset) { drawContainedVisual(context, overlayAsset, rect, { inset: 0, rotation: item.overlayRotated ? 180 : 0, opacity: item.overlayOpacity }); } context.restore(); } function syncExportButton() { if (!exportButtonEl) { return; } const canShow = lightboxState.isOpen && !zoomed; exportButtonEl.style.display = canShow ? "inline-flex" : "none"; exportButtonEl.disabled = !canShow || lightboxState.exportInProgress; exportButtonEl.textContent = lightboxState.exportInProgress ? "Exporting..." : "Export WebP"; exportButtonEl.style.opacity = exportButtonEl.disabled ? "0.6" : "1"; exportButtonEl.style.cursor = exportButtonEl.disabled ? "progress" : "pointer"; } async function exportCurrentLightboxView() { if (!lightboxState.isOpen || lightboxState.exportInProgress) { return; } lightboxState.exportInProgress = true; syncExportButton(); try { closeSettingsMenu(); applyComparePresentation(); await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); const layout = buildLightboxExportLayout(); if (!layout) { throw new Error("Lightbox scene is not ready to export."); } const scale = Math.max(2, Math.min(3, Number(window.devicePixelRatio) || 1)); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.ceil(layout.width * scale)); canvas.height = Math.max(1, Math.ceil(layout.height * scale)); const context = canvas.getContext("2d"); if (!context) { throw new Error("Canvas context is unavailable."); } context.scale(scale, scale); context.imageSmoothingEnabled = true; context.imageSmoothingQuality = "high"; context.fillStyle = lightboxState.deckCompareMode || lightboxState.compareMode ? "rgba(0, 0, 0, 0.88)" : "rgba(0, 0, 0, 0.82)"; context.fillRect(0, 0, layout.width, layout.height); const imageCache = new Map(); const assetEntries = await Promise.all(layout.items .filter((item) => item.type === "frame" || item.type === "deck-card") .flatMap((item) => { const sources = item.type === "frame" ? [item.primarySrc, item.overlaySrc] : [item.src]; return sources.filter(Boolean); }) .map(async (source) => [source, await loadExportImageAsset(source, imageCache)])); const assetsBySource = new Map(assetEntries); layout.items.forEach((item) => { if (item.type === "frame") { drawFrameVisual( context, item, layout, item.primarySrc ? assetsBySource.get(item.primarySrc) || null : null, item.overlaySrc ? assetsBySource.get(item.overlaySrc) || null : null ); return; } if (item.type === "deck-card") { drawDeckCompareCard( context, item, layout, item.src ? assetsBySource.get(item.src) || null : null ); return; } if (item.type === "panel") { drawPanel(context, item, layout); } }); const blob = await canvasToBlobByFormat(canvas, LIGHTBOX_EXPORT_MIME_TYPE, LIGHTBOX_EXPORT_QUALITY); const blobUrl = URL.createObjectURL(blob); const downloadLink = document.createElement("a"); const stamp = new Date().toISOString().slice(0, 19).replace(/[T:]/g, "-"); const baseCardToken = sanitizeExportToken(lightboxState.primaryCard?.label || lightboxState.primaryCard?.cardId || "tarot-lightbox", "tarot-lightbox"); downloadLink.href = blobUrl; downloadLink.download = `${baseCardToken}-${stamp}.webp`; document.body.appendChild(downloadLink); downloadLink.click(); downloadLink.remove(); URL.revokeObjectURL(blobUrl); } catch (error) { window.alert(error?.message || "Lightbox export failed."); } finally { lightboxState.exportInProgress = false; syncExportButton(); restoreLightboxFocus(); } } 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; } const compareDeckLimit = getEffectiveMaxCompareDecks(); lightboxState.deckCompareCards = lightboxState.selectedCompareDeckIds .slice(0, compareDeckLimit) .map((deckId) => resolveDeckCardRequest(lightboxState.primaryCard.cardId, deckId)) .filter(Boolean); if (lightboxState.selectedCompareDeckIds.length > compareDeckLimit) { lightboxState.selectedCompareDeckIds = lightboxState.selectedCompareDeckIds.slice(0, compareDeckLimit); } } 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; lightboxState.mobileInfoView = "primary"; if (overlayImageEl) { overlayImageEl.removeAttribute("src"); overlayImageEl.alt = ""; overlayImageEl.style.display = "none"; } syncComparePanels(); syncOpacityControl(); } function clearDeckCompareState() { lightboxState.deckCompareMode = false; lightboxState.selectedCompareDeckIds = []; lightboxState.deckCompareCards = []; closeDeckComparePanel(); lightboxState.deckCompareMessage = ""; } function closeDeckComparePanel() { lightboxState.deckComparePickerOpen = false; if (deckComparePanelEl) { deckComparePanelEl.style.display = "none"; } if (deckCompareDeckListEl) { deckCompareDeckListEl.replaceChildren(); } } function closeSettingsMenu() { lightboxState.settingsMenuOpen = false; if (settingsPanelEl) { settingsPanelEl.style.display = "none"; } } function toggleSettingsMenu() { if (!lightboxState.isOpen || zoomed) { return; } const nextOpen = !lightboxState.settingsMenuOpen; lightboxState.settingsMenuOpen = nextOpen; if (nextOpen) { lightboxState.helpOpen = false; closeDeckComparePanel(); } applyComparePresentation(); } function suppressDeckCompareToggle(durationMs = 400) { suppressDeckCompareToggleUntil = Date.now() + Math.max(0, Number(durationMs) || 0); } function shouldSuppressDeckCompareToggle() { return Date.now() < suppressDeckCompareToggleUntil; } function updateDeckCompareMode(deckIds, preservePanel = true) { const compareDeckLimit = getEffectiveMaxCompareDecks(); const uniqueDeckIds = [...new Set((Array.isArray(deckIds) ? deckIds : []).map((deckId) => String(deckId || "").trim()).filter(Boolean))] .filter((deckId) => deckId !== lightboxState.activeDeckId) .slice(0, compareDeckLimit); lightboxState.selectedCompareDeckIds = uniqueDeckIds; lightboxState.deckCompareMode = uniqueDeckIds.length > 0; lightboxState.deckCompareMessage = ""; setInfoPanelOpen(getPersistedInfoPanelVisibility(), { persist: false }); lightboxState.mobileInfoView = "primary"; if (!lightboxState.deckCompareMode) { lightboxState.deckCompareCards = []; if (!preservePanel) { closeDeckComparePanel(); } return; } lightboxState.compareMode = false; clearSecondaryCard(); syncDeckCompareCards(); } function toggleDeckCompareSelection(deckId) { const normalizedDeckId = String(deckId || "").trim(); if (!normalizedDeckId) { return; } const compareDeckLimit = getEffectiveMaxCompareDecks(); const nextSelection = [...lightboxState.selectedCompareDeckIds]; const existingIndex = nextSelection.indexOf(normalizedDeckId); if (existingIndex >= 0) { nextSelection.splice(existingIndex, 1); updateDeckCompareMode(nextSelection); suppressDeckCompareToggle(); closeDeckComparePanel(); applyComparePresentation(); return; } if (nextSelection.length >= compareDeckLimit) { lightboxState.deckCompareMessage = compareDeckLimit === 1 && isCompactLightboxLayout() ? "Mobile compare supports one extra deck at a time." : `You can compare up to ${compareDeckLimit} decks at once.`; applyComparePresentation(); return; } nextSelection.push(normalizedDeckId); updateDeckCompareMode(nextSelection); suppressDeckCompareToggle(); closeDeckComparePanel(); applyComparePresentation(); } function toggleDeckComparePanel() { if (!lightboxState.allowDeckCompare) { closeSettingsMenu(); lightboxState.deckComparePickerOpen = true; lightboxState.deckCompareMessage = "Add another registered deck to use deck compare."; applyComparePresentation(); return; } if (lightboxState.deckComparePickerOpen) { closeDeckComparePanel(); } else { closeSettingsMenu(); lightboxState.deckComparePickerOpen = true; } 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?.zoomLayerEl) { slot.zoomLayerEl.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 || !slot?.zoomLayerEl) { return; } slot.zoomLayerEl.style.transform = `scale(${activeZoomScale})`; slot.imageEl.style.transform = 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 normalizeCompareGroupTitle(title) { return String(title || "").trim().toUpperCase(); } function getDeckCompareInfoGroups(cardRequest) { const desiredLeadTitle = Array.isArray(cardRequest?.compareDetails) && cardRequest.compareDetails.some((group) => normalizeCompareGroupTitle(group?.title) === "DECANS") ? "DECANS" : "SIGNS"; const desiredOrder = [desiredLeadTitle, "ELEMENT", "TETRAGRAMMATON"]; const groupsByTitle = new Map(); if (Array.isArray(cardRequest?.compareDetails)) { cardRequest.compareDetails.forEach((group) => { const title = normalizeCompareGroupTitle(group?.title); if (desiredOrder.includes(title) && !groupsByTitle.has(title)) { groupsByTitle.set(title, { ...group, title }); } }); } return desiredOrder.map((title) => groupsByTitle.get(title)).filter(Boolean); } function renderDeckCompareInfoPanel(panelEl, titleEl, groupsEl, hintEl, cardRequest, roleLabel, hintText, isVisible, horizontal = false) { 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 ? `${roleLabel}: ${cardRequest.label}` : cardRequest.label; groupsEl.replaceChildren(); const compareGroups = getDeckCompareInfoGroups(cardRequest); if (compareGroups.length) { groupsEl.style.display = horizontal ? "grid" : "flex"; groupsEl.style.gridTemplateColumns = horizontal ? "repeat(2, minmax(0, 1fr))" : "none"; groupsEl.style.gridAutoFlow = horizontal ? "row" : "initial"; groupsEl.style.flexDirection = horizontal ? "row" : "column"; groupsEl.style.flexWrap = horizontal ? "wrap" : "nowrap"; groupsEl.style.gap = horizontal ? "10px 14px" : "0"; groupsEl.style.alignItems = horizontal ? "start" : "stretch"; compareGroups.forEach((group) => { const sectionEl = createCompareGroupElement(group); sectionEl.style.minWidth = "0"; sectionEl.style.width = "100%"; if (horizontal && normalizeCompareGroupTitle(group.title) === "TETRAGRAMMATON") { sectionEl.style.gridColumn = "1 / -1"; } groupsEl.appendChild(sectionEl); }); } else { groupsEl.style.display = "flex"; groupsEl.style.flexDirection = "column"; groupsEl.style.gap = "0"; 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 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 ? `${roleLabel}: ${cardRequest.label}` : 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 renderMobileInfoPanel(cardRequest, roleLabel, hintText, isVisible) { renderComparePanel( mobileInfoPanelEl, mobileInfoTitleEl, mobileInfoGroupsEl, mobileInfoHintEl, cardRequest, roleLabel, hintText, isVisible ); } function syncInfoPanelContentLayout(panelEl, groupsEl, hintEl, options = {}) { if (!panelEl || !groupsEl || !hintEl) { return; } const horizontal = Boolean(options.horizontal); groupsEl.style.display = horizontal ? "grid" : "flex"; groupsEl.style.gridTemplateColumns = horizontal ? "repeat(auto-fit, minmax(220px, 1fr))" : "none"; groupsEl.style.flexDirection = horizontal ? "row" : "column"; groupsEl.style.flexWrap = horizontal ? "wrap" : "nowrap"; groupsEl.style.gap = horizontal ? "10px 14px" : "0"; hintEl.style.marginTop = horizontal ? "2px" : "0"; Array.from(groupsEl.children).forEach((child) => { if (!(child instanceof HTMLElement)) { return; } if (child.tagName === "SECTION") { child.style.flex = horizontal ? "1 1 auto" : "0 0 auto"; child.style.minWidth = horizontal ? "0" : "0"; child.style.paddingTop = horizontal ? "10px" : "8px"; return; } child.style.flex = horizontal ? "1 1 100%" : "0 0 auto"; child.style.minWidth = "0"; }); } function syncMobileInfoControls() { if (!mobileInfoButtonEl || !mobileInfoPrimaryTabEl || !mobileInfoSecondaryTabEl || !mobileInfoPanelEl) { return; } const isCompact = isCompactLightboxLayout(); const canShowDeckCompareInfo = Boolean( lightboxState.isOpen && !zoomed && lightboxState.deckCompareMode && lightboxState.primaryCard?.label ); const canShowOverlayInfo = Boolean( lightboxState.isOpen && isCompact && !zoomed && !lightboxState.deckCompareMode && lightboxState.allowOverlayCompare && lightboxState.primaryCard?.label ); const canShowInfo = canShowDeckCompareInfo || canShowOverlayInfo; const hasOverlayInfo = Boolean(lightboxState.compareMode && hasSecondaryCard() && lightboxState.secondaryCard?.label); const activeView = getActiveMobileInfoView(); mobileInfoButtonEl.style.display = canShowInfo ? "inline-flex" : "none"; mobileInfoButtonEl.textContent = lightboxState.mobileInfoOpen ? "Hide Info" : "Info"; mobileInfoButtonEl.setAttribute("aria-pressed", lightboxState.mobileInfoOpen ? "true" : "false"); mobileInfoPrimaryTabEl.style.display = canShowOverlayInfo && lightboxState.mobileInfoOpen && hasOverlayInfo ? "inline-flex" : "none"; mobileInfoSecondaryTabEl.style.display = canShowOverlayInfo && lightboxState.mobileInfoOpen && hasOverlayInfo ? "inline-flex" : "none"; mobileInfoPrimaryTabEl.setAttribute("aria-pressed", activeView === "primary" ? "true" : "false"); mobileInfoSecondaryTabEl.setAttribute("aria-pressed", activeView === "overlay" ? "true" : "false"); mobileInfoPrimaryTabEl.style.background = activeView === "primary" ? "rgba(51, 65, 85, 0.96)" : "rgba(15, 23, 42, 0.84)"; mobileInfoSecondaryTabEl.style.background = activeView === "overlay" ? "rgba(51, 65, 85, 0.96)" : "rgba(15, 23, 42, 0.84)"; mobileInfoPanelEl.style.pointerEvents = canShowInfo && lightboxState.mobileInfoOpen ? "auto" : "none"; } function syncMobileNavigationControls() { if (!mobilePrevButtonEl || !mobileNextButtonEl) { return; } if (mobilePrevButtonEl.parentElement !== overlayEl) { overlayEl.appendChild(mobilePrevButtonEl); } if (mobileNextButtonEl.parentElement !== overlayEl) { overlayEl.appendChild(mobileNextButtonEl); } const canNavigate = Boolean( lightboxState.isOpen && isCompactLightboxLayout() && !zoomed && !lightboxState.deckComparePickerOpen && hasSequenceNavigation() ); const previousLabel = lightboxState.compareMode ? "Previous overlay card" : (lightboxState.deckCompareMode ? "Previous compared card" : "Previous card"); const nextLabel = lightboxState.compareMode ? "Next overlay card" : (lightboxState.deckCompareMode ? "Next compared card" : "Next card"); mobilePrevButtonEl.style.display = canNavigate ? "inline-flex" : "none"; mobileNextButtonEl.style.display = canNavigate ? "inline-flex" : "none"; mobilePrevButtonEl.setAttribute("aria-label", previousLabel); mobileNextButtonEl.setAttribute("aria-label", nextLabel); if (!canNavigate) { return; } const mobileInfoPanelVisible = Boolean( lightboxState.mobileInfoOpen && mobileInfoPanelEl && mobileInfoPanelEl.style.display !== "none" ); const settingsPanelVisible = Boolean( lightboxState.settingsMenuOpen && settingsPanelEl && settingsPanelEl.style.display !== "none" ); const helpPanelVisible = Boolean( lightboxState.helpOpen && helpPanelEl && helpPanelEl.style.display !== "none" ); const deckPickerVisible = Boolean( lightboxState.deckComparePickerOpen && deckComparePanelEl && deckComparePanelEl.style.display !== "none" ); const toolbarHeight = toolbarEl instanceof HTMLElement && toolbarEl.style.display !== "none" ? toolbarEl.offsetHeight : 0; const infoPanelHeight = mobileInfoPanelVisible && mobileInfoPanelEl instanceof HTMLElement ? mobileInfoPanelEl.offsetHeight : 0; const floatingPanelHeight = Math.max( settingsPanelVisible && settingsPanelEl instanceof HTMLElement ? settingsPanelEl.offsetHeight + 12 : 0, helpPanelVisible && helpPanelEl instanceof HTMLElement ? helpPanelEl.offsetHeight + 12 : 0, deckPickerVisible && deckComparePanelEl instanceof HTMLElement ? deckComparePanelEl.offsetHeight + 12 : 0 ); const bottomOffset = toolbarHeight + floatingPanelHeight + (mobileInfoPanelVisible ? infoPanelHeight + 32 : 24); mobilePrevButtonEl.style.top = "auto"; mobileNextButtonEl.style.top = "auto"; mobilePrevButtonEl.style.bottom = `${bottomOffset}px`; mobileNextButtonEl.style.bottom = `${bottomOffset}px`; mobilePrevButtonEl.style.transform = "none"; mobileNextButtonEl.style.transform = "none"; } function syncComparePanels() { if (lightboxState.deckCompareMode) { const isCompact = isCompactLightboxLayout(); const sharedHint = isCompact ? "Use the side arrows to move through compared decks." : "Shared card info for all compared decks."; const showDesktopPanel = Boolean( !isCompact && lightboxState.isOpen && lightboxState.mobileInfoOpen && lightboxState.primaryCard?.label && !zoomed ); const showMobilePanel = Boolean( isCompact && lightboxState.isOpen && lightboxState.mobileInfoOpen && lightboxState.primaryCard?.label && !zoomed ); renderDeckCompareInfoPanel( primaryInfoEl, primaryTitleEl, primaryGroupsEl, primaryHintEl, lightboxState.primaryCard, "Card", sharedHint, showDesktopPanel, true ); renderComparePanel(secondaryInfoEl, secondaryTitleEl, secondaryGroupsEl, secondaryHintEl, null, "", "", false); renderDeckCompareInfoPanel( mobileInfoPanelEl, mobileInfoTitleEl, mobileInfoGroupsEl, mobileInfoHintEl, lightboxState.primaryCard, "Card", sharedHint, showMobilePanel, false ); return; } syncInfoPanelContentLayout(primaryInfoEl, primaryGroupsEl, primaryHintEl, { horizontal: false }); syncInfoPanelContentLayout(secondaryInfoEl, secondaryGroupsEl, secondaryHintEl, { horizontal: false }); syncInfoPanelContentLayout(mobileInfoPanelEl, mobileInfoGroupsEl, mobileInfoHintEl, { horizontal: false }); const isCompact = isCompactLightboxLayout(); const isComparing = lightboxState.compareMode; const overlaySelected = hasSecondaryCard(); const primaryHint = isComparing ? (overlaySelected ? "Use the side arrows to move through the overlaid card." : "Use the side arrows to pick the first overlay card.") : "Use the side arrows to move through cards. Tap Overlay to compare."; const secondaryHint = overlaySelected ? "Use the side arrows to swap the overlay card." : ""; const showPrimaryPanel = Boolean( !isCompact && lightboxState.isOpen && lightboxState.allowOverlayCompare && lightboxState.primaryCard?.label && !zoomed ); const showSecondaryPanel = Boolean( !isCompact && isComparing && overlaySelected && lightboxState.secondaryCard?.label && !zoomed ); renderComparePanel( primaryInfoEl, primaryTitleEl, primaryGroupsEl, primaryHintEl, lightboxState.primaryCard, "Base", primaryHint, showPrimaryPanel ); renderComparePanel( secondaryInfoEl, secondaryTitleEl, secondaryGroupsEl, secondaryHintEl, lightboxState.secondaryCard, "Overlay", secondaryHint, showSecondaryPanel ); if (!isCompact) { renderMobileInfoPanel(null, "", "", false); return; } const activeView = getActiveMobileInfoView(); const mobileCard = activeView === "overlay" ? lightboxState.secondaryCard : lightboxState.primaryCard; const mobileRole = activeView === "overlay" ? "Overlay" : (isComparing ? "Base" : "Card"); const mobileHint = activeView === "overlay" ? secondaryHint : primaryHint; const showMobileInfo = Boolean( lightboxState.isOpen && lightboxState.mobileInfoOpen && !zoomed && lightboxState.allowOverlayCompare && mobileCard?.label ); renderMobileInfoPanel(mobileCard, mobileRole, mobileHint, showMobileInfo); } 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 syncSettingsUi() { if (!settingsButtonEl || !settingsPanelEl) { return; } const canShow = lightboxState.isOpen && !zoomed; settingsButtonEl.style.display = canShow ? "inline-flex" : "none"; settingsButtonEl.textContent = lightboxState.settingsMenuOpen ? "Hide Settings" : "Settings"; settingsButtonEl.setAttribute("aria-expanded", canShow && lightboxState.settingsMenuOpen ? "true" : "false"); settingsPanelEl.style.display = canShow && lightboxState.settingsMenuOpen ? "flex" : "none"; settingsPanelEl.style.pointerEvents = canShow && lightboxState.settingsMenuOpen ? "auto" : "none"; } 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 ? getCompareDeckLimitMessage() : "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 >= getEffectiveMaxCompareDecks(); 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", (event) => { event.preventDefault(); event.stopPropagation(); 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", (event) => { event.preventDefault(); event.stopPropagation(); updateDeckCompareMode([]); suppressDeckCompareToggle(); closeDeckComparePanel(); applyComparePresentation(); restoreLightboxFocus(); }); deckCompareDeckListEl.appendChild(clearButtonEl); } } function renderDeckCompareGrid() { if (!compareGridEl || !compareGridSlots.length) { return; } const isCompact = isCompactLightboxLayout(); 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"; if (slot.zoomLayerEl) { slot.zoomLayerEl.style.display = "none"; } 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.gap = isCompact ? "6px" : "14px"; compareGridEl.style.padding = isCompact ? (lightboxState.mobileInfoOpen ? "18px 12px 260px" : "8px 6px 88px") : (lightboxState.mobileInfoOpen ? "clamp(210px, 30vh, 290px) 24px 24px" : "76px 24px 24px"); compareGridEl.style.gridTemplateColumns = `repeat(${Math.max(1, visibleCards.length)}, minmax(0, 1fr))`; compareGridEl.style.alignItems = isCompact ? "center" : "stretch"; compareGridEl.style.alignContent = isCompact ? "center" : "stretch"; compareGridSlots.forEach((slot, index) => { const cardRequest = visibleCards[index] || null; if (!cardRequest) { slot.slotEl.style.display = "none"; slot.imageEl.removeAttribute("src"); if (slot.zoomLayerEl) { slot.zoomLayerEl.style.display = "none"; } slot.imageEl.style.display = "none"; slot.fallbackEl.style.display = "none"; return; } slot.slotEl.style.display = "flex"; slot.slotEl.style.width = "100%"; slot.slotEl.style.height = isCompact ? "auto" : "100%"; slot.slotEl.style.maxWidth = isCompact ? "220px" : "none"; slot.slotEl.style.justifySelf = isCompact ? "center" : "stretch"; slot.slotEl.style.alignSelf = isCompact ? "center" : "stretch"; slot.slotEl.style.borderRadius = isCompact ? "12px" : "22px"; slot.slotEl.style.boxShadow = isCompact ? "0 8px 18px rgba(0, 0, 0, 0.22)" : "0 24px 64px rgba(0, 0, 0, 0.36)"; slot.headerEl.style.padding = isCompact ? "6px 8px" : "10px 12px"; slot.headerEl.style.gap = isCompact ? "4px" : "10px"; slot.badgeEl.style.font = isCompact ? "700 9px/1.15 sans-serif" : "700 11px/1.2 sans-serif"; slot.cardLabelEl.style.font = isCompact ? "500 9px/1.2 sans-serif" : "500 11px/1.3 sans-serif"; slot.mediaEl.style.flex = isCompact ? "0 0 auto" : "1 1 auto"; slot.mediaEl.style.aspectRatio = isCompact ? "2 / 3" : "auto"; slot.mediaEl.style.padding = isCompact ? "4px" : "16px"; slot.zoomLayerEl.style.inset = isCompact ? "4px" : "16px"; slot.fallbackEl.style.maxWidth = isCompact ? "100%" : "260px"; slot.fallbackEl.style.padding = isCompact ? "12px 8px" : "16px"; 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"; if (slot.zoomLayerEl) { slot.zoomLayerEl.style.display = "flex"; } slot.imageEl.style.display = "block"; slot.fallbackEl.style.display = "none"; } else { slot.imageEl.removeAttribute("src"); slot.imageEl.alt = ""; if (slot.zoomLayerEl) { slot.zoomLayerEl.style.display = "none"; } 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; } const isCompact = isCompactLightboxLayout(); if (!isCompact) { settingsPanelEl.style.top = "72px"; settingsPanelEl.style.right = "24px"; settingsPanelEl.style.bottom = "auto"; settingsPanelEl.style.left = "auto"; settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))"; settingsPanelEl.style.maxHeight = "none"; settingsPanelEl.style.overflowY = "visible"; helpPanelEl.style.top = "72px"; helpPanelEl.style.right = "24px"; helpPanelEl.style.bottom = "auto"; helpPanelEl.style.left = "auto"; helpPanelEl.style.width = "min(320px, calc(100vw - 48px))"; helpPanelEl.style.maxHeight = "none"; helpPanelEl.style.overflowY = "visible"; deckComparePanelEl.style.top = "72px"; deckComparePanelEl.style.right = "24px"; deckComparePanelEl.style.bottom = "auto"; deckComparePanelEl.style.left = "auto"; deckComparePanelEl.style.width = "min(280px, calc(100vw - 48px))"; deckComparePanelEl.style.maxHeight = "none"; deckComparePanelEl.style.overflowY = "visible"; } compareButtonEl.hidden = zoomed || lightboxState.deckCompareMode || !lightboxState.allowOverlayCompare || (!isCompact && lightboxState.compareMode && !hasSecondaryCard()); compareButtonEl.textContent = lightboxState.compareMode ? "Done Overlay" : "Overlay"; syncSettingsUi(); syncHelpUi(); syncZoomControl(); syncExportButton(); syncOpacityControl(); syncDeckComparePicker(); syncComparePanels(); syncMobileInfoControls(); syncMobileNavigationControls(); 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 = isCompact ? "auto" : "24px"; toolbarEl.style.right = isCompact ? "12px" : "24px"; toolbarEl.style.bottom = isCompact ? "calc(12px + env(safe-area-inset-bottom, 0px))" : "auto"; toolbarEl.style.left = isCompact ? "12px" : "auto"; toolbarEl.style.flexDirection = isCompact ? "row" : "column"; toolbarEl.style.flexWrap = isCompact ? "wrap" : "nowrap"; toolbarEl.style.alignItems = isCompact ? "center" : "flex-end"; toolbarEl.style.justifyContent = isCompact ? "center" : "flex-start"; stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.display = "block"; stageEl.style.alignItems = "stretch"; stageEl.style.justifyContent = "stretch"; stageEl.style.width = "auto"; stageEl.style.height = "auto"; stageEl.style.transform = "none"; stageEl.style.pointerEvents = "auto"; compareGridEl.style.padding = isCompact ? (lightboxState.mobileInfoOpen ? "18px 12px 260px" : "18px 12px 84px") : (lightboxState.mobileInfoOpen ? "clamp(210px, 30vh, 290px) 24px 24px" : "76px 24px 24px"); frameEl.style.display = "none"; primaryInfoEl.style.left = "24px"; primaryInfoEl.style.right = "24px"; primaryInfoEl.style.top = "72px"; primaryInfoEl.style.bottom = "auto"; primaryInfoEl.style.width = "auto"; primaryInfoEl.style.maxHeight = "clamp(140px, 24vh, 210px)"; primaryInfoEl.style.transform = "none"; secondaryInfoEl.style.display = "none"; if (mobileInfoPanelEl && !isCompact) { mobileInfoPanelEl.style.display = "none"; } syncMobileNavigationControls(); renderDeckCompareGrid(); return; } frameEl.style.display = "block"; compareGridEl.style.display = "none"; if (isCompact) { overlayEl.style.pointerEvents = "none"; backdropEl.style.display = "block"; backdropEl.style.pointerEvents = "auto"; backdropEl.style.background = lightboxState.compareMode ? "rgba(0, 0, 0, 0.88)" : "rgba(0, 0, 0, 0.82)"; toolbarEl.style.top = "auto"; toolbarEl.style.right = "12px"; toolbarEl.style.bottom = "calc(12px + env(safe-area-inset-bottom, 0px))"; toolbarEl.style.left = "12px"; toolbarEl.style.flexDirection = "row"; toolbarEl.style.flexWrap = "wrap"; toolbarEl.style.alignItems = "center"; toolbarEl.style.justifyContent = "center"; settingsPanelEl.style.top = "auto"; settingsPanelEl.style.right = "12px"; settingsPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))"; settingsPanelEl.style.left = "12px"; settingsPanelEl.style.width = "auto"; settingsPanelEl.style.maxHeight = "min(56svh, 440px)"; settingsPanelEl.style.overflowY = "auto"; helpPanelEl.style.top = "auto"; helpPanelEl.style.right = "12px"; helpPanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))"; helpPanelEl.style.left = "12px"; helpPanelEl.style.width = "auto"; helpPanelEl.style.maxHeight = "min(42svh, 360px)"; helpPanelEl.style.overflowY = "auto"; deckComparePanelEl.style.top = "auto"; deckComparePanelEl.style.right = "12px"; deckComparePanelEl.style.bottom = "calc(72px + env(safe-area-inset-bottom, 0px))"; deckComparePanelEl.style.left = "12px"; deckComparePanelEl.style.width = "auto"; deckComparePanelEl.style.maxHeight = "min(42svh, 360px)"; deckComparePanelEl.style.overflowY = "auto"; stageEl.style.top = "calc(12px + env(safe-area-inset-top, 0px))"; stageEl.style.right = "12px"; stageEl.style.bottom = "calc(84px + env(safe-area-inset-bottom, 0px))"; stageEl.style.left = "12px"; stageEl.style.display = "flex"; stageEl.style.alignItems = "center"; stageEl.style.justifyContent = "center"; stageEl.style.width = "auto"; stageEl.style.height = "auto"; stageEl.style.transform = "none"; stageEl.style.pointerEvents = "auto"; frameEl.style.position = "relative"; frameEl.style.width = "min(100%, 520px)"; frameEl.style.height = "100%"; frameEl.style.maxWidth = "520px"; frameEl.style.maxHeight = "100%"; frameEl.style.borderRadius = zoomed && hasSecondaryCard() ? "0" : "24px"; frameEl.style.background = zoomed && hasSecondaryCard() ? "transparent" : "rgba(11, 15, 26, 0.92)"; frameEl.style.boxShadow = zoomed && hasSecondaryCard() ? "none" : "0 24px 64px rgba(0, 0, 0, 0.44)"; frameEl.style.overflow = "hidden"; imageEl.style.width = "100%"; imageEl.style.height = "100%"; imageEl.style.maxWidth = "none"; imageEl.style.maxHeight = "none"; imageEl.style.objectFit = "contain"; overlayImageEl.style.display = hasSecondaryCard() ? "block" : "none"; primaryInfoEl.style.maxHeight = "min(78vh, 760px)"; primaryInfoEl.style.display = "none"; secondaryInfoEl.style.display = "none"; applyZoomTransform(); setOverlayOpacity(lightboxState.overlayOpacity); return; } 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.bottom = "auto"; toolbarEl.style.left = "auto"; toolbarEl.style.flexDirection = "column"; toolbarEl.style.flexWrap = "nowrap"; toolbarEl.style.alignItems = "flex-end"; toolbarEl.style.justifyContent = "flex-start"; stageEl.style.top = "0"; stageEl.style.right = "0"; stageEl.style.bottom = "0"; stageEl.style.left = "0"; stageEl.style.display = "block"; stageEl.style.alignItems = "stretch"; stageEl.style.justifyContent = "stretch"; 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.maxHeight = "min(78vh, 760px)"; 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"; applyZoomTransform(); return; } overlayEl.style.pointerEvents = "none"; backdropEl.style.display = "none"; backdropEl.style.pointerEvents = "none"; toolbarEl.style.top = "18px"; toolbarEl.style.right = "18px"; toolbarEl.style.bottom = "auto"; toolbarEl.style.left = "auto"; toolbarEl.style.flexDirection = "column"; toolbarEl.style.flexWrap = "nowrap"; toolbarEl.style.alignItems = "flex-end"; toolbarEl.style.justifyContent = "flex-start"; stageEl.style.display = "block"; stageEl.style.alignItems = "stretch"; stageEl.style.justifyContent = "stretch"; 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.maxHeight = "min(78vh, 760px)"; 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.maxHeight = "min(78vh, 760px)"; 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"; } applyZoomTransform(); setOverlayOpacity(lightboxState.overlayOpacity); } function resetZoom() { if (!imageEl && !overlayImageEl) { return; } clearActivePointerGesture(); clearActivePinchGesture(); suppressNextCardClick = false; lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); zoomed = false; applyZoomTransform(); } function updateZoomOrigin(clientX, clientY, targetImage = imageEl, targetFrame = null) { const referenceEl = targetFrame || targetImage; if (!zoomed || !referenceEl) { return; } const rect = referenceEl.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 handleCompactPointerDown(event) { if (!shouldHandleCompactPointerGesture(event)) { return false; } activePointerId = event.pointerId; activePointerStartX = Number(event.clientX) || 0; activePointerStartY = Number(event.clientY) || 0; activePointerMoved = false; if (zoomed) { event.preventDefault(); } return true; } function handleCompactPointerMove(event, targetImage = imageEl, targetFrame = null) { if (!shouldHandleCompactPointerGesture(event) || event.pointerId !== activePointerId || !zoomed) { return; } const deltaX = Math.abs((Number(event.clientX) || 0) - activePointerStartX); const deltaY = Math.abs((Number(event.clientY) || 0) - activePointerStartY); if (!activePointerMoved && Math.max(deltaX, deltaY) < 6) { event.preventDefault(); return; } activePointerMoved = true; suppressNextCardClick = true; event.preventDefault(); updateZoomOrigin(event.clientX, event.clientY, targetImage, targetFrame); } function handleCompactPointerEnd(event, targetImage = imageEl) { if (!shouldHandleCompactPointerGesture(event) || event.pointerId !== activePointerId) { return; } if (activePointerMoved) { suppressNextCardClick = true; } if (typeof targetImage?.releasePointerCapture === "function" && targetImage.hasPointerCapture?.(event.pointerId)) { targetImage.releasePointerCapture(event.pointerId); } clearActivePointerGesture(); } function handleCompactPinchStart(event, targetImage = imageEl, targetFrame = null) { if (!lightboxState.isOpen || !isCompactLightboxLayout() || !targetImage || event.touches.length < 2) { return false; } const midpoint = getTouchMidpoint(event.touches); const distance = getTouchDistance(event.touches); if (!midpoint || !(distance > 0)) { return false; } clearActivePointerGesture(); activePinchGesture = { targetImage, targetFrame, startDistance: distance, startScale: zoomed ? lightboxState.zoomScale : 1 }; suppressNextCardClick = true; event.preventDefault(); return true; } function handleCompactPinchMove(event) { if (!activePinchGesture || event.touches.length < 2) { return false; } const midpoint = getTouchMidpoint(event.touches); const distance = getTouchDistance(event.touches); if (!midpoint || !(distance > 0)) { return false; } const nextScale = clampZoomScale(activePinchGesture.startScale * (distance / activePinchGesture.startDistance)); zoomed = nextScale > 1; setZoomScale(nextScale); if (zoomed) { updateZoomOrigin(midpoint.x, midpoint.y, activePinchGesture.targetImage, activePinchGesture.targetFrame); } else { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); } suppressNextCardClick = true; event.preventDefault(); return true; } function handleCompactPinchEnd(event) { if (!activePinchGesture) { return false; } if (event.touches.length >= 2) { const targetImage = activePinchGesture.targetImage; const targetFrame = activePinchGesture.targetFrame; clearActivePinchGesture(); handleCompactPinchStart(event, targetImage, targetFrame); return true; } clearActivePinchGesture(); return true; } function preventCompactTouchScroll(event) { if (!lightboxState.isOpen || !isCompactLightboxLayout() || (!zoomed && !activePinchGesture)) { return; } event.preventDefault(); } function preventBrowserZoomGesture(event) { if (!lightboxState.isOpen) { return; } if (event.type === "wheel" && !event.ctrlKey) { return; } event.preventDefault(); event.stopPropagation(); } function isPointOnCard(clientX, clientY, targetImage = imageEl, targetFrame = null) { const frameElForHitTest = targetFrame || targetImage; if (!targetImage || !frameElForHitTest) { return false; } const rect = frameElForHitTest.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"; overlayEl.style.overscrollBehavior = "contain"; settingsButtonEl = document.createElement("button"); settingsButtonEl.type = "button"; settingsButtonEl.textContent = "Settings"; settingsButtonEl.style.display = "none"; settingsButtonEl.style.alignItems = "center"; settingsButtonEl.style.justifyContent = "center"; settingsButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; settingsButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; settingsButtonEl.style.color = "#f8fafc"; settingsButtonEl.style.borderRadius = "999px"; settingsButtonEl.style.padding = "10px 14px"; settingsButtonEl.style.font = "600 13px/1.1 sans-serif"; settingsButtonEl.style.cursor = "pointer"; settingsButtonEl.style.backdropFilter = "blur(12px)"; helpButtonEl = document.createElement("button"); helpButtonEl.type = "button"; helpButtonEl.textContent = "Help"; helpButtonEl.style.display = "none"; helpButtonEl.style.alignItems = "center"; helpButtonEl.style.justifyContent = "center"; helpButtonEl.style.width = "100%"; 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)"; settingsPanelEl = document.createElement("div"); settingsPanelEl.style.position = "fixed"; settingsPanelEl.style.top = "72px"; settingsPanelEl.style.right = "24px"; settingsPanelEl.style.display = "none"; settingsPanelEl.style.flexDirection = "column"; settingsPanelEl.style.gap = "10px"; settingsPanelEl.style.width = "min(320px, calc(100vw - 48px))"; settingsPanelEl.style.padding = "14px 16px"; settingsPanelEl.style.borderRadius = "18px"; settingsPanelEl.style.background = "rgba(2, 6, 23, 0.88)"; settingsPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)"; settingsPanelEl.style.color = "#f8fafc"; settingsPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)"; settingsPanelEl.style.backdropFilter = "blur(12px)"; settingsPanelEl.style.pointerEvents = "auto"; settingsPanelEl.style.zIndex = "3"; const settingsTitleEl = document.createElement("div"); settingsTitleEl.textContent = "Lightbox Settings"; settingsTitleEl.style.font = "700 13px/1.3 sans-serif"; 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 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)"; compareButtonEl.style.display = "inline-flex"; compareButtonEl.style.alignItems = "center"; compareButtonEl.style.justifyContent = "center"; compareButtonEl.style.width = "100%"; 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)"; deckCompareButtonEl.style.alignItems = "center"; deckCompareButtonEl.style.justifyContent = "center"; deckCompareButtonEl.style.width = "100%"; zoomControlEl = document.createElement("label"); zoomControlEl.style.display = "flex"; zoomControlEl.style.alignItems = "center"; zoomControlEl.style.justifyContent = "space-between"; zoomControlEl.style.width = "100%"; 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.justifyContent = "space-between"; opacityControlEl.style.width = "100%"; 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); exportButtonEl = document.createElement("button"); exportButtonEl.type = "button"; exportButtonEl.textContent = "Export WebP"; exportButtonEl.style.display = "none"; exportButtonEl.style.alignItems = "center"; exportButtonEl.style.justifyContent = "center"; exportButtonEl.style.width = "100%"; exportButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; exportButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; exportButtonEl.style.color = "#f8fafc"; exportButtonEl.style.borderRadius = "999px"; exportButtonEl.style.padding = "10px 14px"; exportButtonEl.style.font = "600 13px/1.1 sans-serif"; exportButtonEl.style.cursor = "pointer"; exportButtonEl.style.backdropFilter = "blur(12px)"; 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.touchAction = "manipulation"; deckComparePanelEl.style.zIndex = "2"; const deckCompareHeaderEl = document.createElement("div"); deckCompareHeaderEl.style.display = "flex"; deckCompareHeaderEl.style.alignItems = "center"; deckCompareHeaderEl.style.justifyContent = "space-between"; deckCompareHeaderEl.style.gap = "10px"; const deckCompareTitleEl = document.createElement("div"); deckCompareTitleEl.textContent = "Compare Registered Decks"; deckCompareTitleEl.style.font = "700 13px/1.3 sans-serif"; const deckCompareCloseButtonEl = document.createElement("button"); deckCompareCloseButtonEl.type = "button"; deckCompareCloseButtonEl.textContent = "Close"; deckCompareCloseButtonEl.style.padding = "6px 10px"; deckCompareCloseButtonEl.style.borderRadius = "999px"; deckCompareCloseButtonEl.style.border = "1px solid rgba(248, 250, 252, 0.16)"; deckCompareCloseButtonEl.style.background = "rgba(15, 23, 42, 0.44)"; deckCompareCloseButtonEl.style.color = "rgba(248, 250, 252, 0.92)"; deckCompareCloseButtonEl.style.cursor = "pointer"; deckCompareCloseButtonEl.style.font = "600 11px/1.2 sans-serif"; deckCompareHeaderEl.append(deckCompareTitleEl, deckCompareCloseButtonEl); 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(deckCompareHeaderEl, deckCompareMessageEl, deckCompareDeckListEl); mobileInfoButtonEl = document.createElement("button"); mobileInfoButtonEl.type = "button"; mobileInfoButtonEl.textContent = "Info"; mobileInfoButtonEl.style.display = "none"; mobileInfoButtonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; mobileInfoButtonEl.style.background = "rgba(15, 23, 42, 0.84)"; mobileInfoButtonEl.style.color = "#f8fafc"; mobileInfoButtonEl.style.borderRadius = "999px"; mobileInfoButtonEl.style.padding = "10px 14px"; mobileInfoButtonEl.style.font = "600 13px/1.1 sans-serif"; mobileInfoButtonEl.style.cursor = "pointer"; mobileInfoButtonEl.style.backdropFilter = "blur(12px)"; mobileInfoButtonEl.style.alignItems = "center"; mobileInfoButtonEl.style.justifyContent = "center"; mobileInfoButtonEl.style.width = "100%"; mobileInfoPrimaryTabEl = document.createElement("button"); mobileInfoPrimaryTabEl.type = "button"; mobileInfoPrimaryTabEl.textContent = "Base"; mobileInfoPrimaryTabEl.style.display = "none"; mobileInfoPrimaryTabEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; mobileInfoPrimaryTabEl.style.background = "rgba(15, 23, 42, 0.84)"; mobileInfoPrimaryTabEl.style.color = "#f8fafc"; mobileInfoPrimaryTabEl.style.borderRadius = "999px"; mobileInfoPrimaryTabEl.style.padding = "10px 14px"; mobileInfoPrimaryTabEl.style.font = "600 13px/1.1 sans-serif"; mobileInfoPrimaryTabEl.style.cursor = "pointer"; mobileInfoPrimaryTabEl.style.backdropFilter = "blur(12px)"; mobileInfoPrimaryTabEl.style.alignItems = "center"; mobileInfoPrimaryTabEl.style.justifyContent = "center"; mobileInfoPrimaryTabEl.style.width = "100%"; mobileInfoSecondaryTabEl = document.createElement("button"); mobileInfoSecondaryTabEl.type = "button"; mobileInfoSecondaryTabEl.textContent = "Overlay"; mobileInfoSecondaryTabEl.style.display = "none"; mobileInfoSecondaryTabEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; mobileInfoSecondaryTabEl.style.background = "rgba(15, 23, 42, 0.84)"; mobileInfoSecondaryTabEl.style.color = "#f8fafc"; mobileInfoSecondaryTabEl.style.borderRadius = "999px"; mobileInfoSecondaryTabEl.style.padding = "10px 14px"; mobileInfoSecondaryTabEl.style.font = "600 13px/1.1 sans-serif"; mobileInfoSecondaryTabEl.style.cursor = "pointer"; mobileInfoSecondaryTabEl.style.backdropFilter = "blur(12px)"; mobileInfoSecondaryTabEl.style.alignItems = "center"; mobileInfoSecondaryTabEl.style.justifyContent = "center"; mobileInfoSecondaryTabEl.style.width = "100%"; settingsPanelEl.append( settingsTitleEl, compareButtonEl, deckCompareButtonEl, mobileInfoButtonEl, mobileInfoPrimaryTabEl, mobileInfoSecondaryTabEl, exportButtonEl, helpButtonEl, zoomControlEl, opacityControlEl ); toolbarEl.append(settingsButtonEl); 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.overscrollBehavior = "contain"; 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.touchAction = "none"; frameEl.style.overscrollBehavior = "contain"; 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"; mediaEl.style.touchAction = "none"; mediaEl.style.overscrollBehavior = "contain"; const zoomLayerEl = document.createElement("div"); zoomLayerEl.style.position = "absolute"; zoomLayerEl.style.inset = "16px"; zoomLayerEl.style.display = "flex"; zoomLayerEl.style.alignItems = "center"; zoomLayerEl.style.justifyContent = "center"; zoomLayerEl.style.transform = "scale(1)"; zoomLayerEl.style.transformOrigin = "50% 50%"; zoomLayerEl.style.transition = "transform 120ms ease-out"; 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 = "rotate(0deg)"; compareImageEl.style.transformOrigin = "center center"; compareImageEl.style.transition = "transform 120ms ease-out"; compareImageEl.style.touchAction = "none"; 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)"; zoomLayerEl.appendChild(compareImageEl); mediaEl.append(zoomLayerEl, fallbackEl); slotEl.append(headerEl, mediaEl); return { slotEl, headerEl, badgeEl, cardLabelEl, mediaEl, zoomLayerEl, 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.touchAction = "none"; 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; mobileInfoPanelEl = document.createElement("div"); mobileInfoPanelEl.style.position = "absolute"; mobileInfoPanelEl.style.left = "12px"; mobileInfoPanelEl.style.right = "12px"; mobileInfoPanelEl.style.bottom = "12px"; mobileInfoPanelEl.style.display = "none"; mobileInfoPanelEl.style.flexDirection = "column"; mobileInfoPanelEl.style.gap = "10px"; mobileInfoPanelEl.style.padding = "14px 16px"; mobileInfoPanelEl.style.borderRadius = "18px"; mobileInfoPanelEl.style.background = "rgba(2, 6, 23, 0.86)"; mobileInfoPanelEl.style.border = "1px solid rgba(148, 163, 184, 0.16)"; mobileInfoPanelEl.style.color = "#f8fafc"; mobileInfoPanelEl.style.backdropFilter = "blur(12px)"; mobileInfoPanelEl.style.boxShadow = "0 16px 42px rgba(0, 0, 0, 0.34)"; mobileInfoPanelEl.style.maxHeight = "min(46%, 320px)"; mobileInfoPanelEl.style.overflowY = "auto"; mobileInfoPanelEl.style.pointerEvents = "auto"; mobileInfoPanelEl.style.zIndex = "3"; mobileInfoTitleEl = document.createElement("div"); mobileInfoTitleEl.style.font = "700 13px/1.3 sans-serif"; mobileInfoTitleEl.style.color = "#f8fafc"; mobileInfoGroupsEl = document.createElement("div"); mobileInfoGroupsEl.style.display = "flex"; mobileInfoGroupsEl.style.flexDirection = "column"; mobileInfoGroupsEl.style.gap = "0"; mobileInfoHintEl = document.createElement("div"); mobileInfoHintEl.style.font = "500 11px/1.35 sans-serif"; mobileInfoHintEl.style.color = "rgba(226, 232, 240, 0.82)"; mobileInfoPanelEl.append(mobileInfoTitleEl, mobileInfoGroupsEl, mobileInfoHintEl); function createMobileNavButton(label, ariaLabel) { const buttonEl = document.createElement("button"); buttonEl.type = "button"; buttonEl.textContent = label; buttonEl.setAttribute("aria-label", ariaLabel); buttonEl.style.position = "fixed"; buttonEl.style.top = "auto"; buttonEl.style.display = "none"; buttonEl.style.alignItems = "center"; buttonEl.style.justifyContent = "center"; buttonEl.style.width = "44px"; buttonEl.style.height = "44px"; buttonEl.style.border = "1px solid rgba(255, 255, 255, 0.2)"; buttonEl.style.borderRadius = "999px"; buttonEl.style.background = "rgba(15, 23, 42, 0.84)"; buttonEl.style.color = "#f8fafc"; buttonEl.style.font = "700 20px/1 sans-serif"; buttonEl.style.cursor = "pointer"; buttonEl.style.backdropFilter = "blur(12px)"; buttonEl.style.transform = "none"; buttonEl.style.pointerEvents = "auto"; buttonEl.style.zIndex = "6"; return buttonEl; } mobilePrevButtonEl = createMobileNavButton("<", "Previous card"); mobilePrevButtonEl.style.left = "12px"; mobileNextButtonEl = createMobileNavButton(">", "Next card"); mobileNextButtonEl.style.right = "12px"; baseLayerEl.appendChild(imageEl); overlayLayerEl.appendChild(overlayImageEl); frameEl.append(baseLayerEl, overlayLayerEl, mobileInfoPanelEl); stageEl.append(frameEl, compareGridEl, primaryInfoEl, secondaryInfoEl); overlayEl.append(backdropEl, stageEl, toolbarEl, settingsPanelEl, deckComparePanelEl, helpPanelEl, mobilePrevButtonEl, mobileNextButtonEl); 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.settingsMenuOpen = false; lightboxState.helpOpen = false; lightboxState.primaryRotated = false; lightboxState.overlayRotated = false; setInfoPanelOpen(false, { persist: false }); lightboxState.mobileInfoView = "primary"; lightboxState.exportInProgress = false; clearActivePointerGesture(); suppressNextCardClick = 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; if (isCompactLightboxLayout()) { lightboxState.mobileInfoView = "overlay"; } 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; if (lightboxState.helpOpen) { closeSettingsMenu(); } syncHelpUi(); restoreLightboxFocus(); }); settingsButtonEl.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); toggleSettingsMenu(); restoreLightboxFocus(); }); settingsPanelEl.addEventListener("pointerdown", (event) => { event.stopPropagation(); }); settingsPanelEl.addEventListener("click", (event) => { event.stopPropagation(); }); compareButtonEl.addEventListener("click", () => { toggleCompareMode(); restoreLightboxFocus(); }); deckCompareButtonEl.addEventListener("click", () => { if (shouldSuppressDeckCompareToggle()) { restoreLightboxFocus(); return; } toggleDeckComparePanel(); restoreLightboxFocus(); }); deckComparePanelEl.addEventListener("pointerdown", (event) => { event.stopPropagation(); }); deckComparePanelEl.addEventListener("click", (event) => { event.stopPropagation(); }); deckCompareCloseButtonEl.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); suppressDeckCompareToggle(); closeDeckComparePanel(); applyComparePresentation(); restoreLightboxFocus(); }); mobileInfoButtonEl.addEventListener("click", () => { setInfoPanelOpen(!lightboxState.mobileInfoOpen); applyComparePresentation(); restoreLightboxFocus(); }); mobileInfoPrimaryTabEl.addEventListener("click", () => { lightboxState.mobileInfoView = "primary"; applyComparePresentation(); restoreLightboxFocus(); }); mobileInfoSecondaryTabEl.addEventListener("click", () => { lightboxState.mobileInfoView = "overlay"; applyComparePresentation(); 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); }); exportButtonEl.addEventListener("click", async (event) => { event.preventDefault(); event.stopPropagation(); await exportCurrentLightboxView(); }); opacitySliderEl.addEventListener("change", restoreLightboxFocus); opacitySliderEl.addEventListener("pointerup", restoreLightboxFocus); mobilePrevButtonEl.addEventListener("click", (event) => { event.stopPropagation(); if (lightboxState.compareMode) { stepSecondaryCard(-1); } else { stepPrimaryCard(-1); } restoreLightboxFocus(); }); mobileNextButtonEl.addEventListener("click", (event) => { event.stopPropagation(); if (lightboxState.compareMode) { stepSecondaryCard(1); } else { stepPrimaryCard(1); } restoreLightboxFocus(); }); imageEl.addEventListener("click", (event) => { event.stopPropagation(); if (consumeSuppressedCardClick()) { return; } 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("pointerdown", (event) => { if (handleCompactPointerDown(event)) { imageEl.setPointerCapture?.(event.pointerId); } }); imageEl.addEventListener("pointermove", (event) => { handleCompactPointerMove(event, imageEl, null); }); imageEl.addEventListener("pointerup", (event) => { handleCompactPointerEnd(event, imageEl); }); imageEl.addEventListener("pointercancel", (event) => { handleCompactPointerEnd(event, imageEl); }); imageEl.addEventListener("touchstart", (event) => { handleCompactPinchStart(event, imageEl, null); }, { passive: false }); imageEl.addEventListener("touchmove", preventCompactTouchScroll, { passive: false }); imageEl.addEventListener("touchmove", (event) => { handleCompactPinchMove(event); }, { passive: false }); imageEl.addEventListener("touchend", (event) => { handleCompactPinchEnd(event); }, { passive: false }); imageEl.addEventListener("touchcancel", (event) => { handleCompactPinchEnd(event); }, { passive: false }); imageEl.addEventListener("mouseleave", () => { if (zoomed) { lightboxState.zoomOriginX = 50; lightboxState.zoomOriginY = 50; applyTransformOrigins(); } }); compareGridSlots.forEach((slot) => { slot.imageEl.addEventListener("click", (event) => { event.stopPropagation(); if (consumeSuppressedCardClick()) { return; } if (!isPointOnCard(event.clientX, event.clientY, slot.imageEl, slot.mediaEl)) { close(); return; } if (!zoomed) { zoomed = true; applyZoomTransform(); updateZoomOrigin(event.clientX, event.clientY, slot.imageEl, slot.mediaEl); applyComparePresentation(); return; } resetZoom(); applyComparePresentation(); }); slot.imageEl.addEventListener("mousemove", (event) => { updateZoomOrigin(event.clientX, event.clientY, slot.imageEl, slot.mediaEl); }); slot.imageEl.addEventListener("pointerdown", (event) => { if (handleCompactPointerDown(event)) { slot.imageEl.setPointerCapture?.(event.pointerId); } }); slot.imageEl.addEventListener("pointermove", (event) => { handleCompactPointerMove(event, slot.imageEl, slot.mediaEl); }); slot.imageEl.addEventListener("pointerup", (event) => { handleCompactPointerEnd(event, slot.imageEl); }); slot.imageEl.addEventListener("pointercancel", (event) => { handleCompactPointerEnd(event, slot.imageEl); }); slot.imageEl.addEventListener("touchstart", (event) => { handleCompactPinchStart(event, slot.imageEl, slot.mediaEl); }, { passive: false }); slot.imageEl.addEventListener("touchmove", preventCompactTouchScroll, { passive: false }); slot.imageEl.addEventListener("touchmove", (event) => { handleCompactPinchMove(event); }, { passive: false }); slot.imageEl.addEventListener("touchend", (event) => { handleCompactPinchEnd(event); }, { passive: false }); slot.imageEl.addEventListener("touchcancel", (event) => { handleCompactPinchEnd(event); }, { passive: false }); 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.addEventListener("wheel", preventBrowserZoomGesture, { capture: true, passive: false }); ["gesturestart", "gesturechange", "gestureend"].forEach((eventName) => { document.addEventListener(eventName, preventBrowserZoomGesture, { capture: true, passive: false }); }); window.addEventListener("resize", () => { if (!lightboxState.isOpen) { return; } applyComparePresentation(); }); 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.settingsMenuOpen = false; lightboxState.helpOpen = false; lightboxState.primaryRotated = false; lightboxState.overlayRotated = false; setInfoPanelOpen(getPersistedInfoPanelVisibility(), { persist: false }); lightboxState.mobileInfoView = "primary"; 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 }; })();