Custom Frutiger Aero 404

app/not-found.tsx renders a standalone screen: the root layout's ice
scene is the backdrop, the glass card/rim/bubbles come from the shared
recipes (full Aero with that theme on, frosted glass in light/dark),
and the mascot is the scene's glossy ice orb at full strength with a
snowflake and a "404" chip. Two ways out - back to the library and
into the chat - both 44px targets, en + cs.

The regression suite pins the 404 status, both links and the Czech
copy; the pixel-contrast scan covers the screen in all three themes.
The service worker no longer caches non-OK navigations as the offline
shell (a typo'd URL used to write the error page into the shell
cache). Version 0.5.6, SW -> freezmo-v78.
This commit is contained in:
Martin Dvořák 2026-10-14 15:27:47 +00:00
commit 7e42b03f7e
9 changed files with 137 additions and 4 deletions

View file

@ -0,0 +1,50 @@
"use client";
import Link from "next/link";
import { ChatIcon, HomeIcon, SnowflakeIcon } from "@/components/icons";
import { useI18n } from "@/lib/i18n";
/**
* The custom 404. The root layout already paints the ice scene behind it, so
* this is just the glass card: in Frutiger Aero the wallpaper bake, the bright
* rim and the rising bubbles all come from the shared recipes, and the glossy
* orb is the scene's own ice sphere at full strength. Standalone (no rail, no
* tab bar), like the offline screen.
*/
export function NotFoundScreen() {
const { t } = useI18n();
return (
<main className="frost-bg grid min-h-dvh place-items-center px-4 py-10">
<div className="card w-full max-w-md space-y-4 p-6 text-center sm:p-8">
<div className="not-found-orb mx-auto grid size-24 place-items-center">
<SnowflakeIcon className="size-10 text-white drop-shadow-[0_2px_6px_rgb(3_74_124/0.6)]" />
</div>
<p className="pt-1">
<span className="glass-chip inline-flex items-center rounded-full px-3 py-1 text-[12px] font-semibold tracking-[0.18em] text-[var(--text-subtle)]">
404
</span>
</p>
<h1 className="text-xl font-semibold tracking-[-0.01em] text-[var(--text)]">{t("notFound.title")}</h1>
<p className="mx-auto max-w-sm text-sm leading-relaxed text-[var(--text-muted)]">{t("notFound.hint")}</p>
<div className="flex flex-wrap justify-center gap-2 pt-2">
<Link
href="/"
className="btn-primary inline-flex h-11 items-center justify-center gap-2 rounded-[14px] px-5 text-[15px] font-medium"
>
<HomeIcon className="size-4" />
{t("notFound.home")}
</Link>
<Link
href="/chat"
className="btn-secondary inline-flex h-11 items-center justify-center gap-2 rounded-[14px] px-5 text-[15px] font-medium"
>
<ChatIcon className="size-4" />
{t("notFound.chat")}
</Link>
</div>
</div>
</main>
);
}