Migrate some scripts to Alpine

This commit is contained in:
Bad Manners 2024-09-25 12:36:10 -03:00
parent aa5759d6f5
commit 85c11bc02a
10 changed files with 408 additions and 428 deletions

View file

@ -3,99 +3,56 @@ import AgeRestrictedScriptInline from "./AgeRestrictedScriptInline.astro";
import { IconTriangleExclamation } from "./icons";
---
<div
id="modal-age-restricted"
class="fixed inset-0 bg-stone-50 dark:bg-zinc-900"
role="dialog"
aria-labelledby="title-age-restricted"
hidden
>
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
<div class="text-bm-500 dark:text-bm-400">
<IconTriangleExclamation width="3rem" height="3rem" />
</div>
<div
id="title-age-restricted"
class="pb-3 pt-2 text-3xl font-normal text-stone-700 sm:pb-4 sm:pt-2 dark:text-zinc-50"
>
Age verification
</div>
<div
class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-xl font-medium text-stone-700 dark:border-zinc-300 dark:text-zinc-50"
>
You must be 18+ to access this page.
</div>
<p class="px-8 text-lg font-normal leading-snug text-stone-700 sm:max-w-2xl dark:text-zinc-50">
By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this
screen from appearing in the future.
</p>
<div
id="age-verification-button-list"
class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 font-medium sm:max-w-2xl sm:flex-row"
hidden
>
<button
data-modal-reject
id="age-verification-reject"
class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-zinc-300 dark:text-zinc-900 dark:hover:bg-zinc-600 dark:hover:text-zinc-50 dark:focus:bg-zinc-600 dark:focus:text-zinc-50"
<template x-if="!ageVerified">
<div
id="modal-age-restricted"
class="fixed inset-0 z-10 bg-stone-50 dark:bg-zinc-900"
role="dialog"
aria-labelledby="title-age-restricted"
>
<div class="mx-auto flex min-h-screen max-w-3xl flex-col items-center justify-center text-center tracking-tight">
<div class="text-bm-500 dark:text-bm-400">
<IconTriangleExclamation width="3rem" height="3rem" />
</div>
<div
id="title-age-restricted"
class="pb-3 pt-2 text-3xl font-normal text-stone-700 sm:pb-4 sm:pt-2 dark:text-zinc-50"
>
Cancel
</button>
<button
data-modal-accept
id="age-verification-accept"
class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-zinc-900 dark:hover:bg-zinc-600 dark:hover:text-zinc-50 dark:focus:bg-zinc-600 dark:focus:text-zinc-50"
Age verification
</div>
<div
class="mx-6 mb-4 max-w-xl border-b border-stone-300 pb-4 text-xl font-medium text-stone-700 dark:border-zinc-300 dark:text-zinc-50"
>
I'm at least 18 years old
</button>
You must be 18+ to access this page.
</div>
<p class="px-8 text-lg font-normal leading-snug text-stone-700 sm:max-w-2xl dark:text-zinc-50">
By confirming that you are at least 18 years old, your selection will be saved to your browser to prevent this
screen from appearing in the future.
</p>
<div
id="age-verification-button-list"
class="flex w-full max-w-md flex-col-reverse justify-evenly gap-y-5 px-6 pt-5 font-medium sm:max-w-2xl sm:flex-row"
hidden
>
<button
data-modal-reject
id="age-verification-reject"
class="rounded bg-stone-400 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-zinc-300 dark:text-zinc-900 dark:hover:bg-zinc-600 dark:hover:text-zinc-50 dark:focus:bg-zinc-600 dark:focus:text-zinc-50"
@click="location.href = 'about:blank'"
>
Cancel
</button>
<button
data-modal-accept
id="age-verification-accept"
class="rounded bg-bm-500 py-3 text-lg text-stone-900 hover:bg-stone-500 hover:text-stone-50 focus:bg-stone-500 focus:text-stone-50 sm:px-9 dark:bg-bm-400 dark:text-zinc-900 dark:hover:bg-zinc-600 dark:hover:text-zinc-50 dark:focus:bg-zinc-600 dark:focus:text-zinc-50"
@click="localStorage.ageVerified = 'true'; ageVerified = true"
>
I'm at least 18 years old
</button>
</div>
</div>
</div>
</div>
</template>
<AgeRestrictedScriptInline />
<script>
const ageRestrictedModalSetup = () => {
const modal = document.querySelector<HTMLElementTagNameMap["div"]>("div#modal-age-restricted");
// Not an age-restricted page
if (!modal) {
return;
}
if (modal !== document.querySelector("body>div#modal-age-restricted")) {
throw new Error("#modal-age-restricted must be a direct child of the body element!");
}
const addAgeVerifiedQueryToLinks = () =>
document.body.querySelectorAll<HTMLElementTagNameMap["a"]>("a[href][data-age-restricted]").forEach((el) => {
let newHref = new URL(el.href);
newHref.searchParams.set("ageVerified", "true");
el.href = newHref.toString();
});
if (localStorage.ageVerified === "true") {
addAgeVerifiedQueryToLinks();
} else {
const rejectButton = modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-reject]")!;
const onRejectButtonClick = (e: MouseEvent) => {
e.preventDefault();
location.href = "about:blank";
};
rejectButton.addEventListener("click", onRejectButtonClick);
modal.querySelector<HTMLElementTagNameMap["button"]>("button[data-modal-accept]")!.addEventListener(
"click",
(e: MouseEvent) => {
e.preventDefault();
rejectButton.removeEventListener("click", onRejectButtonClick);
localStorage.ageVerified = "true";
document.body.style.overflow = "auto";
document.querySelectorAll("body>:not(#modal-age-restricted)").forEach((el) => el.removeAttribute("inert"));
modal.hidden = true;
addAgeVerifiedQueryToLinks();
},
{ once: true },
);
modal.querySelector<HTMLElementTagNameMap["div"]>("div#age-verification-button-list")!.hidden = false;
rejectButton.focus();
}
};
ageRestrictedModalSetup();
</script>

