commit 36d8215f0b82201a63a47b6cb8427376baf678ec
Author: hexin-codex-bot
Date: Thu Oct 8 14:05:37 2026 +0800
feat: build responsive short-video feed and Docker deployment
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..fecbcea
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,5 @@
+node_modules
+.next
+.git
+test-results
+*.tsbuildinfo
diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
new file mode 100644
index 0000000..e957fda
--- /dev/null
+++ b/.gitea/workflows/deploy.yml
@@ -0,0 +1,29 @@
+name: Build and deploy Docker
+on:
+ push:
+ branches: [main]
+ workflow_dispatch:
+concurrency:
+ group: tiktok-ui-production
+ cancel-in-progress: false
+jobs:
+ deploy:
+ runs-on: ces-control
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+ - name: Check Docker deployment environment
+ run: |
+ docker version
+ docker compose version
+ docker info --format 'Docker host: {{.Name}}'
+ docker ps --format 'table {{.Names}}\t{{.Ports}}'
+ - name: Build tested production image
+ run: docker build -t tiktok-ui:${{ gitea.sha }} -t tiktok-ui:latest .
+ - name: Deploy persistent service
+ env:
+ IMAGE_TAG: ${{ gitea.sha }}
+ run: |
+ docker compose up -d --no-build --wait --wait-timeout 120
+ docker compose ps
+ echo 'Application is published on the Docker host at port 18080.'
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..96953a8
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+node_modules/
+.next/
+.env*
+!.env.example
+*.tsbuildinfo
+test-results/
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..2dbd1c5
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,22 @@
+FROM node:22-alpine AS base
+WORKDIR /app
+ENV NEXT_TELEMETRY_DISABLED=1
+RUN corepack enable && corepack prepare pnpm@10.28.2 --activate
+FROM base AS deps
+COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
+RUN pnpm install --frozen-lockfile --ignore-scripts
+FROM base AS builder
+COPY --from=deps /app/node_modules ./node_modules
+COPY . .
+RUN pnpm typecheck && pnpm build
+FROM node:22-alpine AS runner
+WORKDIR /app
+ENV NODE_ENV=production NEXT_TELEMETRY_DISABLED=1 PORT=3000 HOSTNAME=0.0.0.0
+RUN addgroup --system --gid 1001 nextjs && adduser --system --uid 1001 nextjs
+COPY --from=builder --chown=nextjs:nextjs /app/.next/standalone ./
+COPY --from=builder --chown=nextjs:nextjs /app/.next/static ./.next/static
+COPY --from=builder --chown=nextjs:nextjs /app/public ./public
+USER nextjs
+EXPOSE 3000
+HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD node -e "fetch('http://127.0.0.1:3000').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
+CMD ["node", "server.js"]
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..23a539a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+# 瞬间 FLOW
+
+Next.js Pages Router + React + TypeScript 的抖音风格单页演示。无登录、无数据库、无后端业务接口。
+
+## 本地启动
+
+Node.js 22,pnpm 10.28.2:
+
+```sh
+corepack enable
+pnpm install --frozen-lockfile
+pnpm dev
+```
+
+浏览器打开 http://localhost:3000 。手机与电脑自适应:手机全屏触摸上下切换,电脑宽屏播放,支持滚轮、上下方向键及右侧切换按钮。空格暂停,M 切换声音。
+
+## 交互
+
+- 三段本地 MP4 无限循环切换,单击暂停、双击点赞、拖动进度、静音/开启声音、桌面全屏。
+- 点赞、收藏、关注按视频保存在本机 localStorage,无需账号。
+- 搜索默认数据、标签搜索、分享视频专属链接、评论抽屉及本次会话评论。
+- 其他导航提供演示提示,不包含其他业务页面。
+
+## 重新生成视频
+
+```sh
+pnpm generate:videos
+```
+
+`scripts/generate-videos.mjs` 使用 Canvas 绘图和 FFmpeg 生成日落、海岸、霓虹三段 8 秒 H.264/AAC 视频及封面。无外部素材下载;成品已提交,正常构建和运行无需 FFmpeg。音轨为程序生成的轻声合成音,初始静音。
+
+## 检查与构建
+
+```sh
+pnpm typecheck
+pnpm build
+pnpm start
+```
+
+## Docker 持久化部署
+
+```sh
+docker compose up -d --build --wait
+```
+
+默认发布到宿主机 `18080` 端口,可用 `PUBLIC_PORT` 覆盖。容器以非 root 用户运行,具有健康检查、日志轮转及 `unless-stopped` 重启策略。
+
+推送 main 触发 `.gitea/workflows/deploy.yml`,使用 `ces-control` 标签 runner 构建并部署。该 runner 必须能访问目标宿主机 Docker daemon 并具备 Compose v2;安全组需允许公网访问所选端口。运行持久性由宿主机 Docker 提供,runner 任务结束不会删除应用容器。若同标签有多个 runner,应由管理员保证标签指向预期部署主机。
+
+后续更新:提交并推送 main 即可。镜像保留提交 SHA 标签,可用 `IMAGE_TAG=<旧提交SHA> docker compose up -d --no-build` 回滚到该宿主机已保留的镜像。
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..bc16b55
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,18 @@
+name: tiktok-ui
+services:
+ web:
+ image: tiktok-ui:${IMAGE_TAG:-latest}
+ build: .
+ restart: unless-stopped
+ ports:
+ - "${PUBLIC_PORT:-18080}:3000"
+ init: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ logging:
+ driver: json-file
+ options:
+ max-size: "10m"
+ max-file: "3"
diff --git a/lib/videos.ts b/lib/videos.ts
new file mode 100644
index 0000000..1cf74a4
--- /dev/null
+++ b/lib/videos.ts
@@ -0,0 +1,7 @@
+export const videos = [
+ {id:'afterglow', title:'把日落,留在这一刻。', description:'山野没有答案,但晚风会让一切慢下来。', author:'山野来信', handle:'outdoor.notes', avatar:'山', color:'#ed9772', tags:['日落收集计划','治愈系风景','慢下来'], music:'Golden hour · 原创氛围音乐', likes:12840, saves:1860, comments:328, src:'/videos/afterglow.mp4', poster:'/videos/afterglow.jpg', location:'山野之间', label:'CHASE THE LAST LIGHT', subtitle:'日落不落幕'},
+ {id:'ocean', title:'今天的烦恼,就交给海吧。', description:'听见浪的声音,就像世界按下了暂停键。', author:'蓝色航线', handle:'blue.route', avatar:'海', color:'#55b9c3', tags:['看海日记','蓝色时刻','自由的风'], music:'Ocean breathing · 原创氛围音乐', likes:23670, saves:3201, comments:516, src:'/videos/ocean.mp4', poster:'/videos/ocean.jpg', location:'蔚蓝海岸', label:'SOMEWHERE, BY THE SEA', subtitle:'去有风的地方'},
+ {id:'night', title:'城市睡了,浪漫还没有。', description:'沿着霓虹的方向,开启一场没有目的地的夜游。', author:'午夜放映室', handle:'midnight.film', avatar:'夜', color:'#b594ed', tags:['城市夜游','霓虹浪漫','赛博空间'], music:'Midnight drive · 原创氛围音乐', likes:9652, saves:987, comments:164, src:'/videos/night.mp4', poster:'/videos/night.jpg', location:'午夜之城', label:'THE NIGHT IS STILL YOUNG', subtitle:'夜色正好'},
+];
+export type VideoItem = typeof videos[number];
+export const compact = (n:number) => n >= 10000 ? `${(n/10000).toFixed(1)}万` : n.toLocaleString('zh-CN');
diff --git a/next-env.d.ts b/next-env.d.ts
new file mode 100644
index 0000000..254b73c
--- /dev/null
+++ b/next-env.d.ts
@@ -0,0 +1,6 @@
+///
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.
diff --git a/next.config.ts b/next.config.ts
new file mode 100644
index 0000000..e441cb7
--- /dev/null
+++ b/next.config.ts
@@ -0,0 +1,3 @@
+import type { NextConfig } from 'next';
+const config: NextConfig = { output: 'standalone', poweredByHeader: false, reactStrictMode: true };
+export default config;
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..faeb175
--- /dev/null
+++ b/package.json
@@ -0,0 +1,7 @@
+{
+ "name": "tiktok-ui", "version": "1.0.0", "private": true,
+ "scripts": {"dev":"next dev --hostname 0.0.0.0", "build":"next build", "start":"next start --hostname 0.0.0.0", "typecheck":"tsc --noEmit", "generate:videos":"node scripts/generate-videos.mjs"},
+ "dependencies": {"next":"^15.5.0", "react":"^19.1.0", "react-dom":"^19.1.0", "lucide-react":"^0.468.0"},
+ "devDependencies": {"typescript":"^5.9.0", "@types/node":"^22.0.0", "@types/react":"^19.0.0", "@types/react-dom":"^19.0.0", "@napi-rs/canvas":"^0.1.80", "ffmpeg-static":"^5.2.0"},
+ "packageManager":"pnpm@10.28.2"
+}
diff --git a/pages/_app.tsx b/pages/_app.tsx
new file mode 100644
index 0000000..5248cf2
--- /dev/null
+++ b/pages/_app.tsx
@@ -0,0 +1,3 @@
+import type { AppProps } from 'next/app';
+import '../styles/globals.css';
+export default function App({ Component, pageProps }: AppProps) { return ; }
diff --git a/pages/_document.tsx b/pages/_document.tsx
new file mode 100644
index 0000000..9ce96a7
--- /dev/null
+++ b/pages/_document.tsx
@@ -0,0 +1,2 @@
+import { Html, Head, Main, NextScript } from 'next/document';
+export default function Document() { return
; }
diff --git a/pages/index.tsx b/pages/index.tsx
new file mode 100644
index 0000000..bc8bfa0
--- /dev/null
+++ b/pages/index.tsx
@@ -0,0 +1,46 @@
+import Head from 'next/head';
+import { useCallback, useEffect, useRef, useState } from 'react';
+import { Heart, MessageCircle, Bookmark, Share2, Plus, Play, Pause, Volume2, VolumeX, ChevronUp, ChevronDown, Search, Home, Compass, Users, Music2, Maximize, X, Check, MapPin, Send, ArrowUpRight, TvMinimal, Ellipsis, UserRound } from 'lucide-react';
+import { videos, compact } from '../lib/videos';
+type Reaction = { liked?:boolean; saved?:boolean; followed?:boolean };
+type Comment = {name:string; text:string};
+const initialComments:Comment[]=[{name:'小岛同学',text:'这一刻,连时间都慢下来了。'},{name:'今天也要开心',text:'好喜欢这个氛围 ✨'},{name:'路过人间',text:'收藏了,留给忙碌的一天。'}];
+export default function Feed(){
+ const [index,setIndex]=useState(0),[playing,setPlaying]=useState(true),[muted,setMuted]=useState(true),[progress,setProgress]=useState(0),[duration,setDuration]=useState(8),[reactions,setReactions]=useState>({}),[ready,setReady]=useState(false),[panel,setPanel]=useState<'comments'|'search'|null>(null),[toast,setToast]=useState(''),[burst,setBurst]=useState(false),[query,setQuery]=useState(''),[draft,setDraft]=useState(''),[comments,setComments]=useState>({}),[tab,setTab]=useState('推荐'),[error,setError]=useState(false);
+ const video=useRef(null),stage=useRef(null),touch=useRef({x:0,y:0}),lastScroll=useRef(0),clickTimer=useRef|null>(null),toastTimer=useRef|null>(null),burstTimer=useRef|null>(null);
+ const item=videos[index],reaction=reactions[item.id]||{};
+ const notify=useCallback((text:string)=>{setToast(text);if(toastTimer.current)clearTimeout(toastTimer.current);toastTimer.current=setTimeout(()=>setToast(''),2400);},[]);
+ useEffect(()=>{try{const data=JSON.parse(localStorage.getItem('flow-reactions')||'{}');if(data && typeof data==='object' && !Array.isArray(data))setReactions(data);}catch{}setReady(true);const id=new URLSearchParams(location.search).get('video');const found=videos.findIndex(v=>v.id===id);if(found>=0)setIndex(found);return()=>{if(clickTimer.current)clearTimeout(clickTimer.current);if(toastTimer.current)clearTimeout(toastTimer.current);if(burstTimer.current)clearTimeout(burstTimer.current);};},[]);
+ useEffect(()=>{if(ready)try{localStorage.setItem('flow-reactions',JSON.stringify(reactions));}catch{}},[reactions,ready]);
+ const next=useCallback((direction:number)=>{setIndex(i=>(i+direction+videos.length)%videos.length);setProgress(0);setPlaying(true);setError(false);setBurst(false);setDraft('');if(clickTimer.current)clearTimeout(clickTimer.current);},[]);
+ useEffect(()=>{const el=video.current;if(!el)return;if(playing){el.play().catch(()=>setPlaying(false));}else el.pause();},[index,playing]);
+ useEffect(()=>{function key(e:KeyboardEvent){if((e.target as HTMLElement).closest('input,textarea,button')||panel)return;if(e.code==='ArrowDown'||e.code==='ArrowUp'){e.preventDefault();next(e.code==='ArrowDown'?1:-1);}if(e.code==='Space'){e.preventDefault();setPlaying(p=>!p);}if(e.code==='KeyM')setMuted(m=>!m);}window.addEventListener('keydown',key);return()=>window.removeEventListener('keydown',key);},[next,panel]);
+ useEffect(()=>{const el=stage.current;if(!el)return;const wheel=(e:WheelEvent)=>{if(panel||Math.abs(e.deltaY)<12||e.ctrlKey)return;e.preventDefault();if(Date.now()-lastScroll.current<750)return;lastScroll.current=Date.now();next(e.deltaY>0?1:-1);};el.addEventListener('wheel',wheel,{passive:false});return()=>el.removeEventListener('wheel',wheel);},[next,panel]);
+ useEffect(()=>{if(!panel)return;const before=document.activeElement as HTMLElement;const dialog=document.querySelector('[role="dialog"]');dialog?.querySelector('button,input')?.focus();const key=(e:KeyboardEvent)=>{if(e.key==='Escape')setPanel(null);if(e.key==='Tab'){const list=dialog?.querySelectorAll('button,input,[tabindex="0"]');if(!list?.length)return;const first=list[0],last=list[list.length-1];if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}}};document.addEventListener('keydown',key);return()=>{document.removeEventListener('keydown',key);before?.focus();};},[panel]);
+ function toggle(field:keyof Reaction){setReactions(r=>({...r,[item.id]:{...r[item.id],[field]:!r[item.id]?.[field]}}));}
+ function like(){setReactions(r=>({...r,[item.id]:{...r[item.id],liked:true}}));setBurst(true);if(burstTimer.current)clearTimeout(burstTimer.current);burstTimer.current=setTimeout(()=>setBurst(false),800);}
+ async function share(){const url=`${location.origin}/?video=${item.id}`;try{if(navigator.share){await navigator.share({title:item.title,url});}else{await navigator.clipboard.writeText(url);notify('视频链接已复制');}}catch(e){if((e as Error).name!=='AbortError')notify('请复制地址栏链接进行分享');}}
+ const time=(s:number)=>`00:${Math.floor(s).toString().padStart(2,'0')}`;
+ const results=videos.filter(v=>`${v.title}${v.author}${v.tags.join('')}`.includes(query.trim()));
+ return <>瞬间 FLOW · 发现生活的另一面
+
+
+
FOR YOU
发现生活的另一面精选推荐
偶遇一个好瞬间
+ {touch.current={x:e.touches[0].clientX,y:e.touches[0].clientY};}} onTouchEnd={e=>{if(panel||(e.target as HTMLElement).closest('button,input'))return;const dy=e.changedTouches[0].clientY-touch.current.y,dx=e.changedTouches[0].clientX-touch.current.x;if(Math.abs(dy)>55&&Math.abs(dy)>Math.abs(dx))next(dy<0?1:-1);}}>
+ ↑↓ 切换视频 Space 暂停 / 播放0{index+1} / 0{videos.length} 好风景,循环看
+
+
+
+ {panel&&setPanel(null)}>
e.stopPropagation()}>{panel==='comments'?`评论 · ${item.comments+(comments[item.id]?.length||0)}`:'发现好瞬间'}
{panel==='comments'?<>演示评论 · 新评论仅在本次浏览中保留
{[...initialComments,...comments[item.id]||[]].map((c,i)=>
{c.name[0]})}
>:<>setQuery(e.target.value)}/>
{results.map(v=>
)}{results.length===0&&
还没有找到,试试“日落”或“海”。
}
>} }
+ {toast}
+ >;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
new file mode 100644
index 0000000..ea71748
--- /dev/null
+++ b/pnpm-lock.yaml
@@ -0,0 +1,916 @@
+lockfileVersion: '9.0'
+
+settings:
+ autoInstallPeers: true
+ excludeLinksFromLockfile: false
+
+importers:
+
+ .:
+ dependencies:
+ lucide-react:
+ specifier: ^0.468.0
+ version: 0.468.0(react@19.3.0)
+ next:
+ specifier: ^15.5.0
+ version: 15.5.27(@types/node@22.20.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0)
+ react:
+ specifier: ^19.1.0
+ version: 19.3.0
+ react-dom:
+ specifier: ^19.1.0
+ version: 19.3.0(react@19.3.0)
+ devDependencies:
+ '@napi-rs/canvas':
+ specifier: ^0.1.80
+ version: 0.1.100
+ '@types/node':
+ specifier: ^22.0.0
+ version: 22.20.5
+ '@types/react':
+ specifier: ^19.0.0
+ version: 19.3.0
+ '@types/react-dom':
+ specifier: ^19.0.0
+ version: 19.3.0(@types/react@19.3.0)
+ ffmpeg-static:
+ specifier: ^5.2.0
+ version: 5.3.0
+ typescript:
+ specifier: ^5.9.0
+ version: 5.9.3
+
+packages:
+
+ '@derhuerst/http-basic@8.2.4':
+ resolution: {integrity: sha512-F9rL9k9Xjf5blCz8HsJRO4diy111cayL2vkY2XE4r4t3n0yPXVYy3KD3nJ1qbrSn9743UWSXH4IwuCa/HWlGFw==}
+ engines: {node: '>=6.0.0'}
+
+ '@emnapi/runtime@1.11.3':
+ resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==}
+
+ '@img/colour@1.1.0':
+ resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
+ engines: {node: '>=18'}
+
+ '@img/sharp-darwin-arm64@0.35.5':
+ resolution: {integrity: sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-darwin-x64@0.35.5':
+ resolution: {integrity: sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ resolution: {integrity: sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==}
+ engines: {node: '>=20.9.0'}
+ os: [freebsd]
+
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ resolution: {integrity: sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ resolution: {integrity: sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ resolution: {integrity: sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ resolution: {integrity: sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ resolution: {integrity: sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ resolution: {integrity: sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ resolution: {integrity: sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ resolution: {integrity: sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ resolution: {integrity: sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ resolution: {integrity: sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linux-arm64@0.35.5':
+ resolution: {integrity: sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-arm@0.35.5':
+ resolution: {integrity: sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-ppc64@0.35.5':
+ resolution: {integrity: sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [ppc64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-riscv64@0.35.5':
+ resolution: {integrity: sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==}
+ engines: {node: '>=20.9.0'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-s390x@0.35.5':
+ resolution: {integrity: sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [s390x]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linux-x64@0.35.5':
+ resolution: {integrity: sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ resolution: {integrity: sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ resolution: {integrity: sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@img/sharp-wasm32@0.35.5':
+ resolution: {integrity: sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==}
+ engines: {node: '>=20.9.0'}
+
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ resolution: {integrity: sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==}
+ engines: {node: '>=20.9.0'}
+ cpu: [wasm32]
+
+ '@img/sharp-win32-arm64@0.35.5':
+ resolution: {integrity: sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==}
+ engines: {node: '>=20.9.0'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@img/sharp-win32-ia32@0.35.5':
+ resolution: {integrity: sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==}
+ engines: {node: ^20.9.0}
+ cpu: [ia32]
+ os: [win32]
+
+ '@img/sharp-win32-x64@0.35.5':
+ resolution: {integrity: sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==}
+ engines: {node: '>=20.9.0'}
+ cpu: [x64]
+ os: [win32]
+
+ '@napi-rs/canvas-android-arm64@0.1.100':
+ resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [android]
+
+ '@napi-rs/canvas-darwin-arm64@0.1.100':
+ resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@napi-rs/canvas-darwin-x64@0.1.100':
+ resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+ resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
+ engines: {node: '>= 10'}
+ cpu: [arm]
+ os: [linux]
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+ resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+ resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+ resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
+ engines: {node: '>= 10'}
+ cpu: [riscv64]
+ os: [linux]
+ libc: [glibc]
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+ resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.100':
+ resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+ resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+ resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@napi-rs/canvas@0.1.100':
+ resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
+ engines: {node: '>= 10'}
+
+ '@next/env@15.5.27':
+ resolution: {integrity: sha512-WC5hvVqiuFOcRyz6gKPeJkiVvSMf3scNeAV49CdWtVqxmUlzh8UKR6YHt9yiNkh8LngtzqSlVz07/Ma3ZAzchA==}
+
+ '@next/swc-darwin-arm64@15.5.27':
+ resolution: {integrity: sha512-fBA9QurmK4+uj4wNhmRTuO5zMVRaBHCvp6H93zTG/5i6Yi6TewbvbesXB8YQNj8T99Cwrow87hHiFV6wL/hyMA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@next/swc-darwin-x64@15.5.27':
+ resolution: {integrity: sha512-FGZwJwry8qZNHxD/nfwrCwCHHrqx6epqItkkxbYcBQEQ2uw5S43bY83fQB5zbAsZE7TELMjoYkbhsQ4oKM7wwg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [darwin]
+
+ '@next/swc-linux-arm64-gnu@15.5.27':
+ resolution: {integrity: sha512-GQulAaJYNF1RYuatlsw9/uzbKz4if2V7Ic9nVO+P0ljYPm4yBRTFIl6g5YkKTw40lMLbpy9edLoH44g3jSHMYA==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-arm64-musl@15.5.27':
+ resolution: {integrity: sha512-URFFsUmu8Pi0/1qdnUI7tx7keZw1Mt5zHfrIX1RfDG9Nih8DYxK54WZsDJUX2ZTlii1jgotL7qcbwKnSAES6xQ==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@next/swc-linux-x64-gnu@15.5.27':
+ resolution: {integrity: sha512-aJp74z7uvU5jQhyH7lyk+4qSRcTdKbKT9NQE3jiO9kWYkoHZjKi0zzHDbCoZ+v2sJDEiJ/z58R0UlWMGWNF3HA==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@next/swc-linux-x64-musl@15.5.27':
+ resolution: {integrity: sha512-ILl2bQ8MbpvyMpY8AXljb3BXs0MJSC8HXZ6QV5AWpVS7ZELqNPtrHvhUk+SNarvjvICXX8bCS287TCi+gUFVYg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@next/swc-win32-arm64-msvc@15.5.27':
+ resolution: {integrity: sha512-1MB9Rh5y8TOPN2x7naNTJNkCZQav9c70AI7EBYXxsEewwOffC4DjjwBwKf20NBvcA+u+5jDv7nVmHxunlP2d7Q==}
+ engines: {node: '>= 10'}
+ cpu: [arm64]
+ os: [win32]
+
+ '@next/swc-win32-x64-msvc@15.5.27':
+ resolution: {integrity: sha512-XtYgpGOswgnyBWXzOEG6q9RhvnSw0CEZ0MbWqCsl5PISgt9EzvJvNW5mU5n1PsphX/lnamzxjgQXH2GUJL8nqg==}
+ engines: {node: '>= 10'}
+ cpu: [x64]
+ os: [win32]
+
+ '@swc/helpers@0.5.15':
+ resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+
+ '@types/node@10.17.60':
+ resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==}
+
+ '@types/node@22.20.5':
+ resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==}
+
+ '@types/react-dom@19.3.0':
+ resolution: {integrity: sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==}
+ peerDependencies:
+ '@types/react': ^19.3.0
+
+ '@types/react@19.3.0':
+ resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==}
+
+ agent-base@6.0.2:
+ resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
+ engines: {node: '>= 6.0.0'}
+
+ buffer-from@1.1.2:
+ resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+ caniuse-lite@1.0.30001815:
+ resolution: {integrity: sha512-uDybUEyfgbyJJ93J1vikTylBX31AkdVYeVPeffMQpf8XN7hSfr0oZOn4VFOIMNvtU5+rHvC5Udg2TgtJKHm23Q==}
+
+ caseless@0.12.0:
+ resolution: {integrity: sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==}
+
+ client-only@0.0.1:
+ resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
+ concat-stream@2.0.0:
+ resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
+ engines: {'0': node >= 6.0}
+
+ csstype@3.2.3:
+ resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+ debug@4.4.3:
+ resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+ engines: {node: '>=6.0'}
+ peerDependencies:
+ supports-color: '*'
+ peerDependenciesMeta:
+ supports-color:
+ optional: true
+
+ detect-libc@2.1.2:
+ resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
+ engines: {node: '>=8'}
+
+ env-paths@2.2.1:
+ resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
+ engines: {node: '>=6'}
+
+ ffmpeg-static@5.3.0:
+ resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==}
+ engines: {node: '>=16'}
+
+ http-response-object@3.0.2:
+ resolution: {integrity: sha512-bqX0XTF6fnXSQcEJ2Iuyr75yVakyjIDCqroJQ/aHfSdlM743Cwqoi2nDYMzLGWUcuTWGWy8AAvOKXTfiv6q9RA==}
+
+ https-proxy-agent@5.0.1:
+ resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
+ engines: {node: '>= 6'}
+
+ inherits@2.0.4:
+ resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+ lucide-react@0.468.0:
+ resolution: {integrity: sha512-6koYRhnM2N0GGZIdXzSeiNwguv1gt/FAjZOiPl76roBi3xKEXa4WmfpxgQwTTL4KipXjefrnf3oV4IsYhi4JFA==}
+ peerDependencies:
+ react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc
+
+ ms@2.1.3:
+ resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+ nanoid@3.3.20:
+ resolution: {integrity: sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==}
+ engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+ hasBin: true
+
+ next@15.5.27:
+ resolution: {integrity: sha512-F82CrlPZ8GRxBH9RIIoR3qU1t5RT3dv1K8moe8SjlDEquiFipvEI13R1Om90TaUOEwShkWCMlg8+gvTNzfMhvg==}
+ engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+ hasBin: true
+ peerDependencies:
+ '@opentelemetry/api': ^1.1.0
+ '@playwright/test': ^1.51.1
+ babel-plugin-react-compiler: '*'
+ react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+ sass: ^1.3.0
+ peerDependenciesMeta:
+ '@opentelemetry/api':
+ optional: true
+ '@playwright/test':
+ optional: true
+ babel-plugin-react-compiler:
+ optional: true
+ sass:
+ optional: true
+
+ parse-cache-control@1.0.1:
+ resolution: {integrity: sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==}
+
+ picocolors@1.1.1:
+ resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+ postcss@8.4.31:
+ resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+ engines: {node: ^10 || ^12 || >=14}
+
+ progress@2.0.3:
+ resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
+ engines: {node: '>=0.4.0'}
+
+ react-dom@19.3.0:
+ resolution: {integrity: sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==}
+ peerDependencies:
+ react: ^19.3.0
+
+ react@19.3.0:
+ resolution: {integrity: sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==}
+ engines: {node: '>=0.10.0'}
+
+ readable-stream@3.6.2:
+ resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
+ engines: {node: '>= 6'}
+
+ safe-buffer@5.2.1:
+ resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==}
+
+ scheduler@0.28.0:
+ resolution: {integrity: sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==}
+
+ semver@7.8.5:
+ resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
+ engines: {node: '>=10'}
+ hasBin: true
+
+ sharp@0.35.5:
+ resolution: {integrity: sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==}
+ engines: {node: '>=20.9.0'}
+ peerDependencies:
+ '@types/node': '*'
+ peerDependenciesMeta:
+ '@types/node':
+ optional: true
+
+ source-map-js@1.2.2:
+ resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==}
+ engines: {node: '>=0.10.0'}
+
+ string_decoder@1.3.0:
+ resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+
+ styled-jsx@5.1.6:
+ resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+ engines: {node: '>= 12.0.0'}
+ peerDependencies:
+ '@babel/core': '*'
+ babel-plugin-macros: '*'
+ react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+ peerDependenciesMeta:
+ '@babel/core':
+ optional: true
+ babel-plugin-macros:
+ optional: true
+
+ tslib@2.8.1:
+ resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+ typedarray@0.0.6:
+ resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
+
+ typescript@5.9.3:
+ resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+ engines: {node: '>=14.17'}
+ hasBin: true
+
+ undici-types@6.21.0:
+ resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+ util-deprecate@1.0.2:
+ resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
+
+snapshots:
+
+ '@derhuerst/http-basic@8.2.4':
+ dependencies:
+ caseless: 0.12.0
+ concat-stream: 2.0.0
+ http-response-object: 3.0.2
+ parse-cache-control: 1.0.1
+
+ '@emnapi/runtime@1.11.3':
+ dependencies:
+ tslib: 2.8.1
+ optional: true
+
+ '@img/colour@1.1.0':
+ optional: true
+
+ '@img/sharp-darwin-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-darwin-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-freebsd-wasm32@0.35.5':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.5
+ optional: true
+
+ '@img/sharp-libvips-darwin-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-darwin-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-arm@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-ppc64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-riscv64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-s390x@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linux-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-arm64@1.3.4':
+ optional: true
+
+ '@img/sharp-libvips-linuxmusl-x64@1.3.4':
+ optional: true
+
+ '@img/sharp-linux-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-arm@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-ppc64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-riscv64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-s390x@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ optional: true
+
+ '@img/sharp-linux-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-linuxmusl-arm64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ optional: true
+
+ '@img/sharp-linuxmusl-x64@0.35.5':
+ optionalDependencies:
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ optional: true
+
+ '@img/sharp-wasm32@0.35.5':
+ dependencies:
+ '@emnapi/runtime': 1.11.3
+ optional: true
+
+ '@img/sharp-webcontainers-wasm32@0.35.5':
+ dependencies:
+ '@img/sharp-wasm32': 0.35.5
+ optional: true
+
+ '@img/sharp-win32-arm64@0.35.5':
+ optional: true
+
+ '@img/sharp-win32-ia32@0.35.5':
+ optional: true
+
+ '@img/sharp-win32-x64@0.35.5':
+ optional: true
+
+ '@napi-rs/canvas-android-arm64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-darwin-arm64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-darwin-x64@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-arm64-musl@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-gnu@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-linux-x64-musl@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-win32-arm64-msvc@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas-win32-x64-msvc@0.1.100':
+ optional: true
+
+ '@napi-rs/canvas@0.1.100':
+ optionalDependencies:
+ '@napi-rs/canvas-android-arm64': 0.1.100
+ '@napi-rs/canvas-darwin-arm64': 0.1.100
+ '@napi-rs/canvas-darwin-x64': 0.1.100
+ '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
+ '@napi-rs/canvas-linux-arm64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-arm64-musl': 0.1.100
+ '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-x64-gnu': 0.1.100
+ '@napi-rs/canvas-linux-x64-musl': 0.1.100
+ '@napi-rs/canvas-win32-arm64-msvc': 0.1.100
+ '@napi-rs/canvas-win32-x64-msvc': 0.1.100
+
+ '@next/env@15.5.27': {}
+
+ '@next/swc-darwin-arm64@15.5.27':
+ optional: true
+
+ '@next/swc-darwin-x64@15.5.27':
+ optional: true
+
+ '@next/swc-linux-arm64-gnu@15.5.27':
+ optional: true
+
+ '@next/swc-linux-arm64-musl@15.5.27':
+ optional: true
+
+ '@next/swc-linux-x64-gnu@15.5.27':
+ optional: true
+
+ '@next/swc-linux-x64-musl@15.5.27':
+ optional: true
+
+ '@next/swc-win32-arm64-msvc@15.5.27':
+ optional: true
+
+ '@next/swc-win32-x64-msvc@15.5.27':
+ optional: true
+
+ '@swc/helpers@0.5.15':
+ dependencies:
+ tslib: 2.8.1
+
+ '@types/node@10.17.60': {}
+
+ '@types/node@22.20.5':
+ dependencies:
+ undici-types: 6.21.0
+
+ '@types/react-dom@19.3.0(@types/react@19.3.0)':
+ dependencies:
+ '@types/react': 19.3.0
+
+ '@types/react@19.3.0':
+ dependencies:
+ csstype: 3.2.3
+
+ agent-base@6.0.2:
+ dependencies:
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ buffer-from@1.1.2: {}
+
+ caniuse-lite@1.0.30001815: {}
+
+ caseless@0.12.0: {}
+
+ client-only@0.0.1: {}
+
+ concat-stream@2.0.0:
+ dependencies:
+ buffer-from: 1.1.2
+ inherits: 2.0.4
+ readable-stream: 3.6.2
+ typedarray: 0.0.6
+
+ csstype@3.2.3: {}
+
+ debug@4.4.3:
+ dependencies:
+ ms: 2.1.3
+
+ detect-libc@2.1.2:
+ optional: true
+
+ env-paths@2.2.1: {}
+
+ ffmpeg-static@5.3.0:
+ dependencies:
+ '@derhuerst/http-basic': 8.2.4
+ env-paths: 2.2.1
+ https-proxy-agent: 5.0.1
+ progress: 2.0.3
+ transitivePeerDependencies:
+ - supports-color
+
+ http-response-object@3.0.2:
+ dependencies:
+ '@types/node': 10.17.60
+
+ https-proxy-agent@5.0.1:
+ dependencies:
+ agent-base: 6.0.2
+ debug: 4.4.3
+ transitivePeerDependencies:
+ - supports-color
+
+ inherits@2.0.4: {}
+
+ lucide-react@0.468.0(react@19.3.0):
+ dependencies:
+ react: 19.3.0
+
+ ms@2.1.3: {}
+
+ nanoid@3.3.20: {}
+
+ next@15.5.27(@types/node@22.20.5)(react-dom@19.3.0(react@19.3.0))(react@19.3.0):
+ dependencies:
+ '@next/env': 15.5.27
+ '@swc/helpers': 0.5.15
+ caniuse-lite: 1.0.30001815
+ postcss: 8.4.31
+ react: 19.3.0
+ react-dom: 19.3.0(react@19.3.0)
+ styled-jsx: 5.1.6(react@19.3.0)
+ optionalDependencies:
+ '@next/swc-darwin-arm64': 15.5.27
+ '@next/swc-darwin-x64': 15.5.27
+ '@next/swc-linux-arm64-gnu': 15.5.27
+ '@next/swc-linux-arm64-musl': 15.5.27
+ '@next/swc-linux-x64-gnu': 15.5.27
+ '@next/swc-linux-x64-musl': 15.5.27
+ '@next/swc-win32-arm64-msvc': 15.5.27
+ '@next/swc-win32-x64-msvc': 15.5.27
+ sharp: 0.35.5(@types/node@22.20.5)
+ transitivePeerDependencies:
+ - '@babel/core'
+ - '@types/node'
+ - babel-plugin-macros
+
+ parse-cache-control@1.0.1: {}
+
+ picocolors@1.1.1: {}
+
+ postcss@8.4.31:
+ dependencies:
+ nanoid: 3.3.20
+ picocolors: 1.1.1
+ source-map-js: 1.2.2
+
+ progress@2.0.3: {}
+
+ react-dom@19.3.0(react@19.3.0):
+ dependencies:
+ react: 19.3.0
+ scheduler: 0.28.0
+
+ react@19.3.0: {}
+
+ readable-stream@3.6.2:
+ dependencies:
+ inherits: 2.0.4
+ string_decoder: 1.3.0
+ util-deprecate: 1.0.2
+
+ safe-buffer@5.2.1: {}
+
+ scheduler@0.28.0: {}
+
+ semver@7.8.5:
+ optional: true
+
+ sharp@0.35.5(@types/node@22.20.5):
+ dependencies:
+ '@img/colour': 1.1.0
+ detect-libc: 2.1.2
+ semver: 7.8.5
+ optionalDependencies:
+ '@img/sharp-darwin-arm64': 0.35.5
+ '@img/sharp-darwin-x64': 0.35.5
+ '@img/sharp-freebsd-wasm32': 0.35.5
+ '@img/sharp-libvips-darwin-arm64': 1.3.4
+ '@img/sharp-libvips-darwin-x64': 1.3.4
+ '@img/sharp-libvips-linux-arm': 1.3.4
+ '@img/sharp-libvips-linux-arm64': 1.3.4
+ '@img/sharp-libvips-linux-ppc64': 1.3.4
+ '@img/sharp-libvips-linux-riscv64': 1.3.4
+ '@img/sharp-libvips-linux-s390x': 1.3.4
+ '@img/sharp-libvips-linux-x64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-arm64': 1.3.4
+ '@img/sharp-libvips-linuxmusl-x64': 1.3.4
+ '@img/sharp-linux-arm': 0.35.5
+ '@img/sharp-linux-arm64': 0.35.5
+ '@img/sharp-linux-ppc64': 0.35.5
+ '@img/sharp-linux-riscv64': 0.35.5
+ '@img/sharp-linux-s390x': 0.35.5
+ '@img/sharp-linux-x64': 0.35.5
+ '@img/sharp-linuxmusl-arm64': 0.35.5
+ '@img/sharp-linuxmusl-x64': 0.35.5
+ '@img/sharp-webcontainers-wasm32': 0.35.5
+ '@img/sharp-win32-arm64': 0.35.5
+ '@img/sharp-win32-ia32': 0.35.5
+ '@img/sharp-win32-x64': 0.35.5
+ '@types/node': 22.20.5
+ optional: true
+
+ source-map-js@1.2.2: {}
+
+ string_decoder@1.3.0:
+ dependencies:
+ safe-buffer: 5.2.1
+
+ styled-jsx@5.1.6(react@19.3.0):
+ dependencies:
+ client-only: 0.0.1
+ react: 19.3.0
+
+ tslib@2.8.1: {}
+
+ typedarray@0.0.6: {}
+
+ typescript@5.9.3: {}
+
+ undici-types@6.21.0: {}
+
+ util-deprecate@1.0.2: {}
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
new file mode 100644
index 0000000..aabb9ae
--- /dev/null
+++ b/pnpm-workspace.yaml
@@ -0,0 +1,2 @@
+onlyBuiltDependencies:
+ - ffmpeg-static
diff --git a/scripts/generate-videos.mjs b/scripts/generate-videos.mjs
new file mode 100644
index 0000000..653cdf6
--- /dev/null
+++ b/scripts/generate-videos.mjs
@@ -0,0 +1,36 @@
+import { createCanvas } from '@napi-rs/canvas';
+import ffmpeg from 'ffmpeg-static';
+import { spawn } from 'node:child_process';
+import { mkdir, writeFile } from 'node:fs/promises';
+import { once } from 'node:events';
+const W=960,H=960,FPS=24,SECONDS=8;
+await mkdir('public/videos',{recursive:true});
+const canvas=createCanvas(W,H), c=canvas.getContext('2d');
+function gradient(top,bottom) {const g=c.createLinearGradient(0,0,0,H);g.addColorStop(0,top);g.addColorStop(1,bottom);c.fillStyle=g;c.fillRect(0,0,W,H);}
+function hill(y,color,phase,height){c.beginPath();c.moveTo(0,H);for(let x=0;x<=W;x+=4){c.lineTo(x,y+Math.sin(x/190+phase)*height+Math.cos(x/77+phase)*height*.22);}c.lineTo(W,H);c.fillStyle=color;c.fill();}
+function draw(scene,t){
+ if(scene==='afterglow'){
+ gradient('#273d65','#f9b990');const glow=c.createRadialGradient(560,335,10,560,335,370);glow.addColorStop(0,'#ffd9a4aa');glow.addColorStop(1,'#ffd9a400');c.fillStyle=glow;c.fillRect(0,0,W,H);
+ c.fillStyle='#ffe1ad';c.beginPath();c.arc(560,338+Math.sin(t)*3,68,0,Math.PI*2);c.fill();
+ for(let i=0;i<7;i++)hill(490+i*66,['#b798aa','#947f9d','#706d8e','#52617a','#3c5267','#2c4255','#1b303e'][i],i*.8+Math.sin(t)*.025,42+i*10);
+ c.strokeStyle='#262f49';c.lineWidth=2;for(let i=0;i<6;i++){let x=130+i*25+Math.sin(t)*28,y=300+Math.sin(i)*14;c.beginPath();c.moveTo(x-6,y);c.quadraticCurveTo(x,y-6,x+6,y);c.quadraticCurveTo(x+12,y-5,x+18,y);c.stroke();}
+ } else if(scene==='ocean'){
+ gradient('#456f8a','#f5d4b5');c.fillStyle='#f9e2bf';c.beginPath();c.arc(660,260,44,0,7);c.fill();
+ const sea=c.createLinearGradient(0,400,0,H);sea.addColorStop(0,'#438597');sea.addColorStop(1,'#092e44');c.fillStyle=sea;c.fillRect(0,400,W,560);
+ for(let row=0;row<75;row++){let y=405+row*7.7;c.beginPath();for(let x=0;x<=W;x+=6)c.lineTo(x,y+Math.sin(x/(60-row*.5)+t*2+row*.55)*(2+row*.10));c.strokeStyle=`rgba(174,229,221,${.12+row/280})`;c.lineWidth=1+row/60;c.stroke();}
+ for(let i=0;i<90;i++){let y=410+i*5,x=660+Math.sin(i*4+t)*i*.7;c.fillStyle=`rgba(255,219,158,${.28-i*.002})`;c.fillRect(x,y,10+i*.8,2);}
+ hill(860,'#172e3c',2,60);hill(930,'#10232e',4,65);
+ } else {
+ gradient('#0c112a','#4f315d');c.fillStyle='#d9c9ff';c.beginPath();c.arc(700,210,48,0,7);c.fill();
+ for(let i=0;i<90;i++){let x=(i*137.3)%W,y=(i*51.7)%450;c.globalAlpha=.3+.5*Math.abs(Math.sin(t+i));c.fillRect(x,y,2,2);}c.globalAlpha=1;
+ for(let i=0;i<24;i++){let x=i*44-30,h=80+((i*137)%210);c.fillStyle=i%2?'#171c38':'#1b2343';c.fillRect(x,620-h,40,h);for(let j=0;j<9;j++)for(let k=0;k<3;k++){if((i+j+k)%3){c.fillStyle=(i+j)%2?'#ce84bc':'#63b9c6';c.globalAlpha=.45;c.fillRect(x+7+k*10,625-h+j*22,3,7);}}}c.globalAlpha=1;
+ c.fillStyle='#0d1326';c.fillRect(0,620,W,340);c.strokeStyle='#734277';for(let i=-10;i<=10;i++){c.beginPath();c.moveTo(480+i*9,620);c.lineTo(480+i*150,H);c.stroke();}for(let i=0;i<18;i++){let p=((i/18+t/(Math.PI*2))%1);let y=620+p*p*340;c.strokeStyle=`rgba(176,93,164,${p*.6})`;c.beginPath();c.moveTo(0,y);c.lineTo(W,y);c.stroke();}
+ c.strokeStyle='#ff709d';c.lineWidth=3;c.beginPath();c.moveTo(440,620);c.lineTo(135,H);c.stroke();c.strokeStyle='#6cdbdf';c.beginPath();c.moveTo(520,620);c.lineTo(825,H);c.stroke();
+ }
+}
+for(const scene of ['afterglow','ocean','night']){
+ const child=spawn(ffmpeg,['-y','-f','image2pipe','-vcodec','png','-r',String(FPS),'-i','pipe:0','-f','lavfi','-i',`sine=frequency=${scene==='afterglow'?220:scene==='ocean'?174:261}:sample_rate=44100`,'-t',String(SECONDS),'-af','volume=0.025,afade=t=in:d=1,afade=t=out:st=7:d=1','-c:v','libx264','-preset','fast','-crf','24','-pix_fmt','yuv420p','-c:a','aac','-movflags','+faststart',`public/videos/${scene}.mp4`],{stdio:['pipe','ignore','pipe']});
+ let errors='';child.stderr.on('data',d=>errors+=d);const finished=once(child,'close');
+ for(let frame=0;framespan,.header-note>span{height:5px;width:5px;border-radius:50%;background:#66c3a1}.main{flex:1;min-width:0;padding:26px 82px 0 32px;position:relative;display:flex;flex-direction:column}.desktop-header{display:flex;align-items:center;justify-content:space-between;flex-shrink:0;padding:0 0 23px}.eyebrow{color:#656574;font-size:9px;letter-spacing:3px;font-weight:700}h1{font-size:20px;letter-spacing:1px;margin:8px 0 0;font-weight:650}h1 span{font-size:10px;letter-spacing:0;color:#ada6a2;border:1px solid #6c5d503f;background:#302a232f;padding:4px 7px;border-radius:4px;vertical-align:middle;margin-left:14px;font-weight:400}.header-note{display:flex;align-items:center;gap:7px;color:#858591;font-size:11px}.stage{position:relative;flex:1;min-height:0;overflow:hidden;border-radius:13px;background:#182333;isolation:isolate;touch-action:none}.video{position:absolute;width:100%;height:100%;object-fit:cover;object-position:center 48%}.scrim{position:absolute;inset:0;background:linear-gradient(0deg,#05070be6 0%,#07090e80 16%,transparent 55%,#0d152533 100%);pointer-events:none}.video-hit{position:absolute;inset:0;width:100%;height:100%}.stage-top{position:absolute;top:24px;left:25px;right:24px;display:flex;gap:16px;align-items:center;pointer-events:none}.curated{font-size:11px;letter-spacing:2px;border:1px solid #ffffff38;border-radius:4px;padding:6px 9px;background:#ffffff0b;display:flex;align-items:center;gap:7px}.curated>span{width:4px;height:4px;background:#fff;border-radius:50%}.location{font-size:11px;display:flex;gap:4px;align-items:center;color:#fffc}.sound-top{margin-left:auto;background:#0003;display:flex;padding:9px;border-radius:50%;pointer-events:auto}.scene-copy{position:absolute;top:25%;left:0;width:100%;text-align:center;pointer-events:none;text-shadow:0 3px 20px #152c3d44}.scene-copy>span{display:block;letter-spacing:6px;font-size:10px;opacity:.8}.scene-copy strong{font-family:'Songti SC','Noto Serif SC',SimSun,serif;display:block;letter-spacing:15px;font-weight:400;font-size:clamp(35px,4.7vw,75px);margin:21px 0 18px}.scene-copy i{font-style:normal;font-size:8px;letter-spacing:3px;opacity:.5}.video-info{position:absolute;bottom:83px;left:28px;right:110px;pointer-events:none}.video-info button{pointer-events:auto}.editor-label{font-size:10px;color:#fff9;letter-spacing:1px}.editor-label span{margin:0 8px}.video-info h2{font-size:20px;margin:11px 0 12px;display:flex;align-items:center;gap:8px;letter-spacing:.2px}.verified{display:inline-flex;background:#54b8d8;border-radius:50%;padding:2px}.video-info h3{font-size:16px;font-weight:500;margin:0 0 7px}.video-info p{font-size:13px;color:#ffffffc0;margin:0 0 10px;line-height:1.6}.tags{display:flex;gap:12px;flex-wrap:wrap}.tags button{font-size:12px;color:#ece0c8}.music{display:flex;align-items:center;gap:7px;margin-top:16px;font-size:11px;color:#fff9}.music-bars{margin-left:12px;color:#fffa;font-size:10px}.actions{position:absolute;right:26px;bottom:88px;display:flex;align-items:center;gap:22px;flex-direction:column}.avatar{border-radius:50%;height:44px;width:44px;position:relative;border:2px solid white;font-family:serif;font-size:21px;color:#fff;text-shadow:0 1px 4px #0004}.avatar>span{display:flex;align-items:center;justify-content:center;background:#ff3058;border-radius:50%;position:absolute;width:19px;height:19px;bottom:-9px;left:11px}.action{display:flex;flex-direction:column;align-items:center;gap:7px;filter:drop-shadow(0 2px 4px #0003)}.action>svg{height:30px;width:30px;stroke-width:1.4;transition:transform .2s}.action:active>svg{transform:scale(1.22)}.action span{font-size:11px;font-weight:600}.action.liked{color:#ff365e}.action.saved{color:#ffd266}.record{width:39px;height:39px;background:repeating-radial-gradient(#26262c 0 2px,#121216 3px 5px);border:1px solid #ffffff1a;border-radius:50%;display:grid;place-items:center}.record>span{width:23px;height:23px;border-radius:50%;display:grid;place-items:center}.spinning{animation:spin 8s linear infinite}.player-controls{position:absolute;bottom:0;width:100%;padding:0 24px;background:linear-gradient(transparent,#080b1166)}.progress{--progress:0%;display:block;appearance:none;width:100%;height:3px;border:0;padding:0;cursor:pointer;background:linear-gradient(to right,#fffc var(--progress),#ffffff35 var(--progress));border-radius:9px;margin:0;touch-action:none}.progress::-webkit-slider-thumb{appearance:none;width:8px;height:8px;border-radius:50%;background:white}.progress::-moz-range-thumb{width:8px;height:8px;background:white;border:0;border-radius:50%}.control-row{display:flex;height:51px;align-items:center;gap:18px}.control-row button{display:flex;align-items:center}.time{font-size:10px;font-variant-numeric:tabular-nums;letter-spacing:.5px}.time>span{color:#fff7}.control-hint{font-size:10px;color:#fff5;margin:auto}.quality{font-size:11px;gap:5px}.quality span{font-size:8px;padding:1px 3px;border:1px solid #fff5;border-radius:2px}.desktop-footer{height:53px;display:flex;align-items:center;justify-content:space-between;color:#575764;font-size:10px;flex-shrink:0}.desktop-footer>span{display:flex;align-items:center;gap:6px}.key{border:1px solid #303039;padding:2px 5px;border-radius:4px;color:#858591}.key.space{font-size:9px}.footer-divider{height:10px;width:1px;background:#303039;margin:0 9px}.current-number{color:#d9d9df}.next-controls{position:absolute;right:21px;top:47%;display:flex;align-items:center;flex-direction:column;border:1px solid #ffffff09;background:#16161c;border-radius:24px}.next-controls button{padding:14px 10px;display:flex;color:#9696a2}.next-controls>span{height:1px;width:18px;background:#ffffff0b}.next-controls svg{width:20px;height:20px}.paused,.heart-burst{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);pointer-events:none;color:#ffffffaa;filter:drop-shadow(0 4px 18px #0003)}.heart-burst{color:#ff3058;animation:heart .8s ease-out both}.video-error{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);padding:24px;background:#16161ceb;border-radius:12px;font-size:14px}.video-error button{display:block;margin:16px auto 0;color:#61dfd6}.mobile-top,.mobile-bottom{display:none}.modal-backdrop{position:fixed;inset:0;z-index:20;background:#0008;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px)}.panel{width:min(450px,94vw);max-height:80dvh;background:#19191f;border:1px solid #ffffff12;border-radius:18px;padding:24px;display:flex;flex-direction:column;box-shadow:0 24px 100px #0008}.panel>header{display:flex;align-items:center;justify-content:space-between}.panel h2{font-size:18px;margin:0}.panel header button{display:flex;color:#aaa}.panel-note{font-size:11px;color:#828290;margin-top:17px}.comments{overflow:auto;min-height:120px;max-height:42dvh}.comments article{display:flex;gap:12px;margin:24px 0;align-items:flex-start}.comment-avatar{display:grid;place-items:center;border-radius:50%;width:34px;height:34px;background:#343441;color:#c6bdd7;flex-shrink:0}.comments article>div{flex:1}.comments strong{font-size:12px;color:#92929f}.comments p{font-size:13px;line-height:1.6;margin:6px 0}.comments small{font-size:10px;color:#62626f}.comments article>svg{color:#747480;margin-top:18px}.panel form,.search-input{display:flex;align-items:center;gap:12px;background:#272730;border-radius:9px;padding:11px 13px}.panel form{margin-top:20px}.panel input{border:0;outline:none;background:none;color:#fff;font:inherit;font-size:13px;flex:1;min-width:0}.panel form button{color:#ff476d}.search-input{margin-top:25px;color:#9494a2}.search-results{overflow:auto}.search-results>button{display:flex;align-items:center;width:100%;text-align:left;gap:14px;margin:20px 0}.search-results img{width:60px;height:75px;object-fit:cover;border-radius:7px}.search-results span{flex:1}.search-results strong{font-size:13px;display:block}.search-results small{font-size:11px;color:#878795;display:block;margin-top:10px}.search-results>p{font-size:13px;color:#999;padding:20px}.toast{position:fixed;z-index:50;bottom:100px;left:50%;transform:translate(-50%,15px);background:#24242ef2;border:1px solid #ffffff17;box-shadow:0 8px 30px #0005;border-radius:30px;padding:12px 22px;max-width:85vw;font-size:13px;opacity:0;pointer-events:none;transition:.2s;text-align:center}.toast.visible{opacity:1;transform:translate(-50%,0)}.stage:fullscreen{border-radius:0}.stage:fullscreen .video{object-fit:contain;background:#000}@keyframes spin{to{transform:rotate(360deg)}}@keyframes heart{0%{opacity:0;transform:translate(-50%,-50%) scale(.3) rotate(-20deg)}35%{opacity:1;transform:translate(-50%,-50%) scale(1.1) rotate(-12deg)}100%{opacity:0;transform:translate(-50%,-90%) scale(1.3) rotate(-12deg)}}
+@media(min-width:1600px){.sidebar{width:260px}.main{padding-left:42px;padding-right:94px}.video-info{bottom:95px;left:35px}.actions{gap:27px;right:32px}.video-info h2{font-size:23px}.scene-copy{top:26%}}
+@media(max-width:1050px) and (min-width:701px){.sidebar{width:190px;padding:28px 14px}.brand{font-size:24px}.brand-en{font-size:8px}.main{padding-left:20px;padding-right:65px}.header-note{display:none}.scene-copy strong{font-size:45px;letter-spacing:8px}.scene-copy>span{letter-spacing:3px}.actions{right:20px;gap:20px}.video-info{left:23px}.video-info p{max-width:80%}.control-hint{visibility:hidden}.next-controls{right:12px}}
+@media(max-width:700px){.sidebar,.desktop-header,.desktop-footer,.next-controls,.stage-top,.control-row,.editor-label{display:none}.main{padding:0}.stage{border-radius:0}.app-shell{flex-direction:column}.mobile-bottom{display:flex;flex-shrink:0;height:calc(58px + env(safe-area-inset-bottom));padding:6px 8px calc(6px + env(safe-area-inset-bottom));background:#08080b;align-items:center;justify-content:space-around;border-top:1px solid #ffffff12}.mobile-bottom button{display:flex;flex-direction:column;gap:3px;align-items:center;font-size:10px;color:#aaaab1;width:54px}.mobile-bottom button svg{width:22px;height:22px}.mobile-bottom .selected{color:white;font-weight:600}.mobile-bottom .create{width:42px;height:29px;border-radius:7px;background:#fff;color:#08080b;box-shadow:-5px 0 #25e2db,5px 0 #fc315a;display:grid;place-items:center;margin:0 6px}.mobile-bottom .create svg{width:26px;height:26px}.mobile-top{position:absolute;top:calc(18px + env(safe-area-inset-top));left:18px;right:18px;display:flex;justify-content:space-between;align-items:center}.mobile-top>div{display:flex;gap:28px}.mobile-top button{font-size:17px;font-weight:600;color:#fffb;position:relative}.mobile-top button.selected{color:#fff}.mobile-top .selected:after{content:'';position:absolute;width:22px;height:3px;background:white;bottom:-10px;left:50%;transform:translateX(-50%);border-radius:2px}.mobile-top>button{display:flex}.video{object-position:50% 50%}.scene-copy{top:29%}.scene-copy>span{font-size:8px;letter-spacing:3px}.scene-copy strong{font-size:43px;letter-spacing:8px;margin:22px 0}.scene-copy i{font-size:6px;letter-spacing:2px}.video-info{left:15px;bottom:24px;right:69px}.video-info h2{font-size:17px;margin-bottom:11px}.video-info h3{font-size:14px;line-height:1.5;margin-bottom:4px}.video-info p{font-size:12px;line-height:1.6;margin-bottom:7px}.tags{gap:6px 9px}.tags button{font-size:12px}.music{margin-top:12px;font-size:11px;gap:6px}.music-bars{display:none}.actions{right:12px;bottom:32px;gap:23px}.action>svg{width:32px;height:32px}.action span{font-size:11px}.avatar{height:43px;width:43px;margin-bottom:3px}.record{width:36px;height:36px}.player-controls{padding:0;bottom:1px}.progress{height:2px}.progress::-webkit-slider-thumb{height:5px;width:5px}.scrim{background:linear-gradient(0deg,#06090bcc,transparent 60%,#0c172044)}.modal-backdrop{align-items:flex-end;backdrop-filter:blur(4px)}.panel{width:100%;max-height:78dvh;border-radius:18px 18px 0 0;padding:22px 20px calc(22px + env(safe-area-inset-bottom))}.toast{bottom:85px;font-size:12px}.paused svg{width:46px}.video-error{width:75%;text-align:center}}
+@media(max-height:680px){.actions{gap:13px}.action>svg{width:27px;height:27px}.scene-copy{top:23%}.sidebar-bottom{padding-top:0}.sidebar-bottom p{margin-top:10px}.video-info p{display:none}.video-info{bottom:72px}.record{display:none}}
+@media(max-height:680px) and (max-width:700px){.video-info{bottom:22px}.actions{bottom:30px}.scene-copy strong{font-size:35px}.scene-copy i{display:none}}
+@media(prefers-reduced-motion:reduce){*,*:before,*:after{animation:none!important;transition:none!important}}
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..f535ab3
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1 @@
+{"compilerOptions":{"target":"ES2017","lib":["dom","dom.iterable","esnext"],"allowJs":true,"skipLibCheck":true,"strict":true,"noEmit":true,"esModuleInterop":true,"module":"esnext","moduleResolution":"bundler","resolveJsonModule":true,"isolatedModules":true,"jsx":"preserve","incremental":true},"include":["next-env.d.ts","**/*.ts","**/*.tsx"],"exclude":["node_modules"]}