updated breakcore theme
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { deletePost } from '../../lib/api';
|
import { deletePost } from '../../lib/api';
|
||||||
|
import { confirmDialog, notify } from '../../lib/confirm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
slug: string;
|
slug: string;
|
||||||
@@ -12,13 +13,19 @@ export default function DeletePostButton({ slug, title, variant = 'full' }: Prop
|
|||||||
|
|
||||||
async function handleClick() {
|
async function handleClick() {
|
||||||
if (busy) return;
|
if (busy) return;
|
||||||
if (!window.confirm(`Delete "${title}"? This cannot be undone.`)) return;
|
const ok = await confirmDialog({
|
||||||
|
title: 'Delete this work?',
|
||||||
|
message: `“${title}” will be permanently removed. This cannot be undone.`,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
cancelLabel: 'Cancel',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await deletePost(slug);
|
await deletePost(slug);
|
||||||
window.location.href = '/';
|
window.location.href = '/';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.alert(`Failed to delete: ${e instanceof Error ? e.message : 'unknown error'}`);
|
notify(`Failed to delete: ${e instanceof Error ? e.message : 'unknown error'}`);
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
import { deletePost } from '../../lib/api';
|
import { deletePost } from '../../lib/api';
|
||||||
|
import { confirmDialog, notify } from '../../lib/confirm';
|
||||||
|
|
||||||
const PAGE_SIZE = 9;
|
const PAGE_SIZE = 9;
|
||||||
|
|
||||||
@@ -66,13 +67,19 @@ export default function PostList({ posts: initialPosts, isAdmin = false }: Props
|
|||||||
|
|
||||||
async function handleDelete(slug: string, title: string) {
|
async function handleDelete(slug: string, title: string) {
|
||||||
if (deleting) return;
|
if (deleting) return;
|
||||||
if (!window.confirm(`Take "${title}" off the wall? This cannot be undone.`)) return;
|
const ok = await confirmDialog({
|
||||||
|
title: 'Take this off the wall?',
|
||||||
|
message: `“${title}” will be removed from the catalogue. This cannot be undone.`,
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
cancelLabel: 'Keep',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
setDeleting(slug);
|
setDeleting(slug);
|
||||||
try {
|
try {
|
||||||
await deletePost(slug);
|
await deletePost(slug);
|
||||||
setPosts(p => p.filter(x => x.slug !== slug));
|
setPosts(p => p.filter(x => x.slug !== slug));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
window.alert(`Failed to remove: ${e instanceof Error ? e.message : 'unknown error'}`);
|
notify(`Failed to remove: ${e instanceof Error ? e.message : 'unknown error'}`);
|
||||||
} finally {
|
} finally {
|
||||||
setDeleting(null);
|
setDeleting(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { getAssets, uploadAsset, deleteAsset as deleteAssetApi, ApiError } from '../../../lib/api';
|
import { getAssets, uploadAsset, deleteAsset as deleteAssetApi, ApiError } from '../../../lib/api';
|
||||||
import type { Asset } from '../../../lib/types';
|
import type { Asset } from '../../../lib/types';
|
||||||
|
import { confirmDialog } from '../../../lib/confirm';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
mode?: 'manage' | 'select';
|
mode?: 'manage' | 'select';
|
||||||
@@ -42,7 +43,12 @@ export default function AssetManager({ mode = 'manage', onSelect }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleDelete(name: string) {
|
async function handleDelete(name: string) {
|
||||||
if (!confirm(`Delete "${name}" permanently?`)) return;
|
const ok = await confirmDialog({
|
||||||
|
title: 'Delete asset?',
|
||||||
|
message: `“${name}” will be permanently deleted. Posts referencing it will break.`,
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await deleteAssetApi(name);
|
await deleteAssetApi(name);
|
||||||
showAlert('File deleted.', 'success');
|
showAlert('File deleted.', 'success');
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { search, searchKeymap } from '@codemirror/search';
|
|||||||
import { closeBrackets } from '@codemirror/autocomplete';
|
import { closeBrackets } from '@codemirror/autocomplete';
|
||||||
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
import { getPost, savePost, deletePost, getAssets, uploadAsset, ApiError } from '../../../lib/api';
|
||||||
import type { Asset } from '../../../lib/types';
|
import type { Asset } from '../../../lib/types';
|
||||||
|
import { confirmDialog } from '../../../lib/confirm';
|
||||||
|
|
||||||
const AssetManager = lazy(() => import('./AssetManager'));
|
const AssetManager = lazy(() => import('./AssetManager'));
|
||||||
|
|
||||||
@@ -386,7 +387,12 @@ export default function Editor({ editSlug }: Props) {
|
|||||||
|
|
||||||
async function handleDelete() {
|
async function handleDelete() {
|
||||||
const target = originalSlug || slug;
|
const target = originalSlug || slug;
|
||||||
if (!confirm(`Remove work "${target}" from the catalogue permanently?`)) return;
|
const ok = await confirmDialog({
|
||||||
|
title: 'Remove from catalogue?',
|
||||||
|
message: `“${target}” will be permanently removed. This cannot be undone.`,
|
||||||
|
confirmLabel: 'Remove',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await deletePost(target);
|
await deletePost(target);
|
||||||
window.location.href = '/admin';
|
window.location.href = '/admin';
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { listMessages, deleteMessage, ApiError } from '../../../lib/api';
|
import { listMessages, deleteMessage, ApiError } from '../../../lib/api';
|
||||||
import type { Message } from '../../../lib/types';
|
import type { Message } from '../../../lib/types';
|
||||||
|
import { confirmDialog, notify } from '../../../lib/confirm';
|
||||||
|
|
||||||
export default function Inbox() {
|
export default function Inbox() {
|
||||||
const [messages, setMessages] = useState<Message[] | null>(null);
|
const [messages, setMessages] = useState<Message[] | null>(null);
|
||||||
@@ -23,12 +24,17 @@ export default function Inbox() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function remove(id: string) {
|
async function remove(id: string) {
|
||||||
if (!confirm('Delete this message? This cannot be undone.')) return;
|
const ok = await confirmDialog({
|
||||||
|
title: 'Delete this message?',
|
||||||
|
message: 'This cannot be undone.',
|
||||||
|
confirmLabel: 'Delete',
|
||||||
|
});
|
||||||
|
if (!ok) return;
|
||||||
try {
|
try {
|
||||||
await deleteMessage(id);
|
await deleteMessage(id);
|
||||||
setMessages(prev => (prev ?? []).filter(m => m.id !== id));
|
setMessages(prev => (prev ?? []).filter(m => m.id !== id));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(e instanceof ApiError ? e.message : 'Failed to delete.');
|
notify(e instanceof ApiError ? e.message : 'Failed to delete.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const hasContact = (siteConfig.contact_links?.length ?? 0) > 0;
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link rel="icon" href={siteConfig.favicon} />
|
<link rel="icon" href={siteConfig.favicon} />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<title>{fullTitle}</title>
|
<title>{fullTitle}</title>
|
||||||
|
|||||||
@@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
* Imperative confirm() / notify() that replace native window.confirm /
|
||||||
|
* window.alert. The site hydrates many independent React islands that share
|
||||||
|
* no React root, so this is a vanilla DOM singleton: any island (or inline
|
||||||
|
* script) can `await confirmDialog(...)`. Styling is class-driven, so it
|
||||||
|
* inherits every theme — including the breakcore neon/hazard layer — for free.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface ConfirmOptions {
|
||||||
|
title: string;
|
||||||
|
message?: string;
|
||||||
|
confirmLabel?: string;
|
||||||
|
cancelLabel?: string;
|
||||||
|
/** Visual weight of the confirm button. Defaults to 'danger'. */
|
||||||
|
tone?: 'danger' | 'primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
let activeCleanup: (() => void) | null = null;
|
||||||
|
|
||||||
|
export function confirmDialog(opts: ConfirmOptions): Promise<boolean> {
|
||||||
|
// Collapse any in-flight dialog (treat as cancel) before opening a new one.
|
||||||
|
activeCleanup?.();
|
||||||
|
|
||||||
|
return new Promise<boolean>(resolve => {
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
confirmLabel = 'Confirm',
|
||||||
|
cancelLabel = 'Cancel',
|
||||||
|
tone = 'danger',
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const lastFocused = document.activeElement as HTMLElement | null;
|
||||||
|
const prevOverflow = document.body.style.overflow;
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'cdialog-overlay';
|
||||||
|
overlay.setAttribute('role', 'alertdialog');
|
||||||
|
overlay.setAttribute('aria-modal', 'true');
|
||||||
|
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'cdialog-backdrop';
|
||||||
|
backdrop.setAttribute('aria-hidden', 'true');
|
||||||
|
|
||||||
|
const panel = document.createElement('div');
|
||||||
|
panel.className = 'glass cdialog-panel';
|
||||||
|
|
||||||
|
const titleId = `cdlg-t-${Math.random().toString(36).slice(2)}`;
|
||||||
|
const h = document.createElement('h2');
|
||||||
|
h.className = 'cdialog-title';
|
||||||
|
h.id = titleId;
|
||||||
|
h.textContent = title;
|
||||||
|
overlay.setAttribute('aria-labelledby', titleId);
|
||||||
|
panel.appendChild(h);
|
||||||
|
|
||||||
|
if (message) {
|
||||||
|
const p = document.createElement('p');
|
||||||
|
p.className = 'cdialog-msg';
|
||||||
|
p.textContent = message;
|
||||||
|
panel.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actions = document.createElement('div');
|
||||||
|
actions.className = 'cdialog-actions';
|
||||||
|
|
||||||
|
const cancelBtn = document.createElement('button');
|
||||||
|
cancelBtn.type = 'button';
|
||||||
|
cancelBtn.className = 'btn btn--ghost';
|
||||||
|
cancelBtn.textContent = cancelLabel;
|
||||||
|
|
||||||
|
const confirmBtn = document.createElement('button');
|
||||||
|
confirmBtn.type = 'button';
|
||||||
|
confirmBtn.className = `btn ${tone === 'primary' ? 'btn--primary' : 'btn--danger'}`;
|
||||||
|
confirmBtn.textContent = confirmLabel;
|
||||||
|
|
||||||
|
actions.append(cancelBtn, confirmBtn);
|
||||||
|
panel.appendChild(actions);
|
||||||
|
overlay.append(backdrop, panel);
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
let settled = false;
|
||||||
|
function close(result: boolean) {
|
||||||
|
if (settled) return;
|
||||||
|
settled = true;
|
||||||
|
activeCleanup = null;
|
||||||
|
document.removeEventListener('keydown', onKey, true);
|
||||||
|
document.body.style.overflow = prevOverflow;
|
||||||
|
overlay.remove();
|
||||||
|
lastFocused?.focus?.();
|
||||||
|
resolve(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKey(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
close(false);
|
||||||
|
} else if (e.key === 'Tab') {
|
||||||
|
// Two-stop focus trap.
|
||||||
|
e.preventDefault();
|
||||||
|
const next = document.activeElement === confirmBtn ? cancelBtn : confirmBtn;
|
||||||
|
next.focus();
|
||||||
|
} else if (e.key === 'Enter' && document.activeElement === confirmBtn) {
|
||||||
|
e.preventDefault();
|
||||||
|
close(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backdrop.addEventListener('click', () => close(false));
|
||||||
|
cancelBtn.addEventListener('click', () => close(false));
|
||||||
|
confirmBtn.addEventListener('click', () => close(true));
|
||||||
|
document.addEventListener('keydown', onKey, true);
|
||||||
|
activeCleanup = () => close(false);
|
||||||
|
|
||||||
|
// Destructive default: focus Cancel so an accidental Enter is safe.
|
||||||
|
(tone === 'primary' ? confirmBtn : cancelBtn).focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Transient bottom-center toast. Replaces window.alert for failures. */
|
||||||
|
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.dataset.notify = '';
|
||||||
|
el.setAttribute('role', tone === 'error' ? 'alert' : 'status');
|
||||||
|
el.textContent = message;
|
||||||
|
el.addEventListener('click', () => el.remove());
|
||||||
|
document.body.appendChild(el);
|
||||||
|
window.setTimeout(() => el.remove(), 4500);
|
||||||
|
}
|
||||||
@@ -688,6 +688,21 @@ code, pre, kbd, samp {
|
|||||||
0 32px 60px -28px rgba(255, 46, 166, 0.45),
|
0 32px 60px -28px rgba(255, 46, 166, 0.45),
|
||||||
0 0 32px -8px color-mix(in srgb, var(--mauve) 50%, transparent);
|
0 0 32px -8px color-mix(in srgb, var(--mauve) 50%, transparent);
|
||||||
}
|
}
|
||||||
|
/* Keyboard focus for the card link — salon-appropriate inset frame + ring. */
|
||||||
|
.plate:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px var(--mauve),
|
||||||
|
0 0 0 3px color-mix(in srgb, var(--mauve) 40%, transparent),
|
||||||
|
0 22px 42px -28px rgba(20, 16, 12, 0.5);
|
||||||
|
}
|
||||||
|
.breakcore .plate:focus-visible {
|
||||||
|
box-shadow:
|
||||||
|
inset 0 0 0 2px var(--mauve),
|
||||||
|
0 0 0 2px var(--green),
|
||||||
|
0 0 28px -6px color-mix(in srgb, var(--mauve) 60%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.plate .plate-image {
|
.plate .plate-image {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -1037,6 +1052,7 @@ code, pre, kbd, samp {
|
|||||||
.topbar-control:focus-visible {
|
.topbar-control:focus-visible {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: var(--mauve);
|
border-color: var(--mauve);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--mauve) 40%, transparent);
|
||||||
}
|
}
|
||||||
.topbar-control:disabled { opacity: 0.5; cursor: default; }
|
.topbar-control:disabled { opacity: 0.5; cursor: default; }
|
||||||
.topbar-control svg { width: 14px; height: 14px; flex-shrink: 0; }
|
.topbar-control svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||||||
@@ -1185,3 +1201,316 @@ input[type="date"] { color-scheme: light; }
|
|||||||
transform: scaleX(0);
|
transform: scaleX(0);
|
||||||
transition: transform 80ms linear;
|
transition: transform 80ms linear;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ═══════════════════════════════════════════════════════════════════════
|
||||||
|
* BREAKCORE — refined-neon layer.
|
||||||
|
* Everything below is scoped to `.breakcore`; salon / salon-noir / gothic
|
||||||
|
* are untouched. Aesthetic: editorial serif body in deliberate tension with
|
||||||
|
* hard-edged web-rot chrome — RGB split, hazard tape, neon outline, hard
|
||||||
|
* offset shadows. Motion is *reactive only* (hover / focus / one-shot on
|
||||||
|
* load) and settles fast. All motion is killed by prefers-reduced-motion
|
||||||
|
* at the very end of this file.
|
||||||
|
* ═══════════════════════════════════════════════════════════════════════ */
|
||||||
|
|
||||||
|
/* CRT tube depth — static vignette layered on the existing base fill. */
|
||||||
|
.breakcore body::before {
|
||||||
|
background-image: radial-gradient(
|
||||||
|
ellipse at center,
|
||||||
|
transparent 52%,
|
||||||
|
color-mix(in srgb, var(--crust) 75%, transparent) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nameplate — striped datamosh underline + glitch-shear burst on hover. */
|
||||||
|
.breakcore .nameplate::after {
|
||||||
|
height: 3px;
|
||||||
|
bottom: -6px;
|
||||||
|
opacity: 0.9;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--mauve) 0 6px,
|
||||||
|
var(--green) 6px 12px,
|
||||||
|
var(--blue) 12px 18px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@keyframes bc-shear {
|
||||||
|
0% { clip-path: inset(0 0 0 0); transform: translateX(0);
|
||||||
|
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
|
||||||
|
20% { clip-path: inset(16% 0 56% 0); transform: translateX(-5px);
|
||||||
|
text-shadow: -5px 0 0 var(--green), 5px 0 0 var(--mauve); }
|
||||||
|
40% { clip-path: inset(62% 0 10% 0); transform: translateX(5px);
|
||||||
|
text-shadow: 5px 0 0 var(--teal), -5px 0 0 var(--red); }
|
||||||
|
60% { clip-path: inset(30% 0 42% 0); transform: translateX(-3px);
|
||||||
|
text-shadow: -3px 0 0 var(--mauve), 3px 0 0 var(--green); }
|
||||||
|
80% { clip-path: inset(6% 0 78% 0); transform: translateX(2px);
|
||||||
|
text-shadow: 2px 0 0 var(--teal), -2px 0 0 var(--mauve); }
|
||||||
|
100% { clip-path: inset(0 0 0 0); transform: translateX(0);
|
||||||
|
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve); }
|
||||||
|
}
|
||||||
|
.breakcore .nameplate:hover .nameplate-title {
|
||||||
|
animation: bc-shear 200ms steps(3, jump-none) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display headings — one-shot glitch-in on page load. The static chromatic
|
||||||
|
* text-shadow (defined earlier) remains as the resting state. */
|
||||||
|
@keyframes bc-load-glitch {
|
||||||
|
0% { opacity: 0; clip-path: inset(46% 0 46% 0); transform: translateX(-9px); }
|
||||||
|
20% { opacity: 1; clip-path: inset(8% 0 70% 0); transform: translateX(7px); }
|
||||||
|
40% { clip-path: inset(68% 0 8% 0); transform: translateX(-5px); }
|
||||||
|
60% { clip-path: inset(24% 0 36% 0); transform: translateX(3px); }
|
||||||
|
80% { clip-path: inset(4% 0 84% 0); transform: translateX(-2px); }
|
||||||
|
100% { opacity: 1; clip-path: inset(0 0 0 0); transform: translateX(0); }
|
||||||
|
}
|
||||||
|
.breakcore .prose h1,
|
||||||
|
.breakcore h1.font-display {
|
||||||
|
animation: bc-load-glitch 460ms steps(5, jump-none) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plate — hard hover (no soft lift), RGB-split image, scanline sweep. */
|
||||||
|
.breakcore .plate:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
}
|
||||||
|
.breakcore .plate:hover .plate-caption-title {
|
||||||
|
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .plate:hover .plate-image img {
|
||||||
|
filter:
|
||||||
|
drop-shadow(-3px 0 0 color-mix(in srgb, var(--mauve) 70%, transparent))
|
||||||
|
drop-shadow(3px 0 0 color-mix(in srgb, var(--teal) 70%, transparent))
|
||||||
|
saturate(1.12) contrast(1.06);
|
||||||
|
}
|
||||||
|
.breakcore .plate .plate-image::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-110%);
|
||||||
|
mix-blend-mode: screen;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
transparent 0%,
|
||||||
|
color-mix(in srgb, var(--sky) 28%, transparent) 46%,
|
||||||
|
color-mix(in srgb, var(--mauve) 70%, transparent) 49%,
|
||||||
|
color-mix(in srgb, var(--green) 55%, transparent) 51%,
|
||||||
|
color-mix(in srgb, var(--sky) 28%, transparent) 54%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@keyframes bc-scan {
|
||||||
|
0% { transform: translateY(-110%); opacity: 0; }
|
||||||
|
12% { opacity: 1; }
|
||||||
|
88% { opacity: 1; }
|
||||||
|
100% { transform: translateY(110%); opacity: 0; }
|
||||||
|
}
|
||||||
|
.breakcore .plate:hover .plate-image::after,
|
||||||
|
.breakcore .plate:focus-visible .plate-image::after {
|
||||||
|
animation: bc-scan 0.62s cubic-bezier(0.4, 0, 0.2, 1) 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section rule — hazard tape. Used on footer, post header, 404. */
|
||||||
|
.breakcore .section-rule {
|
||||||
|
color: var(--green);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
}
|
||||||
|
.breakcore .section-rule::before,
|
||||||
|
.breakcore .section-rule::after {
|
||||||
|
height: 6px;
|
||||||
|
opacity: 0.55;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--yellow) 0 8px,
|
||||||
|
var(--crust) 8px 16px
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.breakcore .section-rule .ornament {
|
||||||
|
color: var(--mauve);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chips — neon outline, monospace caps. */
|
||||||
|
.breakcore .chip {
|
||||||
|
background: transparent;
|
||||||
|
border-color: color-mix(in srgb, var(--teal) 55%, transparent);
|
||||||
|
color: var(--teal);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.breakcore .chip-accent {
|
||||||
|
background: var(--mauve);
|
||||||
|
color: var(--crust);
|
||||||
|
border-color: var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .chip-draft {
|
||||||
|
background: transparent;
|
||||||
|
border-color: color-mix(in srgb, var(--green) 60%, transparent);
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Plate caption meta — bracketed mono coordinates. */
|
||||||
|
.breakcore .plate-caption-meta {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
}
|
||||||
|
.breakcore .plate-caption-sep {
|
||||||
|
color: var(--green);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons & inputs — square, hard offset block-shadow, neon focus. */
|
||||||
|
.breakcore .btn,
|
||||||
|
.breakcore .field-input,
|
||||||
|
.breakcore .topbar-control,
|
||||||
|
.breakcore .topbar-control kbd { border-radius: 0; }
|
||||||
|
.breakcore .btn--primary {
|
||||||
|
color: var(--crust);
|
||||||
|
border-color: var(--mauve);
|
||||||
|
box-shadow: 3px 3px 0 0 var(--green);
|
||||||
|
}
|
||||||
|
.breakcore .btn--primary:hover {
|
||||||
|
background: var(--green);
|
||||||
|
border-color: var(--green);
|
||||||
|
color: var(--crust);
|
||||||
|
box-shadow: 3px 3px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .btn--primary:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 1px 1px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .btn--danger {
|
||||||
|
box-shadow: 3px 3px 0 0 color-mix(in srgb, var(--red) 60%, var(--crust));
|
||||||
|
}
|
||||||
|
.breakcore .btn--danger:hover {
|
||||||
|
box-shadow: 3px 3px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .btn--danger:active {
|
||||||
|
transform: translate(2px, 2px);
|
||||||
|
box-shadow: 1px 1px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
.breakcore .btn:focus-visible {
|
||||||
|
border-color: var(--green);
|
||||||
|
box-shadow: 0 0 0 2px var(--green);
|
||||||
|
}
|
||||||
|
.breakcore .field-input:focus {
|
||||||
|
border-color: var(--green);
|
||||||
|
background: color-mix(in srgb, var(--surface0) 85%, var(--green) 8%);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--green) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prose links — magenta resting, acid-green on hover. */
|
||||||
|
.breakcore .prose a {
|
||||||
|
text-decoration-color: color-mix(in srgb, var(--mauve) 55%, transparent);
|
||||||
|
}
|
||||||
|
.breakcore .prose a:hover {
|
||||||
|
color: var(--green);
|
||||||
|
text-decoration-color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reading progress — acid scan with bloom. */
|
||||||
|
.breakcore .reading-progress {
|
||||||
|
background: var(--green);
|
||||||
|
box-shadow: 0 0 8px var(--green), 0 0 3px var(--mauve);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ───── Confirm dialog (replaces window.confirm) ───── */
|
||||||
|
.cdialog-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 300;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
.cdialog-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: color-mix(in srgb, var(--crust) 60%, transparent);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.cdialog-panel {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 26rem;
|
||||||
|
padding: 1.6rem 1.6rem 1.4rem;
|
||||||
|
animation: cdialog-in 0.18s cubic-bezier(0.2, 0.7, 0.2, 1);
|
||||||
|
}
|
||||||
|
@keyframes cdialog-in {
|
||||||
|
from { opacity: 0; transform: translateY(10px) scale(0.98); }
|
||||||
|
to { opacity: 1; transform: none; }
|
||||||
|
}
|
||||||
|
.cdialog-title {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
line-height: 1.15;
|
||||||
|
color: var(--text);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.cdialog-msg {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.98rem;
|
||||||
|
line-height: 1.55;
|
||||||
|
color: var(--subtext1);
|
||||||
|
margin-top: 0.6rem;
|
||||||
|
}
|
||||||
|
.cdialog-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 0.6rem;
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Breakcore: hard edges + hazard cap + chromatic title. */
|
||||||
|
.breakcore .cdialog-panel {
|
||||||
|
border-radius: 0;
|
||||||
|
padding-top: 1.9rem;
|
||||||
|
}
|
||||||
|
.breakcore .cdialog-panel::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 0 0 auto 0;
|
||||||
|
height: 5px;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
45deg,
|
||||||
|
var(--yellow) 0 8px,
|
||||||
|
var(--crust) 8px 16px
|
||||||
|
);
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
.breakcore .cdialog-title {
|
||||||
|
text-shadow: -1px 0 0 var(--teal), 1px 0 0 var(--mauve);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Toast error variant (replaces window.alert). */
|
||||||
|
.toast--error {
|
||||||
|
border-left: 3px solid var(--red);
|
||||||
|
color: var(--rosewater);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toast--error::before {
|
||||||
|
content: "⚠ ";
|
||||||
|
color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ═══ Reduced motion — universal kill-switch. Final word in the file so it
|
||||||
|
* overrides every animation/transition above, all themes. Content still
|
||||||
|
* resolves to its final state (forwards-filled keyframes complete). ═══ */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
*,
|
||||||
|
*::before,
|
||||||
|
*::after {
|
||||||
|
animation-duration: 0.001ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 0.001ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user