Something went wrong. Try again.
Monorepo for Tangled forked from tangled.org/core
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208<script lang="ts"> import "#lib/components/landing/landing.css"; import "#lib/components/landing/material.css"; import { resolve } from "$app/paths"; import EmbossFilter from "#lib/components/landing/EmbossFilter.svelte"; import Navbar from "#lib/components/landing/shell/Navbar.svelte"; import ArrowRight from "$icon/arrow-right"; import Container from "#lib/components/landing/ui/Container.svelte";
/** * The 404 page, drawn with the landing's tokens: the side grids, the * navbar, and two cut ends of the thread, one trailing in from the top left and one out to * the bottom right, around the copy. Signed-in visitors keep the app's shell, so the navbar * is left out there (`navbar`). * * On arrival the copy rises in and the two ends draw in from the page's corners, fraying * where they meet the gap. Hovering the way home nudges them towards each other. * * Every class carries `nf-`: the landing's section styles (features.css) are global under * `.landing`, and preloading `/` from any link here brings them in. */ interface Props { /** Draw the landing navbar (signed-out visitors, where the page owns the shell). */ navbar?: boolean; }
let { navbar = true }: Props = $props();
/* * The strands, as placed in the 1440px frame: each vector's centre (x, y), its size * unrotated (w, h), its turn and which end of the thread it belongs to (`a` the top left, * `b` the bottom right). The frayed ends share one vector. Both lines run from their * vector's bottom edge to the fray, so one sweep draws either in. */ const STRANDS = [ { part: "line", end: "a", x: 227.09, y: 205.5, w: 108.95, h: 283.19, turn: 120 }, { part: "fray", end: "a", x: 316.94, y: 319.28, w: 12.46, h: 15.11, turn: 120 }, { part: "fray", end: "b", x: 824.3, y: 469.77, w: 12.46, h: 15.11, turn: -60 }, { part: "line", end: "b", x: 1061.45, y: 675.77, w: 189.01, h: 621.08, turn: -60 } ] as const;</script>
<div class={["landing", "nf", !navbar && "nf-embedded"]}> <div class="page-grid l" aria-hidden="true"></div> <div class="page-grid r" aria-hidden="true"></div> <EmbossFilter /> {#if navbar} <Navbar /> {/if} <main class="nf-stage"> <div class="nf-art" aria-hidden="true"> {#each STRANDS as strand, i (i)} <i class={["nf-strand", `nf-${strand.part}`, `nf-${strand.end}`]} style:--x="{strand.x}px" style:--y="{strand.y}px" style:--w="{strand.w}px" style:--h="{strand.h}px" style:--turn="{strand.turn}deg" ></i> {/each} </div> <Container class="nf-copy"> <div class="nf-text"> <h1 data-testid="errorTitle" data-status="404" class="l-heading l-heading-section l-heading-break" > <span class="solid nf-rise" style:--delay="0.1s">404</span> <span class="dim nf-rise" style:--delay="0.2s">This page doesn’t exist.</span> </h1> <p data-testid="errorMessage" class="l-body l-trim nf-rise" style:--delay="0.3s"> It may have moved, or the link may be wrong. </p> </div> <div class="nf-action nf-rise" style:--delay="0.4s"> <a href={resolve("/")} class="inline-flex w-fit items-center gap-2xs border-b border-border-strong pb-0.5 text-foreground-default hover:border-foreground-default" > Go to the homepage <ArrowRight class="size-4" aria-hidden="true" /> </a> </div> </Container> </main></div>
<style> .nf { display: flex; flex-direction: column; } .nf-embedded { flex: 1; min-height: 0; }
.nf-stage { position: relative; display: flex; flex: 1; flex-direction: column; justify-content: center; overflow: clip; padding-block: 96px; }
/* The art's frame, 1440 × 828, centred on the page, with the copy's middle on the stage's. */ .nf-art { position: absolute; top: calc(50% - 423.5px); left: calc(50% - 720px); width: 1440px; height: 828px; pointer-events: none;
@media (width < 64rem) { display: none; } }
.nf-strand { position: absolute; top: calc(var(--y) - var(--h) / 2); left: calc(var(--x) - var(--w) / 2); width: var(--w); height: var(--h); background: var(--thread); mask: var(--part) center / 100% 100% no-repeat; rotate: var(--turn); transition: translate 0.6s var(--ease-out); } .nf-strand.nf-line.nf-a { --part: url("./assets/not-found/strand-top.svg"); } .nf-strand.nf-line.nf-b { --part: url("./assets/not-found/strand-bottom.svg"); } .nf-strand.nf-fray { --part: url("./assets/not-found/strand-end.svg"); }
/* Arriving: each line sweeps in from the corner, then its frayed end springs out. */ .nf-line { animation: nf-draw 1.2s var(--ease-in-out) 0.2s both; } .nf-fray { animation: nf-fray 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 1.25s both; }
/* Heading home: the two ends lean in towards each other. */ .nf-stage:has(.nf-action :global(a:is(:hover, :focus-visible))) { .nf-a { translate: 10px 8px; } .nf-b { translate: -10px -8px; } }
.nf-stage :global(.nf-copy) { display: flex; flex-direction: column; align-items: flex-start; gap: 48px;
@media (width >= 64rem) { padding-inline-start: 188px; } }
.nf-text { display: flex; flex-direction: column; gap: 20px; } .nf-text p { max-width: 420px; }
.nf-rise { animation: nf-rise 0.7s var(--ease-out) var(--delay, 0s) both; }
@keyframes nf-rise { from { opacity: 0; translate: 0 10px; } } @keyframes nf-draw { from { clip-path: inset(100% 0 0 0); } to { clip-path: inset(0); } } @keyframes nf-fray { from { opacity: 0; scale: 0.3; } }</style>