diff --git a/studio/frontend/src/app/router.tsx b/studio/frontend/src/app/router.tsx
index dbb74ee1a1..5c18e637e2 100644
--- a/studio/frontend/src/app/router.tsx
+++ b/studio/frontend/src/app/router.tsx
@@ -3,6 +3,7 @@
import { Link, createRouter, useRouterState } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
+import { MascotImg } from "@/components/mascot-img";
import { useT } from "@/i18n";
import { Route as rootRoute } from "./routes/__root";
import { Route as dataRecipesRoute } from "./routes/data-recipes";
@@ -41,11 +42,7 @@ function DefaultNotFound() {
return (
-

+
{t("shell.notFound.title")}
diff --git a/studio/frontend/src/asset-queries.d.ts b/studio/frontend/src/asset-queries.d.ts
new file mode 100644
index 0000000000..2c7a1cd936
--- /dev/null
+++ b/studio/frontend/src/asset-queries.d.ts
@@ -0,0 +1,9 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+// Vite `?inline` imports (data URIs); vite/client only types bare extensions.
+declare module "*?inline" {
+ const src: string;
+ // biome-ignore lint/style/noDefaultExport: Vite asset modules export default.
+ export default src;
+}
diff --git a/studio/frontend/src/assets/mascot-fallback.webp b/studio/frontend/src/assets/mascot-fallback.webp
new file mode 100644
index 0000000000..ad50361dbb
Binary files /dev/null and b/studio/frontend/src/assets/mascot-fallback.webp differ
diff --git a/studio/frontend/src/components/assistant-ui/thread.tsx b/studio/frontend/src/components/assistant-ui/thread.tsx
index ea660f7af8..3763370dcd 100644
--- a/studio/frontend/src/components/assistant-ui/thread.tsx
+++ b/studio/frontend/src/components/assistant-ui/thread.tsx
@@ -36,6 +36,7 @@ import {
useScrollThreadToBottom,
} from "@/components/assistant-ui/use-intent-aware-autoscroll";
import { Button } from "@/components/ui/button";
+import { MascotImg } from "@/components/mascot-img";
import { Spinner } from "@/components/ui/spinner";
import {
DropdownMenu,
@@ -781,7 +782,7 @@ const ThreadWelcome: FC<{
setWelcome(buildWelcome(new Date().getHours(), name));
}, [displayName]);
- const currentEmojiSrc = `/Sloth emojis/${welcome.sloth}`;
+ const currentEmojiSrc = `Sloth emojis/${welcome.sloth}`;
return (
@@ -789,9 +790,8 @@ const ThreadWelcome: FC<{
{/* Center the greeting (sloth + title) over the composer. */}
-
diff --git a/studio/frontend/src/components/mascot-img.tsx b/studio/frontend/src/components/mascot-img.tsx
new file mode 100644
index 0000000000..cff4d54c4d
--- /dev/null
+++ b/studio/frontend/src/components/mascot-img.tsx
@@ -0,0 +1,54 @@
+// SPDX-License-Identifier: AGPL-3.0-only
+// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+
+import { type ComponentProps, useState } from "react";
+
+// Bundled as a data URI so the fallback ships inside the JS bundle and can
+// never 404, unlike the public-folder originals.
+import fallbackMascot from "@/assets/mascot-fallback.webp?inline";
+
+const LEADING_SLASH = /^\//;
+
+// Resolve a public-folder asset against the deploy base (subpath mounts) and
+// encode the spaced sloth filenames.
+export function publicAssetUrl(path: string): string {
+ return encodeURI(import.meta.env.BASE_URL + path.replace(LEADING_SLASH, ""));
+}
+
+type MascotImgProps = { src: string } & Omit<
+ ComponentProps<"img">,
+ "src" | "onError"
+>;
+
+// Keying on src remounts the inner component, so the retry state resets
+// whenever the source changes (greeting sloths rotate).
+export function MascotImg(props: MascotImgProps) {
+ return ;
+}
+
+type Stage = "primary" | "retry" | "fallback";
+
+// Decorative mascot that degrades gracefully: a failed load retries once with
+// a cache-buster (transient blips, stale caches), then swaps to the bundled
+// fallback sloth. Empty alt by default so a broken image never paints text.
+function MascotImgInner({ src, alt = "", ...rest }: MascotImgProps) {
+ const [stage, setStage] = useState("primary");
+
+ const url = publicAssetUrl(src);
+ const effectiveSrc =
+ stage === "primary"
+ ? url
+ : stage === "retry"
+ ? `${url}${url.includes("?") ? "&" : "?"}retry=1`
+ : fallbackMascot;
+
+ return (
+
setStage((s) => (s === "primary" ? "retry" : "fallback"))}
+ />
+ );
+}
diff --git a/studio/frontend/src/features/auth/components/auth-form.tsx b/studio/frontend/src/features/auth/components/auth-form.tsx
index f2017ff0b6..d109b5f196 100644
--- a/studio/frontend/src/features/auth/components/auth-form.tsx
+++ b/studio/frontend/src/features/auth/components/auth-form.tsx
@@ -3,6 +3,7 @@
import { apiUrl } from "@/lib/api-base";
import { Button } from "@/components/ui/button";
+import { MascotImg } from "@/components/mascot-img";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Link, useNavigate } from "@tanstack/react-router";
@@ -311,9 +312,8 @@ export function AuthForm({ mode }: AuthFormProps): ReactElement | null {
return (
-
{title}
diff --git a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx
index a685515253..22066b6fc3 100644
--- a/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx
+++ b/studio/frontend/src/features/chat/artifacts/artifact-surface.tsx
@@ -9,6 +9,7 @@ import {
unslothDarkTheme,
unslothLightTheme,
} from "@/components/assistant-ui/code-themes";
+import { MascotImg } from "@/components/mascot-img";
import { Button } from "@/components/ui/button";
import { copyToClipboard } from "@/lib/copy-to-clipboard";
import { cn } from "@/lib/utils";
@@ -78,9 +79,8 @@ function ArtifactGeneratingPanel() {
return (
-

diff --git a/studio/frontend/src/features/onboarding/components/splash-screen.tsx b/studio/frontend/src/features/onboarding/components/splash-screen.tsx
index 70438a04d8..47d9c25efe 100644
--- a/studio/frontend/src/features/onboarding/components/splash-screen.tsx
+++ b/studio/frontend/src/features/onboarding/components/splash-screen.tsx
@@ -3,6 +3,7 @@
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
+import { MascotImg } from "@/components/mascot-img";
import { motion } from "motion/react";
interface SplashScreenProps {
@@ -19,10 +20,7 @@ export function SplashScreen({
{/* Mascot */}
-
+ >
+
+
{/* Brand text */}
diff --git a/studio/frontend/src/features/onboarding/components/wizard-content.tsx b/studio/frontend/src/features/onboarding/components/wizard-content.tsx
index 6906b87f8f..40d290a73e 100644
--- a/studio/frontend/src/features/onboarding/components/wizard-content.tsx
+++ b/studio/frontend/src/features/onboarding/components/wizard-content.tsx
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+import { MascotImg } from "@/components/mascot-img";
import { STEPS } from "@/config/training";
import { useTrainingConfigStore } from "@/features/training";
import type { StepNumber } from "@/types/training";
@@ -19,11 +20,11 @@ const STEP_COMPONENTS = {
} as const;
const STEP_MASCOTS: Record = {
- 1: `${import.meta.env.BASE_URL}Sloth emojis/large sloth wave.png`,
- 2: `${import.meta.env.BASE_URL}Sloth emojis/sloth magnify final.png`,
- 3: `${import.meta.env.BASE_URL}Sloth emojis/sloth huglove large.png`,
- 4: `${import.meta.env.BASE_URL}Sloth emojis/large sloth glasses.png`,
- 5: `${import.meta.env.BASE_URL}Sloth emojis/large sloth yay.png`,
+ 1: "Sloth emojis/large sloth wave.png",
+ 2: "Sloth emojis/sloth magnify final.png",
+ 3: "Sloth emojis/sloth huglove large.png",
+ 4: "Sloth emojis/large sloth glasses.png",
+ 5: "Sloth emojis/large sloth yay.png",
};
export function WizardContent() {
@@ -35,7 +36,7 @@ export function WizardContent() {
return (
-
+
{stepConfig.title}
diff --git a/studio/frontend/src/features/studio/training-start-overlay.tsx b/studio/frontend/src/features/studio/training-start-overlay.tsx
index b9fb34fedb..3b0cce658b 100644
--- a/studio/frontend/src/features/studio/training-start-overlay.tsx
+++ b/studio/frontend/src/features/studio/training-start-overlay.tsx
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: AGPL-3.0-only
// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
+import { MascotImg } from "@/components/mascot-img";
import {
AlertDialog,
AlertDialogAction,
@@ -312,11 +313,7 @@ export function TrainingStartOverlay({
return (
-

+