Files
TaroTime/app/ui-tarot-lightbox.js

2793 lines
98 KiB
JavaScript

(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 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 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 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
};
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 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 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 = "";
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 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 syncMobileInfoControls() {
if (!mobileInfoButtonEl || !mobileInfoPrimaryTabEl || !mobileInfoSecondaryTabEl || !mobileInfoPanelEl) {
return;
}
const isCompact = isCompactLightboxLayout();
const canShowInfo = Boolean(
lightboxState.isOpen
&& isCompact
&& !zoomed
&& !lightboxState.deckCompareMode
&& lightboxState.allowOverlayCompare
&& lightboxState.primaryCard?.label
);
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 = canShowInfo && lightboxState.mobileInfoOpen && hasOverlayInfo ? "inline-flex" : "none";
mobileInfoSecondaryTabEl.style.display = canShowInfo && 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) {
renderComparePanel(primaryInfoEl, primaryTitleEl, primaryGroupsEl, primaryHintEl, null, "", "", false);
renderComparePanel(secondaryInfoEl, secondaryTitleEl, secondaryGroupsEl, secondaryHintEl, null, "", "", false);
renderMobileInfoPanel(null, "", "", false);
return;
}
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 ? "8px 6px 88px" : "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();
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 ? "18px 12px 84px" : "76px 24px 24px";
frameEl.style.display = "none";
primaryInfoEl.style.display = "none";
secondaryInfoEl.style.display = "none";
if (mobileInfoPanelEl) {
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.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.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.transform = "translateY(-50%)";
} else {
stageEl.style.top = "50%";
stageEl.style.right = "auto";
stageEl.style.bottom = "auto";
stageEl.style.left = "50%";
stageEl.style.width = "min(44vw, 560px)";
stageEl.style.height = "min(92vh, 1400px)";
stageEl.style.transform = "translate(-50%, -50%)";
frameEl.style.borderRadius = "28px";
frameEl.style.background = "rgba(11, 15, 26, 0.88)";
frameEl.style.boxShadow = "0 30px 90px rgba(0, 0, 0, 0.56)";
primaryInfoEl.style.left = "auto";
primaryInfoEl.style.right = "calc(100% + 10px)";
primaryInfoEl.style.top = "50%";
primaryInfoEl.style.bottom = "auto";
primaryInfoEl.style.width = "clamp(180px, 15vw, 220px)";
primaryInfoEl.style.transform = "translateY(-50%)";
secondaryInfoEl.style.left = "calc(100% + 10px)";
secondaryInfoEl.style.right = "auto";
secondaryInfoEl.style.top = "50%";
secondaryInfoEl.style.bottom = "auto";
secondaryInfoEl.style.width = "clamp(180px, 15vw, 220px)";
secondaryInfoEl.style.transform = "translateY(-50%)";
}
if (hasSecondaryCard()) {
overlayImageEl.style.display = "block";
} else {
overlayImageEl.style.display = "none";
secondaryInfoEl.style.display = "none";
}
applyZoomTransform();
setOverlayOpacity(lightboxState.overlayOpacity);
}
function resetZoom() {
if (!imageEl && !overlayImageEl) {
return;
}
clearActivePointerGesture();
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 preventCompactTouchScroll(event) {
if (!lightboxState.isOpen || !isCompactLightboxLayout() || !zoomed) {
return;
}
event.preventDefault();
}
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);
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,
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;
lightboxState.mobileInfoOpen = false;
lightboxState.mobileInfoView = "primary";
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", () => {
lightboxState.mobileInfoOpen = !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);
});
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("touchmove", preventCompactTouchScroll, { 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("touchmove", preventCompactTouchScroll, { 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);
});
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;
lightboxState.mobileInfoOpen = 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
};
})();