View file

@ -1,4 +1,4 @@
---
---
<script is:inline>(a=>{let b="body>",c="#modal-age-restricted",d="true",e="ageVerified",f="searchParams",g=localStorage,h=new URL(location),i=x=>a.querySelectorAll(x),j=i(b+c)[0];h[f].get(e)==d&&(g[e]=d,h[f].delete(e),history.replaceState({},"",h));j&&g[e]!=d&&((a.body.style.overflow="hidden"),i(b+":not("+c+")").forEach(x=>x.setAttribute("inert",d)),(j.hidden=!1))})(document)</script>
<script is:inline>(()=>{let a="true",b="ageVerified",c="searchParams",d=localStorage,e=new URL(location);e[c].get(b)==a&&(d[b]=a,e[c].delete(b),history.replaceState({},"",e))})()</script>

View file

@ -1,33 +0,0 @@
---
import DarkModeScriptInline from "./DarkModeScriptInline.astro";
---
<DarkModeScriptInline />
<script>
type ColorScheme = "auto" | "dark" | "light" | undefined;
const colorSchemeSetup = () => {
let colorScheme: ColorScheme = localStorage.colorScheme;
if (!colorScheme || colorScheme === "auto") {
colorScheme = matchMedia("(prefers-color-scheme:dark)").matches ? "dark" : "light";
}
const toggleColorScheme = (e: MouseEvent) => {
e.preventDefault();
if (colorScheme === "dark") {
colorScheme = "light";
document.body.classList.remove("dark");
} else {
colorScheme = "dark";
document.body.classList.add("dark");
}
localStorage.colorScheme = colorScheme;
};
document.querySelectorAll<HTMLElementTagNameMap["button"]>("button[data-dark-mode]").forEach((button) => {
button.addEventListener("click", toggleColorScheme);
button.hidden = false;
});
};
colorSchemeSetup();
</script>

View file

@ -1,4 +1,4 @@
---
---
<script is:inline>let g=document,f=a=>{var b="dark",c="colorScheme",d=localStorage,e=d[c];(e=="auto"||!e?matchMedia("(prefers-color-scheme:dark)").matches:e==b)&&a.body.classList.add(b)};g.addEventListener('astro:before-swap',e=>f(e.newDocument));f(g)</script>
<script is:inline>let g=document,f=a=>{var b="dark",c=localStorage,d=c.colorScheme;(d!="light"&&(d==b||matchMedia("(prefers-color-scheme:dark)").matches))&&a.body.classList.add(b)};g.addEventListener('astro:before-swap',e=>f(e.newDocument));f(g)</script>

View file

@ -1,7 +1,7 @@
---
import { ViewTransitions } from "astro:transitions";
import LoadingIndicator from "astro-loading-indicator/component";
import DarkModeScript from "@components/DarkModeScript.astro";
import DarkModeScriptInline from "@components/DarkModeScriptInline.astro";
import NavHeader from "@components/NavHeader.astro";
import { IconSun, IconMoon } from "@components/icons";
import AgeRestrictedModal from "@components/AgeRestrictedModal.astro";
@ -36,9 +36,24 @@ const title = pageTitle ? `${pageTitle} | Bad Manners` : "Bad Manners";
<slot name="head" />
<ViewTransitions />
<LoadingIndicator color="#3b82f6" height="0.25rem" threshold={false} />
<script>
import Alpine from "alpinejs";
document.addEventListener("astro:after-preparation", () => {
Alpine.stopObservingMutations();
});
document.addEventListener("astro:page-load", () => {
document.dispatchEvent(new Event("alpine:init"));
Alpine.initTree(document.documentElement);
Alpine.startObservingMutations();
});
</script>
</head>
<body>
<div class="flex min-h-screen flex-col">
<body
:class="ageVerified ? 'overflow-auto' : 'overflow-hidden'"
x-effect="darkMode ? $el.classList.add('dark') : $el.classList.remove('dark')"
x-data="{ darkMode: localStorage.colorScheme != 'light' && (localStorage.colorScheme == 'dark' || matchMedia('(prefers-color-scheme:dark)').matches), ageVerified: new URL(location).searchParams.get('ageVerified') == 'true' || localStorage.ageVerified == 'true' }"
>
<div class="flex min-h-screen flex-col" x-bind:inert="!ageVerified">
<div
id="bg"
class="fixed h-screen w-screen bg-radial from-bm-300 to-bm-500 dark:from-green-800 dark:to-green-950 print:hidden"
@ -59,25 +74,26 @@ const title = pageTitle ? `${pageTitle} | Bad Manners` : "Bad Manners";
transition:persist
>
<div class="flex items-center">
<span id="copyright"
>&copy; <time datetime="2023">2023</time>&ndash;<time datetime={new Date().getFullYear().toString()}
>{new Date().getFullYear()}</time
<span id="copyright" class="mr-2"
>&copy; <time datetime="2023">2023</time>&ndash;<time
x-data="{ currentYear: new Date().getFullYear() }"
x-text="currentYear"
x-bind:datetime="currentYear"
datetime={new Date().getFullYear().toString()}>{new Date().getFullYear()}</time
> Bad Manners</span
>
<span class="print:hidden" aria-hidden="true">&nbsp;|&nbsp;</span>
<a
href="/licenses.toml"
rel="license"
class="transition-colors hover:text-white hover:underline focus:text-white focus:underline motion-reduce:transition-none dark:hover:text-bm-300 dark:focus:text-bm-300 print:hidden"
class="border-l border-l-black pl-2 transition-colors hover:text-white hover:underline focus:text-white focus:underline motion-reduce:transition-none dark:border-l-white dark:hover:text-bm-300 dark:focus:text-bm-300 print:hidden"
>
Licenses
</a>
</div>
<button
data-dark-mode
x-on:click="darkMode = !darkMode; localStorage.colorScheme = darkMode ? 'dark' : 'light'"
class="mt-2 p-2 transition-colors hover:text-green-700 focus:text-green-700 motion-reduce:transition-none dark:hover:text-bm-300 dark:focus:text-bm-300 print:hidden"
aria-labelledby="label-toggle-dark-mode"
hidden
>
<IconSun width="1.5rem" height="1.5rem" class="hidden dark:block" />
<IconMoon width="1.5rem" height="1.5rem" class="block dark:hidden" />
@ -86,7 +102,7 @@ const title = pageTitle ? `${pageTitle} | Bad Manners` : "Bad Manners";
</footer>
</div>
</div>
<DarkModeScript transition:persist />
<DarkModeScriptInline transition:persist />
<AgeRestrictedModal transition:persist />
</body>
</html>

