feat: unify toast styles and add quest/milestone toast notifications
Security Scan and Upload / Security & DefectDojo Upload (push) Successful in 1m3s
CI / Lint, Build & Test (push) Successful in 1m5s

- Merge .codex-toast and .achievement-toast into a single .game-toast class
- Fix storyToast inner class names and replace <button> wrapper with <div>
- Add QuestCompleteToast and QuestFailedToast components
- Add MilestoneToast for prestige, transcendence, and apotheosis events
- Move shared toast container to gameLayout so all toasts stack in one column
- Wire quest detection in GameContext to store full Quest objects for toast names
- Trigger prestige toast from both auto-prestige and manual prestige panel
This commit is contained in:
2026-03-08 18:47:42 -07:00
committed by Naomi Carrigan
parent 290c06de83
commit f9c925b9fc
10 changed files with 380 additions and 47 deletions
@@ -41,7 +41,7 @@ const ToastItem = ({
const crystals = achievement.reward?.crystals; const crystals = achievement.reward?.crystals;
return ( return (
<div className="achievement-toast" onClick={handleClick}> <div className="game-toast" onClick={handleClick}>
<span className="toast-icon">{achievement.icon}</span> <span className="toast-icon">{achievement.icon}</span>
<div className="toast-content"> <div className="toast-content">
<span className="toast-label">{"Achievement Unlocked!"}</span> <span className="toast-label">{"Achievement Unlocked!"}</span>
@@ -70,7 +70,7 @@ const AchievementToast = (): JSX.Element | null => {
} }
return ( return (
<div className="achievement-toast-container"> <>
{pendingAchievements.map((achievement) => { {pendingAchievements.map((achievement) => {
return ( return (
<ToastItem <ToastItem
@@ -80,7 +80,7 @@ const AchievementToast = (): JSX.Element | null => {
/> />
); );
})} })}
</div> </>
); );
}; };
+3 -3
View File
@@ -47,7 +47,7 @@ const CodexToastItem = ({
} }
return ( return (
<div className="codex-toast" onClick={handleClick}> <div className="game-toast" onClick={handleClick}>
<span className="toast-icon">{"📖"}</span> <span className="toast-icon">{"📖"}</span>
<div className="toast-content"> <div className="toast-content">
<span className="toast-label">{"✨ Lore Unlocked!"}</span> <span className="toast-label">{"✨ Lore Unlocked!"}</span>
@@ -70,13 +70,13 @@ const CodexToast = (): JSX.Element | null => {
} }
return ( return (
<div className="achievement-toast-container"> <>
{pendingEntryIds.map((id) => { {pendingEntryIds.map((id) => {
return ( return (
<CodexToastItem entryId={id} key={id} onDismiss={dismissCodexEntry} /> <CodexToastItem entryId={id} key={id} onDismiss={dismissCodexEntry} />
); );
})} })}
</div> </>
); );
}; };
+10 -3
View File
@@ -27,10 +27,12 @@ import { EditProfileModal } from "./editProfileModal.js";
import { EquipmentPanel } from "./equipmentPanel.js"; import { EquipmentPanel } from "./equipmentPanel.js";
import { ExplorationPanel } from "./explorationPanel.js"; import { ExplorationPanel } from "./explorationPanel.js";
import { LoginBonusModal } from "./loginBonusModal.js"; import { LoginBonusModal } from "./loginBonusModal.js";
import { MilestoneToast } from "./milestoneToast.js";
import { OfflineModal } from "./offlineModal.js"; import { OfflineModal } from "./offlineModal.js";
import { OutdatedSchemaModal } from "./outdatedSchemaModal.js"; import { OutdatedSchemaModal } from "./outdatedSchemaModal.js";
import { PrestigePanel } from "./prestigePanel.js"; import { PrestigePanel } from "./prestigePanel.js";
import { QuestPanel } from "./questPanel.js"; import { QuestPanel } from "./questPanel.js";
import { QuestCompleteToast, QuestFailedToast } from "./questToast.js";
import { StatisticsPanel } from "./statisticsPanel.js"; import { StatisticsPanel } from "./statisticsPanel.js";
import { StoryPanel } from "./storyPanel.js"; import { StoryPanel } from "./storyPanel.js";
import { StoryToast } from "./storyToast.js"; import { StoryToast } from "./storyToast.js";
@@ -164,9 +166,14 @@ const GameLayout = (): JSX.Element => {
{schemaOutdated && !dismissedOutdatedWarning {schemaOutdated && !dismissedOutdatedWarning
? <OutdatedSchemaModal onDismiss={handleDismissOutdated} /> ? <OutdatedSchemaModal onDismiss={handleDismissOutdated} />
: null} : null}
<AchievementToast /> <div className="achievement-toast-container">
<CodexToast /> <AchievementToast />
<StoryToast /> <CodexToast />
<MilestoneToast />
<QuestCompleteToast />
<QuestFailedToast />
<StoryToast />
</div>
{loginBonus === null {loginBonus === null
? null ? null
: <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} /> : <LoginBonusModal bonus={loginBonus} onClose={dismissLoginBonus} />
@@ -0,0 +1,96 @@
/**
* @file Milestone toast notification component for prestige, transcendence, and apotheosis.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
import { type JSX, useEffect } from "react";
import { useGame } from "../../context/gameContext.js";
interface MilestoneToastItemProperties {
readonly icon: string;
readonly label: string;
readonly onDismiss: ()=> void;
}
/**
* Renders a single milestone toast notification.
* @param props - The toast item properties.
* @param props.icon - The emoji icon.
* @param props.label - The label text.
* @param props.onDismiss - Callback to dismiss the toast.
* @returns The JSX element.
*/
const MilestoneToastItem = ({
icon,
label,
onDismiss,
}: MilestoneToastItemProperties): JSX.Element => {
useEffect(() => {
const timer = setTimeout(() => {
onDismiss();
}, 4000);
return (): void => {
clearTimeout(timer);
};
}, [ onDismiss ]);
return (
<div className="game-toast" onClick={onDismiss}>
<span className="toast-icon">{icon}</span>
<div className="toast-content">
<span className="toast-label">{label}</span>
</div>
</div>
);
};
/**
* Renders all milestone toasts (prestige, transcendence, apotheosis).
* @returns The JSX element or null if no milestone toasts are pending.
*/
const MilestoneToast = (): JSX.Element | null => {
const {
showPrestigeToast,
showTranscendenceToast,
showApotheosisToast,
dismissPrestigeToast,
dismissTranscendenceToast,
dismissApotheosisToast,
} = useGame();
const hasAny
= showPrestigeToast || showTranscendenceToast || showApotheosisToast;
if (!hasAny) {
return null;
}
return (
<>
{showPrestigeToast
? <MilestoneToastItem
icon={"⭐"}
label={"⭐ Prestige!"}
onDismiss={dismissPrestigeToast}
/>
: null}
{showTranscendenceToast
? <MilestoneToastItem
icon={"🌌"}
label={"🌌 Transcendence!"}
onDismiss={dismissTranscendenceToast}
/>
: null}
{showApotheosisToast
? <MilestoneToastItem
icon={"✨"}
label={"✨ Apotheosis!"}
onDismiss={dismissApotheosisToast}
/>
: null}
</>
);
};
export { MilestoneToast };
@@ -89,6 +89,7 @@ const PrestigePanel = (): JSX.Element => {
enableNotifications, enableNotifications,
enableSounds, enableSounds,
toggleAutoPrestige, toggleAutoPrestige,
triggerPrestigeToast,
} = useGame(); } = useGame();
const [ isPending, setIsPending ] = useState(false); const [ isPending, setIsPending ] = useState(false);
const [ result, setResult ] = useState<{ const [ result, setResult ] = useState<{
@@ -128,6 +129,7 @@ const PrestigePanel = (): JSX.Element => {
milestoneRunestones: data.milestoneRunestones, milestoneRunestones: data.milestoneRunestones,
runestones: data.runestones, runestones: data.runestones,
}); });
triggerPrestigeToast();
if (enableSounds) { if (enableSounds) {
playSound("prestige"); playSound("prestige");
} }
+5 -4
View File
@@ -190,10 +190,11 @@ const QuestPanel = (): JSX.Element => {
} }
const { adventurers, autoQuest, quests, zones } = state; const { adventurers, autoQuest, quests, zones } = state;
// eslint-disable-next-line unicorn/no-array-reduce -- Need the total! let partyCombatPower = 0;
const partyCombatPower = adventurers.reduce((total, adventurer) => { for (const adventurer of adventurers) {
return total + adventurer.combatPower * adventurer.count; const contribution = adventurer.combatPower * adventurer.count;
}, 0); partyCombatPower = partyCombatPower + contribution;
}
const zoneQuests = quests.filter(({ zoneId }) => { const zoneQuests = quests.filter(({ zoneId }) => {
return zoneId === activeZoneId; return zoneId === activeZoneId;
}); });
+113
View File
@@ -0,0 +1,113 @@
/**
* @file Quest toast notification component for completed and failed quests.
* @copyright nhcarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
/* eslint-disable react/no-multi-comp -- Sub-components are tightly coupled to their containers */
import { type JSX, useEffect } from "react";
import { useGame } from "../../context/gameContext.js";
import type { Quest } from "@elysium/types";
interface QuestToastItemProperties {
readonly quest: Quest;
readonly onDismiss: (id: string)=> void;
// eslint-disable-next-line react/require-default-props -- Default value set in destructuring
readonly isFailure?: boolean;
}
/**
* Renders a single quest toast notification.
* @param props - The toast item properties.
* @param props.quest - The quest to display.
* @param props.onDismiss - Callback to dismiss the toast.
* @param props.isFailure - Whether this is a failure toast.
* @returns The JSX element.
*/
const QuestToastItem = ({
quest,
onDismiss,
isFailure = false,
}: QuestToastItemProperties): JSX.Element => {
useEffect(() => {
const timer = setTimeout(() => {
onDismiss(quest.id);
}, 4000);
return (): void => {
clearTimeout(timer);
};
}, [ quest.id, onDismiss ]);
function handleClick(): void {
onDismiss(quest.id);
}
return (
<div className="game-toast" onClick={handleClick}>
<span className="toast-icon">{isFailure
? "💀"
: "📜"}</span>
<div className="toast-content">
<span className="toast-label">{isFailure
? "Quest Failed!"
: "✨ Quest Complete!"}</span>
<span className="toast-name">{quest.name}</span>
</div>
</div>
);
};
/**
* Renders the quest complete toast container.
* @returns The JSX element or null if there are no pending quest toasts.
*/
const QuestCompleteToast = (): JSX.Element | null => {
const { completedQuestToasts, dismissCompletedQuest } = useGame();
if (completedQuestToasts.length === 0) {
return null;
}
return (
<>
{completedQuestToasts.map((quest) => {
return (
<QuestToastItem
key={quest.id}
onDismiss={dismissCompletedQuest}
quest={quest}
/>
);
})}
</>
);
};
/**
* Renders the quest failed toast container.
* @returns The JSX element or null if there are no pending failure toasts.
*/
const QuestFailedToast = (): JSX.Element | null => {
const { failedQuestToasts, dismissFailedQuest } = useGame();
if (failedQuestToasts.length === 0) {
return null;
}
return (
<>
{failedQuestToasts.map((quest) => {
return (
<QuestToastItem
isFailure={true}
key={quest.id}
onDismiss={dismissFailedQuest}
quest={quest}
/>
);
})}
</>
);
};
export { QuestCompleteToast, QuestFailedToast };
+8 -8
View File
@@ -45,13 +45,13 @@ const StoryToastItem = ({
} }
return ( return (
<button className="achievement-toast" onClick={handleClick} type="button"> <div className="game-toast" onClick={handleClick}>
<span className="achievement-toast-icon">{"📖"}</span> <span className="toast-icon">{"📖"}</span>
<div className="achievement-toast-content"> <div className="toast-content">
<span className="achievement-toast-label">{"✨ New Chapter!"}</span> <span className="toast-label">{"✨ New Chapter!"}</span>
<span className="achievement-toast-name">{chapter.title}</span> <span className="toast-name">{chapter.title}</span>
</div> </div>
</button> </div>
); );
}; };
@@ -65,11 +65,11 @@ const StoryToast = (): JSX.Element | null => {
return null; return null;
} }
return ( return (
<div className="achievement-toast-container"> <>
{pendingChapterIds.map((id) => { {pendingChapterIds.map((id) => {
return <StoryToastItem chapterId={id} key={id} />; return <StoryToastItem chapterId={id} key={id} />;
})} })}
</div> </>
); );
}; };
+138 -10
View File
@@ -20,6 +20,7 @@ import {
type GameState, type GameState,
type LoginBonusResult, type LoginBonusResult,
type NumberFormat, type NumberFormat,
type Quest,
type TranscendenceResponse, type TranscendenceResponse,
isStoryChapterUnlocked, isStoryChapterUnlocked,
} from "@elysium/types"; } from "@elysium/types";
@@ -334,6 +335,61 @@ interface GameContextValue {
*/ */
dismissAchievement: (id: string)=> void; dismissAchievement: (id: string)=> void;
/**
* Queue of newly completed quests (for toast notifications).
*/
completedQuestToasts: Array<Quest>;
/**
* Remove a quest from the completed toast queue.
*/
dismissCompletedQuest: (id: string)=> void;
/**
* Queue of newly failed quests (for toast notifications).
*/
failedQuestToasts: Array<Quest>;
/**
* Remove a quest from the failed toast queue.
*/
dismissFailedQuest: (id: string)=> void;
/**
* Whether the prestige milestone toast is currently showing.
*/
showPrestigeToast: boolean;
/**
* Trigger the prestige milestone toast (called from prestigePanel on manual prestige).
*/
triggerPrestigeToast: ()=> void;
/**
* Dismiss the prestige milestone toast.
*/
dismissPrestigeToast: ()=> void;
/**
* Whether the transcendence milestone toast is currently showing.
*/
showTranscendenceToast: boolean;
/**
* Dismiss the transcendence milestone toast.
*/
dismissTranscendenceToast: ()=> void;
/**
* Whether the apotheosis milestone toast is currently showing.
*/
showApotheosisToast: boolean;
/**
* Dismiss the apotheosis milestone toast.
*/
dismissApotheosisToast: ()=> void;
/** /**
* The player's chosen number display format. * The player's chosen number display format.
*/ */
@@ -514,6 +570,15 @@ export const GameProvider = ({
const [ unlockedAchievements, setUnlockedAchievements ] = useState< const [ unlockedAchievements, setUnlockedAchievements ] = useState<
Array<Achievement> Array<Achievement>
>([]); >([]);
const [ completedQuestToasts, setCompletedQuestToasts ] = useState<
Array<Quest>
>([]);
const [ failedQuestToasts, setFailedQuestToasts ] = useState<Array<Quest>>(
[],
);
const [ showPrestigeToast, setShowPrestigeToast ] = useState(false);
const [ showTranscendenceToast, setShowTranscendenceToast ] = useState(false);
const [ showApotheosisToast, setShowApotheosisToast ] = useState(false);
const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null); const [ lastSavedAt, setLastSavedAt ] = useState<number | null>(null);
const [ isSyncing, setIsSyncing ] = useState(false); const [ isSyncing, setIsSyncing ] = useState(false);
const [ syncError, setSyncError ] = useState<string | null>(null); const [ syncError, setSyncError ] = useState<string | null>(null);
@@ -530,8 +595,8 @@ export const GameProvider = ({
const isSyncingReference = useRef(false); const isSyncingReference = useRef(false);
const rafReference = useRef<number | null>(null); const rafReference = useRef<number | null>(null);
const unlockedAchievementsReference = useRef<Array<Achievement>>([]); const unlockedAchievementsReference = useRef<Array<Achievement>>([]);
const newlyCompletedQuestsCountReference = useRef(0); const newlyCompletedQuestsReference = useRef<Array<Quest>>([]);
const newlyFailedQuestsCountReference = useRef(0); const newlyFailedQuestsReference = useRef<Array<Quest>>([]);
const signatureReference = useRef<string | null>( const signatureReference = useRef<string | null>(
localStorage.getItem("elysium_save_signature"), localStorage.getItem("elysium_save_signature"),
); );
@@ -949,17 +1014,17 @@ export const GameProvider = ({
); );
// Detect newly completed quests // Detect newly completed quests
newlyCompletedQuestsCountReference.current = next.quests.filter( newlyCompletedQuestsReference.current = next.quests.filter(
(q, index) => { (q, index) => {
return ( return (
previous.quests[index]?.status === "active" previous.quests[index]?.status === "active"
&& q.status === "completed" && q.status === "completed"
); );
}, },
).length; );
// Detect newly failed quests // Detect newly failed quests
newlyFailedQuestsCountReference.current = next.quests.filter( newlyFailedQuestsReference.current = next.quests.filter(
(q, index) => { (q, index) => {
const previousFailedAt = previous.quests[index]?.lastFailedAt; const previousFailedAt = previous.quests[index]?.lastFailedAt;
return ( return (
@@ -967,7 +1032,7 @@ export const GameProvider = ({
&& q.lastFailedAt !== previousFailedAt && q.lastFailedAt !== previousFailedAt
); );
}, },
).length; );
return next; return next;
}); });
@@ -987,24 +1052,30 @@ export const GameProvider = ({
unlockedAchievementsReference.current = []; unlockedAchievementsReference.current = [];
} }
if (newlyCompletedQuestsCountReference.current > 0) { if (newlyCompletedQuestsReference.current.length > 0) {
setCompletedQuestToasts((previous) => {
return [ ...previous, ...newlyCompletedQuestsReference.current ];
});
if (enableSoundsReference.current) { if (enableSoundsReference.current) {
playSound("questCompleted"); playSound("questCompleted");
} }
if (enableNotificationsReference.current) { if (enableNotificationsReference.current) {
sendNotification("📜 Quest Complete!", "A quest has been completed."); sendNotification("📜 Quest Complete!", "A quest has been completed.");
} }
newlyCompletedQuestsCountReference.current = 0; newlyCompletedQuestsReference.current = [];
} }
if (newlyFailedQuestsCountReference.current > 0) { if (newlyFailedQuestsReference.current.length > 0) {
setFailedQuestToasts((previous) => {
return [ ...previous, ...newlyFailedQuestsReference.current ];
});
if (enableSoundsReference.current) { if (enableSoundsReference.current) {
playSound("questFailed"); playSound("questFailed");
} }
if (enableNotificationsReference.current) { if (enableNotificationsReference.current) {
sendNotification("💀 Quest Failed!", "A quest has failed."); sendNotification("💀 Quest Failed!", "A quest has failed.");
} }
newlyFailedQuestsCountReference.current = 0; newlyFailedQuestsReference.current = [];
} }
// Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions) // Auto-save every 30 seconds (skip if a force sync is in-flight to avoid signature collisions)
@@ -1054,6 +1125,7 @@ export const GameProvider = ({
isAutoPrestigingReference.current = true; isAutoPrestigingReference.current = true;
void prestigeApi({}). void prestigeApi({}).
then(async() => { then(async() => {
setShowPrestigeToast(true);
if (enableSoundsReference.current) { if (enableSoundsReference.current) {
playSound("prestige"); playSound("prestige");
} }
@@ -1443,6 +1515,7 @@ export const GameProvider = ({
const transcend = useCallback(async() => { const transcend = useCallback(async() => {
const result = await transcendApi({}); const result = await transcendApi({});
setShowTranscendenceToast(true);
if (enableSoundsReference.current) { if (enableSoundsReference.current) {
playSound("transcendence"); playSound("transcendence");
} }
@@ -1455,6 +1528,7 @@ export const GameProvider = ({
const apotheosis = useCallback(async() => { const apotheosis = useCallback(async() => {
const result = await achieveApotheosisApi({}); const result = await achieveApotheosisApi({});
setShowApotheosisToast(true);
if (enableSoundsReference.current) { if (enableSoundsReference.current) {
playSound("apotheosis"); playSound("apotheosis");
} }
@@ -1733,6 +1807,38 @@ export const GameProvider = ({
setBattleResult(null); setBattleResult(null);
}, []); }, []);
const dismissCompletedQuest = useCallback((id: string) => {
setCompletedQuestToasts((previous) => {
return previous.filter((q) => {
return q.id !== id;
});
});
}, []);
const dismissFailedQuest = useCallback((id: string) => {
setFailedQuestToasts((previous) => {
return previous.filter((q) => {
return q.id !== id;
});
});
}, []);
const triggerPrestigeToast = useCallback(() => {
setShowPrestigeToast(true);
}, []);
const dismissPrestigeToast = useCallback(() => {
setShowPrestigeToast(false);
}, []);
const dismissTranscendenceToast = useCallback(() => {
setShowTranscendenceToast(false);
}, []);
const dismissApotheosisToast = useCallback(() => {
setShowApotheosisToast(false);
}, []);
const dismissAchievement = useCallback((id: string) => { const dismissAchievement = useCallback((id: string) => {
setUnlockedAchievements((previous) => { setUnlockedAchievements((previous) => {
return previous.filter((a) => { return previous.filter((a) => {
@@ -1829,18 +1935,25 @@ export const GameProvider = ({
challengeBoss, challengeBoss,
collectExploration, collectExploration,
completeChapter, completeChapter,
completedQuestToasts,
craftRecipe, craftRecipe,
currentSchemaVersion, currentSchemaVersion,
dismissAchievement, dismissAchievement,
dismissApotheosisToast,
dismissBattle, dismissBattle,
dismissCodexEntry, dismissCodexEntry,
dismissCompletedQuest,
dismissFailedQuest,
dismissLoginBonus, dismissLoginBonus,
dismissOfflineGold, dismissOfflineGold,
dismissPrestigeToast,
dismissStoryChapter, dismissStoryChapter,
dismissTranscendenceToast,
enableNotifications, enableNotifications,
enableSounds, enableSounds,
equipItem, equipItem,
error, error,
failedQuestToasts,
forceSync, forceSync,
formatNumber, formatNumber,
handleClick, handleClick,
@@ -1860,6 +1973,9 @@ export const GameProvider = ({
setEnableNotifications, setEnableNotifications,
setEnableSounds, setEnableSounds,
setNumberFormat, setNumberFormat,
showApotheosisToast,
showPrestigeToast,
showTranscendenceToast,
startExploration, startExploration,
startQuest, startQuest,
state, state,
@@ -1868,6 +1984,7 @@ export const GameProvider = ({
toggleAutoPrestige, toggleAutoPrestige,
toggleAutoQuest, toggleAutoQuest,
transcend, transcend,
triggerPrestigeToast,
unlockedAchievements, unlockedAchievements,
unlockedCodexEntryIds, unlockedCodexEntryIds,
unlockedStoryChapterIds, unlockedStoryChapterIds,
@@ -1875,6 +1992,8 @@ export const GameProvider = ({
}, [ }, [
apotheosis, apotheosis,
battleResult, battleResult,
completedQuestToasts,
failedQuestToasts,
formatNumber, formatNumber,
buyAdventurer, buyAdventurer,
buyEchoUpgrade, buyEchoUpgrade,
@@ -1887,11 +2006,16 @@ export const GameProvider = ({
craftRecipe, craftRecipe,
currentSchemaVersion, currentSchemaVersion,
dismissAchievement, dismissAchievement,
dismissApotheosisToast,
dismissBattle, dismissBattle,
dismissCodexEntry, dismissCodexEntry,
dismissCompletedQuest,
dismissFailedQuest,
dismissLoginBonus, dismissLoginBonus,
dismissOfflineGold, dismissOfflineGold,
dismissPrestigeToast,
dismissStoryChapter, dismissStoryChapter,
dismissTranscendenceToast,
enableNotifications, enableNotifications,
enableSounds, enableSounds,
equipItem, equipItem,
@@ -1914,6 +2038,9 @@ export const GameProvider = ({
setEnableNotifications, setEnableNotifications,
setEnableSounds, setEnableSounds,
setNumberFormat, setNumberFormat,
showApotheosisToast,
showPrestigeToast,
showTranscendenceToast,
startExploration, startExploration,
startQuest, startQuest,
state, state,
@@ -1922,6 +2049,7 @@ export const GameProvider = ({
toggleAutoPrestige, toggleAutoPrestige,
toggleAutoQuest, toggleAutoQuest,
transcend, transcend,
triggerPrestigeToast,
unlockedAchievements, unlockedAchievements,
unlockedCodexEntryIds, unlockedCodexEntryIds,
unlockedStoryChapterIds, unlockedStoryChapterIds,
+2 -16
View File
@@ -1432,20 +1432,6 @@ body {
z-index: 200; z-index: 200;
} }
.achievement-toast {
align-items: center;
animation: slide-in-right 0.35s ease-out;
background: var(--colour-surface);
border: 1px solid var(--colour-gold);
border-radius: var(--radius);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
cursor: pointer;
display: flex;
gap: 0.75rem;
max-width: 280px;
padding: 0.75rem 1rem;
}
.toast-icon { .toast-icon {
font-size: 1.5rem; font-size: 1.5rem;
flex-shrink: 0; flex-shrink: 0;
@@ -2481,8 +2467,8 @@ body {
padding: 0.6rem 0.75rem; padding: 0.6rem 0.75rem;
} }
/* Codex toast — uses a different accent from achievement toast */ /* Unified game toast — essence-coloured border used by all in-game notifications */
.codex-toast { .game-toast {
align-items: center; align-items: center;
animation: slide-in-right 0.35s ease-out; animation: slide-in-right 0.35s ease-out;
background: var(--colour-surface); background: var(--colour-surface);