diff --git a/index.html b/index.html
index 1713128..d9ca9ef 100644
--- a/index.html
+++ b/index.html
@@ -5,7 +5,6 @@
Klondike Solitaire
-
diff --git a/script.js b/script.js
index 97b145f..3cafe7d 100644
--- a/script.js
+++ b/script.js
@@ -10,11 +10,12 @@ document.addEventListener('DOMContentLoaded', () => {
let deck, stock, waste, foundations, tableau;
let cardElements = {};
- let dragged = { cards: [], sourcePile: null, startIndex: -1 };
- // 'selected' state is no longer needed for click controls and has been removed.
+ let dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null, offsetX: 0, offsetY: 0 };
const history = [];
let isAutoCompleting = false;
+ let autoCompleteInterval = null;
+ let isDealing = false;
let score = 0;
let highScore = 0;
@@ -65,6 +66,7 @@ document.addEventListener('DOMContentLoaded', () => {
history.push(...gameState.history);
DRAW_COUNT = gameState.drawCount || 3;
score = gameState.score || 0;
+ highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
drawSelect.value = DRAW_COUNT;
Object.values(cardElements).forEach(el => el.remove());
cardElements = {};
@@ -102,10 +104,12 @@ document.addEventListener('DOMContentLoaded', () => {
// ====== SETUP ======
function initState() {
score = 0;
- highScore = localStorage.getItem('solitaire-high-score') || 0;
+ highScore = parseInt(localStorage.getItem('solitaire-high-score'), 10) || 0;
updateScoreDisplay();
isAutoCompleting = false;
+ if (autoCompleteInterval) clearInterval(autoCompleteInterval);
+
Object.values(cardElements).forEach(el => el.remove());
cardElements = {};
deck = []; stock = []; waste = [];
@@ -122,16 +126,54 @@ document.addEventListener('DOMContentLoaded', () => {
const el = createCardElement(cardData);
cardElements[cardData.id] = el;
gameBoard.appendChild(el);
+ // Initially position cards at the stock location to animate out from there
+ const stockEl = document.getElementById('stock');
+ if (stockEl) {
+ const rect = stockEl.getBoundingClientRect();
+ const boardRect = gameBoard.getBoundingClientRect();
+ el.style.top = `${rect.top - boardRect.top + PILE_BORDER_WIDTH}px`;
+ el.style.left = `${rect.left - boardRect.left + PILE_BORDER_WIDTH}px`;
+ }
});
+
+ // Separate cards for tableau and stock
+ const tableauCards = [];
for (let i=0; i<7; i++){
- for (let j=i; j<7; j++) tableau[j].push(deck.pop());
+ for (let j=i; j<7; j++) {
+ const card = deck.pop();
+ tableau[j].push(card);
+ tableauCards.push({ card, pileIndex: j });
+ }
}
- tableau.forEach(pile => { if (pile.length) pile[pile.length-1].faceUp = true; });
stock = deck;
+
winMessage.classList.add('hidden');
history.length = 0;
pushHistory();
- updateBoard(true);
+
+ // Start dealing animation
+ dealTableauSequentially(tableauCards);
+ }
+
+ async function dealTableauSequentially(cardsToDeal) {
+ isDealing = true;
+ // Turn off interactivity during deal
+ gameBoard.style.pointerEvents = 'none';
+
+ for (let i = 0; i < cardsToDeal.length; i++) {
+ const { card, pileIndex } = cardsToDeal[i];
+ // Only flip the last card of each pile
+ const pile = tableau[pileIndex];
+ if (card === pile[pile.length - 1]) {
+ card.faceUp = true;
+ }
+ updateBoard(true);
+ await new Promise(r => setTimeout(r, 60));
+ }
+
+ isDealing = false;
+ gameBoard.style.pointerEvents = 'auto';
+ updateBoard();
saveGame();
}
@@ -148,6 +190,14 @@ document.addEventListener('DOMContentLoaded', () => {
pileData.forEach((cardData, i) => {
const cardEl = cardElements[cardData.id];
if(!cardEl) return;
+ // Don't update position for cards being dragged (ghost)
+ if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) {
+ cardEl.style.visibility = 'hidden'; // Hide the original card
+ return;
+ } else {
+ cardEl.style.visibility = 'visible';
+ }
+
if (type === 'tableau') {
cardEl.style.top = `${baseTop + currentOffset}px`;
currentOffset += cardData.faceUp ? SPACING_FACE_UP : SPACING_FACE_DOWN;
@@ -164,7 +214,6 @@ document.addEventListener('DOMContentLoaded', () => {
cardEl.classList.remove('is-flipped');
cardEl.draggable = false;
}
- // The 'selected' class is no longer applied via click.
const zBase = type === 'tableau' ? 100 : type === 'foundation' ? 400 : 600;
cardEl.style.zIndex = zBase + i;
});
@@ -181,6 +230,14 @@ document.addEventListener('DOMContentLoaded', () => {
waste.forEach((cardData, i) => {
const cardEl = cardElements[cardData.id];
if(!cardEl) return;
+
+ if (dragged.ghostEl && dragged.cards.some(c => c.id === cardData.id)) {
+ cardEl.style.visibility = 'hidden';
+ return;
+ } else {
+ cardEl.style.visibility = 'visible';
+ }
+
const isTopCard = i === waste.length - 1;
const visibleFanIndexStart = Math.max(0, waste.length - DRAW_COUNT);
let leftOffset = 0;
@@ -266,30 +323,61 @@ document.addEventListener('DOMContentLoaded', () => {
if (type === 'stock') return stock;
return null;
}
+
+ // Helper to get the actual pile that a card belongs to
+ function getPileContainingCard(cardId) {
+ const checkPile = (pile) => pile.find(c => c.id === cardId);
+
+ if (checkPile(waste)) return waste;
+ for (const pile of tableau) {
+ if (checkPile(pile)) return pile;
+ }
+ for (const pile of foundations) {
+ if (checkPile(pile)) return pile;
+ }
+ return null;
+ }
+
function findCardData(cardEl) {
const id = cardEl.dataset.id;
let cardIndex;
- const findIn = (pile) => pile.findIndex(c => c.id === id);
- if ((cardIndex = findIn(waste)) > -1 && cardIndex === waste.length - 1) {
- return { sourcePile: waste, cardIndex, card: waste[cardIndex], cardsToMove: [waste[cardIndex]] };
+
+ // Check waste (only top card can be moved)
+ if (waste.length > 0 && waste[waste.length - 1].id === id) {
+ return { sourcePile: waste, cardIndex: waste.length - 1, card: waste[waste.length - 1], cardsToMove: [waste[waste.length - 1]] };
}
+
+ // Check tableau piles (any face-up card and cards above it can be moved)
for (const pile of tableau) {
- if ((cardIndex = findIn(pile)) > -1 && pile[cardIndex].faceUp) {
+ cardIndex = pile.findIndex(c => c.id === id);
+ if (cardIndex > -1 && pile[cardIndex].faceUp) {
const cardsToMove = pile.slice(cardIndex);
return { sourcePile: pile, cardIndex, card: cardsToMove[0], cardsToMove };
}
}
+ // Check foundations (only top card can be moved)
for (const pile of foundations) {
- if ((cardIndex = findIn(pile)) > -1 && cardIndex === pile.length - 1) {
- return { sourcePile: pile, cardIndex, card: pile[cardIndex], cardsToMove: [pile[cardIndex]] };
+ if (pile.length > 0 && pile[pile.length - 1].id === id) {
+ return { sourcePile: pile, cardIndex: pile.length - 1, card: pile[pile.length - 1], cardsToMove: [pile[pile.length - 1]] };
}
}
- return {};
+ return {}; // Return empty object if card not found or not movable
}
+
function canMove(card, destPile) {
if (!card || !destPile) return false;
+
+ // Prevent moving to the stock pile
+ if (destPile === stock) return false;
+
const destType = (foundations.includes(destPile)) ? 'foundation' : (tableau.includes(destPile)) ? 'tableau' : 'other';
- if (destType === 'other') return false;
+ if (destType === 'other' && destPile !== waste) return false; // Only allow tableau, foundation, or waste as actual destinations
+
+ // Waste pile can only receive cards if it's not the source for the current drag (handled by dragged.sourcePile check)
+ // Also, waste doesn't typically accept cards from other piles in Klondike.
+ // We ensure we don't try to drop on the waste pile itself unless it's for internal logic like recycling stock.
+ if (destPile === waste) return false; // Generally, cards don't move TO waste.
+
if (destPile.length === 0) {
return (destType==='tableau' && card.value==='K') || (destType==='foundation' && card.value==='A');
}
@@ -297,13 +385,12 @@ document.addEventListener('DOMContentLoaded', () => {
if (destType === 'tableau') {
const opposite = ((['♥','♦'].includes(card.suit) && ['♣','♠'].includes(top.suit)) || (['♣','♠'].includes(card.suit) && ['♥','♦'].includes(top.suit)));
return opposite && getValueRank(card.value) === getValueRank(top.value) - 1;
- } else {
+ } else if (destType === 'foundation') {
return card.suit === top.suit && getValueRank(card.value) === getValueRank(top.value) + 1;
}
+ return false;
}
- // The `clearSelection` function is no longer needed.
-
function moveSequence(sourcePile, startIndex, destPile) {
const seq = sourcePile.splice(startIndex);
destPile.push(...seq);
@@ -330,19 +417,92 @@ document.addEventListener('DOMContentLoaded', () => {
updateScoreDisplay();
}
winMessage.classList.remove('hidden');
- try {
- confetti({ particleCount: 180, spread: 75, origin:{y:0.35} });
- setTimeout(()=>confetti({ particleCount: 140, spread: 65, origin:{y:.2} }), 350);
- } catch(_) {}
+ triggerBouncingCards();
}
}
+ function triggerBouncingCards() {
+ const boardRect = gameBoard.getBoundingClientRect();
+ const cardsToBounce = [];
+
+ // Gather all top cards from foundations to start the bounce sequence
+ // We'll process them one suit at a time for the classic effect
+ const animationFoundations = foundations.map(f => [...f]); // Clone
+
+ function bounceNextCard() {
+ let cardFound = false;
+ for (let i = 0; i < 4; i++) {
+ if (animationFoundations[i].length > 0) {
+ const cardData = animationFoundations[i].pop();
+ const cardEl = cardElements[cardData.id];
+ if (cardEl) {
+ startCardPhysics(cardEl);
+ cardFound = true;
+ break; // One card per trigger
+ }
+ }
+ }
+ if (cardFound) {
+ setTimeout(bounceNextCard, 180);
+ }
+ }
+
+ function startCardPhysics(el) {
+ const rect = el.getBoundingClientRect();
+ let x = rect.left;
+ let y = rect.top;
+ let vx = (Math.random() * 4 + 2) * (Math.random() > 0.5 ? 1 : -1);
+ let vy = Math.random() * -5 - 2;
+ const gravity = 0.5;
+ const friction = 0.75;
+
+ // Remove transition for physics
+ el.style.transition = 'none';
+ el.style.position = 'fixed';
+ el.style.zIndex = 3000;
+ el.style.pointerEvents = 'none';
+
+ function step() {
+ x += vx;
+ y += vy;
+ vy += gravity;
+
+ if (y + rect.height > window.innerHeight) {
+ y = window.innerHeight - rect.height;
+ vy *= -friction;
+ if (Math.abs(vy) < 2) vy = 0; // Stop vertical bounce if too small
+ }
+
+ if (x + rect.width < 0 || x > window.innerWidth) {
+ el.remove();
+ return; // Card off screen
+ }
+
+ el.style.left = `${x}px`;
+ el.style.top = `${y}px`;
+
+ if (Math.abs(vx) > 0.1 || Math.abs(vy) > 0.1 || y < window.innerHeight - rect.height) {
+ requestAnimationFrame(step);
+ } else {
+ // Small slide before stopping
+ vx *= 0.95;
+ if (Math.abs(vx) > 0.1) requestAnimationFrame(step);
+ else el.remove();
+ }
+ }
+ requestAnimationFrame(step);
+ }
+
+ bounceNextCard();
+ }
+
// ====== ACTIONS ======
function handleStockClick() {
if (isAutoCompleting) return;
pushHistory();
if (stock.length > 0) {
- const cardsToMove = stock.splice(stock.length - DRAW_COUNT, DRAW_COUNT).reverse();
+ const drawCount = Math.min(stock.length, DRAW_COUNT);
+ const cardsToMove = stock.splice(stock.length - drawCount, drawCount).reverse();
cardsToMove.forEach(c => c.faceUp = true);
waste.push(...cardsToMove);
} else if (waste.length > 0) {
@@ -355,10 +515,6 @@ document.addEventListener('DOMContentLoaded', () => {
saveGame();
}
- /**
- * This new function handles the single-click auto-move logic.
- * It finds the best valid move for a card and executes it.
- */
function performAutoMove(cardEl) {
if (isAutoCompleting) return;
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
@@ -374,7 +530,7 @@ document.addEventListener('DOMContentLoaded', () => {
pushHistory();
moveSequence(sourcePile, cardIndex, f);
performMoveCleanup(sourcePile);
- return; // Move executed, action is complete.
+ return;
}
}
}
@@ -388,7 +544,7 @@ document.addEventListener('DOMContentLoaded', () => {
pushHistory();
moveSequence(sourcePile, cardIndex, t);
performMoveCleanup(sourcePile);
- return; // Move executed, action is complete.
+ return;
}
}
}
@@ -422,7 +578,7 @@ document.addEventListener('DOMContentLoaded', () => {
return;
}
isAutoCompleting = true;
- const intervalId = setInterval(() => {
+ autoCompleteInterval = setInterval(() => {
let movedCard = false;
let source = null;
@@ -460,7 +616,8 @@ document.addEventListener('DOMContentLoaded', () => {
if (source) performMoveCleanup(source);
if (!movedCard) {
- clearInterval(intervalId);
+ clearInterval(autoCompleteInterval);
+ autoCompleteInterval = null;
isAutoCompleting = false;
checkWin();
}
@@ -479,7 +636,7 @@ document.addEventListener('DOMContentLoaded', () => {
for(const f of foundations) if(canMove(topCard, f)) return true;
const movableStackIndex = sourcePile.findIndex(card => card.faceUp);
- if (movableStackIndex > -1) { // Fixed: check should be > -1
+ if (movableStackIndex > -1) {
const cardToMove = sourcePile[movableStackIndex];
for (const destPile of tableau) {
if (sourcePile !== destPile && canMove(cardToMove, destPile)) return true;
@@ -562,31 +719,147 @@ document.addEventListener('DOMContentLoaded', () => {
// Clicks on anything else (background, empty piles, etc.) will do nothing.
});
+ // Custom drag and drop functionality
gameBoard.addEventListener('dragstart', e => {
if (isAutoCompleting) { e.preventDefault(); return; }
const cardEl = e.target.closest('.card');
if (!cardEl) { e.preventDefault(); return; }
+
const { sourcePile, cardIndex, cardsToMove } = findCardData(cardEl);
if (!sourcePile || !cardsToMove.length) { e.preventDefault(); return; }
- dragged = { cards: cardsToMove, sourcePile, startIndex: cardIndex };
- setTimeout(() => cardsToMove.forEach(c => cardElements[c.id]?.classList.add('dragging')), 0);
+
+ const rect = cardEl.getBoundingClientRect();
+ dragged.offsetX = e.clientX - rect.left;
+ dragged.offsetY = e.clientY - rect.top;
+ dragged.cards = cardsToMove;
+ dragged.sourcePile = sourcePile;
+ dragged.startIndex = cardIndex;
+
+ // Create a "ghost" element for visual feedback
+ dragged.ghostEl = document.createElement('div');
+ dragged.ghostEl.classList.add('card-ghost-stack');
+ // Add classes for card back if applicable
+ const currentBackClass = document.body.className.match(/card-back-\w+/);
+ if (currentBackClass) {
+ dragged.ghostEl.classList.add(currentBackClass[0]);
+ }
+
+ cardsToMove.forEach((cardData, i) => {
+ const el = cardElements[cardData.id].cloneNode(true);
+ el.style.position = 'absolute'; // Position relative to ghostEl
+ el.style.top = `${i * SPACING_FACE_UP}px`;
+ el.style.left = '0';
+ el.style.zIndex = i + 1; // Stack visually
+ el.classList.add('is-flipped'); // Ensure ghost cards are flipped
+ el.style.visibility = 'visible';
+ dragged.ghostEl.appendChild(el);
+ });
+
+ dragged.ghostEl.style.position = 'fixed';
+ dragged.ghostEl.style.pointerEvents = 'none'; // Ensure it doesn't interfere with drop targets
+ dragged.ghostEl.style.zIndex = 2000; // Above everything else
+ dragged.ghostEl.style.width = cardEl.offsetWidth + 'px'; // Match card width
+ dragged.ghostEl.style.height = (cardEl.offsetHeight + (cardsToMove.length - 1) * SPACING_FACE_UP) + 'px';
+
+ document.body.appendChild(dragged.ghostEl);
+
+ // Hide the original cards visually while dragging
+ cardsToMove.forEach(c => cardElements[c.id].style.visibility = 'hidden');
+
+ // Set custom drag image (a tiny transparent image) to prevent default ghost
+ const img = new Image();
+ img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
+ e.dataTransfer.setDragImage(img, 0, 0);
+
+ // Initial position for the ghost
+ dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`;
+ dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`;
});
- gameBoard.addEventListener('dragover', e => { e.preventDefault(); document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); const targetEl = e.target.closest('.pile, .card'); if (targetEl) targetEl.closest('.pile')?.classList.add('drag-over'); });
- gameBoard.addEventListener('dragend', () => { if (dragged.cards?.length) { dragged.cards.forEach(c => cardElements[c.id]?.classList.remove('dragging')); } dragged = { cards: [], sourcePile: null, startIndex: -1 }; document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over')); });
+
+ gameBoard.addEventListener('drag', e => {
+ if (dragged.ghostEl && e.clientX !== 0) {
+ // Update ghost element position to follow the cursor
+ dragged.ghostEl.style.left = `${e.clientX - dragged.offsetX}px`;
+ dragged.ghostEl.style.top = `${e.clientY - dragged.offsetY}px`;
+ }
+ });
+
+ gameBoard.addEventListener('dragover', e => {
+ e.preventDefault();
+
+ const targetEl = e.target.closest('.card, .pile');
+ let potentialDestPileEl = null;
+
+ if (targetEl) {
+ if (targetEl.classList.contains('card')) {
+ potentialDestPileEl = targetEl.closest('.pile');
+ } else if (targetEl.classList.contains('pile')) {
+ potentialDestPileEl = targetEl;
+ }
+ }
+
+ const currentlyHighlighted = document.querySelector('.pile.drag-over');
+ if (currentlyHighlighted && currentlyHighlighted !== potentialDestPileEl) {
+ currentlyHighlighted.classList.remove('drag-over');
+ }
+
+ if (potentialDestPileEl && dragged.cards.length > 0) {
+ const destPileData = getPileArrayFromElement(potentialDestPileEl);
+ if (canMove(dragged.cards[0], destPileData)) {
+ potentialDestPileEl.classList.add('drag-over');
+ }
+ }
+ });
+
+ gameBoard.addEventListener('dragend', () => {
+ // Clean up ghost element and reset state
+ if (dragged.ghostEl) {
+ dragged.ghostEl.remove();
+ dragged.ghostEl = null;
+ }
+ // Make original cards visible again (updateBoard will re-position them)
+ dragged.cards.forEach(c => cardElements[c.id].style.visibility = 'visible');
+ dragged = { cards: [], sourcePile: null, startIndex: -1, ghostEl: null };
+ document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over'));
+ updateBoard(); // Re-render to ensure everything is correct
+ });
+
gameBoard.addEventListener('drop', e => {
e.preventDefault();
+ document.querySelectorAll('.drag-over').forEach(p => p.classList.remove('drag-over'));
+
if (!dragged.cards.length) return;
+
const dropTargetEl = e.target.closest('.card, .pile');
if (!dropTargetEl) return;
- const destPile = getPileArrayFromElement(dropTargetEl.closest('.pile'));
+
+ let destPile = null;
+ if (dropTargetEl.classList.contains('card')) {
+ // If dropped on a card, find the pile that card belongs to
+ const cardId = dropTargetEl.dataset.id;
+ const targetCardPile = getPileContainingCard(cardId);
+ if (targetCardPile) {
+ // We need to ensure it's the TOP card of the pile to drop ONTO it effectively
+ if (targetCardPile[targetCardPile.length - 1].id === cardId) {
+ destPile = targetCardPile;
+ }
+ }
+ } else if (dropTargetEl.classList.contains('pile')) {
+ // If dropped directly on a pile (e.g., an empty tableau slot)
+ destPile = getPileArrayFromElement(dropTargetEl);
+ }
+
if (destPile && canMove(dragged.cards[0], destPile)) {
+ // --- SCORING LOGIC ---
if (foundations.includes(destPile)) score += 10;
if (tableau.includes(destPile) && dragged.sourcePile === waste) score += 5;
if (tableau.includes(destPile) && foundations.includes(dragged.sourcePile)) score -= 15;
+ // --- END SCORING LOGIC ---
pushHistory();
moveSequence(dragged.sourcePile, dragged.startIndex, destPile);
performMoveCleanup(dragged.sourcePile);
}
+ // dragend will handle cleanup regardless of success
});
document.addEventListener('keydown', (e)=>{
diff --git a/style.css b/style.css
index d3e1535..d9888ef 100644
--- a/style.css
+++ b/style.css
@@ -174,9 +174,10 @@ h1 {
position: absolute;
user-select: none;
touch-action: none;
- transition: top .18s ease, left .18s ease, transform .18s ease, filter .2s ease;
+ transition: top .25s cubic-bezier(0.2, 0.8, 0.2, 1), left .25s cubic-bezier(0.2, 0.8, 0.2, 1), transform .25s cubic-bezier(0.2, 0.8, 0.2, 1), filter .2s ease;
will-change: top, left, transform;
filter: drop-shadow(0 2px 5px var(--shadow-color));
+ font-size: calc(var(--card-width) * 0.28); /* Scalable base font size */
}
.card.is-stacked { filter: none; }
.card-inner {
@@ -196,7 +197,7 @@ h1 {
.card-face--front {
background: linear-gradient(160deg, #ffffff, #f4f4f4);
transform: rotateY(180deg);
- padding: 8px;
+ padding: calc(var(--card-width) * 0.1);
cursor: grab;
border: 1px solid #d0d0d0;
}
@@ -206,8 +207,8 @@ h1 {
.card-value-display--top {
position: absolute;
- top: 8px;
- left: 8px;
+ top: calc(var(--card-width) * 0.1);
+ left: calc(var(--card-width) * 0.1);
display: flex;
flex-direction: column;
align-items: center;
@@ -215,8 +216,8 @@ h1 {
}
.card-value-display--bottom {
position: absolute;
- bottom: 8px;
- right: 8px;
+ bottom: calc(var(--card-width) * 0.1);
+ right: calc(var(--card-width) * 0.1);
display: flex;
flex-direction: column;
align-items: center;
@@ -418,8 +419,29 @@ body.card-back-fade .card .card-face--back,
/* For Mobile Screens (anything below 600px wide) */
@media (max-width: 600px) {
- .foundations {
- gap: 5px;
+ :root {
+ /* Dynamically size cards so 7 columns fit perfectly within the 96vw container */
+ --gap: 1.5vw;
+ --card-width: calc((96vw - 20px - (6 * var(--gap))) / 7);
+ }
+ .topbar {
+ padding: 10px 8px;
+ gap: 8px;
+ }
+ .topbar h1 {
+ font-size: 20px;
+ }
+ .btn {
+ padding: 6px 10px;
+ font-size: 13px;
+ }
+ .draw-toggle {
+ padding: 4px 8px;
+ font-size: 13px;
+ }
+ .scores {
+ font-size: 14px;
+ gap: 12px;
}
}
@@ -435,4 +457,28 @@ body.card-back-fade .card .card-face--back,
:root {
--card-width: max(7.5vmin, 90px);
}
+}
+
+
+
+/* Add this to your style.css */
+.card-ghost-stack {
+ position: fixed;
+ pointer-events: none;
+ z-index: 2000;
+}
+
+.card-ghost-stack .card {
+ position: absolute !important;
+ box-shadow: 0 12px 30px rgba(0,0,0,0.4);
+ transition: none !important;
+}
+
+/* Ensure ghost cards use the current theme's back */
+.card-ghost-stack .card-face--back {
+ border-color: inherit;
+}
+
+.card.dragging {
+ visibility: hidden;
}
\ No newline at end of file