Compare commits

...
10 Commits
Author SHA1 Message Date
arsam f121e33946 Import documentation: docs/superpowers/plans/2026-06-05-cmusic-frontend.md
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:17 +03:30
arsam fa1fb7553f Import source: src/main
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:15 +03:30
arsam 2c8f849e9f Import source: src/index
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:14 +03:30
arsam d05e49b9a7 Import source: src/hooks/useAudioPlayer
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:13 +03:30
arsam f4e877af7a Import source: src/data/mockData
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:12 +03:30
arsam de1aaf6eed Import source: src/components/WaveSection/WaveSection
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:11 +03:30
arsam ef251a4be2 Import source: src/components/TrendingTracks/TrendingTracks
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:09 +03:30
arsam 9ea4a0c92e Import source: src/components/TrendingTracks/TrackRow
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:08 +03:30
arsam 0c02eacfec Import source: src/components/StatsSection/StatsSection
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:07 +03:30
arsam cc284f7f78 Import source: src/components/Player/Waveform3D
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
2026-09-18 23:08:06 +03:30
10 changed files with 2062 additions and 0 deletions
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
import { useRef, useMemo } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import * as THREE from 'three'
const BAR_COUNT = 28
function Waveform({ isPlaying }) {
const barsRef = useRef([])
const phases = useMemo(() => Array.from({ length: BAR_COUNT }, () => Math.random() * Math.PI * 2), [])
useFrame(({ clock }) => {
const t = clock.getElapsedTime()
barsRef.current.forEach((mesh, i) => {
if (!mesh) return
const target = isPlaying
? Math.abs(Math.sin(t * 3.5 + phases[i])) * 0.7 + 0.04
: 0.04 + Math.abs(Math.sin(t * 0.4 + phases[i])) * 0.08
mesh.scale.y = THREE.MathUtils.lerp(mesh.scale.y, target, 0.18)
mesh.position.y = mesh.scale.y * 0.5
})
})
return (
<group>
{Array.from({ length: BAR_COUNT }, (_, i) => (
<mesh
key={i}
ref={el => (barsRef.current[i] = el)}
position={[(i - BAR_COUNT / 2) * 0.075, 0.04, 0]}
>
<boxGeometry args={[0.05, 1, 0.05]} />
<meshBasicMaterial color={i % 2 === 0 ? '#c9a84c' : '#8a6f2e'} />
</mesh>
))}
</group>
)
}
export default function Waveform3D({ isPlaying }) {
return (
<Canvas
camera={{ position: [0, 0.4, 2.2], fov: 45 }}
gl={{ alpha: true, antialias: false }}
style={{ background: 'transparent' }}
>
<Waveform isPlaying={isPlaying} />
</Canvas>
)
}
@@ -0,0 +1,134 @@
import { useRef, useMemo } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { motion, useInView } from 'framer-motion'
import * as THREE from 'three'
const stats = [
{ value: '40M+', label: 'Tracks', sub: 'and growing daily' },
{ value: '2.1M', label: 'Artists', sub: 'worldwide' },
{ value: '180+', label: 'Countries', sub: 'streaming now' },
{ value: '99.9%', label: 'Uptime', sub: 'always on' },
]
const SPIKE_COUNT = 48
function GlobeSpikes() {
const groupRef = useRef()
const spikesRef = useRef([])
const dummy = useMemo(() => new THREE.Object3D(), [])
const spikeData = useMemo(() => {
return Array.from({ length: SPIKE_COUNT }, (_, i) => {
const phi = Math.acos(-1 + (2 * i) / SPIKE_COUNT)
const theta = Math.sqrt(SPIKE_COUNT * Math.PI) * phi
const r = 1.7
return {
x: r * Math.sin(phi) * Math.cos(theta),
y: r * Math.cos(phi),
z: r * Math.sin(phi) * Math.sin(theta),
phi, theta,
phase: Math.random() * Math.PI * 2,
speed: 0.5 + Math.random() * 1.0,
}
})
}, [])
useFrame(({ clock }) => {
const t = clock.getElapsedTime()
if (groupRef.current) groupRef.current.rotation.y = t * 0.12
spikeData.forEach((s, i) => {
if (!spikesRef.current[i]) return
const len = 0.05 + Math.abs(Math.sin(t * s.speed + s.phase)) * 0.55
const dir = new THREE.Vector3(s.x, s.y, s.z).normalize()
dummy.position.set(s.x, s.y, s.z)
dummy.lookAt(s.x + dir.x, s.y + dir.y, s.z + dir.z)
dummy.scale.set(0.02, len, 0.02)
dummy.updateMatrix()
spikesRef.current[i].setMatrixAt(0, dummy.matrix)
spikesRef.current[i].instanceMatrix.needsUpdate = true
})
})
return (
<group ref={groupRef}>
{/* Globe wireframe */}
<mesh>
<sphereGeometry args={[1.7, 24, 24]} />
<meshBasicMaterial color="#c9a84c" transparent opacity={0.04} wireframe />
</mesh>
<mesh>
<sphereGeometry args={[1.65, 32, 32]} />
<meshBasicMaterial color="#c9a84c" transparent opacity={0.02} />
</mesh>
{/* Spikes as instanced meshes */}
{spikeData.map((s, i) => (
<instancedMesh key={i} ref={el => spikesRef.current[i] = el} args={[null, null, 1]}>
<cylinderGeometry args={[1, 0.1, 1, 4]} />
<meshStandardMaterial color="#c9a84c" emissive="#c9a84c" emissiveIntensity={0.8} toneMapped={false} />
</instancedMesh>
))}
</group>
)
}
export default function StatsSection() {
const ref = useRef()
const inView = useInView(ref, { once: true, margin: '-100px' })
return (
<section ref={ref} className="py-28 px-6 relative overflow-hidden" style={{ background: '#050505' }}>
<div className="section-divider" />
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center mt-14">
{/* Stats */}
<div>
<motion.div
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.6 }}
className="mb-12"
>
<p className="text-xs tracking-[0.25em] uppercase gold-text mb-3">By The Numbers</p>
<h2 className="font-display font-bold text-4xl md:text-5xl text-white">
The Scale of <span className="gold-text">Sound</span>
</h2>
</motion.div>
<div className="grid grid-cols-2 gap-6">
{stats.map((stat, i) => (
<motion.div
key={stat.label}
initial={{ opacity: 0, y: 30 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, delay: i * 0.1 }}
className="relative p-6 rounded-2xl group"
style={{ background: '#101010', border: '1px solid #1e1e1e' }}
>
<div className="absolute inset-0 rounded-2xl opacity-0 group-hover:opacity-100 transition-opacity duration-300"
style={{ boxShadow: 'inset 0 0 0 1px rgba(201,168,76,0.2)' }} />
<p className="font-display font-bold text-3xl md:text-4xl gold-text mb-1">{stat.value}</p>
<p className="text-white font-medium text-sm">{stat.label}</p>
<p className="text-gray-700 text-xs mt-0.5">{stat.sub}</p>
</motion.div>
))}
</div>
</div>
{/* Globe */}
<div style={{ height: '420px' }}>
<Canvas camera={{ position: [0, 0, 6], fov: 50 }} gl={{ alpha: true, antialias: true }} style={{ background: 'transparent' }}>
<ambientLight intensity={0.05} />
<pointLight position={[0, 5, 3]} intensity={5} color="#c9a84c" />
<pointLight position={[-4, -3, 2]} intensity={2} color="#e8c547" />
<pointLight position={[4, 3, -2]} intensity={1.5} color="#8a6f2e" />
<GlobeSpikes />
</Canvas>
</div>
</div>
</div>
</section>
)
}
@@ -0,0 +1,79 @@
import { useState } from 'react'
import { motion } from 'framer-motion'
export default function TrackRow({ track, index, isActive, onPlay }) {
const [hovered, setHovered] = useState(false)
return (
<motion.div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
onClick={() => onPlay(track)}
animate={{ x: hovered ? 6 : 0, backgroundColor: hovered ? 'rgba(201,168,76,0.035)' : 'transparent' }}
transition={{ duration: 0.2 }}
className="flex items-center gap-4 px-4 py-3.5 rounded-xl cursor-pointer"
style={isActive ? { background: 'rgba(201,168,76,0.06)', border: '1px solid rgba(201,168,76,0.18)' }
: { border: '1px solid transparent' }}
>
{/* Rank / eq bars */}
<div className="w-5 flex items-center justify-center flex-shrink-0">
{isActive ? (
<div className="flex items-end gap-px h-3.5">
{[1,2,3].map(i => (
<motion.span key={i}
animate={{ scaleY: [1, 0.25, 1] }}
transition={{ repeat: Infinity, duration: 0.65, delay: i * 0.18, ease: 'easeInOut' }}
className="block w-0.5 rounded-full origin-bottom"
style={{ height: '100%', background: 'var(--gold)' }}
/>
))}
</div>
) : (
<span className="text-xs font-mono text-muted">{index + 1}</span>
)}
</div>
{/* Cover */}
<div className="relative w-11 h-11 rounded-lg overflow-hidden flex-shrink-0 shadow-card">
<img src={track.cover} alt={track.title}
className="w-full h-full object-cover transition-transform duration-300"
style={{ transform: hovered ? 'scale(1.12)' : 'scale(1)' }} />
{hovered && !isActive && (
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}
className="absolute inset-0 flex items-center justify-center"
style={{ background: 'rgba(7,7,9,0.55)' }}>
<svg className="w-3.5 h-3.5 ml-0.5" fill="var(--gold)" viewBox="0 0 24 24">
<path d="M8 5v14l11-7z" />
</svg>
</motion.div>
)}
</div>
{/* Title & artist */}
<div className="flex-1 min-w-0">
<p className={`text-sm font-medium truncate transition-colors duration-200 ${
isActive ? 'gold-text' : hovered ? 'text-white' : 'text-white'
}`}>{track.title}</p>
<p className="text-xs text-secondary truncate mt-0.5">{track.artist} · {track.album}</p>
</div>
{/* Plays */}
<span className="hidden sm:block text-xs text-muted font-mono w-14 text-right">{track.plays}</span>
{/* Duration */}
<span className="text-xs text-secondary font-mono w-9 text-right">{track.duration}</span>
{/* Action button */}
<motion.button
animate={{ opacity: hovered ? 1 : 0, scale: hovered ? 1 : 0.75 }}
transition={{ duration: 0.18 }}
className="w-7 h-7 rounded-full flex items-center justify-center flex-shrink-0"
style={{ background: 'rgba(201,168,76,0.12)', border: '1px solid rgba(201,168,76,0.25)' }}
>
<svg className="w-3 h-3" fill="none" stroke="var(--gold)" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M12 5l7 7-7 7" />
</svg>
</motion.button>
</motion.div>
)
}
@@ -0,0 +1,93 @@
import { useRef, useEffect } from 'react'
import { motion, useInView } from 'framer-motion'
import { gsap } from 'gsap'
import { ScrollTrigger } from 'gsap/ScrollTrigger'
import TrackRow from './TrackRow'
import { tracks } from '../../data/mockData'
gsap.registerPlugin(ScrollTrigger)
export default function TrendingTracks({ currentTrack, onPlay }) {
const sectionRef = useRef()
const ref = useRef()
const inView = useInView(ref, { once: true, margin: '-80px' })
useEffect(() => {
const ctx = gsap.context(() => {
gsap.from('.track-item', {
x: -24,
opacity: 0,
stagger: 0.06,
ease: 'power3.out',
scrollTrigger: {
trigger: sectionRef.current,
start: 'top 78%',
},
})
}, sectionRef)
return () => ctx.revert()
}, [])
return (
<section id="charts" ref={sectionRef} className="py-28 px-6"
style={{ background: 'var(--dark-surface)' }}>
<div className="section-divider" />
<div className="max-w-3xl mx-auto" ref={ref}>
<motion.div
initial={{ opacity: 0, y: 28 }}
animate={inView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.7 }}
className="flex items-end justify-between mb-12 mt-14"
>
<div>
<p className="text-xs tracking-[0.28em] uppercase gold-text mb-3 font-medium">Hot Right Now</p>
<h2 className="font-display font-bold text-white leading-tight"
style={{ fontSize: 'clamp(2rem, 5vw, 3.5rem)', letterSpacing: '-0.02em' }}>
Trending <span className="gold-gradient-text">Tracks</span>
</h2>
</div>
<div className="text-right hidden sm:block">
<p className="text-xs text-muted">Updated</p>
<p className="text-xs text-secondary font-mono mt-0.5">Weekly</p>
</div>
</motion.div>
{/* Column headers */}
<div className="flex items-center gap-4 px-4 mb-3 text-xs text-muted uppercase tracking-widest">
<span className="w-5 text-center">#</span>
<span className="w-11 flex-shrink-0" />
<span className="flex-1">Title</span>
<span className="hidden sm:block w-14 text-right">Plays</span>
<span className="w-9 text-right">Time</span>
<span className="w-7" />
</div>
<div className="space-y-0.5">
{tracks.map((track, i) => (
<div key={track.id} className="track-item">
<TrackRow track={track} index={i}
isActive={currentTrack?.id === track.id}
onPlay={onPlay} />
</div>
))}
</div>
{/* Show more */}
<motion.div
initial={{ opacity: 0 }}
animate={inView ? { opacity: 1 } : {}}
transition={{ delay: 0.5 }}
className="mt-8 text-center"
>
<motion.button
whileHover={{ scale: 1.04, borderColor: 'rgba(201,168,76,0.35)', color: 'var(--gold)' }}
className="px-6 py-2.5 rounded-full text-sm text-secondary border border-dark-border transition-all duration-200"
>
Show More Tracks
</motion.button>
</motion.div>
</div>
</section>
)
}
@@ -0,0 +1,88 @@
import { useRef, useMemo } from 'react'
import { Canvas, useFrame } from '@react-three/fiber'
import { motion } from 'framer-motion'
import * as THREE from 'three'
const COLS = 80
const ROWS = 30
function WaveGrid({ isPlaying }) {
const meshRef = useRef()
const dummy = useMemo(() => new THREE.Object3D(), [])
const count = COLS * ROWS
const basePositions = useMemo(() => {
const pos = []
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
pos.push({
x: (c - COLS / 2) * 0.38,
z: (r - ROWS / 2) * 0.55,
phase: (c * 0.25 + r * 0.4),
})
}
}
return pos
}, [])
useFrame(({ clock }) => {
if (!meshRef.current) return
const t = clock.getElapsedTime()
basePositions.forEach((p, i) => {
const wave1 = Math.sin(p.phase + t * 1.4) * 0.9
const wave2 = Math.sin(p.phase * 0.7 + t * 0.9 + 1.5) * 0.5
const y = wave1 + wave2
const scale = 0.04 + Math.abs(y) * 0.06
dummy.position.set(p.x, y * 0.6, p.z)
dummy.scale.setScalar(scale)
dummy.updateMatrix()
meshRef.current.setMatrixAt(i, dummy.matrix)
})
meshRef.current.instanceMatrix.needsUpdate = true
})
return (
<instancedMesh ref={meshRef} args={[null, null, count]}>
<sphereGeometry args={[1, 6, 6]} />
<meshStandardMaterial color="#c9a84c" emissive="#c9a84c" emissiveIntensity={0.5} roughness={0.2} metalness={0.8} toneMapped={false} />
</instancedMesh>
)
}
export default function WaveSection() {
return (
<section className="relative overflow-hidden" style={{ background: '#050505', height: '500px' }}>
<div className="section-divider" />
<Canvas camera={{ position: [0, 6, 14], fov: 52, near: 0.1, far: 100 }} gl={{ antialias: true, alpha: false }}
style={{ background: 'transparent' }}>
<ambientLight intensity={0.1} />
<pointLight position={[0, 8, 4]} intensity={5} color="#c9a84c" />
<pointLight position={[-8, 4, 0]} intensity={2} color="#e8c547" />
<pointLight position={[8, 4, 0]} intensity={2} color="#8a6f2e" />
<WaveGrid />
</Canvas>
{/* Centered content overlay */}
<div className="absolute inset-0 flex flex-col items-center justify-center pointer-events-none">
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
whileInView={{ opacity: 1, scale: 1 }}
viewport={{ once: true }}
transition={{ duration: 0.8 }}
className="text-center glass-gold px-10 py-6 rounded-2xl"
style={{ border: '1px solid rgba(201,168,76,0.2)' }}
>
<p className="text-xs tracking-[0.3em] uppercase gold-text mb-2">Frequency Field</p>
<h2 className="font-display font-bold text-3xl md:text-4xl text-white">
Every Beat. <span className="gold-text">Visualized.</span>
</h2>
</motion.div>
</div>
<div className="absolute top-0 left-0 right-0 h-24 pointer-events-none" style={{ background: 'linear-gradient(to bottom, #0d0d0d, transparent)' }} />
<div className="absolute bottom-0 left-0 right-0 h-24 pointer-events-none" style={{ background: 'linear-gradient(to top, #080808, transparent)' }} />
</section>
)
}
+26
View File
@@ -0,0 +1,26 @@
export const tracks = [
{ id: 1, title: 'Echoes of Tomorrow', artist: 'Solaris', album: 'Horizon', duration: '3:42', plays: '4.1M', cover: 'https://picsum.photos/seed/tr1/400/400' },
{ id: 2, title: 'Midnight Architecture', artist: 'The Void', album: 'Monolith', duration: '4:15', plays: '2.8M', cover: 'https://picsum.photos/seed/tr2/400/400' },
{ id: 3, title: 'Weightless', artist: 'AURA', album: 'Ascent', duration: '3:58', plays: '6.3M', cover: 'https://picsum.photos/seed/tr3/400/400' },
{ id: 4, title: 'Signal to Noise', artist: 'Parallax', album: 'Infinite Loop', duration: '5:02', plays: '1.9M', cover: 'https://picsum.photos/seed/tr4/400/400' },
{ id: 5, title: 'Golden Hour Fade', artist: 'Solaris', album: 'Horizon', duration: '4:33', plays: '3.2M', cover: 'https://picsum.photos/seed/tr5/400/400' },
{ id: 6, title: 'Glass Ceiling', artist: 'Echo System', album: 'Phase Shift', duration: '3:21', plays: '1.1M', cover: 'https://picsum.photos/seed/tr6/400/400' },
]
export const albums = [
{ id: 1, title: 'Horizon', artist: 'Solaris', year: 2024, genre: 'Ambient', cover: 'https://picsum.photos/seed/al1/600/600', tracks: 12 },
{ id: 2, title: 'Monolith', artist: 'The Void', year: 2024, genre: 'Dark Electronic', cover: 'https://picsum.photos/seed/al2/600/600', tracks: 9 },
{ id: 3, title: 'Ascent', artist: 'AURA', year: 2023, genre: 'Cinematic', cover: 'https://picsum.photos/seed/al3/600/600', tracks: 11 },
{ id: 4, title: 'Infinite Loop', artist: 'Parallax', year: 2024, genre: 'Electronic', cover: 'https://picsum.photos/seed/al4/600/600', tracks: 8 },
]
export const artists = [
{
id: 1,
name: 'Solaris',
genre: 'Ambient / Cinematic',
followers: '5.4M',
image: 'https://picsum.photos/seed/art1/800/1000',
bio: 'Architect of sound and space. Solaris crafts immersive sonic landscapes that exist between worlds — where silence becomes texture.',
},
]
+32
View File
@@ -0,0 +1,32 @@
import { useState, useCallback } from 'react'
import { tracks } from '../data/mockData'
export function useAudioPlayer() {
const [currentTrack, setCurrentTrack] = useState(tracks[0])
const [isPlaying, setIsPlaying] = useState(false)
const [progress, setProgress] = useState(0)
const [volume, setVolume] = useState(0.8)
const play = useCallback((track) => {
setCurrentTrack(track)
setIsPlaying(true)
}, [])
const togglePlay = useCallback(() => setIsPlaying(p => !p), [])
const next = useCallback(() => {
setCurrentTrack(t => {
const idx = tracks.findIndex(x => x.id === t.id)
return tracks[(idx + 1) % tracks.length]
})
}, [])
const prev = useCallback(() => {
setCurrentTrack(t => {
const idx = tracks.findIndex(x => x.id === t.id)
return tracks[(idx - 1 + tracks.length) % tracks.length]
})
}, [])
return { currentTrack, isPlaying, progress, volume, play, togglePlay, next, prev, setProgress, setVolume }
}
+150
View File
@@ -0,0 +1,150 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,300;0,400;0,500;0,600;1,300&family=Space+Grotesk:wght@400;500;600;700;800&family=Space+Mono:wght@400;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--gold: #c9a84c;
--gold-light: #e8c547;
--gold-dim: #8a6f2e;
--gold-faint: #3d2f0e;
--dark-base: #070709;
--dark-card: #0f0f12;
--dark-surface:#161619;
--dark-border: #242428;
--text-primary: #f0f0f2;
--text-secondary: #7a7a85;
--text-muted: #3a3a42;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
background: var(--dark-base);
color: var(--text-primary);
font-family: 'Inter', system-ui, sans-serif;
overflow-x: hidden;
line-height: 1.6;
}
/* ─── Scrollbar ─────────────────────────────── */
::-webkit-scrollbar { width: 3px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: var(--gold-dim); border-radius: 2px; }
/* ─── Selection ─────────────────────────────── */
::selection { background: rgba(201,168,76,0.25); color: #fff; }
/* ─── Glass utilities ───────────────────────── */
.glass {
background: rgba(255,255,255,0.035);
backdrop-filter: blur(28px) saturate(120%);
-webkit-backdrop-filter: blur(28px) saturate(120%);
border: 1px solid rgba(255,255,255,0.065);
}
.glass-gold {
background: rgba(201,168,76,0.055);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(201,168,76,0.18);
}
.glass-dark {
background: rgba(7,7,9,0.75);
backdrop-filter: blur(28px);
-webkit-backdrop-filter: blur(28px);
border: 1px solid rgba(255,255,255,0.055);
}
/* ─── Text utilities ────────────────────────── */
.gold-text { color: var(--gold); }
.gold-gradient-text {
background: linear-gradient(135deg, #c9a84c 0%, #e8c547 50%, #c9a84c 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.text-secondary { color: var(--text-secondary); }
.text-muted { color: var(--text-muted); }
/* ─── Glow utilities ────────────────────────── */
.gold-glow { box-shadow: 0 0 24px rgba(201,168,76,0.28), 0 0 64px rgba(201,168,76,0.08); }
.gold-glow-sm { box-shadow: 0 0 12px rgba(201,168,76,0.22); }
.gold-glow-lg { box-shadow: 0 0 40px rgba(201,168,76,0.35), 0 0 100px rgba(201,168,76,0.1); }
/* ─── Border utilities ──────────────────────── */
.gold-border { border: 1px solid rgba(201,168,76,0.25); }
.dim-border { border: 1px solid var(--dark-border); }
/* ─── Section dividers ──────────────────────── */
.section-divider {
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(201,168,76,0.2) 50%, transparent 100%);
}
.section-divider-bright {
height: 1px;
background: linear-gradient(90deg, transparent 0%, rgba(201,168,76,0.45) 50%, transparent 100%);
}
/* ─── Font helpers ──────────────────────────── */
.font-mono-num { font-family: 'Space Mono', monospace; font-variant-numeric: tabular-nums; }
/* ─── Input range ───────────────────────────── */
input[type='range'] {
-webkit-appearance: none;
height: 3px;
border-radius: 2px;
background: var(--dark-border);
outline: none;
cursor: pointer;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
width: 11px; height: 11px;
border-radius: 50%;
background: var(--gold);
cursor: pointer;
box-shadow: 0 0 6px rgba(201,168,76,0.5);
transition: transform 0.15s;
}
input[type='range']::-webkit-slider-thumb:hover { transform: scale(1.3); }
/* ─── Horizontal scroll no-bar ──────────────── */
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
/* ─── Animations ────────────────────────────── */
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-8px); }
}
@keyframes pulse-gold {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
@keyframes spin-slow {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes shimmer {
0% { background-position: -200% center; }
100% { background-position: 200% center; }
}
.animate-float { animation: float 3.5s ease-in-out infinite; }
.animate-pulse-gold{ animation: pulse-gold 2s ease-in-out infinite; }
.animate-spin-slow { animation: spin-slow 12s linear infinite; }
.shimmer {
background: linear-gradient(90deg, transparent 0%, rgba(201,168,76,0.15) 50%, transparent 100%);
background-size: 200% auto;
animation: shimmer 2.5s linear infinite;
}
canvas { display: block; }
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.jsx'
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
)