Identity
diff --git a/frontend/src/lib/confirm.ts b/frontend/src/lib/confirm.ts
index a7f81aa..cb4dfb5 100644
--- a/frontend/src/lib/confirm.ts
+++ b/frontend/src/lib/confirm.ts
@@ -117,15 +117,22 @@ export function confirmDialog(opts: ConfirmOptions): Promise {
});
}
-/** Transient bottom-center toast. Replaces window.alert for failures. */
+/** Transient hovering toast at the top of the viewport. Replaces
+ * window.alert and the old inline save banners. */
export function notify(message: string, tone: 'error' | 'success' = 'error') {
document.querySelector('.toast[data-notify]')?.remove();
const el = document.createElement('div');
- el.className = `toast${tone === 'error' ? ' toast--error' : ''}`;
+ el.className = `toast toast--${tone}`;
el.dataset.notify = '';
el.setAttribute('role', tone === 'error' ? 'alert' : 'status');
el.textContent = message;
- el.addEventListener('click', () => el.remove());
+
+ const dismiss = () => {
+ if (el.classList.contains('toast--out')) return;
+ el.classList.add('toast--out');
+ window.setTimeout(() => el.remove(), 220);
+ };
+ el.addEventListener('click', dismiss);
document.body.appendChild(el);
- window.setTimeout(() => el.remove(), 4500);
+ window.setTimeout(dismiss, 4500);
}
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 16f38d3..e4b4c0e 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -1360,7 +1360,7 @@ select.topbar-control.theme-select {
/* Toast */
.toast {
position: fixed;
- bottom: 1.5rem;
+ top: 1.25rem;
left: 50%;
transform: translateX(-50%);
background: var(--mantle);
@@ -1368,17 +1368,34 @@ select.topbar-control.theme-select {
color: var(--rosewater);
padding: 0.65rem 1.1rem;
border-radius: 1px;
- box-shadow: 0 12px 30px -10px rgba(0, 0, 0, 0.45);
+ box-shadow: 0 14px 34px -12px rgba(0, 0, 0, 0.55);
font-family: var(--font-display);
font-style: italic;
font-size: 0.9rem;
z-index: 200;
- animation: toast-in 0.2s ease;
+ cursor: pointer;
+ animation: toast-in 0.22s cubic-bezier(0.2, 0.7, 0.2, 1);
}
@keyframes toast-in {
- from { opacity: 0; transform: translate(-50%, 8px); }
+ from { opacity: 0; transform: translate(-50%, -10px); }
to { opacity: 1; transform: translate(-50%, 0); }
}
+.toast--out {
+ animation: toast-out 0.2s ease forwards;
+}
+@keyframes toast-out {
+ from { opacity: 1; transform: translate(-50%, 0); }
+ to { opacity: 0; transform: translate(-50%, -10px); }
+}
+/* Success variant — parallels .toast--error. */
+.toast--success {
+ border-left: 3px solid var(--green);
+ color: var(--rosewater);
+}
+.toast--success::before {
+ content: "✓ ";
+ color: var(--green);
+}
/* Salon grid spans driven by --col-span custom prop (avoids Tailwind dynamic class issue). */
@media (min-width: 768px) {
@@ -2023,7 +2040,7 @@ html.cybersigil body::after {
0 0 10px var(--sky),
0 -3px 0 color-mix(in srgb, var(--mauve) 70%, transparent),
0 3px 0 color-mix(in srgb, var(--teal) 60%, transparent);
- animation: cs-tear 8.5s steps(1, jump-none) infinite;
+ animation: cs-tear 8.5s linear infinite;
}
.cybersigil .cs-fx-tear::after {
content: "";
@@ -2046,7 +2063,7 @@ html.cybersigil body::after {
-webkit-mask: var(--cs-corner) center / contain no-repeat;
mask: var(--cs-corner) center / contain no-repeat;
filter: drop-shadow(0 0 5px color-mix(in srgb, var(--sky) 45%, transparent));
- animation: cs-flicker 7s steps(1, jump-none) infinite;
+ animation: cs-flicker 7s linear infinite;
}
.cybersigil .cs-fx-corner--tl { top: 0; left: 0; }
.cybersigil .cs-fx-corner--tr { top: 0; right: 0; transform: scaleX(-1); animation-delay: -1.7s; }
@@ -2118,7 +2135,8 @@ html.cybersigil body::after {
/* Nameplate = the system handle. `> ` prompt + live block caret. */
.cybersigil .nameplate-title::before {
- content: "> ";
+ content: ">";
+ margin-right: 0.4em;
color: var(--sky);
-webkit-text-fill-color: var(--sky);
}
@@ -2131,7 +2149,7 @@ html.cybersigil body::after {
vertical-align: -0.12em;
background: var(--mauve);
box-shadow: 0 0 8px color-mix(in srgb, var(--mauve) 70%, transparent);
- animation: cs-blink 1.05s steps(1, jump-none) infinite;
+ animation: cs-blink 1.05s steps(2, jump-none) infinite;
}
.cybersigil .nameplate-subtitle {
font-family: var(--font-sans);
@@ -2535,7 +2553,7 @@ html.cybersigil body::after {
.cybersigil .back-link::after {
content: "_";
margin-left: -0.1em;
- animation: cs-blink 1.05s steps(1, jump-none) infinite;
+ animation: cs-blink 1.05s steps(2, jump-none) infinite;
}
.cybersigil .back-link:hover,
.cybersigil .back-link:focus-visible {
@@ -2569,7 +2587,7 @@ html.cybersigil body::after {
content: "_";
margin-left: 0.18em;
opacity: 0.85;
- animation: cs-blink 1.05s steps(1, jump-none) infinite;
+ animation: cs-blink 1.05s steps(2, jump-none) infinite;
}
/* Icon-only / collapsed controls have no room for the `>` prompt + blink
* caret — they overflow the 2rem square on phones. Drop the pseudo when
@@ -2845,7 +2863,7 @@ html.cybersigil body::after {
content: "_";
color: var(--mauve);
font-family: var(--font-sans);
- animation: cs-blink 1.05s steps(1, jump-none) infinite;
+ animation: cs-blink 1.05s steps(2, jump-none) infinite;
}
.cybersigil .search-result [class*="line-clamp"] {
font-family: var(--font-sans) !important;
@@ -2916,7 +2934,7 @@ html.cybersigil body::after {
.cybersigil .asset-drop-title::after {
content: "_";
color: var(--mauve);
- animation: cs-blink 1.05s steps(1, jump-none) infinite;
+ animation: cs-blink 1.05s steps(2, jump-none) infinite;
}
.cybersigil .asset-empty {
@@ -3006,6 +3024,41 @@ html.cybersigil body::after {
text-shadow: -1px 0 0 var(--sky), 1px 0 0 var(--mauve);
}
+/* Toast — a terminal status line printed at the top of the tube. */
+.cybersigil .toast {
+ background: color-mix(in srgb, var(--crust) 92%, transparent);
+ border: 1px solid var(--sky);
+ border-radius: 0;
+ color: var(--sky);
+ font-family: var(--font-sans);
+ font-style: normal;
+ font-size: 0.74rem;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ box-shadow:
+ 3px 3px 0 0 var(--mauve),
+ 0 0 22px -6px color-mix(in srgb, var(--sky) 45%, transparent);
+}
+.cybersigil .toast--success {
+ border-color: var(--sky);
+ color: var(--sky);
+}
+.cybersigil .toast--success::before {
+ content: "> OK\00a0\00a0";
+ color: var(--sky);
+}
+.cybersigil .toast--error {
+ border-color: var(--red);
+ color: var(--red);
+ box-shadow:
+ 3px 3px 0 0 var(--mauve),
+ 0 0 22px -6px color-mix(in srgb, var(--red) 50%, transparent);
+}
+.cybersigil .toast--error::before {
+ content: "> ERR\00a0\00a0";
+ color: var(--red);
+}
+
/* ─── Theme keyframes ─── */
@keyframes cs-blink {
0%, 49% { opacity: 1; }