404 Page
This commit is contained in:
@@ -55,17 +55,3 @@ const resolved = resolveImage(image)
|
||||
</figcaption>
|
||||
)}
|
||||
</figure>
|
||||
|
||||
<style>
|
||||
.figcaption {
|
||||
@mixin py var(--size-4);
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--1);
|
||||
line-height: 1;
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
letter-spacing: var(--tracking-0);
|
||||
|
||||
background-color: var(--color-surface-base);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
---
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { trailOf } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
import Cover from './Cover.astro'
|
||||
@@ -11,22 +13,18 @@ type Props = {
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
|
||||
const crumbs = trailOf(article)
|
||||
const published = article.published
|
||||
---
|
||||
|
||||
<header>
|
||||
<Overline article={article} />
|
||||
<Overline
|
||||
crumbs={crumbs}
|
||||
stamp={published ? toMilitaryDTG(published) : 'No publication date set'}
|
||||
datetime={published?.toISOString()}
|
||||
/>
|
||||
<Title title={article.title} subtitle={article.subtitle} />
|
||||
<Cover cover={article.summary.cover} />
|
||||
<Meta article={article} />
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.body,
|
||||
.margin {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.margin {
|
||||
anchor-name: --margin-column;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -48,70 +48,3 @@ const display = date && toMilitaryDTG(date)
|
||||
</div>
|
||||
)}
|
||||
</dl>
|
||||
|
||||
<style>
|
||||
.meta {
|
||||
@mixin layout-wrapper;
|
||||
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-8);
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.section {
|
||||
@mixin py var(--size-4);
|
||||
|
||||
flex: 1 0 auto;
|
||||
min-width: 12ch;
|
||||
}
|
||||
|
||||
.item {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-0);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking--3);
|
||||
}
|
||||
|
||||
.label {
|
||||
@mixin mb var(--size-2);
|
||||
@mixin pb var(--size-2);
|
||||
@mixin border-b 2px, solid, var(--color-border-strong);
|
||||
|
||||
display: block;
|
||||
|
||||
font-family: var(--font-header);
|
||||
font-size: var(--type--2);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking--2);
|
||||
}
|
||||
|
||||
.tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-2);
|
||||
}
|
||||
|
||||
.tag a {
|
||||
@mixin py var(--size-1);
|
||||
@mixin px var(--size-3);
|
||||
|
||||
display: block;
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--1);
|
||||
color: var(--color-text-inverse);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking--3);
|
||||
|
||||
background-color: var(--color-surface-inverse);
|
||||
}
|
||||
|
||||
.tag a:hover,
|
||||
.tag a:focus-visible {
|
||||
color: var(--color-surface-inverse);
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,40 +1,43 @@
|
||||
---
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { trailOf } from '@/lib/site/navigation'
|
||||
import type { Article } from '@/lib/site/types'
|
||||
|
||||
type Props = {
|
||||
article: Article
|
||||
export type Crumb = {
|
||||
title: string
|
||||
path?: string
|
||||
}
|
||||
|
||||
const { article } = Astro.props
|
||||
type Props = {
|
||||
crumbs: Crumb[]
|
||||
stamp?: string
|
||||
datetime?: string
|
||||
}
|
||||
|
||||
const trail = trailOf(article)
|
||||
const { crumbs, stamp, datetime } = Astro.props
|
||||
---
|
||||
|
||||
<div class="overline dark">
|
||||
<div class="wrapper">
|
||||
{trail.length > 1 && (
|
||||
{crumbs.length > 1 && (
|
||||
<nav aria-label="Breadcrumb" class="breadcrumbs">
|
||||
<ol>
|
||||
{trail.map((step, index) => (
|
||||
{crumbs.map((crumb, index) => (
|
||||
<li>
|
||||
{index === trail.length - 1 ? (
|
||||
<span aria-current="page">{step.title}</span>
|
||||
{crumb.path && index < crumbs.length - 1 ? (
|
||||
<a href={crumb.path}>{crumb.title}</a>
|
||||
) : (
|
||||
<a href={step.path}>{step.title}</a>
|
||||
<span aria-current="page">{crumb.title}</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</nav>
|
||||
)}
|
||||
{article.published ? (
|
||||
<time class="publicationdate" datetime={article.published.toISOString()}>
|
||||
{toMilitaryDTG(article.published)}
|
||||
{datetime ? (
|
||||
<time class="stamp" datetime={datetime}>
|
||||
<slot name="stamp">{stamp}</slot>
|
||||
</time>
|
||||
) : (
|
||||
<span class="publicationdate">No publication date set</span>
|
||||
<span class="stamp">
|
||||
<slot name="stamp">{stamp}</slot>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,7 +60,7 @@ const trail = trailOf(article)
|
||||
display: flex;
|
||||
gap: var(--size-4);
|
||||
align-items: baseline;
|
||||
justify-content: space-between; /* breadcrumbs left, date right */
|
||||
justify-content: space-between; /* breadcrumbs left, stamp right */
|
||||
}
|
||||
|
||||
.breadcrumbs {
|
||||
@@ -81,7 +84,7 @@ const trail = trailOf(article)
|
||||
}
|
||||
}
|
||||
|
||||
.publicationdate {
|
||||
.stamp {
|
||||
font-weight: 700;
|
||||
|
||||
&::before {
|
||||
|
||||
@@ -11,28 +11,3 @@ const { title, subtitle } = Astro.props
|
||||
<h1 class="title">{title}</h1>
|
||||
{subtitle && <p class="subtitle">{subtitle}</p>}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.wrapper {
|
||||
@mixin layout-wrapper;
|
||||
@mixin py var(--space-7);
|
||||
}
|
||||
|
||||
.title {
|
||||
@mixin my var(--space-2);
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--type-10);
|
||||
line-height: var(--leading-10);
|
||||
letter-spacing: var(--tracking-10);
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
@mixin mt var(--size-4);
|
||||
font-family: var(--font-header);
|
||||
font-weight: 300;
|
||||
line-height: var(--leading-0);
|
||||
color: var(--color-text-tertiary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.0875em;
|
||||
}
|
||||
</style>
|
||||
|
||||
35
src/lib/site/directory.ts
Normal file
35
src/lib/site/directory.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { DirectoryEntry, Site } from './types'
|
||||
|
||||
const depthOf = (path: string) => path.split('/').filter(Boolean).length
|
||||
|
||||
const byPath = (a: DirectoryEntry, b: DirectoryEntry) => a.path.localeCompare(b.path)
|
||||
|
||||
export function directoryOf(site: Site): DirectoryEntry[] {
|
||||
const articles = [...site.articles.values()]
|
||||
.filter((article) => article.listed)
|
||||
.map((article) => ({
|
||||
title: article.title,
|
||||
path: article.path,
|
||||
collection: article.collection,
|
||||
parent: article.parent?.title ?? null,
|
||||
depth: depthOf(article.path),
|
||||
}))
|
||||
|
||||
const pages = [...site.pages.values()].map((page) => ({
|
||||
title: page.title,
|
||||
path: page.path,
|
||||
collection: page.collection,
|
||||
parent: null,
|
||||
depth: depthOf(page.path),
|
||||
}))
|
||||
|
||||
const tags = [...site.tags.values()].map((tag) => ({
|
||||
title: tag.title,
|
||||
path: tag.path,
|
||||
collection: tag.collection,
|
||||
parent: null,
|
||||
depth: depthOf(tag.path),
|
||||
}))
|
||||
|
||||
return [...articles, ...pages, ...tags].sort(byPath)
|
||||
}
|
||||
@@ -15,7 +15,7 @@ export const isListed = (status: Status, { drafts }: Visibility) =>
|
||||
export const segmentOf = (entry: { title: string; route: { segment: string } }) =>
|
||||
entry.route.segment || slugify(entry.title)
|
||||
|
||||
const RESERVED = [TAGS, 'keystatic']
|
||||
const RESERVED = [TAGS, 'keystatic', 'api']
|
||||
|
||||
export function segmentProblem(segment: string, atRoot: boolean): string | null {
|
||||
if (!segment) return 'The title has no URL-safe character. Set an URL segment.'
|
||||
|
||||
@@ -37,3 +37,11 @@ export type Site = {
|
||||
roots: Article[]
|
||||
diagnostics: Diagnostic[]
|
||||
}
|
||||
|
||||
export type DirectoryEntry = {
|
||||
title: string
|
||||
path: string
|
||||
collection: typeof ARTICLES | typeof PAGES | typeof TAGS
|
||||
parent: string | null
|
||||
depth: number
|
||||
}
|
||||
|
||||
@@ -1,27 +1,292 @@
|
||||
---
|
||||
import Overline from '@/components/post/Header/Overline.astro'
|
||||
import BaseLayout from '@/layouts/BaseLayout.astro'
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
import { getSite } from '@/lib/site'
|
||||
|
||||
const site = await getSite()
|
||||
const roots = site.roots
|
||||
|
||||
const stamp = toMilitaryDTG(new Date())
|
||||
|
||||
const disposition = [
|
||||
{
|
||||
title: 'Return to index',
|
||||
path: '/',
|
||||
},
|
||||
...site.roots.map((root) => ({
|
||||
title: `Browser ${root.title}`,
|
||||
path: root.path,
|
||||
})),
|
||||
]
|
||||
---
|
||||
|
||||
<BaseLayout title="Not found" noindex>
|
||||
<section class="notfound">
|
||||
<p class="code">404</p>
|
||||
<h1 class="title">Nothing here</h1>
|
||||
<p class="text">
|
||||
That page does not exist, or it moved. Try one of these, or
|
||||
<a href="/">start at the beginning</a>.
|
||||
<BaseLayout title="Not on record" noindex>
|
||||
<div class="notfound dark">
|
||||
<Overline crumbs={[{ title: 'Index', path: '/' }, { title: 'Not on record' }]}>
|
||||
<span slot="stamp" data-stamp>
|
||||
{stamp}
|
||||
</span>
|
||||
</Overline>
|
||||
|
||||
<div class="inner">
|
||||
<p class="glyph" aria-hidden="true">
|
||||
404
|
||||
</p>
|
||||
{roots.length > 0 && (
|
||||
<ul class="sections">
|
||||
{roots.map((root) => (
|
||||
<h1 class="title">Not on record</h1>
|
||||
<p class="standfirst">
|
||||
There is no entry under this address. It was never filed, or it has been struck from the
|
||||
register.
|
||||
</p>
|
||||
|
||||
<div class="record dark">
|
||||
<div class="cell">
|
||||
<p class="label">01 ⟫ Requested</p>
|
||||
<p class="value path">
|
||||
<span class="found" data-found></span>
|
||||
<span class="struck" data-struck>
|
||||
—
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<p class="label">02 ⟫ Status</p>
|
||||
<p class="status">✖ Not found</p>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<p class="label">03 ⟫ Checked</p>
|
||||
<p class="value" data-stamp>
|
||||
{stamp}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<p class="label">04 ⟫ Disposition</p>
|
||||
<ul class="links" data-disposition>
|
||||
{disposition.map((link) => (
|
||||
<li>
|
||||
<a href={root.path}>{root.title}</a>
|
||||
<a href={link.path}>{link.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
import { toMilitaryDTG } from '@/lib/content/dates'
|
||||
|
||||
type Link = { title: string; path: string }
|
||||
|
||||
const readIndex = async (): Promise<Record<string, string>> => {
|
||||
try {
|
||||
const response = await fetch('/api/site-index.json')
|
||||
if (!response.ok) return {}
|
||||
const entries: { title: string; path: string }[] = await response.json()
|
||||
return Object.fromEntries(entries.map((entry) => [entry.path, entry.title]))
|
||||
} catch {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
const ancestorsOf = (pathname: string, index: Record<string, string>): Link[] => {
|
||||
const segments = pathname.split('/').filter(Boolean)
|
||||
const found: Link[] = []
|
||||
for (let depth = segments.length; depth > 0; depth--) {
|
||||
const path = `/${segments.slice(0, depth).join('/')}`
|
||||
const title = index[path]
|
||||
if (title) found.push({ title: `Browse ${title}`, path })
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
const render = (list: Element, links: Link[]) => {
|
||||
const template = list.querySelector('li')
|
||||
if (!template) return
|
||||
list.replaceChildren(
|
||||
...links.map(({ title, path }) => {
|
||||
const item = template.cloneNode(true) as HTMLLIElement
|
||||
const anchor = item.querySelector('a')
|
||||
if (anchor) {
|
||||
anchor.href = path
|
||||
anchor.textContent = title
|
||||
}
|
||||
return item
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
const now = toMilitaryDTG(new Date())
|
||||
for (const element of document.querySelectorAll('[data-stamp]')) element.textContent = now
|
||||
|
||||
const requested = location.pathname + location.search
|
||||
const found = document.querySelector('[data-found]')
|
||||
const struck = document.querySelector('[data-struck]')
|
||||
if (struck) struck.textContent = requested
|
||||
|
||||
const ancestors = ancestorsOf(location.pathname, await readIndex())
|
||||
|
||||
const known = ancestors[0]?.path ?? ''
|
||||
if (known) {
|
||||
if (found) found.textContent = `${known}/`
|
||||
if (struck) struck.textContent = requested.slice(known.length + 1)
|
||||
}
|
||||
|
||||
const disposition = document.querySelector('[data-disposition]')
|
||||
if (disposition && ancestors.length > 0)
|
||||
render(disposition, [...ancestors, { title: 'Return to index', path: '/' }])
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.notfound {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-height: 100dvh;
|
||||
|
||||
color: var(--color-text-primary);
|
||||
|
||||
background-color: var(--color-surface-base);
|
||||
}
|
||||
|
||||
.inner {
|
||||
@mixin layout-wrapper;
|
||||
@mixin py var(--size-14);
|
||||
}
|
||||
|
||||
.glyph {
|
||||
@mixin my var(--size-0);
|
||||
|
||||
font-family: var(--font-display);
|
||||
font-size: var(--type-slab);
|
||||
font-weight: var(--font-weight-display);
|
||||
line-height: 0.9;
|
||||
color: var(--color-primary);
|
||||
text-shadow: var(--size-4) var(--size-4) 0 var(--color-text-primary);
|
||||
}
|
||||
|
||||
.title {
|
||||
@mixin mt var(--size-10);
|
||||
@mixin mb var(--size-9);
|
||||
|
||||
font-family: var(--font-header);
|
||||
font-size: var(--type-9);
|
||||
font-weight: var(--font-weight-heading);
|
||||
line-height: 1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: var(--tracking-9);
|
||||
}
|
||||
|
||||
.standfirst {
|
||||
@mixin typo_body;
|
||||
@mixin mt var(--size-0);
|
||||
@mixin mb var(--size-12);
|
||||
|
||||
max-width: 52ch;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.record {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: var(--border-card);
|
||||
|
||||
max-width: 52rem;
|
||||
border: var(--border-card) solid var(--color-border-strong);
|
||||
|
||||
background-color: var(--color-border-strong);
|
||||
|
||||
@media (--bp-mobile) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
@mixin py var(--size-8);
|
||||
@mixin px var(--size-9);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-5);
|
||||
background-color: var(--color-surface-base);
|
||||
}
|
||||
|
||||
.label {
|
||||
@mixin my var(--size-0);
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type--3);
|
||||
color: var(--color-text-quarternary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.value {
|
||||
@mixin my var(--size-0);
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-0);
|
||||
font-weight: var(--font-weight-emphasis);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.path {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.found {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.struck {
|
||||
color: var(--color-text-quiet);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.status {
|
||||
@mixin my var(--size-0);
|
||||
@mixin py var(--size-px);
|
||||
@mixin px var(--size-4);
|
||||
align-self: flex-start;
|
||||
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-0);
|
||||
color: var(--color-text-on-accent);
|
||||
text-transform: uppercase;
|
||||
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.links {
|
||||
@mixin ma var(--size-0);
|
||||
@mixin pl var(--size-0);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--size-3);
|
||||
list-style: none;
|
||||
|
||||
a {
|
||||
font-family: var(--font-mono);
|
||||
font-size: var(--type-0);
|
||||
color: var(--color-text-primary);
|
||||
text-decoration: none;
|
||||
text-transform: uppercase;
|
||||
|
||||
transition: color var(--motion-fast) var(--ease-out);
|
||||
|
||||
&::before {
|
||||
content: '⟫ ' / '';
|
||||
color: var(--color-text-quiet);
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
6
src/pages/api/site-index.json.ts
Normal file
6
src/pages/api/site-index.json.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import type { APIRoute } from 'astro'
|
||||
|
||||
import { getSite } from '@/lib/site'
|
||||
import { directoryOf } from '@/lib/site/directory'
|
||||
|
||||
export const GET: APIRoute = async () => Response.json(directoryOf(await getSite()))
|
||||
@@ -45,6 +45,7 @@
|
||||
--color-text-quiet: var(--color-palette-ash);
|
||||
--color-text-disabled: var(--color-palette-stone);
|
||||
--color-text-inverse: var(--color-palette-paper);
|
||||
--color-text-on-accent: var(--color-palette-black-ink);
|
||||
|
||||
/* == Surfaces == */
|
||||
--color-surface-base: var(--color-palette-stock);
|
||||
@@ -121,6 +122,7 @@
|
||||
--color-text-quiet: var(--color-palette-sand);
|
||||
--color-text-disabled: var(--color-palette-stone);
|
||||
--color-text-inverse: var(--color-palette-black-ink);
|
||||
--color-text-on-accent: var(--color-palette-black-ink);
|
||||
|
||||
/* == Surfaces: elevation still means lighter == */
|
||||
--color-surface-base: var(--color-palette-black-ink);
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
--type-8: 2.1875rem; /* 35px */
|
||||
--type-9: 2.5rem; /* 40px */
|
||||
--type-10: 2.75rem; /* 44px */
|
||||
--type-slab: clamp(4rem, 9vw, 7.5rem);
|
||||
|
||||
/* == Line heights == */
|
||||
--leading--3: 1.6;
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
@layer components {
|
||||
[class*="ratio--"] img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
object-position: center;
|
||||
}
|
||||
|
||||
.ratio--21-9 img {
|
||||
aspect-ratio: 21 / 9;
|
||||
}
|
||||
|
||||
.ratio--16-9 img {
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
||||
.ratio--3-2 img {
|
||||
aspect-ratio: 3 / 2;
|
||||
}
|
||||
|
||||
.ratio--4-3 img {
|
||||
aspect-ratio: 4 / 3;
|
||||
}
|
||||
|
||||
.ratio--1-1 img {
|
||||
aspect-ratio: 1 / 1;
|
||||
}
|
||||
|
||||
.ratio--4-5 img {
|
||||
aspect-ratio: 4 / 5;
|
||||
}
|
||||
|
||||
.effect--xeroxed {
|
||||
background-color: var(--color-surface-elevated-1);
|
||||
}
|
||||
|
||||
.effect--xeroxed img {
|
||||
display: block;
|
||||
filter: grayscale(1) brightness(1.1) contrast(30);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
||||
.effect--glitched {
|
||||
isolation: isolate;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.effect--glitched img {
|
||||
display: block;
|
||||
filter: grayscale(1) contrast(1.1);
|
||||
}
|
||||
|
||||
.effect--glitched::before,
|
||||
.effect--glitched::after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
opacity: 0;
|
||||
background: var(--figure-src) center / cover no-repeat;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
.effect--glitched:hover::before {
|
||||
opacity: 0.8;
|
||||
background-color: oklch(63% 0.2 25deg);
|
||||
background-blend-mode: multiply;
|
||||
animation: glitch-x 320ms steps(2, end) infinite;
|
||||
}
|
||||
|
||||
.effect--glitched:hover::after {
|
||||
opacity: 0.8;
|
||||
background-color: oklch(70% 0.15 200deg);
|
||||
background-blend-mode: multiply;
|
||||
animation: glitch-x 260ms steps(3, end) infinite reverse;
|
||||
}
|
||||
|
||||
.effect--glitched:hover img {
|
||||
filter: grayscale(0.15) contrast(1.3);
|
||||
}
|
||||
|
||||
@keyframes glitch-x {
|
||||
0% {
|
||||
transform: translate(0, 0);
|
||||
clip-path: inset(0 0 72% 0);
|
||||
}
|
||||
|
||||
25% {
|
||||
transform: translate(-3px, 1px);
|
||||
clip-path: inset(38% 0 34% 0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translate(3px, -1px);
|
||||
clip-path: inset(66% 0 8% 0);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translate(-2px, 0);
|
||||
clip-path: inset(12% 0 60% 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate(0, 0);
|
||||
clip-path: inset(0 0 72% 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.effect--glitched:hover::before,
|
||||
.effect--glitched:hover::after {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
@layer components {
|
||||
.prose {
|
||||
@mixin typo_body;
|
||||
@mixin py var(--space-8);
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
@import url("base/foundation.css");
|
||||
@import url("base/colors.css");
|
||||
@import url("base/primitives.css");
|
||||
@import url("components/figure.css");
|
||||
@import url("components/prose.css");
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
|
||||
Reference in New Issue
Block a user