Security improvements, title fixes
This commit is contained in:
parent
40d3e6bb1e
commit
b076856110
12
index.html
12
index.html
@ -6,7 +6,7 @@
|
||||
<title>BTCPay Day Prague 2026</title>
|
||||
<meta name="description" content="Join BTCPay Server contributors and community in Prague on June 14, 2026. Keynotes, panels, and surprise guests." />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<!-- Open Graph — replace /og-image.png with a real 1200×630 image before launch -->
|
||||
<meta property="og:title" content="BTCPay Day Prague 2026" />
|
||||
<meta property="og:description" content="Once a year, BTCPay Server contributors and community gather to celebrate achievements, network, and discuss the project's future." />
|
||||
<meta property="og:type" content="website" />
|
||||
@ -19,7 +19,12 @@
|
||||
<meta name="twitter:description" content="Join us in Prague on June 14, 2026 for BTCPay Day — keynotes, panels, and surprise guests." />
|
||||
<meta name="twitter:image" content="/og-image.png" />
|
||||
|
||||
<!-- CSP -->
|
||||
<!--
|
||||
CSP note: meta CSP cannot enforce frame-ancestors (clickjacking protection).
|
||||
Add the following response header at the edge/CDN for full protection:
|
||||
Content-Security-Policy: frame-ancestors 'none'
|
||||
X-Frame-Options: DENY
|
||||
-->
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https://img.youtube.com https://pbs.twimg.com; frame-src https://www.youtube.com https://www.openstreetmap.org; connect-src 'self'" />
|
||||
|
||||
<script src="/theme-init.js"></script>
|
||||
@ -29,7 +34,8 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="shortcut icon" href="/favicon.ico" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<!-- apple-touch-icon: replace with a 180×180 PNG before launch -->
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/favicon-32x32.png" />
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const RIGA_VIDEO_ID = 'JWZPN-SAO3U'
|
||||
import { EVENT } from '@/data/event'
|
||||
|
||||
export default function About() {
|
||||
const { ref, isVisible } = useScrollReveal()
|
||||
@ -39,10 +38,11 @@ export default function About() {
|
||||
<div className="glass rounded-2xl overflow-hidden">
|
||||
<div className="aspect-video w-full">
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${RIGA_VIDEO_ID}?start=6423`}
|
||||
src={`https://www.youtube.com/embed/${EVENT.youtubeRigaId}?start=${EVENT.rigaTimestamp}`}
|
||||
title="BTCPay Day Riga 2024"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
loading="lazy"
|
||||
className="w-full h-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@ -1,30 +1,45 @@
|
||||
const TICKET_URL = '#register'
|
||||
const PRAGUE_VIDEO_ID = 'rIjNPuYxVMo'
|
||||
import { EVENT } from '@/data/event'
|
||||
|
||||
// Check once at module load — user must reload to pick up system-level changes,
|
||||
// which is acceptable. Avoids a hook + subscriber just for a background video.
|
||||
const prefersReducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
||||
|
||||
export default function Hero() {
|
||||
return (
|
||||
<section id="home" className="relative overflow-hidden pt-16 min-h-screen flex flex-col justify-center">
|
||||
|
||||
{/* Video background */}
|
||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
width: 'max(100%, 177.78vh)',
|
||||
height: 'max(100%, 56.25vw)',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${PRAGUE_VIDEO_ID}?autoplay=1&mute=1&controls=0&loop=1&playlist=${PRAGUE_VIDEO_ID}&modestbranding=1&playsinline=1`}
|
||||
allow="autoplay; encrypted-media"
|
||||
className="w-full h-full"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
{/* Video background — skipped entirely when OS reduces motion */}
|
||||
{!prefersReducedMotion && (
|
||||
<div className="absolute inset-0 z-0 pointer-events-none">
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
width: 'max(100%, 177.78vh)',
|
||||
height: 'max(100%, 56.25vw)',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
}}>
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${EVENT.youtubeHeroId}?autoplay=1&mute=1&controls=0&loop=1&playlist=${EVENT.youtubeHeroId}&modestbranding=1&playsinline=1`}
|
||||
title="BTCPay Day Prague background"
|
||||
allow="autoplay; encrypted-media"
|
||||
className="w-full h-full"
|
||||
style={{ pointerEvents: 'none' }}
|
||||
/>
|
||||
</div>
|
||||
{/* Dark overlay — heavy enough for text legibility at all video frames */}
|
||||
<div className="absolute inset-0" style={{ background: 'linear-gradient(to bottom, rgba(0,15,6,0.68), rgba(0,10,4,0.78))' }} />
|
||||
</div>
|
||||
{/* Dark overlay — heavy enough for text legibility at all video frames */}
|
||||
<div className="absolute inset-0" style={{ background: 'linear-gradient(to bottom, rgba(0,15,6,0.68), rgba(0,10,4,0.78))' }} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Static gradient fallback shown when motion is reduced */}
|
||||
{prefersReducedMotion && (
|
||||
<div className="absolute inset-0 z-0" style={{
|
||||
background: 'radial-gradient(ellipse 80% 60% at 50% 30%, hsl(110 48% 14% / 0.7) 0%, hsl(216 28% 7%) 70%)',
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="relative z-10 max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 w-full">
|
||||
@ -32,7 +47,7 @@ export default function Hero() {
|
||||
|
||||
<div className="inline-flex items-center gap-2 px-5 py-2.5 rounded-full border border-white/50 bg-black/40 backdrop-blur-md mb-8">
|
||||
<span className="text-sm font-bold tracking-wide text-white">
|
||||
June 14, 2026 · 12:00 PM · Hotel Duo · Prague
|
||||
{EVENT.date} · {EVENT.time} · {EVENT.venue} · Prague
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -47,7 +62,7 @@ export default function Hero() {
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<a
|
||||
href={TICKET_URL}
|
||||
href={EVENT.ticketUrl}
|
||||
className="inline-flex items-center justify-center px-8 py-3 rounded-xl text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 active:scale-[0.98] transition-all duration-150"
|
||||
>
|
||||
Register
|
||||
|
||||
@ -1,15 +1,7 @@
|
||||
import { MapPin, Clock, Calendar, ExternalLink } from 'lucide-react'
|
||||
import { useScrollReveal } from '@/hooks/useScrollReveal'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const VENUE = {
|
||||
name: 'Hotel Duo',
|
||||
address: 'Teplická 492, 190 00 Praha 9',
|
||||
city: 'Prague, Czech Republic',
|
||||
date: 'June 14, 2026',
|
||||
time: '12:00 PM',
|
||||
mapsUrl: 'https://maps.google.com/?q=Hotel+Duo+Praha+Teplická+492',
|
||||
}
|
||||
import { EVENT } from '@/data/event'
|
||||
|
||||
// OpenStreetMap embed for Hotel Duo Praha (approx. coords: 50.1039, 14.4697)
|
||||
const OSM_EMBED = 'https://www.openstreetmap.org/export/embed.html?bbox=14.449%2C50.094%2C14.490%2C50.114&layer=mapnik&marker=50.1039%2C14.4697'
|
||||
@ -32,9 +24,9 @@ export default function Location() {
|
||||
{/* Venue details card */}
|
||||
<div className="glass rounded-2xl p-8 flex flex-col gap-6">
|
||||
<div>
|
||||
<h3 className="font-display font-bold text-2xl text-foreground">{VENUE.name}</h3>
|
||||
<p className="text-muted-foreground mt-1">{VENUE.address}</p>
|
||||
<p className="text-muted-foreground text-sm">{VENUE.city}</p>
|
||||
<h3 className="font-display font-bold text-2xl text-foreground">{EVENT.venue}</h3>
|
||||
<p className="text-muted-foreground mt-1">{EVENT.address}</p>
|
||||
<p className="text-muted-foreground text-sm">{EVENT.city}</p>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-3">
|
||||
@ -42,24 +34,24 @@ export default function Location() {
|
||||
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
|
||||
<Calendar size={14} className="text-primary" />
|
||||
</span>
|
||||
{VENUE.date}
|
||||
{EVENT.date}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-foreground">
|
||||
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
|
||||
<Clock size={14} className="text-primary" />
|
||||
</span>
|
||||
Doors open at {VENUE.time}
|
||||
Doors open at {EVENT.time}
|
||||
</div>
|
||||
<div className="flex items-center gap-3 text-sm text-foreground">
|
||||
<span className="flex items-center justify-center w-8 h-8 rounded-lg bg-primary/10">
|
||||
<MapPin size={14} className="text-primary" />
|
||||
</span>
|
||||
{VENUE.city}
|
||||
{EVENT.city}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={VENUE.mapsUrl}
|
||||
href={EVENT.mapsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 self-start px-5 py-2.5 rounded-xl border border-border text-sm font-medium text-foreground hover:bg-muted transition-colors duration-150"
|
||||
|
||||
@ -1,22 +1,41 @@
|
||||
import { useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Menu, X, Send } from 'lucide-react'
|
||||
import ThemeToggle from '@/components/ThemeToggle'
|
||||
import BTCPayMark from '@/components/BTCPayMark'
|
||||
|
||||
// TODO: Replace with your real ticket registration URL
|
||||
const TICKET_URL = '#register'
|
||||
|
||||
const TELEGRAM_URL = 'https://t.me/+h9RyKmiXBdhhM2I0'
|
||||
import { EVENT } from '@/data/event'
|
||||
|
||||
const NAV_LINKS = [
|
||||
{ label: 'Speakers', href: '#speakers' },
|
||||
{ label: 'Agenda', href: '#agenda' },
|
||||
{ label: 'Location', href: '#location' },
|
||||
{ label: 'Speakers', href: '#speakers' },
|
||||
{ label: 'Agenda', href: '#agenda' },
|
||||
{ label: 'Location', href: '#location' },
|
||||
{ label: 'Supporters', href: '#supporters' },
|
||||
]
|
||||
|
||||
export default function Navbar() {
|
||||
const [mobileOpen, setMobileOpen] = useState(false)
|
||||
const firstLinkRef = useRef<HTMLAnchorElement>(null)
|
||||
|
||||
// Scroll lock
|
||||
useEffect(() => {
|
||||
document.body.style.overflow = mobileOpen ? 'hidden' : ''
|
||||
return () => { document.body.style.overflow = '' }
|
||||
}, [mobileOpen])
|
||||
|
||||
// Escape key closes menu
|
||||
useEffect(() => {
|
||||
const onKey = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape' && mobileOpen) setMobileOpen(false)
|
||||
}
|
||||
document.addEventListener('keydown', onKey)
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [mobileOpen])
|
||||
|
||||
// Move focus into overlay when it opens
|
||||
useEffect(() => {
|
||||
if (mobileOpen) firstLinkRef.current?.focus()
|
||||
}, [mobileOpen])
|
||||
|
||||
const close = () => setMobileOpen(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -30,7 +49,7 @@ export default function Navbar() {
|
||||
</span>
|
||||
</a>
|
||||
|
||||
<nav className="hidden md:flex items-center gap-1">
|
||||
<nav className="hidden md:flex items-center gap-1" aria-label="Primary navigation">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
@ -44,7 +63,7 @@ export default function Navbar() {
|
||||
|
||||
<div className="flex items-center gap-1.5 ml-auto">
|
||||
<a
|
||||
href={TELEGRAM_URL}
|
||||
href={EVENT.telegramUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
aria-label="Join BTCPay Day on Telegram"
|
||||
@ -54,7 +73,7 @@ export default function Navbar() {
|
||||
</a>
|
||||
<ThemeToggle />
|
||||
<a
|
||||
href={TICKET_URL}
|
||||
href={EVENT.ticketUrl}
|
||||
className="hidden sm:flex items-center gap-1.5 px-4 py-1.5 rounded-lg text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity duration-150 ml-1"
|
||||
>
|
||||
Register Now
|
||||
@ -63,6 +82,8 @@ export default function Navbar() {
|
||||
type="button"
|
||||
className="flex md:hidden items-center justify-center w-8 h-8 rounded-lg text-muted-foreground hover:text-foreground hover:bg-muted transition-all duration-150 ml-1"
|
||||
aria-label={mobileOpen ? 'Close menu' : 'Open menu'}
|
||||
aria-expanded={mobileOpen}
|
||||
aria-controls="mobile-menu"
|
||||
onClick={() => setMobileOpen((v) => !v)}
|
||||
>
|
||||
{mobileOpen ? <X size={18} /> : <Menu size={18} />}
|
||||
@ -71,14 +92,22 @@ export default function Navbar() {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile menu — implemented as a dialog for correct modal semantics */}
|
||||
{mobileOpen && (
|
||||
<div className="fixed inset-0 z-40 bg-background/95 backdrop-blur-2xl flex flex-col pt-20 px-8 pb-10 md:hidden">
|
||||
<nav className="flex flex-col gap-2 flex-1">
|
||||
{NAV_LINKS.map((link) => (
|
||||
<div
|
||||
id="mobile-menu"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Navigation menu"
|
||||
className="fixed inset-0 z-40 bg-background/95 backdrop-blur-2xl flex flex-col pt-20 px-8 pb-10 md:hidden"
|
||||
>
|
||||
<nav className="flex flex-col gap-2 flex-1" aria-label="Mobile navigation">
|
||||
{NAV_LINKS.map((link, i) => (
|
||||
<a
|
||||
key={link.href}
|
||||
ref={i === 0 ? firstLinkRef : undefined}
|
||||
href={link.href}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
onClick={close}
|
||||
className="py-4 text-3xl font-display font-bold text-foreground/80 hover:text-foreground border-b border-border/40 transition-colors duration-150"
|
||||
>
|
||||
{link.label}
|
||||
@ -86,14 +115,14 @@ export default function Navbar() {
|
||||
))}
|
||||
</nav>
|
||||
<a
|
||||
href={TICKET_URL}
|
||||
onClick={() => setMobileOpen(false)}
|
||||
href={EVENT.ticketUrl}
|
||||
onClick={close}
|
||||
className="mt-8 flex items-center justify-center py-4 rounded-2xl text-lg font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity"
|
||||
>
|
||||
Register Now
|
||||
</a>
|
||||
<a
|
||||
href={TELEGRAM_URL}
|
||||
href={EVENT.telegramUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-4 flex items-center justify-center gap-2 py-3 rounded-2xl text-sm text-muted-foreground border border-border/60 hover:bg-muted transition-colors"
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// TODO: Replace with your real ticket registration URL
|
||||
const TICKET_URL = '#register'
|
||||
import { EVENT } from '@/data/event'
|
||||
|
||||
export default function StickyBar() {
|
||||
const [visible, setVisible] = useState(false)
|
||||
@ -26,11 +24,11 @@ export default function StickyBar() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
|
||||
<span className="text-sm font-medium text-foreground hidden sm:block">
|
||||
BTCPay Day ·{' '}
|
||||
<span className="text-muted-foreground">Prague · June 14, 2026 · 12:00 PM</span>
|
||||
<span className="text-muted-foreground">Prague · {EVENT.date} · {EVENT.time}</span>
|
||||
</span>
|
||||
<span className="text-sm font-medium text-foreground sm:hidden">BTCPay Day Prague 2026</span>
|
||||
<a
|
||||
href={TICKET_URL}
|
||||
href={EVENT.ticketUrl}
|
||||
className="shrink-0 flex items-center justify-center px-5 py-1.5 rounded-lg text-sm font-semibold bg-primary text-primary-foreground hover:opacity-90 transition-opacity duration-150"
|
||||
>
|
||||
Register Now
|
||||
|
||||
13
src/data/event.ts
Normal file
13
src/data/event.ts
Normal file
@ -0,0 +1,13 @@
|
||||
export const EVENT = {
|
||||
date: 'June 14, 2026',
|
||||
time: '12:00 PM',
|
||||
venue: 'Hotel Duo',
|
||||
address: 'Teplická 492, 190 00 Praha 9',
|
||||
city: 'Prague, Czech Republic',
|
||||
mapsUrl: 'https://maps.google.com/?q=Hotel+Duo+Praha+Teplická+492',
|
||||
ticketUrl: '#register',
|
||||
telegramUrl: 'https://t.me/+h9RyKmiXBdhhM2I0',
|
||||
youtubeHeroId: 'rIjNPuYxVMo',
|
||||
youtubeRigaId: 'JWZPN-SAO3U',
|
||||
rigaTimestamp: 6423,
|
||||
} as const
|
||||
@ -7,14 +7,14 @@ export interface Speaker {
|
||||
}
|
||||
|
||||
export const speakers: Speaker[] = [
|
||||
{ name: 'DSTRUKT', title: 'Designer & Developer', photo: '/images/speakers/dstrukt__.jpg', xHandle: 'dstrukt__' },
|
||||
{ name: 'Kukks', title: 'Core Contributor', photo: '/images/speakers/MrKukks.jpg', xHandle: 'MrKukks' },
|
||||
{ name: 'DSTRUKT', title: 'Product Designer', photo: '/images/speakers/dstrukt__.jpg', xHandle: 'dstrukt__' },
|
||||
{ name: 'Kukks', title: 'Maintainer', photo: '/images/speakers/MrKukks.jpg', xHandle: 'MrKukks' },
|
||||
{ name: 'ndeet', title: 'Ecommerce Integrations Developer', photo: '/images/speakers/ndeet.jpg', xHandle: 'ndeet' },
|
||||
{ name: 'Nicolas Dorier', title: 'Founder & Lead Developer', photo: '/images/speakers/NicolasDorier.jpg', xHandle: 'NicolasDorier' },
|
||||
{ name: 'Pavlenex', title: 'Project Manager', photo: '/images/speakers/pavlenex.jpg', xHandle: 'pavlenex' },
|
||||
{ name: 'thgO.O', title: 'Full-Stack Developer', photo: '/images/speakers/thgO_O.jpg', xHandle: 'thgO_O' },
|
||||
{ name: 'Tobses', title: 'Full-Time Contributor', photo: '/images/speakers/TChileta.jpg', xHandle: 'TChileta' },
|
||||
{ name: 'Uncle Rockstar Developer', title: 'Core Contributor', photo: '/images/speakers/r0ckstardev.jpg', xHandle: 'r0ckstardev' },
|
||||
{ name: 'webworthy', title: 'Contributor', photo: '/images/speakers/WebWorthy.jpg', xHandle: 'WebWorthy' },
|
||||
{ name: 'TBD', title: 'Speaker', photo: undefined, xHandle: undefined },
|
||||
{ name: 'Pavlenex', title: 'Product Manager', photo: '/images/speakers/pavlenex.jpg', xHandle: 'pavlenex' },
|
||||
{ name: 'thgO.O', title: 'Core Contributor', photo: '/images/speakers/thgO_O.jpg', xHandle: 'thgO_O' },
|
||||
{ name: 'Tobses', title: 'Core Contributor', photo: '/images/speakers/TChileta.jpg', xHandle: 'TChileta' },
|
||||
{ name: 'Uncle Rockstar Developer', title: 'Maintainer', photo: '/images/speakers/r0ckstardev.jpg', xHandle: 'r0ckstardev' },
|
||||
{ name: 'webworthy', title: 'Video & Marketing', photo: '/images/speakers/WebWorthy.jpg', xHandle: 'WebWorthy' },
|
||||
// Add more speakers: { name, title, photo: '/images/speakers/handle.jpg', xHandle }
|
||||
]
|
||||
|
||||
@ -9,8 +9,10 @@ const THEME_COLORS: Record<Theme, string> = {
|
||||
|
||||
function getInitialTheme(): Theme {
|
||||
if (typeof window === 'undefined') return 'dark'
|
||||
const stored = localStorage.getItem('theme')
|
||||
if (stored === 'light' || stored === 'dark') return stored
|
||||
try {
|
||||
const stored = localStorage.getItem('theme')
|
||||
if (stored === 'light' || stored === 'dark') return stored
|
||||
} catch { /* storage blocked — fall through to system preference */ }
|
||||
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||
}
|
||||
|
||||
@ -18,7 +20,9 @@ function applyTheme(theme: Theme) {
|
||||
const root = document.documentElement
|
||||
root.classList.toggle('dark', theme === 'dark')
|
||||
root.style.colorScheme = theme
|
||||
localStorage.setItem('theme', theme)
|
||||
try {
|
||||
localStorage.setItem('theme', theme)
|
||||
} catch { /* storage blocked — theme works for this session only */ }
|
||||
const meta = document.querySelector('meta[name="theme-color"]')
|
||||
if (meta) meta.setAttribute('content', THEME_COLORS[theme])
|
||||
}
|
||||
|
||||
@ -145,6 +145,19 @@
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Respect OS-level reduced-motion preference */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.reveal {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
transition: none;
|
||||
}
|
||||
.card-enter {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Sticky bar slide-in from top */
|
||||
@keyframes slideDown {
|
||||
from { opacity: 0; transform: translateY(-100%); }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user