commit 694a7047a440856a9e40758f01004accd6236f27 Author: Sebastian Krüger Date: Sat Nov 29 17:51:00 2025 +0100 Initial commit diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..3f8642b --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,21 @@ +{ + "permissions": { + "allow": [ + "Bash(chmod:*)", + "Bash(npx tailwindcss:*)", + "Bash(pnpm add:*)", + "Bash(pnpm exec tailwindcss:*)", + "Bash(pnpm css:*)", + "Bash(hugo:*)", + "WebFetch(domain:pivoine.art)", + "Bash(curl:*)", + "WebFetch(domain:github.com)", + "WebSearch", + "WebFetch(domain:htmx.org)", + "Bash(cat:*)", + "Bash(git remote:*)" + ], + "deny": [], + "ask": [] + } +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d53e16 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,28 @@ +# Dependencies +node_modules/ + +# Build output +public/ +resources/ + +# Git +.git/ +.gitignore + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Hugo +hugo_stats.json + +# Development +*.log +*.md +!README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37282ca --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +# Dependencies +node_modules/ + +# Build output +public/ +resources/_gen/ + +# Hugo +hugo_stats.json +.hugo_build.lock + +# Environment +.env +.env.* + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Cache +.cache/ diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..da8f41f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +# Stage 1: Build +FROM node:22-alpine AS builder + +# Install Hugo +RUN apk add --no-cache hugo + +# Install pnpm +RUN corepack enable && corepack prepare pnpm@latest --activate + +WORKDIR /app + +# Copy package files first (for layer caching) +COPY package.json pnpm-lock.yaml ./ +RUN pnpm install --frozen-lockfile + +# Copy source files +COPY . . + +# Build CSS and Hugo site +RUN pnpm build + +# Stage 2: Production +FROM nginx:alpine + +# Copy built site +COPY --from=builder /app/public /usr/share/nginx/html + +# Copy nginx config +COPY nginx.conf /etc/nginx/conf.d/default.conf + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..c36fb2e --- /dev/null +++ b/README.md @@ -0,0 +1,108 @@ +``` + · · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · + · · · +``` + +# PIVOINE.ART + +> Technology and sound, creating massive beats to push the boundaries of audio perception. + +--- + +## About + +An immersive audio experience. Electronic music meets WebGL visualization. +5000 particles react to every frequency. The logo breathes with the beat. + +Dark. Minimal. Precise. + +--- + +## Features + +``` +▸ WebGL Visualizer 5000 particles · 512 FFT · bass/mid/high reactivity +▸ Reactive Logo Canvas-based · audio-driven · mouse-aware +▸ Persistent Player Play · seek · volume · progress +▸ Web Audio API Real-time frequency analysis +▸ SPA Navigation htmx · smooth transitions · Alpine.js state +``` + +--- + +## Stack + +``` +Hugo ─────────────── Static site generation +Tailwind CSS 4 ───── Utility-first styling +Alpine.js ────────── Lightweight reactivity +Three.js ─────────── WebGL rendering +Web Audio API ────── Frequency analysis +pnpm ─────────────── Package management +``` + +--- + +## Development + +```bash +# Install dependencies +pnpm install + +# Development server +pnpm dev + +# Build for production +pnpm build +``` + +--- + +## Deploy + +```bash +# Build image +docker build -t pivoine-art . + +# Run container +docker run -p 8080:80 pivoine-art +``` + +--- + +## Structure + +``` +├── assets/ +│ ├── css/ # Tailwind source +│ └── js/ # Visualizer · Logo · Main +├── config/ # Hugo configuration +├── content/ # Markdown content +├── layouts/ # Hugo templates +└── static/ # Compiled assets +``` + +--- + +## Author + +**Valknar** +valknar@pivoine.art + +--- + +## License + +All audio content © Valknar. All rights reserved. +Code: MIT diff --git a/archetypes/default.md b/archetypes/default.md new file mode 100644 index 0000000..25b6752 --- /dev/null +++ b/archetypes/default.md @@ -0,0 +1,5 @@ ++++ +date = '{{ .Date }}' +draft = true +title = '{{ replace .File.ContentBaseName "-" " " | title }}' ++++ diff --git a/archetypes/tracks.md b/archetypes/tracks.md new file mode 100644 index 0000000..4cda9d0 --- /dev/null +++ b/archetypes/tracks.md @@ -0,0 +1,17 @@ +--- +title: "{{ replace .File.ContentBaseName "-" " " | title }}" +date: {{ .Date }} +draft: true +description: "" + +# Audio +audio: "" +duration: "" + +# Metadata +artist: "Valknar" +genre: "" + +# Taxonomies +tags: [] +--- diff --git a/assets/css/main.css b/assets/css/main.css new file mode 100644 index 0000000..ec3dfe6 --- /dev/null +++ b/assets/css/main.css @@ -0,0 +1,299 @@ +@import "tailwindcss"; + +@source "../../layouts/_default/*.html"; +@source "../../layouts/partials/*.html"; +@source "../../layouts/partials/head/*.html"; +@source "../../layouts/tracks/*.html"; + +html { + scroll-padding-top: 4rem; /* Account for fixed header height */ +} + +:root { + /* === SURFACE COLORS (Dark Theme) === */ + --color-surface-0: #0a0a0a; + --color-surface-1: #121212; + --color-surface-2: #1a1a1a; + --color-surface-3: #232323; + --color-surface-4: #2c2c2c; + + /* === TEXT COLORS === */ + --color-text-primary: #e8e8e8; + --color-text-secondary: #888888; + --color-text-muted: #555555; + + /* === ACCENT === */ + --color-accent: #ffffff; + --color-accent-dim: #666666; + --color-accent-glow: rgba(255, 255, 255, 0.1); + + /* === BORDERS === */ + --color-border: #2a2a2a; + + /* === TYPOGRAPHY === */ + --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + + /* === ANIMATION === */ + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + --duration-page: 400ms; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + + /* === SPACING === */ + --radius-sm: 0.125rem; + --radius-md: 0.25rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; +} + +@theme inline { + /* Map CSS vars to Tailwind colors */ + --color-surface-0: var(--color-surface-0); + --color-surface-1: var(--color-surface-1); + --color-surface-2: var(--color-surface-2); + --color-surface-3: var(--color-surface-3); + --color-surface-4: var(--color-surface-4); + --color-text-primary: var(--color-text-primary); + --color-text-secondary: var(--color-text-secondary); + --color-text-muted: var(--color-text-muted); + --color-accent: var(--color-accent); + --color-border: var(--color-border); + + /* Font family */ + --font-family-mono: var(--font-mono); + + /* Z-index */ + --z-index-sticky: 200; + --z-index-visualizer: 5; + --z-index-player: 400; +} + +@layer base { + html { + font-family: var(--font-mono); + background: var(--color-surface-0); + color: var(--color-text-primary); + letter-spacing: 0.02em; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + } + + body { + min-height: 100vh; + } + + ::selection { + background: var(--color-accent); + color: var(--color-surface-0); + } + + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--color-surface-1); + } + ::-webkit-scrollbar-thumb { + background: var(--color-surface-3); + border-radius: var(--radius-full); + } + ::-webkit-scrollbar-thumb:hover { + background: var(--color-surface-4); + } + + :focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + } + + a { + color: var(--color-text-primary); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-out); + } + a:hover { + color: var(--color-accent); + } +} + +@layer components { + /* htmx transitions */ + .htmx-request #main-content { + opacity: 0; + transform: translateY(8px); + transition: all var(--duration-page) var(--ease-out); + } + .htmx-settling #main-content { + opacity: 1; + transform: translateY(0); + } + + /* Animations */ + .fade-in-up { + opacity: 0; + transform: translateY(24px); + transition: all var(--duration-slow) var(--ease-out); + } + .fade-in-up.is-visible { + opacity: 1; + transform: translateY(0); + } + + .link-hover { + position: relative; + } + .link-hover::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 1px; + background: var(--color-accent); + transform: scaleX(0); + transform-origin: right; + transition: transform var(--duration-normal) var(--ease-out); + } + .link-hover:hover::after { + transform: scaleX(1); + transform-origin: left; + } + + /* Audio Player */ + .audio-player { + background: var(--color-surface-1); + border-top: 1px solid var(--color-border); + } + .audio-player__progress { + appearance: none; + width: 100%; + height: 4px; + background: var(--color-surface-3); + cursor: pointer; + } + .audio-player__progress::-webkit-slider-thumb { + appearance: none; + width: 12px; + height: 12px; + background: var(--color-accent); + border-radius: var(--radius-full); + } + + /* Track Card */ + .track-card { + background: rgba(18, 18, 18, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--color-border); + transition: all var(--duration-normal) var(--ease-out); + } + .track-card:hover { + background: rgba(18, 18, 18, 0.8); + border-color: var(--color-surface-4); + transform: translateY(-2px); + } + .track-card__cover { + aspect-ratio: 1; + object-fit: cover; + filter: grayscale(20%); + transition: filter var(--duration-normal); + } + .track-card:hover .track-card__cover { + filter: grayscale(0%); + } + + /* Prose */ + .prose { + color: var(--color-text-secondary); + line-height: 1.75; + } + .prose h1, + .prose h2, + .prose h3, + .prose h4 { + color: var(--color-text-primary); + font-weight: 500; + margin-top: 2em; + margin-bottom: 0.5em; + } + .prose h1 { + font-size: 2rem; + } + .prose h2 { + font-size: 1.5rem; + } + .prose h3 { + font-size: 1.25rem; + } + .prose p { + margin-bottom: 1.25em; + } + .prose a { + text-decoration: underline; + text-underline-offset: 2px; + } + .prose strong { + color: var(--color-text-primary); + font-weight: 600; + } + .prose ul, + .prose ol { + margin-bottom: 1.25em; + padding-left: 1.5em; + } + .prose li { + margin-bottom: 0.5em; + } + .prose code { + background: var(--color-surface-2); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); + font-size: 0.9em; + } + .prose pre { + background: var(--color-surface-1); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1em; + overflow-x: auto; + margin: 1.5em 0; + } + .prose pre code { + background: none; + padding: 0; + } +} + +@layer utilities { + .container-narrow { + max-width: 48rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .container-wide { + max-width: 80rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .z-sticky { + z-index: 200; + } + .z-visualizer { + z-index: -1; + opacity: 0.25; + } + .z-player { + z-index: 400; + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} diff --git a/assets/js/logo/reactive-logo.js b/assets/js/logo/reactive-logo.js new file mode 100644 index 0000000..19a0236 --- /dev/null +++ b/assets/js/logo/reactive-logo.js @@ -0,0 +1,181 @@ +/** + * Reactive Logo + * Small WebGL canvas logo that reacts to audio and mouse + */ + +import * as THREE from 'three'; + +export class ReactiveLogo { + constructor(canvas, audioManager) { + this.canvas = canvas; + this.audioManager = audioManager; + this.mouse = { x: 0, y: 0 }; + this.running = false; + this.time = 0; + + if (!canvas) { + console.warn('ReactiveLogo: No canvas provided'); + return; + } + + this.init(); + } + + init() { + const size = 32; + + // Scene + this.scene = new THREE.Scene(); + + // Camera + this.camera = new THREE.PerspectiveCamera(50, 1, 0.1, 100); + this.camera.position.z = 3; + + // Renderer + this.renderer = new THREE.WebGLRenderer({ + canvas: this.canvas, + alpha: true, + antialias: true + }); + this.renderer.setSize(size, size); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Create logo geometry (simple points in a circular pattern) + this.createLogoParticles(); + + // Mouse tracking + this.canvas.addEventListener('mouseenter', () => this.onMouseEnter()); + this.canvas.addEventListener('mouseleave', () => this.onMouseLeave()); + window.addEventListener('mousemove', (e) => this.onMouseMove(e)); + + this.start(); + } + + createLogoParticles() { + const count = 50; + const geometry = new THREE.BufferGeometry(); + const positions = new Float32Array(count * 3); + const randoms = new Float32Array(count); + + // Create points in a circular pattern + for (let i = 0; i < count; i++) { + const angle = (i / count) * Math.PI * 2; + const radius = 0.8 + Math.random() * 0.2; + + positions[i * 3] = Math.cos(angle) * radius; + positions[i * 3 + 1] = Math.sin(angle) * radius; + positions[i * 3 + 2] = (Math.random() - 0.5) * 0.2; + + randoms[i] = Math.random(); + } + + geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)); + + const material = new THREE.ShaderMaterial({ + vertexShader: ` + uniform float uTime; + uniform float uAudio; + attribute float aRandom; + + void main() { + vec3 pos = position; + + // Bouncy pulsing + float pulse = 1.0 + sin(uTime * 5.0 + aRandom * 6.28) * 0.15; + pulse += sin(uTime * 8.0 + aRandom * 3.14) * 0.1; + pos *= pulse; + + // Audio reactivity - bouncy + float audioBounce = uAudio * (1.0 + sin(uTime * 10.0) * 0.3); + pos *= 1.0 + audioBounce * 0.6; + + vec4 mvPos = modelViewMatrix * vec4(pos, 1.0); + gl_PointSize = 3.0 * (1.0 + uAudio * 0.8); + gl_Position = projectionMatrix * mvPos; + } + `, + fragmentShader: ` + void main() { + float dist = length(gl_PointCoord - 0.5); + if (dist > 0.5) discard; + float alpha = 1.0 - smoothstep(0.2, 0.5, dist); + gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); + } + `, + uniforms: { + uTime: { value: 0 }, + uAudio: { value: 0 } + }, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + + this.logoMesh = new THREE.Points(geometry, material); + this.scene.add(this.logoMesh); + } + + onMouseEnter() { + this.isHovered = true; + } + + onMouseLeave() { + this.isHovered = false; + } + + onMouseMove(e) { + if (!this.isHovered) return; + const rect = this.canvas.getBoundingClientRect(); + this.mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1; + this.mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1; + } + + start() { + if (this.running) return; + this.running = true; + this.animate(); + } + + pause() { + this.running = false; + } + + animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + + this.time += 0.016; + + // Get audio level + let audioLevel = 0; + if (this.audioManager?.isInitialized) { + const bands = this.audioManager.getFrequencyBands(); + audioLevel = (bands.low + bands.mid + bands.high) / 3; + } + + // Update uniforms + if (this.logoMesh?.material?.uniforms) { + this.logoMesh.material.uniforms.uTime.value = this.time; + this.logoMesh.material.uniforms.uAudio.value = audioLevel; + } + + // Rotate based on mouse + if (this.logoMesh) { + this.logoMesh.rotation.x += (this.mouse.y * 0.5 - this.logoMesh.rotation.x) * 0.1; + this.logoMesh.rotation.y += (this.mouse.x * 0.5 - this.logoMesh.rotation.y) * 0.1; + this.logoMesh.rotation.z = this.time * 0.2; + } + + this.renderer.render(this.scene, this.camera); + } + + destroy() { + this.running = false; + this.logoMesh?.geometry?.dispose(); + this.logoMesh?.material?.dispose(); + this.renderer?.dispose(); + } +} + +export default ReactiveLogo; diff --git a/assets/js/main.js b/assets/js/main.js new file mode 100644 index 0000000..a57061e --- /dev/null +++ b/assets/js/main.js @@ -0,0 +1,257 @@ +/** + * Pivoine.art - Main JavaScript + * Audio blog with WebGL visualizers + */ + +import { Visualizer } from './visualizer/scene.js'; +import { ReactiveLogo } from './logo/reactive-logo.js'; + +// Audio Manager - Web Audio API wrapper +class AudioManager { + constructor() { + this.audio = document.createElement('audio'); + this.audio.crossOrigin = 'anonymous'; + this.audioContext = null; + this.analyser = null; + this.source = null; + this.frequencyData = null; + this.isInitialized = false; + } + + async init() { + if (this.isInitialized) return; + + try { + this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); + this.analyser = this.audioContext.createAnalyser(); + this.analyser.fftSize = 512; + this.analyser.smoothingTimeConstant = 0.8; + + this.source = this.audioContext.createMediaElementSource(this.audio); + this.source.connect(this.analyser); + this.analyser.connect(this.audioContext.destination); + + this.frequencyData = new Uint8Array(this.analyser.frequencyBinCount); + this.isInitialized = true; + + // Update Alpine store on audio events + this.audio.addEventListener('timeupdate', () => { + if (window.Alpine) { + Alpine.store('audio').progress = this.audio.currentTime; + Alpine.store('audio').duration = this.audio.duration || 0; + } + }); + + this.audio.addEventListener('ended', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = false; + } + }); + + this.audio.addEventListener('play', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = true; + } + }); + + this.audio.addEventListener('pause', () => { + if (window.Alpine) { + Alpine.store('audio').isPlaying = false; + } + }); + } catch (e) { + console.error('Failed to initialize audio context:', e); + } + } + + async play(url) { + await this.init(); + + if (this.audioContext?.state === 'suspended') { + await this.audioContext.resume(); + } + + if (url && url !== this.audio.src) { + this.audio.src = url; + } + + await this.audio.play(); + } + + pause() { + this.audio.pause(); + } + + toggle() { + if (this.audio.paused) { + this.audio.play(); + } else { + this.audio.pause(); + } + } + + seek(time) { + this.audio.currentTime = time; + } + + setVolume(v) { + this.audio.volume = Math.max(0, Math.min(1, v)); + } + + getFrequencyData() { + if (this.analyser) { + this.analyser.getByteFrequencyData(this.frequencyData); + } + return this.frequencyData; + } + + getFrequencyBands() { + const data = this.getFrequencyData(); + if (!data) return { low: 0, mid: 0, high: 0 }; + + const len = data.length; + const low = this._avg(data, 0, len * 0.1) / 255; + const mid = this._avg(data, len * 0.1, len * 0.5) / 255; + const high = this._avg(data, len * 0.5, len) / 255; + return { low, mid, high }; + } + + _avg(data, start, end) { + let sum = 0; + for (let i = Math.floor(start); i < Math.floor(end); i++) { + sum += data[i]; + } + return sum / (end - start); + } +} + +// Initialize global instances +if (!window.__pivoine) { + const audioManager = new AudioManager(); + + window.__pivoine = { + audioManager, + visualizer: null, + logo: null + }; + + // Initialize WebGL components after DOM is ready + const initWebGL = () => { + // Main visualizer (fullscreen background) + const visualizerCanvas = document.getElementById('visualizer'); + if (visualizerCanvas && !window.__pivoine.visualizer) { + window.__pivoine.visualizer = new Visualizer(visualizerCanvas, audioManager); + } + + // Logo in header + const logoCanvas = document.getElementById('logo-canvas'); + if (logoCanvas && !window.__pivoine.logo) { + window.__pivoine.logo = new ReactiveLogo(logoCanvas, audioManager); + } + }; + + // Initialize on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initWebGL); + } else { + initWebGL(); + } +} + +// Alpine.js initialization +document.addEventListener('alpine:init', () => { + // Global audio store + Alpine.store('audio', { + currentTrack: null, + isPlaying: false, + progress: 0, + duration: 0, + volume: 0.8 + }); + + // Main app component + Alpine.data('app', () => ({ + init() { + // Restore volume from localStorage + const savedVolume = localStorage.getItem('pivoine-volume'); + if (savedVolume) { + Alpine.store('audio').volume = parseFloat(savedVolume); + window.__pivoine.audioManager.setVolume(parseFloat(savedVolume)); + } + } + })); + + // Player UI component + Alpine.data('playerUI', () => ({ + togglePlay() { + window.__pivoine.audioManager.toggle(); + }, + + seek(time) { + window.__pivoine.audioManager.seek(parseFloat(time)); + }, + + setVolume(v) { + const volume = parseFloat(v); + Alpine.store('audio').volume = volume; + window.__pivoine.audioManager.setVolume(volume); + localStorage.setItem('pivoine-volume', volume); + }, + + toggleMute() { + const store = Alpine.store('audio'); + if (store.volume > 0) { + this._previousVolume = store.volume; + this.setVolume(0); + } else { + this.setVolume(this._previousVolume || 0.8); + } + }, + + formatTime(seconds) { + if (!seconds || isNaN(seconds)) return '0:00'; + const mins = Math.floor(seconds / 60); + const secs = Math.floor(seconds % 60); + return `${mins}:${secs.toString().padStart(2, '0')}`; + }, + + _previousVolume: 0.8 + })); +}); + +// htmx lifecycle hooks +document.body.addEventListener('htmx:beforeSwap', () => { + // Pause visualizer during page transition for performance + window.__pivoine?.visualizer?.pause(); +}); + +document.body.addEventListener('htmx:afterSwap', () => { + // Resume visualizer after swap + window.__pivoine?.visualizer?.resume(); + // Re-initialize scroll animations + window.dispatchEvent(new CustomEvent('page:loaded')); +}); + +// Scroll animations with Intersection Observer +function initScrollAnimations() { + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add('is-visible'); + } + }); + }, + { threshold: 0.1, rootMargin: '0px 0px -50px 0px' } + ); + + document.querySelectorAll('.fade-in-up').forEach((el) => { + observer.observe(el); + }); +} + +// Initialize on page load +document.addEventListener('DOMContentLoaded', initScrollAnimations); +window.addEventListener('page:loaded', initScrollAnimations); + +export { AudioManager }; diff --git a/assets/js/visualizer/particles.js b/assets/js/visualizer/particles.js new file mode 100644 index 0000000..dfb6adb --- /dev/null +++ b/assets/js/visualizer/particles.js @@ -0,0 +1,145 @@ +/** + * Particle System + * Audio-reactive 3D particle cloud + */ + +import * as THREE from 'three'; + +// Vertex Shader +const vertexShader = ` +uniform float uTime; +uniform float uLow; +uniform float uMid; +uniform float uHigh; +uniform float uSize; + +attribute float aRandom; +attribute float aScale; + +varying float vAlpha; + +void main() { + vec3 pos = position; + + // Base rotation + float angle = uTime * 0.2; + mat3 rotation = mat3( + cos(angle), 0.0, sin(angle), + 0.0, 1.0, 0.0, + -sin(angle), 0.0, cos(angle) + ); + pos = rotation * pos; + + // Bass affects scale/expansion + float bassScale = 1.0 + uLow * 0.4; + pos *= bassScale; + + // Mid frequencies create wave motion + float wave = sin(pos.x * 0.1 + uTime * 2.0) * uMid * 8.0; + pos.y += wave * aRandom; + + // High frequencies add jitter/sparkle + vec3 jitter = normalize(pos) * uHigh * aRandom * 5.0; + pos += jitter; + + // Calculate view position + vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); + + // Point size with perspective + float size = uSize * aScale; + size *= (1.0 + uLow * 0.5); // Pulse with bass + gl_PointSize = size * (300.0 / -mvPosition.z); + + // Alpha based on distance and audio + vAlpha = 0.6 + uLow * 0.3; + + gl_Position = projectionMatrix * mvPosition; +} +`; + +// Fragment Shader +const fragmentShader = ` +varying float vAlpha; + +void main() { + // Circular point with soft edge + float dist = length(gl_PointCoord - vec2(0.5)); + if (dist > 0.5) discard; + + float alpha = 1.0 - smoothstep(0.2, 0.5, dist); + alpha *= vAlpha; + + // Pure white color for minimal aesthetic + gl_FragColor = vec4(1.0, 1.0, 1.0, alpha); +} +`; + +export class ParticleSystem { + constructor(count = 5000) { + this.count = count; + this.createGeometry(); + this.createMaterial(); + this.mesh = new THREE.Points(this.geometry, this.material); + } + + createGeometry() { + this.geometry = new THREE.BufferGeometry(); + + const positions = new Float32Array(this.count * 3); + const randoms = new Float32Array(this.count); + const scales = new Float32Array(this.count); + + for (let i = 0; i < this.count; i++) { + const i3 = i * 3; + + // Spherical distribution + const radius = 20 + Math.random() * 30; + const theta = Math.random() * Math.PI * 2; + const phi = Math.acos(2 * Math.random() - 1); + + positions[i3] = radius * Math.sin(phi) * Math.cos(theta); + positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta); + positions[i3 + 2] = radius * Math.cos(phi); + + randoms[i] = Math.random(); + scales[i] = 0.5 + Math.random() * 1.5; + } + + this.geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3)); + this.geometry.setAttribute('aRandom', new THREE.BufferAttribute(randoms, 1)); + this.geometry.setAttribute('aScale', new THREE.BufferAttribute(scales, 1)); + } + + createMaterial() { + this.material = new THREE.ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + uTime: { value: 0 }, + uLow: { value: 0 }, + uMid: { value: 0 }, + uHigh: { value: 0 }, + uSize: { value: 2.0 } + }, + transparent: true, + blending: THREE.AdditiveBlending, + depthWrite: false + }); + } + + update(bands, time) { + if (!this.material) return; + + this.material.uniforms.uTime.value = time; + this.material.uniforms.uLow.value = bands.low || 0; + this.material.uniforms.uMid.value = bands.mid || 0; + this.material.uniforms.uHigh.value = bands.high || 0; + } + + dispose() { + this.geometry?.dispose(); + this.material?.dispose(); + } +} + +export default ParticleSystem; diff --git a/assets/js/visualizer/scene.js b/assets/js/visualizer/scene.js new file mode 100644 index 0000000..deaf287 --- /dev/null +++ b/assets/js/visualizer/scene.js @@ -0,0 +1,110 @@ +/** + * WebGL Visualizer Scene + * Three.js particle system that reacts to audio frequency data + */ + +import * as THREE from 'three'; +import { ParticleSystem } from './particles.js'; + +export class Visualizer { + constructor(canvas, audioManager) { + this.canvas = canvas; + this.audioManager = audioManager; + this.running = false; + this.time = 0; + + if (!canvas) { + console.warn('Visualizer: No canvas provided'); + return; + } + + this.init(); + } + + init() { + // Scene setup + this.scene = new THREE.Scene(); + + // Camera + this.camera = new THREE.PerspectiveCamera( + 75, + window.innerWidth / window.innerHeight, + 0.1, + 1000 + ); + this.camera.position.z = 50; + + // Renderer + this.renderer = new THREE.WebGLRenderer({ + canvas: this.canvas, + alpha: true, + antialias: true, + powerPreference: 'high-performance' + }); + this.renderer.setSize(window.innerWidth, window.innerHeight); + this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // Particles + this.particles = new ParticleSystem(5000); + this.scene.add(this.particles.mesh); + + // Event listeners + window.addEventListener('resize', () => this.resize()); + + // Start animation + this.start(); + } + + resize() { + if (!this.camera || !this.renderer) return; + + this.camera.aspect = window.innerWidth / window.innerHeight; + this.camera.updateProjectionMatrix(); + this.renderer.setSize(window.innerWidth, window.innerHeight); + } + + start() { + if (this.running) return; + this.running = true; + this.animate(); + } + + pause() { + this.running = false; + } + + resume() { + this.start(); + } + + animate() { + if (!this.running) return; + requestAnimationFrame(() => this.animate()); + + this.time += 0.01; + + // Get audio frequency bands + let bands = { low: 0, mid: 0, high: 0 }; + if (this.audioManager?.isInitialized) { + bands = this.audioManager.getFrequencyBands(); + } + + // Update particles with audio data + this.particles.update(bands, this.time); + + // Subtle camera movement + this.camera.position.x = Math.sin(this.time * 0.1) * 3; + this.camera.position.y = Math.cos(this.time * 0.15) * 2; + this.camera.lookAt(0, 0, 0); + + this.renderer.render(this.scene, this.camera); + } + + destroy() { + this.running = false; + this.particles?.dispose(); + this.renderer?.dispose(); + } +} + +export default Visualizer; diff --git a/assets/jsconfig.json b/assets/jsconfig.json new file mode 100644 index 0000000..377218c --- /dev/null +++ b/assets/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "*": [ + "*" + ] + } + } +} \ No newline at end of file diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml new file mode 100644 index 0000000..20809c9 --- /dev/null +++ b/config/_default/hugo.toml @@ -0,0 +1,29 @@ +baseURL = "https://pivoine.art/" +languageCode = "en-us" +title = "Valknar's" + +[permalinks] + tracks = "/tracks/:slug/" + +[taxonomies] + tag = "tags" + genre = "genres" + +[outputs] + home = ["HTML", "RSS"] + section = ["HTML", "RSS"] + +[sitemap] + changefreq = "weekly" + priority = 0.5 + +[markup.goldmark.renderer] + unsafe = true + +[build] + writeStats = true + +[module] + [module.hugoVersion] + extended = false + min = "0.128.0" diff --git a/config/_default/menus.toml b/config/_default/menus.toml new file mode 100644 index 0000000..107e918 --- /dev/null +++ b/config/_default/menus.toml @@ -0,0 +1,14 @@ +[[main]] + name = "Tracks" + url = "/tracks/" + weight = 10 + +[[main]] + name = "About" + url = "/about/" + weight = 20 + +[[main]] + name = "Imprint" + url = "/imprint/" + weight = 30 diff --git a/config/_default/params.toml b/config/_default/params.toml new file mode 100644 index 0000000..7b281b2 --- /dev/null +++ b/config/_default/params.toml @@ -0,0 +1,16 @@ +description = "Valknar's audio" +author = "Valknar" +email = "valknar@pivoine.art" + +[jellyfin] + baseURL = "https://jellyfin.media.pivoine.art" + # API key via environment variable: HUGO_PARAMS_JELLYFIN_APIKEY + +[umami] + enabled = true + websiteID = "" # Set your Umami website ID + src = "" # Your Umami instance URL + +[visualizer] + particleCount = 5000 + fftSize = 512 diff --git a/config/development/hugo.toml b/config/development/hugo.toml new file mode 100644 index 0000000..a283b79 --- /dev/null +++ b/config/development/hugo.toml @@ -0,0 +1,4 @@ +baseURL = "http://localhost:1313/" + +[params.umami] + enabled = false diff --git a/config/production/hugo.toml b/config/production/hugo.toml new file mode 100644 index 0000000..c61b0b4 --- /dev/null +++ b/config/production/hugo.toml @@ -0,0 +1,7 @@ +baseURL = "https://pivoine.art/" + +[minify] + minifyOutput = true + +[params.umami] + enabled = true diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..fe01af1 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,4 @@ +--- +title: "Valknar's" +description: "Valknar's audio" +--- diff --git a/content/about.md b/content/about.md new file mode 100644 index 0000000..a2663ac --- /dev/null +++ b/content/about.md @@ -0,0 +1,21 @@ +--- +title: "About" +description: "About Valknar and his music" +--- + +## About Valknar + +Technology and sound, creating massive beats to push the boundaries of audio perception. + +### Equipment + +Debian GNU/Linux 13, KDE-Plasma 6 on Wayland, 16 × AMD Ryzen 7 8745HS, AI + +### Dedication + +The love of my life, Palina. + +### Contact + +For collaborations, licensing, or just to say hello: +**valknar@pivoine.art** diff --git a/content/imprint.md b/content/imprint.md new file mode 100644 index 0000000..90b2fc6 --- /dev/null +++ b/content/imprint.md @@ -0,0 +1,20 @@ +--- +title: "Imprint" +description: "Legal information and imprint" +--- + +## Imprint + +**Responsible for content:** + +[Valknar](mailto:valknar@pivoine.art) + +## Privacy + +This website uses [Umami](https://umami.is) for privacy-respecting analytics. No personal data is collected or shared with third parties. + +## Copyright + +All audio content on this site is created by Valknar unless otherwise noted. All rights reserved. + +For licensing inquiries, please contact valknar@pivoine.art. diff --git a/content/tracks/_index.md b/content/tracks/_index.md new file mode 100644 index 0000000..a60a53e --- /dev/null +++ b/content/tracks/_index.md @@ -0,0 +1,4 @@ +--- +title: "Tracks" +description: "All audio" +--- diff --git a/content/tracks/changed-her-mind-again/cover.png b/content/tracks/changed-her-mind-again/cover.png new file mode 100644 index 0000000..da49484 Binary files /dev/null and b/content/tracks/changed-her-mind-again/cover.png differ diff --git a/content/tracks/changed-her-mind-again/index.md b/content/tracks/changed-her-mind-again/index.md new file mode 100644 index 0000000..31370b1 --- /dev/null +++ b/content/tracks/changed-her-mind-again/index.md @@ -0,0 +1,17 @@ +--- +title: "Changed Her Mind Again" +date: 2025-09-10 +draft: false +description: "Again..." + +audio: "https://jellyfin.media.pivoine.art/Items/ebd4e9f45b9dda1cada560af5e6cb7a8/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60" +duration: "4:07" + +artist: "Valknar" +genre: "Breakbeat" + +tags: + - blues + - rock + - dope +--- diff --git a/content/tracks/changed-her-mind-again/preview.mp4 b/content/tracks/changed-her-mind-again/preview.mp4 new file mode 100644 index 0000000..8d625ae Binary files /dev/null and b/content/tracks/changed-her-mind-again/preview.mp4 differ diff --git a/content/tracks/shadow/cover.png b/content/tracks/shadow/cover.png new file mode 100644 index 0000000..2af784b Binary files /dev/null and b/content/tracks/shadow/cover.png differ diff --git a/content/tracks/shadow/index.md b/content/tracks/shadow/index.md new file mode 100644 index 0000000..e165cb3 --- /dev/null +++ b/content/tracks/shadow/index.md @@ -0,0 +1,17 @@ +--- +title: "Shadow" +date: 2025-11-06 +draft: false +description: "In my shadow" + +audio: "https://jellyfin.media.pivoine.art/Items/aada4e4ac0320e258595976a9edb6a09/Download?api_key=53d58826a49b4026a815d13db5e38ff7" +duration: "3:26" + +artist: "Valknar" +genre: "Dubstep" + +tags: + - dark + - dope + - ghetto +--- diff --git a/content/tracks/shadow/preview.mp4 b/content/tracks/shadow/preview.mp4 new file mode 100644 index 0000000..0cbd3a6 Binary files /dev/null and b/content/tracks/shadow/preview.mp4 differ diff --git a/content/tracks/the-end-of-all/cover.png b/content/tracks/the-end-of-all/cover.png new file mode 100644 index 0000000..5bc22ee Binary files /dev/null and b/content/tracks/the-end-of-all/cover.png differ diff --git a/content/tracks/the-end-of-all/index.md b/content/tracks/the-end-of-all/index.md new file mode 100644 index 0000000..748afe6 --- /dev/null +++ b/content/tracks/the-end-of-all/index.md @@ -0,0 +1,17 @@ +--- +title: "The End Of All" +date: 2025-11-16 +draft: false +description: "The end of all is just the beginning" + +audio: "https://jellyfin.media.pivoine.art/Items/60d39ab0aad880627e8fb85cf1ee7b40/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60" +duration: "4:35" + +artist: "Valknar" +genre: "Drum'n'Bass" + +tags: + - dark + - deep + - soul +--- diff --git a/content/tracks/the-end-of-all/preview.mp4 b/content/tracks/the-end-of-all/preview.mp4 new file mode 100644 index 0000000..4b0bd89 Binary files /dev/null and b/content/tracks/the-end-of-all/preview.mp4 differ diff --git a/content/tracks/the-moon/cover.png b/content/tracks/the-moon/cover.png new file mode 100644 index 0000000..4ef8534 Binary files /dev/null and b/content/tracks/the-moon/cover.png differ diff --git a/content/tracks/the-moon/index.md b/content/tracks/the-moon/index.md new file mode 100644 index 0000000..fa33e17 --- /dev/null +++ b/content/tracks/the-moon/index.md @@ -0,0 +1,17 @@ +--- +title: "The Moon" +date: 2025-08-19 +draft: false +description: "Because we are the last" + +audio: "https://jellyfin.media.pivoine.art/Items/d91333f9c7c4d8251174c86a81588cbd/Download?api_key=64d0a008577f49a4aa276d4bbe5c5d60" +duration: "3:44" + +artist: "Valknar" +genre: "Drum'n'Bass" + +tags: + - dark + - deep + - soul +--- diff --git a/content/tracks/the-moon/preview.mp4 b/content/tracks/the-moon/preview.mp4 new file mode 100644 index 0000000..b0680a8 Binary files /dev/null and b/content/tracks/the-moon/preview.mp4 differ diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html new file mode 100755 index 0000000..a9b2a4b --- /dev/null +++ b/layouts/_default/baseof.html @@ -0,0 +1,138 @@ + + + + {{- partial "head/meta.html" . -}} + {{- partial "head/opengraph.html" . -}} + {{- partial "head/twitter.html" . -}} + {{- partial "head/json-ld.html" . -}} + {{- partial "head/preload.html" . -}} + {{- partial "head/favicon.html" . -}} + + {{/* CSS - built by Tailwind CLI to static folder */}} + + + + {{/* WebGL Background Canvas (preserved across navigation) */}} + + + {{- partial "header.html" . -}} + +
+ {{- block "main" . }}{{- end -}} +
+ + {{- partial "footer.html" . -}} + + {{/* Persistent Audio Player (preserved across navigation) */}} +
+ {{- partial "player.html" . -}} +
+ + {{/* WebGL Visualizer Canvas (preserved) */}} + + + {{/* Alpine.js - data and stores defined before CDN loads */}} + + + {{/* htmx */}} + + + {{/* Alpine.js */}} + + + {{/* Main JS - audio manager and visualizer */}} + {{- $js := resources.Get "js/main.js" -}} + {{- if $js -}} + {{- $jsOpts := dict "format" "esm" -}} + {{- if hugo.IsProduction -}} + {{- $jsOpts = merge $jsOpts (dict "minify" true) -}} + {{- end -}} + {{- $js = $js | js.Build $jsOpts -}} + {{- if hugo.IsProduction -}} + {{- $js = $js | fingerprint -}} + {{- end -}} + + {{- end -}} + + {{/* Analytics */}} + {{- if and .Site.Params.umami.enabled hugo.IsProduction -}} + {{- partial "analytics.html" . -}} + {{- end -}} + + + + diff --git a/layouts/_default/home.html b/layouts/_default/home.html new file mode 100755 index 0000000..4a20d90 --- /dev/null +++ b/layouts/_default/home.html @@ -0,0 +1,113 @@ +{{ define "main" }} {{/* Hero Section */}} +
+ {{/* Animated background pattern */}} +
+
+
+ +
+

+ VALKNAR'S +

+

+ Pivoine.Art +

+ + {{/* Scroll indicator */}} + +
+
+ +{{/* Latest Tracks */}} +
+
+
+

Latest Tracks

+

Recent audio

+
+ + {{- $tracks := where .Site.RegularPages "Section" "tracks" -}} {{- if + $tracks }} +
+ {{- range first 3 $tracks }} {{ partial "track-card.html" . }} {{- end }} +
+ + + {{- else }} +

No tracks yet. Check back soon.

+ {{- end }} +
+
+ +{{/* About Preview */}} +
+
+

About

+

+ Technology and sound, creating massive beats to push the boundaries of + audio perception. +

+ + Read more + + + + +
+
+{{ end }} diff --git a/layouts/_default/list.html b/layouts/_default/list.html new file mode 100755 index 0000000..d1c9839 --- /dev/null +++ b/layouts/_default/list.html @@ -0,0 +1,26 @@ +{{ define "main" }} +
+
+
+

{{ .Title }}

+ {{- with .Description }} +

{{ . }}

+ {{- end }} +
+ + {{- if .Content }} +
+ {{ .Content }} +
+ {{- end }} + + {{- if .Pages }} +
+ {{- range .Pages }} + {{ partial "track-card.html" . }} + {{- end }} +
+ {{- end }} +
+
+{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html new file mode 100755 index 0000000..5db05df --- /dev/null +++ b/layouts/_default/single.html @@ -0,0 +1,18 @@ +{{ define "main" }} +
+
+
+

{{ .Title }}

+ {{- if not .Date.IsZero }} + + {{- end }} +
+ +
+ {{ .Content }} +
+
+
+{{ end }} diff --git a/layouts/partials/analytics.html b/layouts/partials/analytics.html new file mode 100755 index 0000000..3212cc0 --- /dev/null +++ b/layouts/partials/analytics.html @@ -0,0 +1,8 @@ +{{- if and .Site.Params.umami.enabled .Site.Params.umami.websiteID .Site.Params.umami.src }} + +{{- end }} diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html new file mode 100755 index 0000000..4ef14d2 --- /dev/null +++ b/layouts/partials/footer.html @@ -0,0 +1,34 @@ + diff --git a/layouts/partials/head/favicon.html b/layouts/partials/head/favicon.html new file mode 100644 index 0000000..1fa758b --- /dev/null +++ b/layouts/partials/head/favicon.html @@ -0,0 +1,15 @@ + + + + + + diff --git a/layouts/partials/head/json-ld.html b/layouts/partials/head/json-ld.html new file mode 100755 index 0000000..7ea9e41 --- /dev/null +++ b/layouts/partials/head/json-ld.html @@ -0,0 +1,47 @@ +{{/* Website schema for homepage */}} +{{- if .IsHome }} + +{{- end }} + +{{/* MusicRecording schema for track pages */}} +{{- if and .IsPage (eq .Section "tracks") }} + +{{- end }} diff --git a/layouts/partials/head/meta.html b/layouts/partials/head/meta.html new file mode 100755 index 0000000..7c113e2 --- /dev/null +++ b/layouts/partials/head/meta.html @@ -0,0 +1,29 @@ +{{- $title := cond .IsHome .Site.Title (printf "%s | %s" .Title .Site.Title) -}} +{{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 160 -}} + + + + + +{{ $title }} + + + + + +{{/* Theme */}} + + + +{{/* Favicon */}} + + +{{/* RSS */}} + + +{{/* Robots */}} +{{- $robots := "index, follow" -}} +{{- if .Params.noindex -}} + {{- $robots = "noindex, nofollow" -}} +{{- end -}} + diff --git a/layouts/partials/head/opengraph.html b/layouts/partials/head/opengraph.html new file mode 100755 index 0000000..7dec406 --- /dev/null +++ b/layouts/partials/head/opengraph.html @@ -0,0 +1,30 @@ +{{- $title := .Title | default .Site.Title -}} +{{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}} +{{- $image := .Params.image | default "/images/og-default.png" -}} +{{- if not (strings.HasPrefix $image "http") -}} + {{- $image = $image | absURL -}} +{{- end -}} + + + + + + + + +{{- if $image }} + + +{{- end }} + +{{/* Audio-specific OpenGraph tags */}} +{{- if and (eq .Section "tracks") .Params.audio }} + + + {{- if .Params.duration }} + + {{- end }} + {{- if .Params.artist }} + + {{- end }} +{{- end }} diff --git a/layouts/partials/head/preload.html b/layouts/partials/head/preload.html new file mode 100755 index 0000000..e6149f7 --- /dev/null +++ b/layouts/partials/head/preload.html @@ -0,0 +1,16 @@ +{{/* Preconnect to external domains */}} + + +{{/* DNS prefetch */}} + + +{{/* Preload fonts if we add custom fonts later */}} +{{/* */}} + +{{/* Preload cover image on track pages */}} +{{- if and .IsPage (eq .Section "tracks") }} + {{- with .Resources.GetMatch "cover.*" }} + {{- $webp := .Resize "800x webp q85" }} + + {{- end }} +{{- end }} diff --git a/layouts/partials/head/twitter.html b/layouts/partials/head/twitter.html new file mode 100755 index 0000000..b89f1be --- /dev/null +++ b/layouts/partials/head/twitter.html @@ -0,0 +1,21 @@ +{{- $title := .Title | default .Site.Title -}} +{{- $desc := .Description | default .Summary | default .Site.Params.description | plainify | truncate 200 -}} +{{- $image := .Params.image | default "/images/og-default.png" -}} +{{- if not (strings.HasPrefix $image "http") -}} + {{- $image = $image | absURL -}} +{{- end -}} + +{{/* Determine card type */}} +{{- $cardType := "summary" -}} +{{- if $image -}} + {{- $cardType = "summary_large_image" -}} +{{- end -}} + + + + + +{{- if $image }} + + +{{- end }} diff --git a/layouts/partials/header.html b/layouts/partials/header.html new file mode 100755 index 0000000..0d3b59c --- /dev/null +++ b/layouts/partials/header.html @@ -0,0 +1,44 @@ +
+ +
+ +{{/* Spacer for fixed header */}} + diff --git a/layouts/partials/player.html b/layouts/partials/player.html new file mode 100755 index 0000000..ea20231 --- /dev/null +++ b/layouts/partials/player.html @@ -0,0 +1,122 @@ +{{/* Persistent Audio Player */}} +
+
+ {{/* Track Info */}} +
+ {{/* Cover */}} +
+ +
+ + {{/* Title */}} +
+

+

Valknar

+
+
+ + {{/* Controls */}} +
+ {{/* Play/Pause */}} + +
+ + {{/* Progress */}} + + + {{/* Volume */}} + +
+
+ + diff --git a/layouts/partials/track-card.html b/layouts/partials/track-card.html new file mode 100755 index 0000000..c683e1a --- /dev/null +++ b/layouts/partials/track-card.html @@ -0,0 +1,70 @@ +{{/* Track Card Component */}} +
+ {{/* Cover Image/Video */}} + + {{- with .Resources.GetMatch "cover.*" }} + {{- $img := .Resize "600x webp q85" }} + {{ $.Title }} + {{- else }} +
+ + + +
+ {{- end }} + + {{/* Video preview on hover */}} + {{- with .Resources.GetMatch "preview.*" }} + + {{- end }} + + {{/* Play overlay - hide if video exists */}} + {{- if not (.Resources.GetMatch "preview.*") }} +
+
+ + + +
+
+ {{- end }} +
+ + {{/* Info */}} +
+ +

+ {{ .Title }} +

+
+ +
+ + {{- with .Params.duration }} + {{ . }} + {{- end }} +
+ + {{- with .Params.genre }} + + {{ . }} + + {{- end }} +
+
diff --git a/layouts/tracks/list.html b/layouts/tracks/list.html new file mode 100755 index 0000000..510df97 --- /dev/null +++ b/layouts/tracks/list.html @@ -0,0 +1,43 @@ +{{ define "main" }} +
+
+
+

Tracks

+

{{ .Description | default "All audio experiments" }}

+
+ + {{- $tracks := .Pages -}} + {{- if $tracks }} +
+ {{- range $tracks }} + {{ partial "track-card.html" . }} + {{- end }} +
+ + {{/* Pagination */}} + {{- if .Paginator }} + + {{- end }} + {{- else }} +

No tracks yet. Check back soon.

+ {{- end }} +
+
+{{ end }} diff --git a/layouts/tracks/single.html b/layouts/tracks/single.html new file mode 100755 index 0000000..0112ef4 --- /dev/null +++ b/layouts/tracks/single.html @@ -0,0 +1,138 @@ +{{ define "main" }} +
+
+ {{/* Content area with offset */}} +
+ {{/* Header */}} +
+ {{/* Meta */}} +
+ + {{- with .Params.genre }} + {{ . }} + {{- end }} +
+ +

+ {{ .Title }} +

+ + {{- with .Description }} +

{{ . }}

+ {{- end }} +
+ + {{/* Cover Image + Play Widget - 2 Column Layout */}} +
+ {{/* Cover Image - 1 column */}} + {{- with .Resources.GetMatch "cover.*" }} + {{- $img := .Resize "400x webp q90" }} +
+ {{ $.Title }} + {{/* Video preview on hover */}} + {{- with $.Resources.GetMatch "preview.*" }} + + {{- end }} +
+ {{- end }} + + {{/* Play Widget - 2 columns */}} + {{- if .Params.audio }} +
+ +
+

Play Track

+ {{- with .Params.duration }} +

{{ . }}

+ {{- end }} +
+
+ {{- end }} +
+ {{/* Content */}} + {{- if .Content }} +
+ {{ .Content }} +
+ {{- end }} + + {{/* Tags */}} + {{- with .GetTerms "tags" }} + + {{- end }} + + {{/* Related Tracks */}} + {{- $related := .Site.RegularPages.Related . | first 3 }} + {{- if $related }} + + {{- end }} +
+
+
+{{ end }} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..061a431 --- /dev/null +++ b/nginx.conf @@ -0,0 +1,48 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1024; + gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml application/json application/rss+xml image/svg+xml; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN" always; + add_header X-Content-Type-Options "nosniff" always; + add_header X-XSS-Protection "1; mode=block" always; + add_header Referrer-Policy "strict-origin-when-cross-origin" always; + + # Static assets with long cache + location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff|woff2|ttf|eot|webp|mp3|mp4|webm|ogg)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; + } + + # HTML files - no cache for fresh content + location ~* \.html$ { + expires -1; + add_header Cache-Control "no-store, no-cache, must-revalidate"; + } + + # Clean URLs - try files, then directories, then fallback to index.html + location / { + try_files $uri $uri/ $uri.html /index.html; + } + + # RSS feed + location = /index.xml { + types { application/rss+xml xml; } + } + + # Deny access to hidden files + location ~ /\. { + deny all; + access_log off; + log_not_found off; + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..86c47a5 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "pivoine.art", + "version": "0.1.0", + "type": "module", + "private": true, + "description": "Valknar's Pivoine.Art", + "scripts": { + "css": "postcss assets/css/main.css -o static/css/main.css", + "css:watch": "postcss assets/css/main.css -o static/css/main.css --watch", + "dev": "pnpm css && hugo server -D", + "build": "NODE_ENV=production pnpm css && hugo --minify" + }, + "dependencies": { + "three": "^0.170.0" + }, + "devDependencies": { + "@tailwindcss/cli": "^4.1.17", + "@tailwindcss/postcss": "^4.0.0-beta.9", + "postcss": "^8.4.49", + "postcss-cli": "^11.0.0", + "tailwindcss": "^4.0.0-beta.9" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..9210dd5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,1011 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + three: + specifier: ^0.170.0 + version: 0.170.0 + devDependencies: + '@tailwindcss/cli': + specifier: ^4.1.17 + version: 4.1.17 + '@tailwindcss/postcss': + specifier: ^4.0.0-beta.9 + version: 4.1.17 + postcss: + specifier: ^8.4.49 + version: 8.5.6 + postcss-cli: + specifier: ^11.0.0 + version: 11.0.1(jiti@2.6.1)(postcss@8.5.6) + tailwindcss: + specifier: ^4.0.0-beta.9 + version: 4.1.17 + +packages: + + '@alloc/quick-lru@5.2.0': + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@tailwindcss/cli@4.1.17': + resolution: {integrity: sha512-jUIxcyUNlCC2aNPnyPEWU/L2/ik3pB4fF3auKGXr8AvN3T3OFESVctFKOBoPZQaZJIeUpPn1uCLp0MRxuek8gg==} + hasBin: true + + '@tailwindcss/node@4.1.17': + resolution: {integrity: sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==} + + '@tailwindcss/oxide-android-arm64@4.1.17': + resolution: {integrity: sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + resolution: {integrity: sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.17': + resolution: {integrity: sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + resolution: {integrity: sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + resolution: {integrity: sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + resolution: {integrity: sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + resolution: {integrity: sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + resolution: {integrity: sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + resolution: {integrity: sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + resolution: {integrity: sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + resolution: {integrity: sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + resolution: {integrity: sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.17': + resolution: {integrity: sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==} + engines: {node: '>= 10'} + + '@tailwindcss/postcss@4.1.17': + resolution: {integrity: sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + dependency-graph@1.0.0: + resolution: {integrity: sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==} + engines: {node: '>=4'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + fs-extra@11.3.2: + resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} + engines: {node: '>=14.14'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + jsonfile@6.2.0: + resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==} + + lightningcss-android-arm64@1.30.2: + resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.30.2: + resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.2: + resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.2: + resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.2: + resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.2: + resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.2: + resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.2: + resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.2: + resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.2: + resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.2: + resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.2: + resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==} + engines: {node: '>= 12.0.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pify@2.3.0: + resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} + engines: {node: '>=0.10.0'} + + postcss-cli@11.0.1: + resolution: {integrity: sha512-0UnkNPSayHKRe/tc2YGW6XnSqqOA9eqpiRMgRlV1S6HdGi16vwJBx7lviARzbV1HpQHqLLRH3o8vTcB0cLc+5g==} + engines: {node: '>=18'} + hasBin: true + peerDependencies: + postcss: ^8.0.0 + + postcss-load-config@5.1.0: + resolution: {integrity: sha512-G5AJ+IX0aD0dygOE0yFZQ/huFFMSNneyfp0e3/bT05a8OfPC5FUoZRPfGijUdGOJNMewJiwzcHJXFafFzeKFVA==} + engines: {node: '>= 18'} + peerDependencies: + jiti: '>=1.21.0' + postcss: '>=8.0.9' + tsx: ^4.8.1 + peerDependenciesMeta: + jiti: + optional: true + postcss: + optional: true + tsx: + optional: true + + postcss-reporter@7.1.0: + resolution: {integrity: sha512-/eoEylGWyy6/DOiMP5lmFRdmDKThqgn7D6hP2dXKJI/0rJSO1ADFNngZfDzxL0YAxFvws+Rtpuji1YIHj4mySA==} + engines: {node: '>=10'} + peerDependencies: + postcss: ^8.1.0 + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-hrtime@1.0.3: + resolution: {integrity: sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==} + engines: {node: '>= 0.8'} + + read-cache@1.0.0: + resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + slash@5.1.0: + resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} + engines: {node: '>=14.16'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + tailwindcss@4.1.17: + resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==} + + tapable@2.3.0: + resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} + engines: {node: '>=6'} + + thenby@1.3.4: + resolution: {integrity: sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ==} + + three@0.170.0: + resolution: {integrity: sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.2: + resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} + engines: {node: '>=12'} + +snapshots: + + '@alloc/quick-lru@5.2.0': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@tailwindcss/cli@4.1.17': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + enhanced-resolve: 5.18.3 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.1.17 + + '@tailwindcss/node@4.1.17': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.2 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.1.17 + + '@tailwindcss/oxide-android-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.17': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.17': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.17': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.17': + optional: true + + '@tailwindcss/oxide@4.1.17': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-arm64': 4.1.17 + '@tailwindcss/oxide-darwin-x64': 4.1.17 + '@tailwindcss/oxide-freebsd-x64': 4.1.17 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.17 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.17 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.17 + '@tailwindcss/oxide-linux-x64-musl': 4.1.17 + '@tailwindcss/oxide-wasm32-wasi': 4.1.17 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.17 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.17 + + '@tailwindcss/postcss@4.1.17': + dependencies: + '@alloc/quick-lru': 5.2.0 + '@tailwindcss/node': 4.1.17 + '@tailwindcss/oxide': 4.1.17 + postcss: 8.5.6 + tailwindcss: 4.1.17 + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + + binary-extensions@2.3.0: {} + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + dependency-graph@1.0.0: {} + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + emoji-regex@8.0.0: {} + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.0 + + escalade@3.2.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + fs-extra@11.3.2: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.0 + universalify: 2.0.1 + + fsevents@2.3.3: + optional: true + + get-caller-file@2.0.5: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + graceful-fs@4.2.11: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + jiti@2.6.1: {} + + jsonfile@6.2.0: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + lightningcss-android-arm64@1.30.2: + optional: true + + lightningcss-darwin-arm64@1.30.2: + optional: true + + lightningcss-darwin-x64@1.30.2: + optional: true + + lightningcss-freebsd-x64@1.30.2: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.2: + optional: true + + lightningcss-linux-arm64-gnu@1.30.2: + optional: true + + lightningcss-linux-arm64-musl@1.30.2: + optional: true + + lightningcss-linux-x64-gnu@1.30.2: + optional: true + + lightningcss-linux-x64-musl@1.30.2: + optional: true + + lightningcss-win32-arm64-msvc@1.30.2: + optional: true + + lightningcss-win32-x64-msvc@1.30.2: + optional: true + + lightningcss@1.30.2: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.30.2 + lightningcss-darwin-arm64: 1.30.2 + lightningcss-darwin-x64: 1.30.2 + lightningcss-freebsd-x64: 1.30.2 + lightningcss-linux-arm-gnueabihf: 1.30.2 + lightningcss-linux-arm64-gnu: 1.30.2 + lightningcss-linux-arm64-musl: 1.30.2 + lightningcss-linux-x64-gnu: 1.30.2 + lightningcss-linux-x64-musl: 1.30.2 + lightningcss-win32-arm64-msvc: 1.30.2 + lightningcss-win32-x64-msvc: 1.30.2 + + lilconfig@3.1.3: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + mri@1.2.0: {} + + nanoid@3.3.11: {} + + node-addon-api@7.1.1: {} + + normalize-path@3.0.0: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.3: {} + + pify@2.3.0: {} + + postcss-cli@11.0.1(jiti@2.6.1)(postcss@8.5.6): + dependencies: + chokidar: 3.6.0 + dependency-graph: 1.0.0 + fs-extra: 11.3.2 + picocolors: 1.1.1 + postcss: 8.5.6 + postcss-load-config: 5.1.0(jiti@2.6.1)(postcss@8.5.6) + postcss-reporter: 7.1.0(postcss@8.5.6) + pretty-hrtime: 1.0.3 + read-cache: 1.0.0 + slash: 5.1.0 + tinyglobby: 0.2.15 + yargs: 17.7.2 + transitivePeerDependencies: + - jiti + - tsx + + postcss-load-config@5.1.0(jiti@2.6.1)(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + yaml: 2.8.1 + optionalDependencies: + jiti: 2.6.1 + postcss: 8.5.6 + + postcss-reporter@7.1.0(postcss@8.5.6): + dependencies: + picocolors: 1.1.1 + postcss: 8.5.6 + thenby: 1.3.4 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-hrtime@1.0.3: {} + + read-cache@1.0.0: + dependencies: + pify: 2.3.0 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.1 + + require-directory@2.1.1: {} + + slash@5.1.0: {} + + source-map-js@1.2.1: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + tailwindcss@4.1.17: {} + + tapable@2.3.0: {} + + thenby@1.3.4: {} + + three@0.170.0: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + universalify@2.0.1: {} + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + y18n@5.0.8: {} + + yaml@2.8.1: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.2: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..1d5a3b2 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,5 @@ +export default { + plugins: { + '@tailwindcss/postcss': {} + } +} diff --git a/static/css/main.css b/static/css/main.css new file mode 100644 index 0000000..bb9eebe --- /dev/null +++ b/static/css/main.css @@ -0,0 +1,1466 @@ +/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ +@layer properties; +@layer theme, base, components, utilities; +@layer theme { + :root, :host { + --font-sans: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", + "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; + --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", + "Courier New", monospace; + --spacing: 0.25rem; + --container-md: 28rem; + --text-xs: 0.75rem; + --text-xs--line-height: calc(1 / 0.75); + --text-sm: 0.875rem; + --text-sm--line-height: calc(1.25 / 0.875); + --text-lg: 1.125rem; + --text-lg--line-height: calc(1.75 / 1.125); + --text-xl: 1.25rem; + --text-xl--line-height: calc(1.75 / 1.25); + --text-2xl: 1.5rem; + --text-2xl--line-height: calc(2 / 1.5); + --text-3xl: 1.875rem; + --text-3xl--line-height: calc(2.25 / 1.875); + --text-4xl: 2.25rem; + --text-4xl--line-height: calc(2.5 / 2.25); + --text-5xl: 3rem; + --text-5xl--line-height: 1; + --text-7xl: 4.5rem; + --text-7xl--line-height: 1; + --font-weight-medium: 500; + --tracking-tighter: -0.05em; + --tracking-tight: -0.025em; + --tracking-wide: 0.025em; + --leading-relaxed: 1.625; + --radius-sm: 0.25rem; + --radius-lg: 0.5rem; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --animate-bounce: bounce 1s infinite; + --blur-md: 12px; + --default-transition-duration: 150ms; + --default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + --default-font-family: var(--font-sans); + --default-mono-font-family: var(--font-mono); + --color-surface-0: var(--color-surface-0); + --color-surface-1: var(--color-surface-1); + --color-surface-2: var(--color-surface-2); + --color-surface-3: var(--color-surface-3); + --color-surface-4: var(--color-surface-4); + --color-text-primary: var(--color-text-primary); + --color-text-secondary: var(--color-text-secondary); + --color-text-muted: var(--color-text-muted); + --color-accent: var(--color-accent); + --color-border: var(--color-border); + } +} +@layer base { + *, ::after, ::before, ::backdrop, ::file-selector-button { + box-sizing: border-box; + margin: 0; + padding: 0; + border: 0 solid; + } + html, :host { + line-height: 1.5; + -webkit-text-size-adjust: 100%; + tab-size: 4; + font-family: var(--default-font-family, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"); + font-feature-settings: var(--default-font-feature-settings, normal); + font-variation-settings: var(--default-font-variation-settings, normal); + -webkit-tap-highlight-color: transparent; + } + hr { + height: 0; + color: inherit; + border-top-width: 1px; + } + abbr:where([title]) { + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + } + h1, h2, h3, h4, h5, h6 { + font-size: inherit; + font-weight: inherit; + } + a { + color: inherit; + -webkit-text-decoration: inherit; + text-decoration: inherit; + } + b, strong { + font-weight: bolder; + } + code, kbd, samp, pre { + font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace); + font-feature-settings: var(--default-mono-font-feature-settings, normal); + font-variation-settings: var(--default-mono-font-variation-settings, normal); + font-size: 1em; + } + small { + font-size: 80%; + } + sub, sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; + } + sub { + bottom: -0.25em; + } + sup { + top: -0.5em; + } + table { + text-indent: 0; + border-color: inherit; + border-collapse: collapse; + } + :-moz-focusring { + outline: auto; + } + progress { + vertical-align: baseline; + } + summary { + display: list-item; + } + ol, ul, menu { + list-style: none; + } + img, svg, video, canvas, audio, iframe, embed, object { + display: block; + vertical-align: middle; + } + img, video { + max-width: 100%; + height: auto; + } + button, input, select, optgroup, textarea, ::file-selector-button { + font: inherit; + font-feature-settings: inherit; + font-variation-settings: inherit; + letter-spacing: inherit; + color: inherit; + border-radius: 0; + background-color: transparent; + opacity: 1; + } + :where(select:is([multiple], [size])) optgroup { + font-weight: bolder; + } + :where(select:is([multiple], [size])) optgroup option { + padding-inline-start: 20px; + } + ::file-selector-button { + margin-inline-end: 4px; + } + ::placeholder { + opacity: 1; + } + @supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) { + ::placeholder { + color: currentcolor; + @supports (color: color-mix(in lab, red, red)) { + color: color-mix(in oklab, currentcolor 50%, transparent); + } + } + } + textarea { + resize: vertical; + } + ::-webkit-search-decoration { + -webkit-appearance: none; + } + ::-webkit-date-and-time-value { + min-height: 1lh; + text-align: inherit; + } + ::-webkit-datetime-edit { + display: inline-flex; + } + ::-webkit-datetime-edit-fields-wrapper { + padding: 0; + } + ::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field { + padding-block: 0; + } + ::-webkit-calendar-picker-indicator { + line-height: 1; + } + :-moz-ui-invalid { + box-shadow: none; + } + button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button { + appearance: button; + } + ::-webkit-inner-spin-button, ::-webkit-outer-spin-button { + height: auto; + } + [hidden]:where(:not([hidden="until-found"])) { + display: none !important; + } +} +@layer utilities { + .pointer-events-none { + pointer-events: none; + } + .absolute { + position: absolute; + } + .fixed { + position: fixed; + } + .relative { + position: relative; + } + .static { + position: static; + } + .inset-0 { + inset: calc(var(--spacing) * 0); + } + .top-0 { + top: calc(var(--spacing) * 0); + } + .right-0 { + right: calc(var(--spacing) * 0); + } + .bottom-0 { + bottom: calc(var(--spacing) * 0); + } + .left-0 { + left: calc(var(--spacing) * 0); + } + .-z-10 { + z-index: calc(10 * -1); + } + .z-10 { + z-index: 10; + } + .z-player { + z-index: 400; + } + .z-sticky { + z-index: 200; + } + .z-visualizer { + z-index: 5; + } + .mx-auto { + margin-inline: auto; + } + .mt-2 { + margin-top: calc(var(--spacing) * 2); + } + .mt-3 { + margin-top: calc(var(--spacing) * 3); + } + .mt-12 { + margin-top: calc(var(--spacing) * 12); + } + .mt-16 { + margin-top: calc(var(--spacing) * 16); + } + .mt-auto { + margin-top: auto; + } + .mb-2 { + margin-bottom: calc(var(--spacing) * 2); + } + .mb-3 { + margin-bottom: calc(var(--spacing) * 3); + } + .mb-4 { + margin-bottom: calc(var(--spacing) * 4); + } + .mb-6 { + margin-bottom: calc(var(--spacing) * 6); + } + .mb-8 { + margin-bottom: calc(var(--spacing) * 8); + } + .mb-12 { + margin-bottom: calc(var(--spacing) * 12); + } + .ml-0\.5 { + margin-left: calc(var(--spacing) * 0.5); + } + .ml-1 { + margin-left: calc(var(--spacing) * 1); + } + .block { + display: block; + } + .flex { + display: flex; + } + .grid { + display: grid; + } + .hidden { + display: none; + } + .inline-block { + display: inline-block; + } + .inline-flex { + display: inline-flex; + } + .aspect-square { + aspect-ratio: 1 / 1; + } + .h-4 { + height: calc(var(--spacing) * 4); + } + .h-5 { + height: calc(var(--spacing) * 5); + } + .h-6 { + height: calc(var(--spacing) * 6); + } + .h-8 { + height: calc(var(--spacing) * 8); + } + .h-10 { + height: calc(var(--spacing) * 10); + } + .h-12 { + height: calc(var(--spacing) * 12); + } + .h-14 { + height: calc(var(--spacing) * 14); + } + .h-16 { + height: calc(var(--spacing) * 16); + } + .h-48 { + height: calc(var(--spacing) * 48); + } + .h-full { + height: 100%; + } + .min-h-\[calc\(100vh-4rem\)\] { + min-height: calc(100vh - 4rem); + } + .min-h-screen { + min-height: 100vh; + } + .w-4 { + width: calc(var(--spacing) * 4); + } + .w-5 { + width: calc(var(--spacing) * 5); + } + .w-6 { + width: calc(var(--spacing) * 6); + } + .w-8 { + width: calc(var(--spacing) * 8); + } + .w-10 { + width: calc(var(--spacing) * 10); + } + .w-12 { + width: calc(var(--spacing) * 12); + } + .w-14 { + width: calc(var(--spacing) * 14); + } + .w-20 { + width: calc(var(--spacing) * 20); + } + .w-48 { + width: calc(var(--spacing) * 48); + } + .w-full { + width: 100%; + } + .max-w-md { + max-width: var(--container-md); + } + .max-w-none { + max-width: none; + } + .min-w-0 { + min-width: calc(var(--spacing) * 0); + } + .flex-1 { + flex: 1; + } + .flex-shrink-0 { + flex-shrink: 0; + } + .scale-90 { + --tw-scale-x: 90%; + --tw-scale-y: 90%; + --tw-scale-z: 90%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + .transform { + transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,); + } + .animate-bounce { + animation: var(--animate-bounce); + } + .cursor-pointer { + cursor: pointer; + } + .grid-cols-1 { + grid-template-columns: repeat(1, minmax(0, 1fr)); + } + .flex-col { + flex-direction: column; + } + .flex-wrap { + flex-wrap: wrap; + } + .items-center { + align-items: center; + } + .justify-between { + justify-content: space-between; + } + .justify-center { + justify-content: center; + } + .gap-2 { + gap: calc(var(--spacing) * 2); + } + .gap-3 { + gap: calc(var(--spacing) * 3); + } + .gap-4 { + gap: calc(var(--spacing) * 4); + } + .gap-6 { + gap: calc(var(--spacing) * 6); + } + .gap-8 { + gap: calc(var(--spacing) * 8); + } + .truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .overflow-hidden { + overflow: hidden; + } + .rounded { + border-radius: 0.25rem; + } + .rounded-full { + border-radius: calc(infinity * 1px); + } + .rounded-lg { + border-radius: var(--radius-lg); + } + .border { + border-style: var(--tw-border-style); + border-width: 1px; + } + .border-t { + border-top-style: var(--tw-border-style); + border-top-width: 1px; + } + .border-b { + border-bottom-style: var(--tw-border-style); + border-bottom-width: 1px; + } + .border-border { + border-color: var(--color-border); + } + .border-text-primary { + border-color: var(--color-text-primary); + } + .bg-accent { + background-color: var(--color-accent); + } + .bg-surface-0\/60 { + background-color: var(--color-surface-0); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-surface-0) 60%, transparent); + } + } + .bg-surface-0\/80 { + background-color: var(--color-surface-0); + @supports (color: color-mix(in lab, red, red)) { + background-color: color-mix(in oklab, var(--color-surface-0) 80%, transparent); + } + } + .bg-surface-2 { + background-color: var(--color-surface-2); + } + .bg-gradient-to-b { + --tw-gradient-position: to bottom in oklab; + background-image: linear-gradient(var(--tw-gradient-stops)); + } + .from-transparent { + --tw-gradient-from: transparent; + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .via-surface-1\/50 { + --tw-gradient-via: var(--color-surface-1); + @supports (color: color-mix(in lab, red, red)) { + --tw-gradient-via: color-mix(in oklab, var(--color-surface-1) 50%, transparent); + } + --tw-gradient-via-stops: var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position); + --tw-gradient-stops: var(--tw-gradient-via-stops); + } + .to-surface-0 { + --tw-gradient-to: var(--color-surface-0); + --tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position)); + } + .object-cover { + object-fit: cover; + } + .p-2 { + padding: calc(var(--spacing) * 2); + } + .p-4 { + padding: calc(var(--spacing) * 4); + } + .px-2 { + padding-inline: calc(var(--spacing) * 2); + } + .px-3 { + padding-inline: calc(var(--spacing) * 3); + } + .px-4 { + padding-inline: calc(var(--spacing) * 4); + } + .px-6 { + padding-inline: calc(var(--spacing) * 6); + } + .py-1 { + padding-block: calc(var(--spacing) * 1); + } + .py-2 { + padding-block: calc(var(--spacing) * 2); + } + .py-3 { + padding-block: calc(var(--spacing) * 3); + } + .py-12 { + padding-block: calc(var(--spacing) * 12); + } + .py-16 { + padding-block: calc(var(--spacing) * 16); + } + .py-24 { + padding-block: calc(var(--spacing) * 24); + } + .pt-8 { + padding-top: calc(var(--spacing) * 8); + } + .pt-12 { + padding-top: calc(var(--spacing) * 12); + } + .pb-24 { + padding-bottom: calc(var(--spacing) * 24); + } + .text-center { + text-align: center; + } + .text-2xl { + font-size: var(--text-2xl); + line-height: var(--tw-leading, var(--text-2xl--line-height)); + } + .text-3xl { + font-size: var(--text-3xl); + line-height: var(--tw-leading, var(--text-3xl--line-height)); + } + .text-4xl { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + .text-5xl { + font-size: var(--text-5xl); + line-height: var(--tw-leading, var(--text-5xl--line-height)); + } + .text-lg { + font-size: var(--text-lg); + line-height: var(--tw-leading, var(--text-lg--line-height)); + } + .text-sm { + font-size: var(--text-sm); + line-height: var(--tw-leading, var(--text-sm--line-height)); + } + .text-xl { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + .text-xs { + font-size: var(--text-xs); + line-height: var(--tw-leading, var(--text-xs--line-height)); + } + .leading-relaxed { + --tw-leading: var(--leading-relaxed); + line-height: var(--leading-relaxed); + } + .font-medium { + --tw-font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-medium); + } + .tracking-tight { + --tw-tracking: var(--tracking-tight); + letter-spacing: var(--tracking-tight); + } + .tracking-tighter { + --tw-tracking: var(--tracking-tighter); + letter-spacing: var(--tracking-tighter); + } + .tracking-wide { + --tw-tracking: var(--tracking-wide); + letter-spacing: var(--tracking-wide); + } + .text-surface-0 { + color: var(--color-surface-0); + } + .text-text-muted { + color: var(--color-text-muted); + } + .text-text-primary { + color: var(--color-text-primary); + } + .text-text-secondary { + color: var(--color-text-secondary); + } + .tabular-nums { + --tw-numeric-spacing: tabular-nums; + font-variant-numeric: var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,); + } + .opacity-0 { + opacity: 0%; + } + .opacity-10 { + opacity: 10%; + } + .grayscale { + --tw-grayscale: grayscale(100%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + .backdrop-blur-md { + --tw-backdrop-blur: blur(var(--blur-md)); + -webkit-backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + backdrop-filter: var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,); + } + .transition-all { + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-colors { + transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-opacity { + transition-property: opacity; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .transition-transform { + transition-property: transform, translate, scale, rotate; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + .duration-300 { + --tw-duration: 300ms; + transition-duration: 300ms; + } + .duration-500 { + --tw-duration: 500ms; + transition-duration: 500ms; + } + .group-hover\:translate-x-1 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-translate-x: calc(var(--spacing) * 1); + translate: var(--tw-translate-x) var(--tw-translate-y); + } + } + } + .group-hover\:scale-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-scale-x: 100%; + --tw-scale-y: 100%; + --tw-scale-z: 100%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + } + .group-hover\:scale-110 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-scale-x: 110%; + --tw-scale-y: 110%; + --tw-scale-z: 110%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + } + .group-hover\:text-accent { + &:is(:where(.group):hover *) { + @media (hover: hover) { + color: var(--color-accent); + } + } + } + .group-hover\:opacity-100 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + opacity: 100%; + } + } + } + .group-hover\:grayscale-0 { + &:is(:where(.group):hover *) { + @media (hover: hover) { + --tw-grayscale: grayscale(0%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + } + } + .after\:absolute { + &::after { + content: var(--tw-content); + position: absolute; + } + } + .after\:bottom-0 { + &::after { + content: var(--tw-content); + bottom: calc(var(--spacing) * 0); + } + } + .after\:left-0 { + &::after { + content: var(--tw-content); + left: calc(var(--spacing) * 0); + } + } + .after\:h-px { + &::after { + content: var(--tw-content); + height: 1px; + } + } + .after\:w-0 { + &::after { + content: var(--tw-content); + width: calc(var(--spacing) * 0); + } + } + .after\:bg-accent { + &::after { + content: var(--tw-content); + background-color: var(--color-accent); + } + } + .after\:transition-all { + &::after { + content: var(--tw-content); + transition-property: all; + transition-timing-function: var(--tw-ease, var(--default-transition-timing-function)); + transition-duration: var(--tw-duration, var(--default-transition-duration)); + } + } + .after\:duration-300 { + &::after { + content: var(--tw-content); + --tw-duration: 300ms; + transition-duration: 300ms; + } + } + .hover\:scale-105 { + &:hover { + @media (hover: hover) { + --tw-scale-x: 105%; + --tw-scale-y: 105%; + --tw-scale-z: 105%; + scale: var(--tw-scale-x) var(--tw-scale-y); + } + } + } + .hover\:border-accent { + &:hover { + @media (hover: hover) { + border-color: var(--color-accent); + } + } + } + .hover\:border-text-muted { + &:hover { + @media (hover: hover) { + border-color: var(--color-text-muted); + } + } + } + .hover\:bg-accent { + &:hover { + @media (hover: hover) { + background-color: var(--color-accent); + } + } + } + .hover\:bg-surface-3 { + &:hover { + @media (hover: hover) { + background-color: var(--color-surface-3); + } + } + } + .hover\:text-surface-0 { + &:hover { + @media (hover: hover) { + color: var(--color-surface-0); + } + } + } + .hover\:text-text-primary { + &:hover { + @media (hover: hover) { + color: var(--color-text-primary); + } + } + } + .hover\:text-text-secondary { + &:hover { + @media (hover: hover) { + color: var(--color-text-secondary); + } + } + } + .hover\:grayscale-0 { + &:hover { + @media (hover: hover) { + --tw-grayscale: grayscale(0%); + filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,); + } + } + } + .hover\:after\:w-full { + &:hover { + @media (hover: hover) { + &::after { + content: var(--tw-content); + width: 100%; + } + } + } + } + .sm\:flex { + @media (width >= 40rem) { + display: flex; + } + } + .md\:mx-72 { + @media (width >= 48rem) { + margin-inline: calc(var(--spacing) * 72); + } + } + .md\:flex { + @media (width >= 48rem) { + display: flex; + } + } + .md\:h-64 { + @media (width >= 48rem) { + height: calc(var(--spacing) * 64); + } + } + .md\:w-64 { + @media (width >= 48rem) { + width: calc(var(--spacing) * 64); + } + } + .md\:grid-cols-2 { + @media (width >= 48rem) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + } + .md\:grid-cols-3 { + @media (width >= 48rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .md\:grid-cols-\[auto_1fr\] { + @media (width >= 48rem) { + grid-template-columns: auto 1fr; + } + } + .md\:flex-row { + @media (width >= 48rem) { + flex-direction: row; + } + } + .md\:gap-8 { + @media (width >= 48rem) { + gap: calc(var(--spacing) * 8); + } + } + .md\:text-4xl { + @media (width >= 48rem) { + font-size: var(--text-4xl); + line-height: var(--tw-leading, var(--text-4xl--line-height)); + } + } + .md\:text-7xl { + @media (width >= 48rem) { + font-size: var(--text-7xl); + line-height: var(--tw-leading, var(--text-7xl--line-height)); + } + } + .md\:text-xl { + @media (width >= 48rem) { + font-size: var(--text-xl); + line-height: var(--tw-leading, var(--text-xl--line-height)); + } + } + .lg\:grid-cols-3 { + @media (width >= 64rem) { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } + } + .xl\:grid-cols-4 { + @media (width >= 80rem) { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } + } +} +html { + scroll-padding-top: 4rem; +} +:root { + --color-surface-0: #0a0a0a; + --color-surface-1: #121212; + --color-surface-2: #1a1a1a; + --color-surface-3: #232323; + --color-surface-4: #2c2c2c; + --color-text-primary: #e8e8e8; + --color-text-secondary: #888888; + --color-text-muted: #555555; + --color-accent: #ffffff; + --color-accent-dim: #666666; + --color-accent-glow: rgba(255, 255, 255, 0.1); + --color-border: #2a2a2a; + --font-mono: "JetBrains Mono", "Fira Code", "SF Mono", monospace; + --duration-fast: 150ms; + --duration-normal: 300ms; + --duration-slow: 500ms; + --duration-page: 400ms; + --ease-out: cubic-bezier(0, 0, 0.2, 1); + --radius-sm: 0.125rem; + --radius-md: 0.25rem; + --radius-lg: 0.5rem; + --radius-full: 9999px; +} +@layer base { + html { + font-family: var(--font-mono); + background: var(--color-surface-0); + color: var(--color-text-primary); + letter-spacing: 0.02em; + line-height: 1.6; + -webkit-font-smoothing: antialiased; + } + body { + min-height: 100vh; + } + ::selection { + background: var(--color-accent); + color: var(--color-surface-0); + } + ::-webkit-scrollbar { + width: 8px; + height: 8px; + } + ::-webkit-scrollbar-track { + background: var(--color-surface-1); + } + ::-webkit-scrollbar-thumb { + background: var(--color-surface-3); + border-radius: var(--radius-full); + } + ::-webkit-scrollbar-thumb:hover { + background: var(--color-surface-4); + } + :focus-visible { + outline: 2px solid var(--color-accent); + outline-offset: 2px; + } + a { + color: var(--color-text-primary); + text-decoration: none; + transition: color var(--duration-fast) var(--ease-out); + } + a:hover { + color: var(--color-accent); + } +} +@layer components { + .htmx-request #main-content { + opacity: 0; + transform: translateY(8px); + transition: all var(--duration-page) var(--ease-out); + } + .htmx-settling #main-content { + opacity: 1; + transform: translateY(0); + } + .fade-in-up { + opacity: 0; + transform: translateY(24px); + transition: all var(--duration-slow) var(--ease-out); + } + .fade-in-up.is-visible { + opacity: 1; + transform: translateY(0); + } + .link-hover { + position: relative; + } + .link-hover::after { + content: ""; + position: absolute; + bottom: -2px; + left: 0; + width: 100%; + height: 1px; + background: var(--color-accent); + transform: scaleX(0); + transform-origin: right; + transition: transform var(--duration-normal) var(--ease-out); + } + .link-hover:hover::after { + transform: scaleX(1); + transform-origin: left; + } + .audio-player { + background: var(--color-surface-1); + border-top: 1px solid var(--color-border); + } + .audio-player__progress { + appearance: none; + width: 100%; + height: 4px; + background: var(--color-surface-3); + cursor: pointer; + } + .audio-player__progress::-webkit-slider-thumb { + appearance: none; + width: 12px; + height: 12px; + background: var(--color-accent); + border-radius: var(--radius-full); + } + .track-card { + background: rgba(18, 18, 18, 0.6); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--color-border); + transition: all var(--duration-normal) var(--ease-out); + } + .track-card:hover { + background: rgba(18, 18, 18, 0.8); + border-color: var(--color-surface-4); + transform: translateY(-2px); + } + .track-card__cover { + aspect-ratio: 1; + object-fit: cover; + filter: grayscale(20%); + transition: filter var(--duration-normal); + } + .track-card:hover .track-card__cover { + filter: grayscale(0%); + } + .prose { + color: var(--color-text-secondary); + line-height: 1.75; + } + .prose h1, + .prose h2, + .prose h3, + .prose h4 { + color: var(--color-text-primary); + font-weight: 500; + margin-top: 2em; + margin-bottom: 0.5em; + } + .prose h1 { + font-size: 2rem; + } + .prose h2 { + font-size: 1.5rem; + } + .prose h3 { + font-size: 1.25rem; + } + .prose p { + margin-bottom: 1.25em; + } + .prose a { + text-decoration: underline; + text-underline-offset: 2px; + } + .prose strong { + color: var(--color-text-primary); + font-weight: 600; + } + .prose ul, + .prose ol { + margin-bottom: 1.25em; + padding-left: 1.5em; + } + .prose li { + margin-bottom: 0.5em; + } + .prose code { + background: var(--color-surface-2); + padding: 0.2em 0.4em; + border-radius: var(--radius-sm); + font-size: 0.9em; + } + .prose pre { + background: var(--color-surface-1); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 1em; + overflow-x: auto; + margin: 1.5em 0; + } + .prose pre code { + background: none; + padding: 0; + } +} +@layer utilities { + .container-narrow { + max-width: 48rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .container-wide { + max-width: 80rem; + margin-inline: auto; + padding-inline: 1.5rem; + } + .z-sticky { + z-index: 200; + } + .z-visualizer { + z-index: -1; + opacity: 0.25; + } + .z-player { + z-index: 400; + } +} +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + transition-duration: 0.01ms !important; + } +} +@property --tw-scale-x { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-y { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-scale-z { + syntax: "*"; + inherits: false; + initial-value: 1; +} +@property --tw-rotate-x { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-y { + syntax: "*"; + inherits: false; +} +@property --tw-rotate-z { + syntax: "*"; + inherits: false; +} +@property --tw-skew-x { + syntax: "*"; + inherits: false; +} +@property --tw-skew-y { + syntax: "*"; + inherits: false; +} +@property --tw-border-style { + syntax: "*"; + inherits: false; + initial-value: solid; +} +@property --tw-gradient-position { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-via { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-to { + syntax: ""; + inherits: false; + initial-value: #0000; +} +@property --tw-gradient-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-via-stops { + syntax: "*"; + inherits: false; +} +@property --tw-gradient-from-position { + syntax: ""; + inherits: false; + initial-value: 0%; +} +@property --tw-gradient-via-position { + syntax: ""; + inherits: false; + initial-value: 50%; +} +@property --tw-gradient-to-position { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-leading { + syntax: "*"; + inherits: false; +} +@property --tw-font-weight { + syntax: "*"; + inherits: false; +} +@property --tw-tracking { + syntax: "*"; + inherits: false; +} +@property --tw-ordinal { + syntax: "*"; + inherits: false; +} +@property --tw-slashed-zero { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-figure { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-spacing { + syntax: "*"; + inherits: false; +} +@property --tw-numeric-fraction { + syntax: "*"; + inherits: false; +} +@property --tw-blur { + syntax: "*"; + inherits: false; +} +@property --tw-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-invert { + syntax: "*"; + inherits: false; +} +@property --tw-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-color { + syntax: "*"; + inherits: false; +} +@property --tw-drop-shadow-alpha { + syntax: ""; + inherits: false; + initial-value: 100%; +} +@property --tw-drop-shadow-size { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-blur { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-brightness { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-contrast { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-grayscale { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-hue-rotate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-invert { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-opacity { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-saturate { + syntax: "*"; + inherits: false; +} +@property --tw-backdrop-sepia { + syntax: "*"; + inherits: false; +} +@property --tw-duration { + syntax: "*"; + inherits: false; +} +@property --tw-translate-x { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-y { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-translate-z { + syntax: "*"; + inherits: false; + initial-value: 0; +} +@property --tw-content { + syntax: "*"; + initial-value: ""; + inherits: false; +} +@keyframes bounce { + 0%, 100% { + transform: translateY(-25%); + animation-timing-function: cubic-bezier(0.8, 0, 1, 1); + } + 50% { + transform: none; + animation-timing-function: cubic-bezier(0, 0, 0.2, 1); + } +} +@layer properties { + @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop { + --tw-scale-x: 1; + --tw-scale-y: 1; + --tw-scale-z: 1; + --tw-rotate-x: initial; + --tw-rotate-y: initial; + --tw-rotate-z: initial; + --tw-skew-x: initial; + --tw-skew-y: initial; + --tw-border-style: solid; + --tw-gradient-position: initial; + --tw-gradient-from: #0000; + --tw-gradient-via: #0000; + --tw-gradient-to: #0000; + --tw-gradient-stops: initial; + --tw-gradient-via-stops: initial; + --tw-gradient-from-position: 0%; + --tw-gradient-via-position: 50%; + --tw-gradient-to-position: 100%; + --tw-leading: initial; + --tw-font-weight: initial; + --tw-tracking: initial; + --tw-ordinal: initial; + --tw-slashed-zero: initial; + --tw-numeric-figure: initial; + --tw-numeric-spacing: initial; + --tw-numeric-fraction: initial; + --tw-blur: initial; + --tw-brightness: initial; + --tw-contrast: initial; + --tw-grayscale: initial; + --tw-hue-rotate: initial; + --tw-invert: initial; + --tw-opacity: initial; + --tw-saturate: initial; + --tw-sepia: initial; + --tw-drop-shadow: initial; + --tw-drop-shadow-color: initial; + --tw-drop-shadow-alpha: 100%; + --tw-drop-shadow-size: initial; + --tw-backdrop-blur: initial; + --tw-backdrop-brightness: initial; + --tw-backdrop-contrast: initial; + --tw-backdrop-grayscale: initial; + --tw-backdrop-hue-rotate: initial; + --tw-backdrop-invert: initial; + --tw-backdrop-opacity: initial; + --tw-backdrop-saturate: initial; + --tw-backdrop-sepia: initial; + --tw-duration: initial; + --tw-translate-x: 0; + --tw-translate-y: 0; + --tw-translate-z: 0; + --tw-content: ""; + } + } +} diff --git a/static/favicon/apple-touch-icon.png b/static/favicon/apple-touch-icon.png new file mode 100644 index 0000000..b046dce Binary files /dev/null and b/static/favicon/apple-touch-icon.png differ diff --git a/static/favicon/favicon-96x96.png b/static/favicon/favicon-96x96.png new file mode 100644 index 0000000..a0bea17 Binary files /dev/null and b/static/favicon/favicon-96x96.png differ diff --git a/static/favicon/favicon.ico b/static/favicon/favicon.ico new file mode 100644 index 0000000..78bdad4 Binary files /dev/null and b/static/favicon/favicon.ico differ diff --git a/static/favicon/favicon.svg b/static/favicon/favicon.svg new file mode 100644 index 0000000..42a18b3 --- /dev/null +++ b/static/favicon/favicon.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/favicon/site.webmanifest b/static/favicon/site.webmanifest new file mode 100644 index 0000000..4aa3c44 --- /dev/null +++ b/static/favicon/site.webmanifest @@ -0,0 +1,21 @@ +{ + "name": "Valknar's Pivoine.Art", + "short_name": "Valknar's", + "icons": [ + { + "src": "/favicon/web-app-manifest-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "/favicon/web-app-manifest-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "theme_color": "#0a0a0a", + "background_color": "#0a0a0a", + "display": "standalone" +} \ No newline at end of file diff --git a/static/favicon/web-app-manifest-192x192.png b/static/favicon/web-app-manifest-192x192.png new file mode 100644 index 0000000..bf3a21e Binary files /dev/null and b/static/favicon/web-app-manifest-192x192.png differ diff --git a/static/favicon/web-app-manifest-512x512.png b/static/favicon/web-app-manifest-512x512.png new file mode 100644 index 0000000..5ddce07 Binary files /dev/null and b/static/favicon/web-app-manifest-512x512.png differ diff --git a/static/icon-large.svg b/static/icon-large.svg new file mode 100644 index 0000000..2488459 --- /dev/null +++ b/static/icon-large.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icon.svg b/static/icon.svg new file mode 100644 index 0000000..5464eaf --- /dev/null +++ b/static/icon.svg @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +