From 8081af980378985d4837ddc40f8ee899e91a6e97 Mon Sep 17 00:00:00 2001 From: bowen Date: Sun, 25 Jan 2026 02:33:03 -0500 Subject: [PATCH 1/2] created a component for events, added projection tools --- frontend/package-lock.json | 52 ++++++++++++++ frontend/package.json | 3 + frontend/src/App.tsx | 44 +++++++++++- frontend/src/components/EventModal.css | 95 ++++++++++++++++++++++++++ frontend/src/components/EventModal.tsx | 27 ++++++++ frontend/src/components/Projection.css | 83 ++++++++++++++++++++++ frontend/src/components/Projection.tsx | 47 +++++++++++++ frontend/src/engine/event.ts | 56 ++++++++++----- frontend/src/engine/init.ts | 4 +- frontend/src/engine/round.ts | 36 +++++----- frontend/src/state/useGameState.ts | 37 ++++++++-- 11 files changed, 441 insertions(+), 43 deletions(-) create mode 100644 frontend/src/components/EventModal.css create mode 100644 frontend/src/components/EventModal.tsx create mode 100644 frontend/src/components/Projection.css create mode 100644 frontend/src/components/Projection.tsx diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f554413..f2d3c65 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,14 +9,17 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/postcss": "^4.1.18", + "chart.js": "^4.5.1", "mongoose": "^9.1.5", "postcss": "^8.5.6", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "tailwindcss": "^4.1.18" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/chart.js": "^2.9.41", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", @@ -979,6 +982,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", + "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", + "license": "MIT" + }, "node_modules/@mongodb-js/saslprep": { "version": "1.4.5", "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.5.tgz", @@ -1827,6 +1836,16 @@ "tailwindcss": "4.1.18" } }, + "node_modules/@types/chart.js": { + "version": "2.9.41", + "resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.41.tgz", + "integrity": "sha512-3dvkDvueckY83UyUXtJMalYoH6faOLkWQoaTlJgB4Djde3oORmNP0Jw85HtzTuXyliUHcdp704s0mZFQKio/KQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "moment": "^2.10.2" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2359,6 +2378,19 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", + "integrity": "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3379,6 +3411,16 @@ "node": "*" } }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/mongodb": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz", @@ -3675,6 +3717,16 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.3.1.tgz", + "integrity": "sha512-h5IPXKg9EXpjoBzUfyWJvllMjG2mQ4EiuHQFhms/AjUm0XSZHhyRy2xVmLXHKrtcdrPO4mnGqRtYoD0vp95A0A==", + "license": "MIT", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/react-dom": { "version": "19.2.3", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index 229909f..60ad1f8 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,14 +11,17 @@ }, "dependencies": { "@tailwindcss/postcss": "^4.1.18", + "chart.js": "^4.5.1", "mongoose": "^9.1.5", "postcss": "^8.5.6", "react": "^19.2.0", + "react-chartjs-2": "^5.3.1", "react-dom": "^19.2.0", "tailwindcss": "^4.1.18" }, "devDependencies": { "@eslint/js": "^9.39.1", + "@types/chart.js": "^2.9.41", "@types/node": "^24.10.1", "@types/react": "^19.2.5", "@types/react-dom": "^19.2.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index ac67a98..4fbeb0c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -12,6 +12,9 @@ import GameOverScreen from "../src/components/GameOverScreen"; import bigHouse from "../src/assets/Big_House.png"; import smallHouse from "../src/assets/Small_House.png"; import Dorm from "../src/assets/Dorm.png"; +import { FinalStatsOverlay } from "../src/components/Projection"; +import EventModal from "../src/components/EventModal" + interface InvestmentData { personal?: number; @@ -28,7 +31,8 @@ function App() { 2: bigHouse, }; - const { state, nextRound, resetGame} = useGameState(); + const [showFinalStats, setShowFinalStats] = useState(false); + const [personal, setPersonal] = useState(''); const [career, setCareer] = useState(''); @@ -40,6 +44,14 @@ function App() { const getGif = () => state.sanity >= 50 ? happy : state.sanity >= 25 ? neutral : sad; + const { + state, + nextRound, + pendingEvent, + continueEvent, + resetGame + } = useGameState(); + useEffect(() => { if (!audioRef.current) return; @@ -78,6 +90,25 @@ function App() { return ( <> + + {showFinalStats && ( + { + console.log("Retire button clicked, closing overlay"); + setShowFinalStats(false); + resetGame(); + }} + /> + )} + {state.gameOver ? ( ) : ( @@ -122,6 +153,17 @@ function App() { + + + )} diff --git a/frontend/src/components/EventModal.css b/frontend/src/components/EventModal.css new file mode 100644 index 0000000..f39345e --- /dev/null +++ b/frontend/src/components/EventModal.css @@ -0,0 +1,95 @@ +@font-face { + font-family: 'Upheavel'; + src: url('../assets/fonts/upheavtt.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +/* Event Modal Overlay */ +.event-overlay { + position: fixed; + inset: 0; + background: rgba(200, 180, 150, 0.5); /* soft beige semi-transparent */ + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; +} + +/* Popup box */ +.event-popup { + position: relative; + width: 420px; + background: #fff4e6; /* cream/beige */ + border: 4px solid #d2b48c; /* soft brown border */ + border-radius: 16px; + box-shadow: 0 8px 20px rgba(100, 60, 20, 0.25); + overflow: hidden; + transform: scale(0.95); + animation: popIn 0.2s ease-out forwards; + font-family: 'Upheavel', sans-serif; +} + +/* Optional top background (like header) */ +.event-bg { + width: 100%; + border-bottom: 2px solid #d2b48c; + border-radius: 16px 16px 0 0; +} + +/* Content area */ +.event-content { + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; + color: #5b4636; /* muted brown text */ + text-align: center; +} + +/* Heading */ +.event-content h2 { + font-family: 'Upheavel', sans-serif; + font-size: 1.5rem; + color: #8b5e3c; /* soft brown heading */ + margin: 0; + text-shadow: 1px 1px 2px rgba(255,255,255,0.6); +} + +/* Paragraph text */ +.event-content p { + font-family: 'Upheavel', sans-serif; + font-size: 1.1rem; + margin: 0; + line-height: 1.4; +} + +/* Button */ +.event-content button { + font-family: 'Upheavel', sans-serif; + font-size: 1rem; + background-color: #d2b48c; /* light brown */ + color: #fffaf0; /* cream text */ + border: none; + border-radius: 12px; + padding: 8px 16px; + cursor: pointer; + transition: transform 0.1s ease, background 0.2s ease; +} + +.event-content button:hover { + transform: scale(1.05); + background-color: #a67c52; /* darker brown hover */ +} + +/* Pop-in animation */ +@keyframes popIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} diff --git a/frontend/src/components/EventModal.tsx b/frontend/src/components/EventModal.tsx new file mode 100644 index 0000000..8c80752 --- /dev/null +++ b/frontend/src/components/EventModal.tsx @@ -0,0 +1,27 @@ +import type { GameEvent } from "../engine/event"; +import "./EventModal.css"; // make sure CSS is imported + +interface EventModalProps { + event: GameEvent | null; + onContinue: () => void; +} + +export default function EventModal({ event, onContinue }: EventModalProps) { + if (!event) return null; + + return ( +
+
+ {event.name} +
+

{event.name}

+

{event.description}

+ +
+
+
+ ); +} diff --git a/frontend/src/components/Projection.css b/frontend/src/components/Projection.css new file mode 100644 index 0000000..743b977 --- /dev/null +++ b/frontend/src/components/Projection.css @@ -0,0 +1,83 @@ +@font-face { + font-family: "Upheavtt"; + src: url("../assets/fonts/upheavtt.ttf") format("truetype"); + font-weight: normal; + font-style: normal; +} + + +.overlay { + position: fixed; + inset: 0; + width: 100vw; + height: 100vh; + background: linear-gradient( + 135deg, + rgba(255, 228, 239, 0.9), + rgba(224, 231, 255, 0.9) + ); /* soft pink → lavender */ + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + font-family: "Upheavtt", sans-serif; +} + +.overlay-box { + text-align: center; + padding: 2.5rem 3rem; + border-radius: 20px; + background: #fff7fb; /* very soft pink */ + box-shadow: 0 15px 40px rgba(180, 140, 200, 0.25); + color: #5b4b6b; /* muted purple text */ + min-width: 360px; +} + +.overlay-box h1 { + font-size: 2rem; + margin-bottom: 1.5rem; + color: #6d5acf; +} + +.stats-grid { + display: flex; + flex-direction: column; + gap: 1.25rem; + margin-top: 1rem; +} + +.stat-card { + background: #f1f0ff; /* pastel lavender */ + padding: 1rem; + border-radius: 14px; +} + +.stat-card label { + display: block; + font-size: 0.9rem; + opacity: 0.85; + margin-bottom: 0.25rem; +} + +.stat-card span { + font-size: 1.6rem; + font-weight: bold; + color: #3f3cbb; +} + +.btn { + margin-top: 2rem; + padding: 0.9rem 1.6rem; + background: linear-gradient(135deg, #f9a8d4, #c7b5ff); + color: #3b2f4a; + border: none; + border-radius: 999px; + font-size: 1.1rem; + cursor: pointer; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 15px rgba(199, 181, 255, 0.6); +} diff --git a/frontend/src/components/Projection.tsx b/frontend/src/components/Projection.tsx new file mode 100644 index 0000000..d298534 --- /dev/null +++ b/frontend/src/components/Projection.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import type { RoundData } from "../engine/types"; +import "./Projection.css"; + +export const FinalStatsOverlay: React.FC<{ + roundData?: RoundData; + onRestart: () => void; +}> = ({ roundData, onRestart }) => { + const RETIREMENT_AGE = 60; + const AVG_RETURN = 0.04; + + if (!roundData) return ( +
+
+

SYSTEM_SUMMARY: LOADING...

+
+
+ ); + + const { age, indexTotal } = roundData; + const yearsInvested = Math.max(age - 18, 1); + const avgContribution = indexTotal / yearsInvested; + + let projectedTotal = indexTotal; + for (let futureAge = age + 1; futureAge <= RETIREMENT_AGE; futureAge++) { + projectedTotal = (projectedTotal + avgContribution) * (1 + AVG_RETURN); + } + + return ( +
+
+

Current age: {age}

+
+
+ + ${indexTotal.toLocaleString()} +
+
+ + ${projectedTotal.toLocaleString(undefined, { maximumFractionDigits: 0 })} +
+
+ +
+
+ ); +}; diff --git a/frontend/src/engine/event.ts b/frontend/src/engine/event.ts index b119377..02a7a5f 100644 --- a/frontend/src/engine/event.ts +++ b/frontend/src/engine/event.ts @@ -1,6 +1,6 @@ import type { RoundData } from './types'; -interface GameEvent { +export interface GameEvent { id: string; name: string; description: string; @@ -38,9 +38,9 @@ const eventPool: GameEvent[] = [ description: 'You were distracted and rear-ended a luxury sedan. Your car is a mess, and the other driver is furious.', effect: (state) => { let basecost = 4000; - if (state.insurance >= 10) basecost = 1000; - if (state.insurance >= 30) basecost = 500; if (state.insurance >= 40) basecost = 30; + else if (state.insurance >= 30) basecost = 500; + else if (state.insurance >= 10) basecost = 1000; state.total -= basecost; state.sanity -= 10; @@ -116,23 +116,43 @@ const eventPool: GameEvent[] = [ } }, ]; -export function triggerRandomEvent(state: RoundData): RoundData { - // 1. Pick a random index - const randomIndex = Math.floor(Math.random() * eventPool.length); - const selectedEvent = eventPool[randomIndex]; +// export function triggerRandomEvent(state: RoundData): RoundData { +// // 1. Pick a random index +// const randomIndex = Math.floor(Math.random() * eventPool.length); +// const selectedEvent = eventPool[randomIndex]; - // 2. Log it so the player knows what happened - console.group(`EVENT: ${selectedEvent.name}`); - console.log(selectedEvent.description); +// // 2. Log it so the player knows what happened +// console.group(`EVENT: ${selectedEvent.name}`); +// console.log(selectedEvent.description); - // 3. Apply the specific logic of that event - const nextState = selectedEvent.effect(state); +// // 3. Apply the specific logic of that event +// const nextState = selectedEvent.effect(state); - if (state.sanity < 0) { - state.sanity = 0; - } +// if (state.sanity < 0) { +// state.sanity = 0; +// } - console.groupEnd(); +// console.groupEnd(); - return nextState; -} \ No newline at end of file +// return nextState; +// } + +export function getRandomEvent(): GameEvent { + const randomIndex = Math.floor(Math.random() * eventPool.length); + return eventPool[randomIndex]; +} + +export function applyEvent( + state: RoundData, + event: GameEvent +): RoundData { + const next = structuredClone(state); + const result = event.effect(next); + + if (result.sanity < 0) { + result.sanity = 0; + } + + return result; +} + diff --git a/frontend/src/engine/init.ts b/frontend/src/engine/init.ts index 22ffd37..a0f9317 100644 --- a/frontend/src/engine/init.ts +++ b/frontend/src/engine/init.ts @@ -4,11 +4,11 @@ import type { RoundData } from './types'; export function createInitialRound(): RoundData { return { name: "Player", - income: 5000, + income: 1200, total: 0, sanity: 100, insurance: 0, - rent: 700, + rent: 800, careerInvestmentTotal: 0, indexTotal: 0, debtTotal: 0, diff --git a/frontend/src/engine/round.ts b/frontend/src/engine/round.ts index 77ef777..52c5b19 100644 --- a/frontend/src/engine/round.ts +++ b/frontend/src/engine/round.ts @@ -1,51 +1,55 @@ import type { RoundData } from './types'; +import type { GameEvent } from './event'; +import { getRandomEvent } from './event'; + import { applyIncome } from "./income"; import { applyIndexGrowth } from "./indexLogic"; import { applySanityDecay } from "./sanity"; import { applyFixedExpenses } from "./fixed_expenses"; -import { triggerRandomEvent } from './event'; function checkLives(state: RoundData): RoundData { const next = { ...state }; - if (next.sanity <= 0 || next.total < 0) { + if (next.sanity <= 0 || (next.total < 0 && next.level > 1)) { next.lives = Math.max(0, next.lives - 1); if (next.total < 0) next.total = 0; if (next.sanity <= 0) next.sanity = 50; - - console.log(`You lost a life! Lives remaining: ${next.lives}`); } return next; } -export function advanceRound(state: RoundData): RoundData { +export function advanceRound(state: RoundData): { + state: RoundData; + pendingEvent: GameEvent | null; +} { let next = structuredClone(state); next = checkLives(next); if (next.lives === 0) { - console.log("GAME OVER! You have no lives left."); next.gameOver = true; - return next; + return { state: next, pendingEvent: null }; } - console.group(`🌀 ROUND ${next.level}`); - + // ---- Round simulation ---- next = applyIncome(next); next = applyFixedExpenses(next); next = applyIndexGrowth(next); next = applySanityDecay(next); + next.age += 0.25; next.level += 1; - if (next.level % 3 === 0) { - next = triggerRandomEvent(next); - } - - console.log("End of round:", next); - console.groupEnd(); + // ---- Event emission (NOT application) ---- + const pendingEvent = + next.level % 3 === 0 + ? getRandomEvent() + : null; - return next; + return { + state: next, + pendingEvent + }; } diff --git a/frontend/src/state/useGameState.ts b/frontend/src/state/useGameState.ts index 48f1352..ca5dfe0 100644 --- a/frontend/src/state/useGameState.ts +++ b/frontend/src/state/useGameState.ts @@ -1,16 +1,33 @@ import { useState } from "react"; import { createInitialRound } from "../engine/init"; import { advanceRound } from "../engine/round"; -import { investPersonal, investCareer, investIndex, investInsurance } from "../engine/buckets"; -import type { RoundData } from '../engine/types'; +import { applyEvent } from "../engine/event"; // ⬅️ ADD +import { + investPersonal, + investCareer, + investIndex, + investInsurance +} from "../engine/buckets"; + +import type { RoundData } from "../engine/types"; +import type { GameEvent } from "../engine/event"; // ⬅️ ADD + export function useGameState() { const [state, setState] = useState(createInitialRound()); + const [pendingEvent, setPendingEvent] = useState(null); function resetGame() { setState(createInitialRound()); // Reverts state back to the beginning } + function continueEvent() { + if (!pendingEvent) return; + + setState(prev => applyEvent(prev, pendingEvent)); + setPendingEvent(null); +} + function nextRound(investments?: { personal?: number; career?: number; @@ -44,11 +61,19 @@ export function useGameState() { } // Advance the round - nextState = advanceRound(nextState); + const result = advanceRound(nextState); + setPendingEvent(result.pendingEvent); - return nextState; +return result.state; }); } - return { state, nextRound, resetGame}; -} + return { + state, + nextRound, + resetGame, + pendingEvent, + continueEvent + }; +}; + From 36ccf2db8c014440dd1120ee8a47a05114b0a87c Mon Sep 17 00:00:00 2001 From: bowen Date: Sun, 25 Jan 2026 02:37:27 -0500 Subject: [PATCH 2/2] light styling --- frontend/src/components/HUD/HUD.css | 1 + frontend/src/components/Projection.css | 75 ++++++++++++++++---------- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/frontend/src/components/HUD/HUD.css b/frontend/src/components/HUD/HUD.css index 3bf89de..5f41f18 100644 --- a/frontend/src/components/HUD/HUD.css +++ b/frontend/src/components/HUD/HUD.css @@ -26,6 +26,7 @@ color: white; font-family: monospace; + font-size: 1.2rem; } .hud-left h3 { diff --git a/frontend/src/components/Projection.css b/frontend/src/components/Projection.css index 743b977..168178e 100644 --- a/frontend/src/components/Projection.css +++ b/frontend/src/components/Projection.css @@ -1,44 +1,50 @@ @font-face { - font-family: "Upheavtt"; - src: url("../assets/fonts/upheavtt.ttf") format("truetype"); + font-family: 'Upheavel'; + src: url('../assets/fonts/upheavtt.ttf') format('truetype'); font-weight: normal; font-style: normal; } - +/* Full-screen overlay */ .overlay { position: fixed; inset: 0; width: 100vw; height: 100vh; - background: linear-gradient( - 135deg, - rgba(255, 228, 239, 0.9), - rgba(224, 231, 255, 0.9) - ); /* soft pink → lavender */ + background: rgba(200, 180, 150, 0.5); /* soft beige semi-transparent */ display: flex; align-items: center; justify-content: center; z-index: 99999; - font-family: "Upheavtt", sans-serif; + font-family: 'Upheavel', sans-serif; } +/* Popup box */ .overlay-box { + position: relative; + width: 420px; + background: #fff4e6; /* cream/beige */ + border: 4px solid #d2b48c; /* soft brown border */ + border-radius: 16px; + box-shadow: 0 8px 20px rgba(100, 60, 20, 0.25); + padding: 2rem 3rem; text-align: center; - padding: 2.5rem 3rem; - border-radius: 20px; - background: #fff7fb; /* very soft pink */ - box-shadow: 0 15px 40px rgba(180, 140, 200, 0.25); - color: #5b4b6b; /* muted purple text */ - min-width: 360px; + overflow: hidden; + transform: scale(0.95); + animation: popIn 0.2s ease-out forwards; + color: #5b4636; /* muted brown text */ } +/* Optional top header (if you want a separate background) */ .overlay-box h1 { - font-size: 2rem; + font-family: 'Upheavel', sans-serif; + font-size: 1.8rem; + color: #8b5e3c; /* soft brown heading */ margin-bottom: 1.5rem; - color: #6d5acf; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.6); } +/* Stats grid (vertical for simple layout) */ .stats-grid { display: flex; flex-direction: column; @@ -46,15 +52,16 @@ margin-top: 1rem; } +/* Individual stat cards */ .stat-card { - background: #f1f0ff; /* pastel lavender */ + background: #fdf2e9; /* lighter beige card */ padding: 1rem; border-radius: 14px; } .stat-card label { display: block; - font-size: 0.9rem; + font-size: 1.5rem; opacity: 0.85; margin-bottom: 0.25rem; } @@ -62,22 +69,36 @@ .stat-card span { font-size: 1.6rem; font-weight: bold; - color: #3f3cbb; + color: #6b4f3c; /* deeper brown */ } +/* Restart button */ .btn { margin-top: 2rem; padding: 0.9rem 1.6rem; - background: linear-gradient(135deg, #f9a8d4, #c7b5ff); - color: #3b2f4a; + background-color: #d2b48c; /* light brown */ + color: #fffaf0; /* cream text */ border: none; - border-radius: 999px; - font-size: 1.1rem; + border-radius: 12px; + font-size: 1.5rem; + font-family: 'Upheavel', sans-serif; cursor: pointer; - transition: transform 0.15s ease, box-shadow 0.15s ease; + transition: transform 0.1s ease, background 0.2s ease; } .btn:hover { - transform: translateY(-2px); - box-shadow: 0 6px 15px rgba(199, 181, 255, 0.6); + transform: scale(1.05); + background-color: #a67c52; /* darker brown hover */ +} + +/* Pop-in animation */ +@keyframes popIn { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } }