UI/UX polish: a11y, player controls, undo, stats ranges
- toasts: aria-live region + action button (used for history delete Undo) - history cards: keyboard-reachable title link; delete = remove + Undo (re-POSTs the entry; no full list re-render) - fail(): home/retry affordances instead of a dead-end error line - per-view document.title + move focus to the new view on render - player controls: episode jump (E#), auto-next toggle (persisted), picture-in-picture (when supported) - auto-next off: 'Next: En' toast with Play instead of the countdown - no-results search + empty home: recent-search chips (shared helper) - stats: 30d/90d/1y period chips (api already supported days=N) - search input: autocapitalize/spellcheck off; reduced-motion disables shimmer/toast animation; username in the menu on narrow screens
This commit is contained in:
1 parent
8f519b0251
commit
c2461f8fba
3 files changed
+131
-25
No files matched your search
+123
-22
@@ -32,6 +32,20 @@ function el(tag, cls, text) {
|
|||||||
function fail(err) {
|
function fail(err) {
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
main.append(el("p", "error", "Error: " + err.message));
|
main.append(el("p", "error", "Error: " + err.message));
|
||||||
|
const row = el("div", "chips");
|
||||||
|
const home = el("button", "tiny", "home");
|
||||||
|
home.onclick = () => location.href = BASE;
|
||||||
|
const retry = el("button", "tiny", "retry");
|
||||||
|
retry.onclick = () => location.reload();
|
||||||
|
row.append(home, retry);
|
||||||
|
main.append(row);
|
||||||
|
focusMain();
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusMain() {
|
||||||
|
const t = main.querySelector("h2") || main;
|
||||||
|
t.tabIndex = -1;
|
||||||
|
t.focus({preventScroll: true});
|
||||||
}
|
}
|
||||||
|
|
||||||
function skel(n = 8) {
|
function skel(n = 8) {
|
||||||
@@ -41,10 +55,16 @@ function skel(n = 8) {
|
|||||||
main.append(g);
|
main.append(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
function toast(msg) {
|
function toast(msg, action) {
|
||||||
const t = el("div", "toast", msg);
|
const t = el("div", "toast");
|
||||||
|
t.append(el("span", null, msg));
|
||||||
|
if (action) {
|
||||||
|
const b = el("button", "tiny", action.label);
|
||||||
|
b.onclick = () => { t.remove(); action.fn(); };
|
||||||
|
t.append(b);
|
||||||
|
}
|
||||||
document.getElementById("toasts").append(t);
|
document.getElementById("toasts").append(t);
|
||||||
setTimeout(() => t.remove(), 2600);
|
setTimeout(() => t.remove(), action ? 6000 : 2600);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showStreamError(msg) {
|
function showStreamError(msg) {
|
||||||
@@ -123,20 +143,29 @@ const showSec = (title, items, extra) => {
|
|||||||
main.append(head, grid);
|
main.append(head, grid);
|
||||||
};
|
};
|
||||||
const link = (text, href) => Object.assign(el("a", null, text), {href: BASE + href});
|
const link = (text, href) => Object.assign(el("a", null, text), {href: BASE + href});
|
||||||
|
// recent-search chips under a "no results" / empty placeholder (shared by home + search)
|
||||||
function showHome() {
|
function recentChips(placeholder) {
|
||||||
main.innerHTML = "";
|
const p = el("p", "muted", "loading…");
|
||||||
if (!history.length) {
|
placeholder.after(p);
|
||||||
main.append(el("p", "muted empty", "Search for an anime to get started."));
|
|
||||||
api("/api/searches").then(rs => {
|
api("/api/searches").then(rs => {
|
||||||
if (!rs.length || !main.querySelector("p.empty")) return;
|
if (!rs.length) { p.remove(); return; }
|
||||||
const d = el("div", null);
|
const d = el("div", null);
|
||||||
d.append(el("div", "muted", "recent searches"));
|
d.append(el("div", "muted", "recent searches"));
|
||||||
const chips = el("div", "chips");
|
const chips = el("div", "chips");
|
||||||
for (const q of rs) chips.append(link(q, "?q=" + encodeURIComponent(q)));
|
for (const q of rs) chips.append(link(q, "?q=" + encodeURIComponent(q)));
|
||||||
d.append(chips);
|
d.append(chips);
|
||||||
main.append(d);
|
p.replaceWith(d);
|
||||||
}).catch(() => {});
|
}).catch(() => p.remove());
|
||||||
|
}
|
||||||
|
|
||||||
|
function showHome() {
|
||||||
|
document.title = "ani-web";
|
||||||
|
main.innerHTML = "";
|
||||||
|
if (!history.length) {
|
||||||
|
const p = el("p", "muted empty", "Search for an anime to get started.");
|
||||||
|
main.append(p);
|
||||||
|
recentChips(p);
|
||||||
|
focusMain();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hero = history[0];
|
const hero = history[0];
|
||||||
@@ -179,6 +208,7 @@ function showHome() {
|
|||||||
link("history \u2192", "?history"));
|
link("history \u2192", "?history"));
|
||||||
const sl = el("div", "muted", "loading\u2026");
|
const sl = el("div", "muted", "loading\u2026");
|
||||||
main.append(sh, sl);
|
main.append(sh, sl);
|
||||||
|
focusMain();
|
||||||
api("/api/stats?days=30").then(d => {
|
api("/api/stats?days=30").then(d => {
|
||||||
sl.textContent = `${fmtMin(d.watch_minutes)} watched \u00b7 ${d.episodes.started} eps` +
|
sl.textContent = `${fmtMin(d.watch_minutes)} watched \u00b7 ${d.episodes.started} eps` +
|
||||||
` (${d.episodes.completed} finished) \u00b7 ${d.days_active} active days \u00b7` +
|
` (${d.episodes.completed} finished) \u00b7 ${d.days_active} active days \u00b7` +
|
||||||
@@ -194,10 +224,17 @@ function saveSearch(q) {
|
|||||||
|
|
||||||
async function showSearch(q) {
|
async function showSearch(q) {
|
||||||
saveSearch(q);
|
saveSearch(q);
|
||||||
|
document.title = q + " \u2014 ani-web";
|
||||||
skel(8);
|
skel(8);
|
||||||
const results = await api(`/api/search?q=${encodeURIComponent(q)}`);
|
const results = await api(`/api/search?q=${encodeURIComponent(q)}`);
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
if (!results.length) { main.append(el("p", "muted", "No results.")); return; }
|
if (!results.length) {
|
||||||
|
const p = el("p", "muted", "No results.");
|
||||||
|
main.append(p);
|
||||||
|
recentChips(p);
|
||||||
|
focusMain();
|
||||||
|
return;
|
||||||
|
}
|
||||||
main.append(el("p", "muted", `${results.length} result${results.length > 1 ? "s" : ""}`));
|
main.append(el("p", "muted", `${results.length} result${results.length > 1 ? "s" : ""}`));
|
||||||
const grid = el("div", "grid");
|
const grid = el("div", "grid");
|
||||||
for (const r of results) {
|
for (const r of results) {
|
||||||
@@ -209,6 +246,7 @@ async function showSearch(q) {
|
|||||||
grid.append(card);
|
grid.append(card);
|
||||||
}
|
}
|
||||||
main.append(grid);
|
main.append(grid);
|
||||||
|
focusMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------- recent searches dropdown ----------
|
// ---------- recent searches dropdown ----------
|
||||||
@@ -243,6 +281,7 @@ async function showAnime(slug_id, opts = {}) {
|
|||||||
lang = opts.lang || localStorage.getItem("aniweb_lang_" + slug_id) || "jpn";
|
lang = opts.lang || localStorage.getItem("aniweb_lang_" + slug_id) || "jpn";
|
||||||
seasonId = opts.season || detail.id;
|
seasonId = opts.season || detail.id;
|
||||||
detail.slug_id = slug_id;
|
detail.slug_id = slug_id;
|
||||||
|
document.title = detail.name + " \u2014 ani-web";
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
|
|
||||||
const head = el("section", "detail-head");
|
const head = el("section", "detail-head");
|
||||||
@@ -290,8 +329,36 @@ async function showAnime(slug_id, opts = {}) {
|
|||||||
fillB.classList.toggle("on", hideFiller);
|
fillB.classList.toggle("on", hideFiller);
|
||||||
renderEps();
|
renderEps();
|
||||||
};
|
};
|
||||||
controls.append(langBtn, spd, prevB, nextB, fillB,
|
const jump = el("input", "spd");
|
||||||
el("span", "muted", " — click an episode to play"));
|
jump.type = "number"; jump.min = "1"; jump.inputMode = "numeric";
|
||||||
|
jump.placeholder = "E#"; jump.title = "jump to episode";
|
||||||
|
jump.style.width = "4.5rem";
|
||||||
|
jump.onchange = () => {
|
||||||
|
const t = epsList.find(e => e.number == +jump.value);
|
||||||
|
if (!t) { jump.value = ""; return; }
|
||||||
|
const b = [...document.querySelectorAll(".ep")].find(x => +x.dataset.ep === t.id);
|
||||||
|
if (b) markActive(b);
|
||||||
|
play(t.id, t.number);
|
||||||
|
jump.value = "";
|
||||||
|
};
|
||||||
|
const anB = el("button", "tiny" + (autoNext ? " on" : ""), "auto-next");
|
||||||
|
anB.title = "auto-play the next episode when one ends";
|
||||||
|
anB.onclick = () => {
|
||||||
|
autoNext = !autoNext;
|
||||||
|
localStorage.setItem("aniweb_auto", autoNext ? "1" : "0");
|
||||||
|
anB.classList.toggle("on", autoNext);
|
||||||
|
};
|
||||||
|
const ctl = [langBtn, spd, prevB, nextB, fillB, jump, anB];
|
||||||
|
if (document.pictureInPictureEnabled) {
|
||||||
|
const pipB = el("button", "tiny", "\u29c9 PiP");
|
||||||
|
pipB.title = "picture-in-picture";
|
||||||
|
pipB.onclick = () => {
|
||||||
|
const v = document.querySelector("#player video");
|
||||||
|
if (v && v.requestPictureInPicture) v.requestPictureInPicture().catch(() => {});
|
||||||
|
};
|
||||||
|
ctl.push(pipB);
|
||||||
|
}
|
||||||
|
controls.append(...ctl, el("span", "muted", " — click an episode to play"));
|
||||||
main.append(controls);
|
main.append(controls);
|
||||||
|
|
||||||
const tabs = el("div", "tabs");
|
const tabs = el("div", "tabs");
|
||||||
@@ -342,6 +409,7 @@ async function showAnime(slug_id, opts = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (detail.synopsis) main.append(el("p", "synopsis bottom", detail.synopsis));
|
if (detail.synopsis) main.append(el("p", "synopsis bottom", detail.synopsis));
|
||||||
|
focusMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
function langLabel() { return lang === "eng" ? "▶ dub" : "▶ sub"; }
|
function langLabel() { return lang === "eng" ? "▶ dub" : "▶ sub"; }
|
||||||
@@ -350,12 +418,14 @@ function langLabel() { return lang === "eng" ? "▶ dub" : "▶ sub"; }
|
|||||||
const histClass = x => x.dur > 0 && x.pos >= x.dur * 0.95 ? "done"
|
const histClass = x => x.dur > 0 && x.pos >= x.dur * 0.95 ? "done"
|
||||||
: (x.t && Date.now() / 1000 - x.t > 7 * 86400 ? "hold" : "watching");
|
: (x.t && Date.now() / 1000 - x.t > 7 * 86400 ? "hold" : "watching");
|
||||||
async function showHistory() {
|
async function showHistory() {
|
||||||
|
document.title = "History \u2014 ani-web";
|
||||||
skel(10);
|
skel(10);
|
||||||
const items = await api("/api/history?all=1");
|
const items = await api("/api/history?all=1");
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
main.append(el("h2", null, "History"));
|
main.append(el("h2", null, "History"));
|
||||||
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); return; }
|
if (!items.length) { main.append(el("p", "muted", "Nothing here.")); focusMain(); return; }
|
||||||
main.append(...histBoard(items));
|
main.append(...histBoard(items));
|
||||||
|
focusMain();
|
||||||
}
|
}
|
||||||
|
|
||||||
function histCard(x) {
|
function histCard(x) {
|
||||||
@@ -366,8 +436,10 @@ function histCard(x) {
|
|||||||
const i = el("img"); i.src = x.poster; i.alt = "";
|
const i = el("img"); i.src = x.poster; i.alt = "";
|
||||||
c.append(i);
|
c.append(i);
|
||||||
}
|
}
|
||||||
|
const nm = el("a", "card-name", x.title);
|
||||||
|
nm.href = histHref(x);
|
||||||
const info = el("div", "mini-info");
|
const info = el("div", "mini-info");
|
||||||
info.append(el("div", "card-name", x.title),
|
info.append(nm,
|
||||||
el("div", "mini-meta", `E${x.ep} \u00b7 ${x.lang === "eng" ? "dub" : "sub"} \u00b7 ${timeago(x.t)}` +
|
el("div", "mini-meta", `E${x.ep} \u00b7 ${x.lang === "eng" ? "dub" : "sub"} \u00b7 ${timeago(x.t)}` +
|
||||||
(hold ? " \u00b7 on hold" : "")));
|
(hold ? " \u00b7 on hold" : "")));
|
||||||
const pb = bar(x, "small");
|
const pb = bar(x, "small");
|
||||||
@@ -381,8 +453,11 @@ function histCard(x) {
|
|||||||
await fetch(BASE + "api/history?slug_id=" + encodeURIComponent(x.slug_id) +
|
await fetch(BASE + "api/history?slug_id=" + encodeURIComponent(x.slug_id) +
|
||||||
"&ep_id=" + encodeURIComponent(x.ep_id),
|
"&ep_id=" + encodeURIComponent(x.ep_id),
|
||||||
{method: "DELETE", headers: {"X-Aniweb": "1"}});
|
{method: "DELETE", headers: {"X-Aniweb": "1"}});
|
||||||
toast("Removed from history");
|
c.remove();
|
||||||
await showHistory();
|
const i = history.indexOf(x);
|
||||||
|
if (i >= 0) history.splice(i, 1);
|
||||||
|
// undo = re-POST the same entry (the endpoint upserts on (user, slug, ep))
|
||||||
|
toast("Removed from history", {label: "Undo", fn: () => postHistory(x)});
|
||||||
};
|
};
|
||||||
c.append(d);
|
c.append(d);
|
||||||
return c;
|
return c;
|
||||||
@@ -549,12 +624,19 @@ function heatSection() {
|
|||||||
return sec;
|
return sec;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function showStats() {
|
async function renderStats(days) {
|
||||||
|
document.title = "Stats \u2014 ani-web";
|
||||||
skel(5);
|
skel(5);
|
||||||
const d = await api("/api/stats?days=30");
|
const d = await api(`/api/stats?days=${days}`);
|
||||||
main.innerHTML = "";
|
main.innerHTML = "";
|
||||||
main.append(el("h2", null, "Stats \u2014 last 30 days"));
|
main.append(el("h2", null, `Stats \u2014 last ${days === 365 ? "year" : days + " days"}`));
|
||||||
main.append(statsCards(d));
|
const chips = el("div", "chips");
|
||||||
|
for (const dd of [30, 90, 365]) {
|
||||||
|
const b = el("button", "chip" + (dd === days ? " on" : ""), dd === 365 ? "1y" : dd + "d");
|
||||||
|
b.onclick = () => renderStats(dd);
|
||||||
|
chips.append(b);
|
||||||
|
}
|
||||||
|
main.append(chips, statsCards(d));
|
||||||
main.append(heatSection());
|
main.append(heatSection());
|
||||||
const ta = topAnime(d);
|
const ta = topAnime(d);
|
||||||
if (ta) main.append(ta);
|
if (ta) main.append(ta);
|
||||||
@@ -563,6 +645,11 @@ async function showStats() {
|
|||||||
el("h2", null, "Top searches"),
|
el("h2", null, "Top searches"),
|
||||||
el("div", "muted", d.top_searches.join(" \u00b7 "))));
|
el("div", "muted", d.top_searches.join(" \u00b7 "))));
|
||||||
}
|
}
|
||||||
|
focusMain();
|
||||||
|
}
|
||||||
|
function showStats() {
|
||||||
|
const p = +new URLSearchParams(location.search).get("days") || 0;
|
||||||
|
return renderStats([30, 90, 365].includes(p) ? p : 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
function markActive(b) {
|
function markActive(b) {
|
||||||
@@ -571,6 +658,7 @@ function markActive(b) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let epsList = [], hideFiller = localStorage.getItem("aniweb_ff") === "1";
|
let epsList = [], hideFiller = localStorage.getItem("aniweb_ff") === "1";
|
||||||
|
let autoNext = localStorage.getItem("aniweb_auto") !== "0";
|
||||||
let watchDone = new Set(), watchNext = null, epProg = new Map();
|
let watchDone = new Set(), watchNext = null, epProg = new Map();
|
||||||
|
|
||||||
function renderEps() {
|
function renderEps() {
|
||||||
@@ -652,6 +740,15 @@ function onEnded() {
|
|||||||
const i = vis.findIndex(e => String(e.id) === String(currentEp.ep_id));
|
const i = vis.findIndex(e => String(e.id) === String(currentEp.ep_id));
|
||||||
const nxt = vis[i + 1];
|
const nxt = vis[i + 1];
|
||||||
if (!nxt) { playerHideOverlay(); toast("No more episodes"); return; }
|
if (!nxt) { playerHideOverlay(); toast("No more episodes"); return; }
|
||||||
|
if (!autoNext) {
|
||||||
|
playerHideOverlay();
|
||||||
|
toast("Next: E" + nxt.number, {label: "\u25b6 Play", fn: () => {
|
||||||
|
const b = [...document.querySelectorAll(".ep")].find(x => +x.dataset.ep === nxt.id);
|
||||||
|
if (b) markActive(b);
|
||||||
|
play(nxt.id, nxt.number);
|
||||||
|
}});
|
||||||
|
return;
|
||||||
|
}
|
||||||
nextCountdown(nxt);
|
nextCountdown(nxt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -788,6 +885,10 @@ window.addEventListener("pagehide", savePos);
|
|||||||
};
|
};
|
||||||
const mb = document.getElementById("menubox");
|
const mb = document.getElementById("menubox");
|
||||||
mb.style.display = "";
|
mb.style.display = "";
|
||||||
|
// #me is hidden on narrow screens: surface the username in the menu there
|
||||||
|
if (matchMedia("(max-width: 640px)").matches) {
|
||||||
|
document.getElementById("menu-items").prepend(el("div", "menu-user", "hi, " + me.user));
|
||||||
|
}
|
||||||
document.getElementById("menu").onclick = e => {
|
document.getElementById("menu").onclick = e => {
|
||||||
e.stopPropagation(); mb.classList.toggle("open");
|
e.stopPropagation(); mb.classList.toggle("open");
|
||||||
};
|
};
|
||||||
|
|||||||
+2
-2
@@ -12,7 +12,7 @@
|
|||||||
<header>
|
<header>
|
||||||
<a class="logo" href="/">ani-web</a>
|
<a class="logo" href="/">ani-web</a>
|
||||||
<form onsubmit="const q=document.getElementById('q').value.trim(); if(q) location='?q='+encodeURIComponent(q); return false">
|
<form onsubmit="const q=document.getElementById('q').value.trim(); if(q) location='?q='+encodeURIComponent(q); return false">
|
||||||
<input id="q" placeholder="search anime…" autocomplete="off">
|
<input id="q" placeholder="search anime…" autocomplete="off" autocapitalize="off" spellcheck="false">
|
||||||
</form>
|
</form>
|
||||||
<span id="me" class="muted"></span>
|
<span id="me" class="muted"></span>
|
||||||
<div id="menubox" style="display:none">
|
<div id="menubox" style="display:none">
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main id="main"><p class="muted">Search for an anime to get started.</p></main>
|
<main id="main"><p class="muted">Search for an anime to get started.</p></main>
|
||||||
<div id="toasts"></div>
|
<div id="toasts" aria-live="polite"></div>
|
||||||
<script src="hls.min.js"></script>
|
<script src="hls.min.js"></script>
|
||||||
<script src="app.js"></script>
|
<script src="app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+6
-1
@@ -16,6 +16,8 @@ button.tiny.danger { border-color: #7f1d1d; color: #f87171; }
|
|||||||
background: var(--card); border: 1px solid var(--bd); border-radius: 8px;
|
background: var(--card); border: 1px solid var(--bd); border-radius: 8px;
|
||||||
padding: .35rem; min-width: 160px; z-index: 2; }
|
padding: .35rem; min-width: 160px; z-index: 2; }
|
||||||
#menubox.open #menu-items { display: block; }
|
#menubox.open #menu-items { display: block; }
|
||||||
|
.menu-user { padding: .45rem .6rem .3rem; font-size: .8rem; color: var(--muted);
|
||||||
|
border-bottom: 1px solid var(--bd); margin-bottom: .25rem; }
|
||||||
#menu-items button, #menu-items a { display: block; width: 100%; text-align: left; margin-top: .15rem; }
|
#menu-items button, #menu-items a { display: block; width: 100%; text-align: left; margin-top: .15rem; }
|
||||||
header #me { margin-left: auto; }
|
header #me { margin-left: auto; }
|
||||||
#q { width: 100%; padding: 0.5rem 0.8rem; border-radius: 8px; border: 1px solid var(--bd);
|
#q { width: 100%; padding: 0.5rem 0.8rem; border-radius: 8px; border: 1px solid var(--bd);
|
||||||
@@ -93,6 +95,8 @@ h2 { margin: 0.4rem 0; }
|
|||||||
.detail-prog { margin-top: .1rem; font-size: .85rem; }
|
.detail-prog { margin-top: .1rem; font-size: .85rem; }
|
||||||
.mini-info { display: flex; flex-direction: column; padding: .5rem; }
|
.mini-info { display: flex; flex-direction: column; padding: .5rem; }
|
||||||
.mini-meta { font-size: .75rem; color: var(--muted); margin-top: .1rem; }
|
.mini-meta { font-size: .75rem; color: var(--muted); margin-top: .1rem; }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .skel, .toast { animation: none; } }
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.hero-sec { grid-template-columns: 100px 1fr; grid-template-areas: "img right"; gap: .8rem; padding: .8rem; }
|
.hero-sec { grid-template-columns: 100px 1fr; grid-template-areas: "img right"; gap: .8rem; padding: .8rem; }
|
||||||
.hero img { width: 100px; }
|
.hero img { width: 100px; }
|
||||||
@@ -142,7 +146,8 @@ button.tiny.on { border-color: var(--acc); color: var(--acc); }
|
|||||||
display: flex; flex-direction: column; gap: .4rem; z-index: 9; }
|
display: flex; flex-direction: column; gap: .4rem; z-index: 9; }
|
||||||
@keyframes toast-in { from { opacity: 0; transform: translateY(.4rem); } }
|
@keyframes toast-in { from { opacity: 0; transform: translateY(.4rem); } }
|
||||||
.toast { background: var(--card); border: 1px solid var(--acc); color: var(--fg);
|
.toast { background: var(--card); border: 1px solid var(--acc); color: var(--fg);
|
||||||
padding: .45rem .9rem; border-radius: 8px; font-size: .85rem; animation: toast-in .2s ease; }
|
padding: .45rem .9rem; border-radius: 8px; font-size: .85rem; animation: toast-in .2s ease;
|
||||||
|
display: flex; align-items: center; gap: .55rem; }
|
||||||
.crumb { display: inline-block; font-size: .85rem; color: var(--muted); margin-bottom: .2rem; }
|
.crumb { display: inline-block; font-size: .85rem; color: var(--muted); margin-bottom: .2rem; }
|
||||||
.crumb:hover { color: var(--acc); }
|
.crumb:hover { color: var(--acc); }
|
||||||
.sec-head { display: flex; align-items: baseline; gap: .8rem; }
|
.sec-head { display: flex; align-items: baseline; gap: .8rem; }
|
||||||
|
|||||||
Reference in new issue
Block a user