View file

@ -80,7 +80,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
>
{
Astro.site ? (
<li>
<li data-link>
<a
id="permalink"
class="u-url contact-link group"
@ -97,7 +97,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
</li>
) : null
}
<li>
<li data-link>
<a
id="gallery"
class="u-url contact-link group"
@ -112,7 +112,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">Gallery on https://gallery.badmanners.xyz</p>
</a>
</li>
<li>
<li data-link>
<a
id="gallery-feed"
class="u-url contact-link group"
@ -127,7 +127,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">Gallery feed</p>
</a>
</li>
<li>
<li data-link>
<a
id="pronouns"
class="u-url contact-link group"
@ -143,7 +143,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-pronoun hidden">they/them/their/theirs/themself</p>
</a>
</li>
<li>
<li data-link>
<a
id="e-mail"
class="u-email contact-link group"
@ -159,7 +159,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">me@badmanners.xyz</p>
</a>
</li>
<li>
<li data-link>
<a
id="bluesky"
class="u-url contact-link group"
@ -173,7 +173,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">@badmanners.xyz on Bluesky</p>
</a>
</li>
<li>
<li data-link>
<a
id="codeberg"
class="u-url contact-link group"
@ -187,7 +187,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Codeberg</p>
</a>
</li>
<li>
<li data-link>
<button
id="discord"
class="text-link group block w-full py-2 transition-colors motion-reduce:transition-none"
@ -202,7 +202,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners on Discord</p>
</button>
</li>
<li>
<li data-link>
<a
id="eka-s-portal"
class="u-url contact-link group"
@ -216,7 +216,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Eka's Portal</p>
</a>
</li>
<li>
<li data-link>
<a
id="fur-affinity"
class="u-url contact-link group"
@ -230,7 +230,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Fur Affinity</p>
</a>
</li>
<li>
<li data-link>
<a
id="gitgud"
class="u-url contact-link group"
@ -244,7 +244,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadMannersXYZ on GitGud</p>
</a>
</li>
<li>
<li data-link>
<a
id="github"
class="u-url contact-link group"
@ -258,7 +258,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadMannersXYZ on GitHub</p>
</a>
</li>
<li>
<li data-link>
<a
id="gitlab"
class="u-url contact-link group"
@ -272,7 +272,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">Bad_Manners on GitLab</p>
</a>
</li>
<li>
<li data-link>
<a
id="google"
class="u-email contact-link group"
@ -288,7 +288,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">google@badmanners.xyz</p>
</a>
</li>
<li>
<li data-link>
<a
id="gpg"
class="u-key contact-link group"
@ -304,7 +304,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">GPG public key</p>
</a>
</li>
<li>
<li data-link>
<a
id="inkbunny"
class="u-url contact-link group"
@ -318,7 +318,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Inkbunny</p>
</a>
</li>
<li>
<li data-link>
<a
id="itaku"
class="u-url contact-link group"
@ -332,7 +332,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners on Itaku</p>
</a>
</li>
<li>
<li data-link>
<a
id="itch"
class="u-url contact-link group"
@ -346,7 +346,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">Bad Manners on Itch.io</p>
</a>
</li>
<li>
<li data-link>
<a
id="keybase"
class="u-url contact-link group"
@ -360,7 +360,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners on Keybase</p>
</a>
</li>
<li>
<li data-link>
<a
id="keyoxide"
class="u-url contact-link group"
@ -374,7 +374,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-uid sr-only select-none">aspe:keyoxide.org:UWYBVFCBFXTVUF2U6FS6AYJHLU</p>
</a>
</li>
<li>
<li data-link>
<a
id="ko-fi"
class="u-url contact-link group"
@ -388,7 +388,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners on Ko-fi</p>
</a>
</li>
<li>
<li data-link>
<a
id="mastodon"
class="u-url contact-link group"
@ -402,7 +402,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">@BadManners@meow.social on Mastodon</p>
</a>
</li>
<li>
<li data-link>
<a
id="neocities"
class="u-url contact-link group"
@ -416,7 +416,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners.neocities.org on Neocities</p>
</a>
</li>
<li>
<li data-link>
<a
id="picarto"
class="u-url contact-link group"
@ -430,7 +430,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Picarto</p>
</a>
</li>
<li>
<li data-link>
<a
id="signal"
class="u-url contact-link group"
@ -444,7 +444,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners.10 on Signal</p>
</a>
</li>
<li>
<li data-link>
<a
id="sofurry"
class="u-url contact-link group"
@ -458,7 +458,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">Bad Manners on SoFurry</p>
</a>
</li>
<li>
<li data-link>
<a
id="ssh"
class="u-key contact-link group"
@ -474,7 +474,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="sr-only select-none">SSH public key</p>
</a>
</li>
<li>
<li data-link>
<a
id="steam"
class="u-url contact-link group"
@ -488,7 +488,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">badmanners_ on Steam</p>
</a>
</li>
<li>
<li data-link>
<a
id="subscribestar"
class="u-url contact-link group"
@ -502,7 +502,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">Bad Manners on SubscribeStar</p>
</a>
</li>
<li>
<li data-link>
<a
id="telegram"
class="u-url contact-link group"
@ -516,7 +516,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">@bad_manners on Telegram</p>
</a>
</li>
<li>
<li data-link>
<a
id="twitch"
class="u-url contact-link group"
@ -530,7 +530,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">bad__manners on Twitch</p>
</a>
</li>
<li>
<li data-link>
<a
id="weasyl"
class="u-url contact-link group"
@ -544,7 +544,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
<p class="p-nickname sr-only select-none">BadManners on Weasyl</p>
</a>
</li>
<li>
<li data-link>
<a
id="youtube"
class="u-url contact-link group"
@ -580,14 +580,6 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
return;
}
// Validate links
indexLinks.querySelectorAll("li > :not(a, button)").forEach((el) => {
console.warn("Element with unknown type found in #links list:", el);
});
indexLinks.querySelectorAll("li > :is(a, button):not([aria-label])").forEach((el) => {
console.warn("Element with missing aria-label found in #links list:", el);
});
// Instantiate hover tooltips
const tooltipItems = document.querySelectorAll<HTMLElement>("[title][data-tooltip]");
tooltipItems.forEach((el) => el.setAttribute("data-tooltip", el.title));
@ -599,9 +591,7 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
// Add functionality to custom clipboard items
if ("clipboard" in navigator) {
const customClipboardItems = indexLinks.querySelectorAll<HTMLElementTagNameMap["a" | "button"]>(
"li > :is(a, button)[data-clipboard]",
);
const customClipboardItems = document.querySelectorAll<HTMLElement>("ul#links li[data-link] [data-clipboard]");
if (!customClipboardItems.length) {
console.warn("Missing custom clipboard elements in #links list.");
}
@ -633,8 +623,8 @@ const sshKey = await readFile("./public/ssh.pub", { encoding: "utf-8" });
element.addEventListener("click", onClickElement);
});
} else {
const customClipboardButtons = indexLinks.querySelectorAll<HTMLElementTagNameMap["button"]>(
"li > button[data-clipboard][disabled]",
const customClipboardButtons = document.querySelectorAll<HTMLElementTagNameMap["button"]>(
"ul#links li[data-link] button[data-clipboard]",
);
customClipboardButtons.forEach((element) => {
element.removeAttribute("disabled");

View file

@ -19,6 +19,11 @@
@apply border-r-stone-800 dark:border-r-zinc-900;
}
/* Alpine.js */
[x-cloak] {
display: none !important;
}
@layer components {
.text-link {
@apply text-stone-800 hover:text-bm-500 focus:text-bm-500 dark:text-zinc-300 dark:hover:text-bm-400 dark:focus:text-bm-400;