From b0377f5dbaa0c70541c783689a7ed214602739e6 Mon Sep 17 00:00:00 2001
From: xKevIsDev
Date: Thu, 2 Jul 2026 14:28:29 +0100
Subject: [PATCH 1/3] chore: add bolt-slides template
---
bolt-slides/README.md | 34 +
bolt-slides/index.html | 13 +
bolt-slides/package-lock.json | 1779 +++++++++++++++++
bolt-slides/package.json | 22 +
bolt-slides/src/App.tsx | 272 +++
bolt-slides/src/components/Accordion.tsx | 58 +
bolt-slides/src/components/Bento.tsx | 41 +
bolt-slides/src/components/BrowserFrame.tsx | 15 +
bolt-slides/src/components/Charts.tsx | 65 +
bolt-slides/src/components/CodeWindow.tsx | 46 +
bolt-slides/src/components/Comparison.tsx | 34 +
bolt-slides/src/components/CountUp.tsx | 42 +
bolt-slides/src/components/Marquee.tsx | 12 +
bolt-slides/src/components/Split.tsx | 25 +
bolt-slides/src/components/SpotlightCard.tsx | 16 +
bolt-slides/src/components/StatGrid.tsx | 30 +
bolt-slides/src/components/Tabs.tsx | 35 +
bolt-slides/src/components/TiltCard.tsx | 25 +
bolt-slides/src/components/Timeline.tsx | 37 +
.../src/components/VisualDashboard.tsx | 53 +
bolt-slides/src/deck/Annotator.tsx | 153 ++
bolt-slides/src/deck/Build.tsx | 53 +
bolt-slides/src/deck/Deck.tsx | 240 +++
bolt-slides/src/deck/DeckContext.ts | 16 +
bolt-slides/src/deck/Reveal.tsx | 29 +
bolt-slides/src/deck/Slide.tsx | 25 +
bolt-slides/src/deck/icons.tsx | 12 +
bolt-slides/src/deck/useInView.ts | 23 +
bolt-slides/src/main.tsx | 11 +
bolt-slides/src/styles/base.css | 375 ++++
bolt-slides/src/styles/tokens.css | 55 +
bolt-slides/tsconfig.json | 21 +
bolt-slides/tsconfig.node.json | 11 +
bolt-slides/vite.config.ts | 6 +
34 files changed, 3684 insertions(+)
create mode 100644 bolt-slides/README.md
create mode 100644 bolt-slides/index.html
create mode 100644 bolt-slides/package-lock.json
create mode 100644 bolt-slides/package.json
create mode 100644 bolt-slides/src/App.tsx
create mode 100644 bolt-slides/src/components/Accordion.tsx
create mode 100644 bolt-slides/src/components/Bento.tsx
create mode 100644 bolt-slides/src/components/BrowserFrame.tsx
create mode 100644 bolt-slides/src/components/Charts.tsx
create mode 100644 bolt-slides/src/components/CodeWindow.tsx
create mode 100644 bolt-slides/src/components/Comparison.tsx
create mode 100644 bolt-slides/src/components/CountUp.tsx
create mode 100644 bolt-slides/src/components/Marquee.tsx
create mode 100644 bolt-slides/src/components/Split.tsx
create mode 100644 bolt-slides/src/components/SpotlightCard.tsx
create mode 100644 bolt-slides/src/components/StatGrid.tsx
create mode 100644 bolt-slides/src/components/Tabs.tsx
create mode 100644 bolt-slides/src/components/TiltCard.tsx
create mode 100644 bolt-slides/src/components/Timeline.tsx
create mode 100644 bolt-slides/src/components/VisualDashboard.tsx
create mode 100644 bolt-slides/src/deck/Annotator.tsx
create mode 100644 bolt-slides/src/deck/Build.tsx
create mode 100644 bolt-slides/src/deck/Deck.tsx
create mode 100644 bolt-slides/src/deck/DeckContext.ts
create mode 100644 bolt-slides/src/deck/Reveal.tsx
create mode 100644 bolt-slides/src/deck/Slide.tsx
create mode 100644 bolt-slides/src/deck/icons.tsx
create mode 100644 bolt-slides/src/deck/useInView.ts
create mode 100644 bolt-slides/src/main.tsx
create mode 100644 bolt-slides/src/styles/base.css
create mode 100644 bolt-slides/src/styles/tokens.css
create mode 100644 bolt-slides/tsconfig.json
create mode 100644 bolt-slides/tsconfig.node.json
create mode 100644 bolt-slides/vite.config.ts
diff --git a/bolt-slides/README.md b/bolt-slides/README.md
new file mode 100644
index 00000000..5cd949f4
--- /dev/null
+++ b/bolt-slides/README.md
@@ -0,0 +1,34 @@
+# Bolt slides skill
+
+A Bolt skill that builds a premium, **responsive React presentation deck** — classic
+paged slides you present one at a time, with a Slidev-style floating dock + thumbnail
+rail, click-builds, auto-play, annotation, and presenter mode — but each slide is a
+responsive web layout (no fixed canvas, no clipping) built from a rich component
+library.
+
+## The skill
+
+This repo **is** the running app. The authoring guide lives at
+[`.bolt/skills/slides.md`](./.bolt/skills/slides.md); the app itself sits at the repo
+root — a complete Vite + React deck: the paged engine + chrome (`src/deck/`), ~14
+components (`src/components/`: Bento, Split, StatGrid, Comparison, Tabs, Accordion,
+Timeline, CodeWindow, BrowserFrame, SpotlightCard, charts, CountUp, TiltCard,
+Marquee, …), and the token-driven theme (`src/styles/`). The engine is left as-is;
+only the `:root` token block and the slides in `src/App.tsx` are authored per deck.
+
+## Add it in Bolt
+
+1. In Bolt's **Add skill from GitHub**, paste this repo's URL —
+ `https://github.com/inkko44/bolt-slides-skill`.
+2. The `slides` skill auto-discovers at `.bolt/skills/slides.md`.
+3. Tell Bolt to use the `slides` skill and build a deck about your topic/brand.
+
+## Run it locally
+
+```bash
+npm install
+npm run dev
+```
+
+`npm run dev` opens the deck at `/`. Re-theme everything by editing one `:root` block
+in `src/styles/tokens.css`.
diff --git a/bolt-slides/index.html b/bolt-slides/index.html
new file mode 100644
index 00000000..56383bc9
--- /dev/null
+++ b/bolt-slides/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+ Replace — your deck title
+
+
+
+
+
+
+
diff --git a/bolt-slides/package-lock.json b/bolt-slides/package-lock.json
new file mode 100644
index 00000000..4b5d7aae
--- /dev/null
+++ b/bolt-slides/package-lock.json
@@ -0,0 +1,1779 @@
+{
+ "name": "slides",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "slides",
+ "dependencies": {
+ "framer-motion": "^11.3.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
+ "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
+ "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
+ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-compilation-targets": "^7.29.7",
+ "@babel/helper-module-transforms": "^7.29.7",
+ "@babel/helpers": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz",
+ "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
+ "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.29.7",
+ "@babel/helper-validator-option": "^7.29.7",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
+ "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
+ "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
+ "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7",
+ "@babel/traverse": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz",
+ "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
+ "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
+ "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
+ "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
+ "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
+ "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz",
+ "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz",
+ "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
+ "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/types": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz",
+ "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.7",
+ "@babel/generator": "^7.29.7",
+ "@babel/helper-globals": "^7.29.7",
+ "@babel/parser": "^7.29.7",
+ "@babel/template": "^7.29.7",
+ "@babel/types": "^7.29.7",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
+ "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.29.7",
+ "@babel/helper-validator-identifier": "^7.29.7"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz",
+ "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz",
+ "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz",
+ "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz",
+ "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz",
+ "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz",
+ "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz",
+ "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz",
+ "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz",
+ "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz",
+ "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz",
+ "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz",
+ "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz",
+ "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz",
+ "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz",
+ "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz",
+ "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz",
+ "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz",
+ "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz",
+ "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz",
+ "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz",
+ "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz",
+ "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz",
+ "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz",
+ "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz",
+ "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.31",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz",
+ "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.40",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.40.tgz",
+ "integrity": "sha512-BSSLZ9/Cjjv7Gtj5B68ZzXcXUg8iOf3fme+FCuh8rC/Go+Kmh8cox7M3A8dolou16s64QjLPOSdngh7GxXvkSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.4.tgz",
+ "integrity": "sha512-MTc8i/x9jBQd1iMw2CFGS+rwMa07eYjLR0CCTLDACl9xhxy+nIs3KeML/biicXtk9JrZ6dnnTatmc7ErPXIxqw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.38",
+ "caniuse-lite": "^1.0.30001799",
+ "electron-to-chromium": "^1.5.376",
+ "node-releases": "^2.0.48",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001800",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001800.tgz",
+ "integrity": "sha512-MMHtuAz9Ys840zAY5F4k6fV5GaivZ9sPk+nz0mY+GYVzRBnYkN0mpqkSR92oWRQ19yQWo4HvBV/FnC16AJX8MA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.382",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.382.tgz",
+ "integrity": "sha512-8ETaWbV6SZOrno+G93Ffd9ENsMtetqdnqj4nlfxFW90Sm5GgnuV28Kf62hqQVD6VUgzm7qFQKsTsAPmeUiU3Ug==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/framer-motion": {
+ "version": "11.18.2",
+ "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.18.2.tgz",
+ "integrity": "sha512-5F5Och7wrvtLVElIpclDT0CBzMVg3dL22B64aZwHtsIY8RB4mXICLrkajK4G9R+ieSAGcgrLeae2SeUTg2pr6w==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-dom": "^11.18.1",
+ "motion-utils": "^11.18.1",
+ "tslib": "^2.4.0"
+ },
+ "peerDependencies": {
+ "@emotion/is-prop-valid": "*",
+ "react": "^18.0.0 || ^19.0.0",
+ "react-dom": "^18.0.0 || ^19.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@emotion/is-prop-valid": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/motion-dom": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.18.1.tgz",
+ "integrity": "sha512-g76KvA001z+atjfxczdRtw/RXOM3OMSdd1f4DL77qCTF/+avrRJiawSG4yDibEQ215sr9kpinSlX2pCTJ9zbhw==",
+ "license": "MIT",
+ "dependencies": {
+ "motion-utils": "^11.18.1"
+ }
+ },
+ "node_modules/motion-utils": {
+ "version": "11.18.1",
+ "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.18.1.tgz",
+ "integrity": "sha512-49Kt+HKjtbJKLtgO/LKj9Ld+6vw9BjH5d9sc40R/kVyH8GLAXgT42M2NnuPcJNuA3s9ZfZBUcwIgpmZWGEE+hA==",
+ "license": "MIT"
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.15",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
+ "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.50",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
+ "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.16",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
+ "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.12",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.62.2",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz",
+ "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.9"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.62.2",
+ "@rollup/rollup-android-arm64": "4.62.2",
+ "@rollup/rollup-darwin-arm64": "4.62.2",
+ "@rollup/rollup-darwin-x64": "4.62.2",
+ "@rollup/rollup-freebsd-arm64": "4.62.2",
+ "@rollup/rollup-freebsd-x64": "4.62.2",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.62.2",
+ "@rollup/rollup-linux-arm-musleabihf": "4.62.2",
+ "@rollup/rollup-linux-arm64-gnu": "4.62.2",
+ "@rollup/rollup-linux-arm64-musl": "4.62.2",
+ "@rollup/rollup-linux-loong64-gnu": "4.62.2",
+ "@rollup/rollup-linux-loong64-musl": "4.62.2",
+ "@rollup/rollup-linux-ppc64-gnu": "4.62.2",
+ "@rollup/rollup-linux-ppc64-musl": "4.62.2",
+ "@rollup/rollup-linux-riscv64-gnu": "4.62.2",
+ "@rollup/rollup-linux-riscv64-musl": "4.62.2",
+ "@rollup/rollup-linux-s390x-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-gnu": "4.62.2",
+ "@rollup/rollup-linux-x64-musl": "4.62.2",
+ "@rollup/rollup-openbsd-x64": "4.62.2",
+ "@rollup/rollup-openharmony-arm64": "4.62.2",
+ "@rollup/rollup-win32-arm64-msvc": "4.62.2",
+ "@rollup/rollup-win32-ia32-msvc": "4.62.2",
+ "@rollup/rollup-win32-x64-gnu": "4.62.2",
+ "@rollup/rollup-win32-x64-msvc": "4.62.2",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "license": "0BSD"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.21",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
+ "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/bolt-slides/package.json b/bolt-slides/package.json
new file mode 100644
index 00000000..40aae25c
--- /dev/null
+++ b/bolt-slides/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "slides",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "framer-motion": "^11.3.0",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
+ },
+ "devDependencies": {
+ "@types/react": "^18.3.3",
+ "@types/react-dom": "^18.3.0",
+ "@vitejs/plugin-react": "^4.3.1",
+ "typescript": "^5.5.3",
+ "vite": "^5.4.0"
+ }
+}
diff --git a/bolt-slides/src/App.tsx b/bolt-slides/src/App.tsx
new file mode 100644
index 00000000..f150ad15
--- /dev/null
+++ b/bolt-slides/src/App.tsx
@@ -0,0 +1,272 @@
+import Deck from './deck/Deck'
+import Slide from './deck/Slide'
+import Build from './deck/Build'
+import Reveal from './deck/Reveal'
+import Bento from './components/Bento'
+import Split from './components/Split'
+import CountUp from './components/CountUp'
+import TiltCard from './components/TiltCard'
+import Marquee from './components/Marquee'
+import VisualDashboard from './components/VisualDashboard'
+import StatGrid from './components/StatGrid'
+import Accordion from './components/Accordion'
+import Comparison from './components/Comparison'
+import Tabs from './components/Tabs'
+import Timeline from './components/Timeline'
+import CodeWindow from './components/CodeWindow'
+import BrowserFrame from './components/BrowserFrame'
+import SpotlightCard from './components/SpotlightCard'
+import { BarChart, LineChart, DonutChart } from './components/Charts'
+
+/* ══════════════════════════════════════════════════════════════════════
+ ⚠️ THROWAWAY DEMO showing every component. DELETE these slides and AUTHOR
+ THE USER'S DECK. Each child of is one slide. Add speaker notes with
+ notes="…" on any slide (shown in presenter mode — press P). (A = auto-play.)
+ ══════════════════════════════════════════════════════════════════════ */
+const panel = (extra = 0.22): React.CSSProperties => ({ position: 'absolute', inset: 0, background: `radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--primary) ${extra * 100}%, transparent), transparent 60%), var(--surface-2)` })
+const card: React.CSSProperties = { padding: 22, borderRadius: 'var(--radius)', background: 'var(--surface)', border: '1px solid var(--hair)' }
+
+export default function App() {
+ return (
+
+ {/* Cover */}
+
+
+ Helix · Component demo
+ Helix
+ A responsive React deck engine. Delete this and build the real one.
+
+
+
+ {/* Statement + click-build */}
+
+
+ Dashboards are everywhere. Insight isn't.
+
+
+ Helix turns raw events into answers — automatically.
+
+
+
+ {/* Split feature */}
+ Everything, as it happens. >}
+ body="Live metrics with sub-second latency — no pipelines to babysit."
+ media={<>
>}
+ />
+
+ {/* Bento */}
+ , body: 'events / min at peak.', c: 5, r: 2, variant: 'glow' },
+ { k: 'Uptime', fig: , c: 4 },
+ { k: 'Regions', fig: , c: 3, variant: 'accent' },
+ { k: 'Connectors', title: '120+ native', body: 'Snowflake, Kafka, dbt…', c: 4 },
+ { k: 'Compliance', title: 'SOC 2 · HIPAA', c: 3 },
+ ]}
+ />
+
+ {/* StatGrid — traction */}
+ , label: 'ARR', caption: 'up 3× year over year' },
+ { value: , label: 'Net retention', caption: 'best in class' },
+ { value: , label: 'Enterprise logos', caption: 'across six industries' },
+ ]}
+ />
+
+ {/* Comparison */}
+
+
+ Why teams switch
+ The honest comparison.
+
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ One platform
+ Built for every team.
+
+
+ Trace any request end-to-end, alert on anomalies, ship with confidence.
},
+ { label: 'Data', content:
},
+ { label: 'Ops', content: One source of truth for uptime, cost, and capacity — no spreadsheets.
},
+ ]} />
+
+
+
+ {/* Split + code */}
+ Drop-in simple. >}
+ body="Add it to your app in three lines. No SDK to learn, no schema to define."
+ media={<>
+
+
>}
+ />
+
+ {/* Browser frame */}
+
+
+ See it live
+ Your data, one screen.
+
+
+
+
+
+
+
Helix
+ {['Overview', 'Events', 'Funnels', 'Cohorts', 'Settings'].map((n, i) => (
+
{n}
+ ))}
+
+
+
+
Overview
+ Last 30 days
+
+
+ {[['Revenue', '$1.24M', '▲ 18.2%'], ['Active users', '48,210', '▲ 9.4%'], ['Churn', '1.9%', '▼ 0.6%']].map(([l, v, d]) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ {/* Charts */}
+
+
+ The numbers
+ Growth you can see.
+
+
+
+
+
+
+ {/* Timeline */}
+
+
+ Where we're going
+ The roadmap.
+
+
+
+
+
+
+ {/* Spotlight principles */}
+
+
+ What we believe
+ Three principles.
+
+
+
+ {[
+ { k: '01', t: 'Fast by default', d: 'Speed is a feature. Everything is realtime.' },
+ { k: '02', t: 'Yours to own', d: 'Your data, your infra, no lock-in.' },
+ { k: '03', t: 'Honest pricing', d: 'No per-seat tax. Scale without surprises.' },
+ ].map((p) => (
+
+ {p.k}
+ {p.t}
+ {p.d}
+
+ ))}
+
+
+
+
+ {/* Accordion — FAQ */}
+
+
+ Common questions
+ Frequently asked.
+
+
+
+
+
+
+ {/* Logos */}
+
+ Trusted by teams everywhere
+
+
+
+ {/* Quote */}
+
+
+
+ “We replaced four tools with Helix and never looked back.”
+
+ — Dana Kim, VP Engineering
+
+
+
+ {/* CTA */}
+
+
+ Let's talk.
+ helix.io · hello@helix.io
+
+
+
+ )
+}
diff --git a/bolt-slides/src/components/Accordion.tsx b/bolt-slides/src/components/Accordion.tsx
new file mode 100644
index 00000000..44175f04
--- /dev/null
+++ b/bolt-slides/src/components/Accordion.tsx
@@ -0,0 +1,58 @@
+import { useState, type ReactNode } from 'react'
+import { AnimatePresence, motion } from 'framer-motion'
+
+/* An expand/collapse accordion. Data-driven like Bento/StatGrid.
+
+ - single (default true): only one panel open at a time.
+ - defaultOpen: index open on mount (null = all closed). */
+export type AccordionItem = { title: string; body: ReactNode }
+
+const Chevron = () => (
+
+
+
+)
+
+export default function Accordion({
+ items, single = true, defaultOpen = 0,
+}: {
+ items: AccordionItem[]
+ single?: boolean
+ defaultOpen?: number | null
+}) {
+ const [open, setOpen] = useState(defaultOpen == null ? [] : [defaultOpen])
+ const isOpen = (i: number) => open.includes(i)
+ const toggle = (i: number) =>
+ setOpen((cur) => {
+ const has = cur.includes(i)
+ if (single) return has ? [] : [i]
+ return has ? cur.filter((x) => x !== i) : [...cur, i]
+ })
+
+ return (
+
+ {items.map((it, i) => (
+
+
toggle(i)} aria-expanded={isOpen(i)}>
+ {String(i + 1).padStart(2, '0')}
+ {it.title}
+
+
+
+ {isOpen(i) && (
+
+ {it.body}
+
+ )}
+
+
+ ))}
+
+ )
+}
diff --git a/bolt-slides/src/components/Bento.tsx b/bolt-slides/src/components/Bento.tsx
new file mode 100644
index 00000000..6f455ccb
--- /dev/null
+++ b/bolt-slides/src/components/Bento.tsx
@@ -0,0 +1,41 @@
+import type { CSSProperties, ReactNode } from 'react'
+import Reveal from '../deck/Reveal'
+
+/* A full-viewport slide laid out as an asymmetric bento grid. Spans via c
+ (columns of 12) and r (rows). Collapses to one column on narrow screens.
+ nav/notes are read by the engine (rail label / presenter notes). */
+export type BentoTile = {
+ k?: string; fig?: ReactNode; title?: string; body?: string
+ c?: number; r?: number; variant?: 'accent' | 'glow'
+}
+
+export default function Bento({ kicker, title, tiles }: {
+ kicker?: string; title?: string; tiles: BentoTile[]; nav?: string; notes?: string
+}) {
+ return (
+
+
+
+ {kicker && {kicker}
}
+ {title && {title} }
+
+
+ {tiles.map((t, i) => (
+
+ {t.k &&
{t.k}
}
+
+ {t.fig != null &&
{t.fig}
}
+ {t.title &&
{t.title} }
+ {t.body &&
{t.body}
}
+
+
+ ))}
+
+
+
+ )
+}
diff --git a/bolt-slides/src/components/BrowserFrame.tsx b/bolt-slides/src/components/BrowserFrame.tsx
new file mode 100644
index 00000000..31834e13
--- /dev/null
+++ b/bolt-slides/src/components/BrowserFrame.tsx
@@ -0,0 +1,15 @@
+import type { ReactNode } from 'react'
+
+/* Wrap a screenshot or UI mock in browser chrome. children fill the body —
+ an , a , anything. */
+export default function BrowserFrame({ url = 'app.acme.com', children }: { url?: string; children: ReactNode }) {
+ return (
+
+
+
+ {url}
+
+
{children}
+
+ )
+}
diff --git a/bolt-slides/src/components/Charts.tsx b/bolt-slides/src/components/Charts.tsx
new file mode 100644
index 00000000..130be0e2
--- /dev/null
+++ b/bolt-slides/src/components/Charts.tsx
@@ -0,0 +1,65 @@
+import { motion } from 'framer-motion'
+import type { CSSProperties } from 'react'
+import { useInView } from '../deck/useInView'
+
+/* A small hand-built chart kit — bar, line/area, and donut. Each draws itself in
+ when scrolled into view. All token-driven (one accent), no chart library.
+
+
+ */
+
+export function BarChart({ data, height = 200 }: { data: { label: string; value: number }[]; height?: number }) {
+ const { ref, inView } = useInView(0.3)
+ const max = Math.max(...data.map((d) => d.value)) || 1
+ return (
+
+ {data.map((d, i) => (
+
+ ))}
+
+ )
+}
+
+export function LineChart({ points, height = 200 }: { points: number[]; height?: number }) {
+ const { ref, inView } = useInView(0.3)
+ const w = 300, h = 120
+ const max = Math.max(...points), min = Math.min(...points)
+ const span = max - min || 1
+ const coords = points.map((p, i) => [(i / (points.length - 1)) * w, h - ((p - min) / span) * (h - 10) - 5])
+ const line = coords.map((c, i) => `${i === 0 ? 'M' : 'L'}${c[0].toFixed(1)},${c[1].toFixed(1)}`).join(' ')
+ const area = `${line} L${w},${h} L0,${h} Z`
+ return (
+
+
+
+
+
+
+
+ )
+}
+
+export function DonutChart({ value, label, size = 168 }: { value: number; label?: string; size?: number }) {
+ const { ref, inView } = useInView(0.4)
+ const r = 54
+ const circ = 2 * Math.PI * r
+ return (
+
+
+
+
+ {value}%
+
+ {label &&
{label}
}
+
+ )
+}
diff --git a/bolt-slides/src/components/CodeWindow.tsx b/bolt-slides/src/components/CodeWindow.tsx
new file mode 100644
index 00000000..251b5ca8
--- /dev/null
+++ b/bolt-slides/src/components/CodeWindow.tsx
@@ -0,0 +1,46 @@
+import { Fragment, type ReactNode } from 'react'
+
+/* A macOS-style code window with line numbers, restrained syntax tinting, and
+ optional line highlighting. Highlighting is intentionally minimal (keywords,
+ strings, comments) to stay on-brand — one accent, no rainbow.
+ */
+const KW = /\b(const|let|var|function|return|if|else|for|while|import|from|export|default|class|new|await|async|type|interface|extends|implements|public|private|true|false|null|undefined)\b/g
+
+function hl(line: string): ReactNode {
+ // split off a trailing line comment
+ const ci = line.indexOf('//')
+ const code = ci >= 0 ? line.slice(0, ci) : line
+ const comment = ci >= 0 ? line.slice(ci) : ''
+ // strings first (odd indices), then keywords inside the rest
+ const segs = code.split(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g)
+ return (
+ <>
+ {segs.map((seg, i) =>
+ i % 2 === 1
+ ? {seg}
+ : {seg.split(KW).map((s, j) => (j % 2 === 1 ? {s} : s))} ,
+ )}
+ {comment && {comment} }
+ >
+ )
+}
+
+export default function CodeWindow({ title = 'app.tsx', code, highlight = [] }: { title?: string; code: string; highlight?: number[] }) {
+ const lines = code.replace(/\n$/, '').split('\n')
+ return (
+
+
+
+ {title}
+
+
+ {lines.map((ln, i) => (
+
+ {i + 1}
+ {ln.length ? hl(ln) : ' '}
+
+ ))}
+
+
+ )
+}
diff --git a/bolt-slides/src/components/Comparison.tsx b/bolt-slides/src/components/Comparison.tsx
new file mode 100644
index 00000000..1800e2eb
--- /dev/null
+++ b/bolt-slides/src/components/Comparison.tsx
@@ -0,0 +1,34 @@
+/* A "us vs. them" feature matrix. One value column is highlighted (your column).
+
+ values are booleans (→ ✓/✗) or strings; `highlight` indexes the value columns. */
+import type { CSSProperties } from 'react'
+
+export type CompRow = { label: string; values: (boolean | string)[] }
+
+const Check = () => ( )
+const Cross = () => ( )
+
+export default function Comparison({ cols, rows, highlight = 0 }: { cols: string[]; rows: CompRow[]; highlight?: number }) {
+ const vcols = cols.length - 1
+ return (
+
+
+ {cols.map((c, i) => (
+
{c}
+ ))}
+
+ {rows.map((r, ri) => (
+
+
{r.label}
+ {r.values.map((v, vi) => (
+
+ {typeof v === 'boolean' ? (v ? : ) : {v} }
+
+ ))}
+
+ ))}
+
+ )
+}
diff --git a/bolt-slides/src/components/CountUp.tsx b/bolt-slides/src/components/CountUp.tsx
new file mode 100644
index 00000000..e00be403
--- /dev/null
+++ b/bolt-slides/src/components/CountUp.tsx
@@ -0,0 +1,42 @@
+import { useEffect, useRef, useState } from 'react'
+import { useDeck } from '../deck/DeckContext'
+
+/* A number that animates 0 → to when its slide becomes active. Inherits color,
+ so inside a .figure / .stat-value it picks up the accent gradient. Renders
+ the final value instantly in thumbnails. */
+export default function CountUp({
+ to, from = 0, duration = 1500, decimals = 0, prefix = '', suffix = '',
+}: {
+ to: number; from?: number; duration?: number; decimals?: number; prefix?: string; suffix?: string
+}) {
+ const { isStatic } = useDeck()
+ const ref = useRef(null)
+ const [val, setVal] = useState(isStatic ? to : from)
+ const started = useRef(false)
+
+ useEffect(() => {
+ if (isStatic) { setVal(to); return }
+ const el = ref.current
+ if (!el) return
+ let raf = 0
+ const run = () => {
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setVal(to); return }
+ const t0 = performance.now()
+ const tick = (now: number) => {
+ const p = Math.min(1, (now - t0) / duration)
+ setVal(from + (to - from) * (1 - Math.pow(1 - p, 3)))
+ if (p < 1) raf = requestAnimationFrame(tick)
+ }
+ raf = requestAnimationFrame(tick)
+ }
+ const io = new IntersectionObserver(
+ (entries) => entries.forEach((e) => { if (e.isIntersecting && !started.current) { started.current = true; run() } }),
+ { threshold: 0.4 },
+ )
+ io.observe(el)
+ return () => { io.disconnect(); cancelAnimationFrame(raf) }
+ }, [isStatic, to, from, duration])
+
+ const text = prefix + val.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + suffix
+ return {text}
+}
diff --git a/bolt-slides/src/components/Marquee.tsx b/bolt-slides/src/components/Marquee.tsx
new file mode 100644
index 00000000..4336e4d3
--- /dev/null
+++ b/bolt-slides/src/components/Marquee.tsx
@@ -0,0 +1,12 @@
+/* A continuously-scrolling strip — logo wall, value props, tech stack. */
+export default function Marquee({ items, duration = 26 }: { items: string[]; duration?: number }) {
+ return (
+
+
+ {[...items, ...items].map((it, i) => (
+ {it}
+ ))}
+
+
+ )
+}
diff --git a/bolt-slides/src/components/Split.tsx b/bolt-slides/src/components/Split.tsx
new file mode 100644
index 00000000..6181cefe
--- /dev/null
+++ b/bolt-slides/src/components/Split.tsx
@@ -0,0 +1,25 @@
+import type { ReactNode } from 'react'
+import Reveal from '../deck/Reveal'
+
+/* A full-bleed feature slide: text one side, edge-to-edge media the other.
+ `flip` swaps sides. Stacks vertically on narrow screens. */
+export default function Split({
+ kicker, title, body, media, flip,
+}: {
+ kicker?: string; title: ReactNode; body?: ReactNode; media: ReactNode; flip?: boolean; nav?: string; notes?: string
+}) {
+ return (
+
+
+
+
+ {kicker && {kicker}
}
+ {title}
+ {body && {body}
}
+
+
+
{media}
+
+
+ )
+}
diff --git a/bolt-slides/src/components/SpotlightCard.tsx b/bolt-slides/src/components/SpotlightCard.tsx
new file mode 100644
index 00000000..46a6dc27
--- /dev/null
+++ b/bolt-slides/src/components/SpotlightCard.tsx
@@ -0,0 +1,16 @@
+import { useRef, type ReactNode } from 'react'
+
+/* A card with a soft accent glow that follows the cursor (the Linear/Vercel
+ hover). Wrap content; it provides the card shell + padding.
+ …
… …
*/
+export default function SpotlightCard({ children }: { children: ReactNode }) {
+ const ref = useRef(null)
+ function move(e: React.MouseEvent) {
+ const el = ref.current
+ if (!el) return
+ const r = el.getBoundingClientRect()
+ el.style.setProperty('--mx', `${e.clientX - r.left}px`)
+ el.style.setProperty('--my', `${e.clientY - r.top}px`)
+ }
+ return {children}
+}
diff --git a/bolt-slides/src/components/StatGrid.tsx b/bolt-slides/src/components/StatGrid.tsx
new file mode 100644
index 00000000..4d100e7c
--- /dev/null
+++ b/bolt-slides/src/components/StatGrid.tsx
@@ -0,0 +1,30 @@
+import type { ReactNode } from 'react'
+import Reveal from '../deck/Reveal'
+
+/* A full-viewport proof slide: responsive auto-fit stat cards. Pass a
+ as a stat `value` so figures animate in. */
+export type Stat = { value?: ReactNode; label: string; caption?: string }
+
+export default function StatGrid({ kicker, title, stats }: {
+ kicker?: string; title?: string; stats: Stat[]; nav?: string; notes?: string
+}) {
+ return (
+
+
+
+ {kicker && {kicker}
}
+ {title && {title} }
+
+
+ {stats.map((s, i) => (
+
+
{s.value}
+
{s.label}
+ {s.caption &&
{s.caption}
}
+
+ ))}
+
+
+
+ )
+}
diff --git a/bolt-slides/src/components/Tabs.tsx b/bolt-slides/src/components/Tabs.tsx
new file mode 100644
index 00000000..f55892d7
--- /dev/null
+++ b/bolt-slides/src/components/Tabs.tsx
@@ -0,0 +1,35 @@
+import { useState, type ReactNode } from 'react'
+import { AnimatePresence, motion } from 'framer-motion'
+
+/* Tabbed content with a sliding accent pill (framer layoutId) and a cross-fade
+ between panels. }, …]} /> */
+export type Tab = { label: string; content: ReactNode }
+
+export default function Tabs({ tabs, defaultTab = 0 }: { tabs: Tab[]; defaultTab?: number }) {
+ const [active, setActive] = useState(defaultTab)
+ return (
+
+
+ {tabs.map((t, i) => (
+ setActive(i)} role="tab" aria-selected={i === active}>
+ {i === active && }
+ {t.label}
+
+ ))}
+
+
+
+
+ {tabs[active].content}
+
+
+
+
+ )
+}
diff --git a/bolt-slides/src/components/TiltCard.tsx b/bolt-slides/src/components/TiltCard.tsx
new file mode 100644
index 00000000..1b57ab7a
--- /dev/null
+++ b/bolt-slides/src/components/TiltCard.tsx
@@ -0,0 +1,25 @@
+import { useRef, useState, type CSSProperties, type ReactNode } from 'react'
+
+/* Mouse-tracked 3D tilt + glare. Wrap one hero visual per slide. */
+export default function TiltCard({ children }: { children: ReactNode }) {
+ const ref = useRef(null)
+ const [style, setStyle] = useState({})
+
+ function move(e: React.MouseEvent) {
+ const r = ref.current!.getBoundingClientRect()
+ const px = (e.clientX - r.left) / r.width - 0.5
+ const py = (e.clientY - r.top) / r.height - 0.5
+ setStyle({
+ transform: `perspective(1200px) rotateX(${(-py * 6).toFixed(2)}deg) rotateY(${(px * 9).toFixed(2)}deg)`,
+ ['--gx' as string]: `${((px + 0.5) * 100).toFixed(1)}%`,
+ ['--gy' as string]: `${((py + 0.5) * 100).toFixed(1)}%`,
+ } as CSSProperties)
+ }
+
+ return (
+ setStyle({})}>
+ {children}
+
+
+ )
+}
diff --git a/bolt-slides/src/components/Timeline.tsx b/bolt-slides/src/components/Timeline.tsx
new file mode 100644
index 00000000..25a8d84b
--- /dev/null
+++ b/bolt-slides/src/components/Timeline.tsx
@@ -0,0 +1,37 @@
+import { motion } from 'framer-motion'
+import type { ReactNode } from 'react'
+import { useInView } from '../deck/useInView'
+
+/* A vertical timeline / roadmap. The connector line draws in and milestones
+ reveal in sequence when scrolled into view.
+ */
+export type TimelineItem = { time: string; title: string; body?: ReactNode }
+
+export default function Timeline({ items }: { items: TimelineItem[] }) {
+ const { ref, inView } = useInView(0.2)
+ return (
+
+
+
+
+
+ {items.map((it, i) => (
+
+
+
+
{it.time}
+
{it.title}
+ {it.body &&
{it.body}
}
+
+
+ ))}
+
+
+ )
+}
diff --git a/bolt-slides/src/components/VisualDashboard.tsx b/bolt-slides/src/components/VisualDashboard.tsx
new file mode 100644
index 00000000..af2a75ed
--- /dev/null
+++ b/bolt-slides/src/components/VisualDashboard.tsx
@@ -0,0 +1,53 @@
+import { useEffect, useRef, useState } from 'react'
+import CountUp from './CountUp'
+import { useDeck } from '../deck/DeckContext'
+
+/* Example analytics visual (a self-drawing dashboard mock). ONLY use it when
+ the deck is genuinely about data — otherwise build a topic-fit visual in the
+ same .vframe shell. Draws in when its slide becomes active. */
+const bars = [44, 62, 55, 78, 96, 70, 58]
+const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+export default function VisualDashboard() {
+ const { isStatic } = useDeck()
+ const ref = useRef(null)
+ const [shown, setShown] = useState(isStatic)
+
+ useEffect(() => {
+ if (isStatic) { setShown(true); return }
+ const el = ref.current
+ if (!el) return
+ const io = new IntersectionObserver(
+ (entries) => entries.forEach((e) => { if (e.isIntersecting) setShown(true) }),
+ { threshold: 0.3 },
+ )
+ io.observe(el)
+ return () => io.disconnect()
+ }, [isStatic])
+
+ return (
+
+
+
Overview · Last 30 days
+
Live
+
+
+
+
+
+
+
+
+
+
+ {bars.map((h, i) => (
+
+ ))}
+
+
+ )
+}
diff --git a/bolt-slides/src/deck/Annotator.tsx b/bolt-slides/src/deck/Annotator.tsx
new file mode 100644
index 00000000..fa646102
--- /dev/null
+++ b/bolt-slides/src/deck/Annotator.tsx
@@ -0,0 +1,153 @@
+import { useEffect, useRef, useState } from 'react'
+
+/* State-of-the-art annotation: a full-screen canvas + a floating tool bar.
+ Tools: pen, highlighter, line, arrow, rectangle, ellipse, eraser; a color
+ palette; three sizes; undo + clear. Strokes are stored as data (per slide,
+ in `store`) so they PERSIST on the slide they were made on, and undo /
+ stroke-erase work cleanly. The bar + drawing are only interactive when
+ `active`; otherwise the canvas just displays the slide's saved annotations. */
+
+type Tool = 'pen' | 'highlighter' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'eraser'
+type Pt = { x: number; y: number }
+export type Stroke = { tool: Tool; color: string; size: number; points: Pt[] }
+
+const TOOLS: { id: Tool; label: string; path: string }[] = [
+ { id: 'pen', label: 'Pen', path: 'M4 20h4L18 10a2 2 0 0 0-3-3L5 17z' },
+ { id: 'highlighter', label: 'Highlighter', path: 'M4 20h5l8-8-4-4-9 9zM13 7l4 4' },
+ { id: 'line', label: 'Line', path: 'M5 19L19 5' },
+ { id: 'arrow', label: 'Arrow', path: 'M6 18L18 6M18 6h-6M18 6v6' },
+ { id: 'rect', label: 'Rectangle', path: 'M4 6h16v12H4z' },
+ { id: 'ellipse', label: 'Ellipse', path: 'M12 6c4.5 0 8 2.7 8 6s-3.5 6-8 6-8-2.7-8-6 3.5-6 8-6z' },
+ { id: 'eraser', label: 'Eraser', path: 'M8 18l-4-4a2 2 0 0 1 0-3l7-7a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3l-7 7zM7 17h11' },
+]
+const COLORS = ['var(--primary)', '#ffffff', '#ef4444', '#f5b73a', '#4aa8ff']
+const SIZES = [3, 6, 11]
+
+const IconUndo = () => ( )
+const IconTrash = () => ( )
+
+function distToSeg(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
+ const dx = bx - ax, dy = by - ay
+ const len = dx * dx + dy * dy || 1
+ let t = ((px - ax) * dx + (py - ay) * dy) / len
+ t = Math.max(0, Math.min(1, t))
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy))
+}
+function outline(s: Stroke): Pt[] {
+ if (s.points.length < 2) return s.points
+ const a = s.points[0], b = s.points[s.points.length - 1]
+ if (s.tool === 'rect') return [a, { x: b.x, y: a.y }, b, { x: a.x, y: b.y }, a]
+ if (s.tool === 'ellipse') {
+ const cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2, rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2
+ return Array.from({ length: 25 }, (_, i) => ({ x: cx + rx * Math.cos((i / 24) * 2 * Math.PI), y: cy + ry * Math.sin((i / 24) * 2 * Math.PI) }))
+ }
+ return s.points
+}
+function hits(s: Stroke, x: number, y: number, r: number) {
+ const pts = outline(s)
+ if (pts.length === 1) return Math.hypot(pts[0].x - x, pts[0].y - y) < r + s.size
+ for (let i = 0; i < pts.length - 1; i++) if (distToSeg(x, y, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y) < r + s.size / 2) return true
+ return false
+}
+
+export default function Annotator({ slide, store, active }: { slide: number; store: Record; active: boolean }) {
+ const canvasRef = useRef(null)
+ if (!store[slide]) store[slide] = []
+ const strokes = useRef(store[slide])
+ const draft = useRef(null)
+ const [tool, setTool] = useState('pen')
+ const [color, setColor] = useState(COLORS[0])
+ const [size, setSize] = useState(SIZES[1])
+ const toolRef = useRef(tool), colorRef = useRef(color), sizeRef = useRef(size)
+ toolRef.current = tool; colorRef.current = color; sizeRef.current = size
+
+ const resolve = (c: string) => c.startsWith('var(') ? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4fe5b0') : c
+
+ function paint(ctx: CanvasRenderingContext2D, s: Stroke) {
+ if (!s.points.length) return
+ ctx.save()
+ ctx.strokeStyle = resolve(s.color); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = s.size
+ if (s.tool === 'highlighter') { ctx.globalAlpha = 0.32; ctx.lineWidth = s.size * 3.2 }
+ const p = s.points, a = p[0], b = p[p.length - 1]
+ ctx.beginPath()
+ if (s.tool === 'pen' || s.tool === 'highlighter') { ctx.moveTo(a.x, a.y); for (let i = 1; i < p.length; i++) ctx.lineTo(p[i].x, p[i].y); ctx.stroke() }
+ else if (s.tool === 'line' || s.tool === 'arrow') {
+ ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke()
+ if (s.tool === 'arrow') {
+ const ang = Math.atan2(b.y - a.y, b.x - a.x), h = 8 + s.size * 1.8
+ ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.lineTo(b.x - h * Math.cos(ang - 0.4), b.y - h * Math.sin(ang - 0.4))
+ ctx.moveTo(b.x, b.y); ctx.lineTo(b.x - h * Math.cos(ang + 0.4), b.y - h * Math.sin(ang + 0.4)); ctx.stroke()
+ }
+ } else if (s.tool === 'rect') { ctx.strokeRect(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.abs(b.x - a.x), Math.abs(b.y - a.y)) }
+ else if (s.tool === 'ellipse') { ctx.ellipse((a.x + b.x) / 2, (a.y + b.y) / 2, Math.abs(b.x - a.x) / 2, Math.abs(b.y - a.y) / 2, 0, 0, Math.PI * 2); ctx.stroke() }
+ ctx.restore()
+ }
+ function redraw() {
+ const cv = canvasRef.current; if (!cv) return
+ const ctx = cv.getContext('2d'); if (!ctx) return
+ const dpr = window.devicePixelRatio || 1
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
+ ctx.clearRect(0, 0, cv.width / dpr, cv.height / dpr)
+ for (const s of strokes.current) paint(ctx, s)
+ if (draft.current) paint(ctx, draft.current)
+ }
+ function commit() { store[slide] = strokes.current; redraw() }
+ function erase(x: number, y: number) {
+ const before = strokes.current.length
+ strokes.current = strokes.current.filter((s) => !hits(s, x, y, 12))
+ if (strokes.current.length !== before) commit()
+ }
+
+ useEffect(() => {
+ const cv = canvasRef.current; if (!cv) return
+ const fit = () => { const dpr = window.devicePixelRatio || 1; cv.width = window.innerWidth * dpr; cv.height = window.innerHeight * dpr; cv.style.width = window.innerWidth + 'px'; cv.style.height = window.innerHeight + 'px'; redraw() }
+ fit()
+ const onUp = () => { if (draft.current) { strokes.current.push(draft.current); draft.current = null; commit() } }
+ window.addEventListener('resize', fit); window.addEventListener('pointerup', onUp)
+ return () => { window.removeEventListener('resize', fit); window.removeEventListener('pointerup', onUp) }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ function down(e: React.PointerEvent) {
+ const x = e.clientX, y = e.clientY
+ if (toolRef.current === 'eraser') { erase(x, y); return }
+ draft.current = { tool: toolRef.current, color: colorRef.current, size: sizeRef.current, points: [{ x, y }] }
+ redraw()
+ }
+ function move(e: React.PointerEvent) {
+ const x = e.clientX, y = e.clientY
+ if (toolRef.current === 'eraser') { if (e.buttons) erase(x, y); return }
+ const d = draft.current; if (!d) return
+ if (d.tool === 'pen' || d.tool === 'highlighter') d.points.push({ x, y })
+ else d.points = [d.points[0], { x, y }]
+ redraw()
+ }
+
+ return (
+ <>
+
+ {active && (
+
+ {TOOLS.map((t) => (
+
setTool(t.id)}>
+
+
+ ))}
+
+ {COLORS.map((c) => (
+
setColor(c)} style={{ background: c }} />
+ ))}
+
+ {SIZES.map((s) => (
+ setSize(s)}>
+
+
+ ))}
+
+ { strokes.current = strokes.current.slice(0, -1); commit() }}>
+ { strokes.current = []; commit() }}>
+
+ )}
+ >
+ )
+}
diff --git a/bolt-slides/src/deck/Build.tsx b/bolt-slides/src/deck/Build.tsx
new file mode 100644
index 00000000..50de6813
--- /dev/null
+++ b/bolt-slides/src/deck/Build.tsx
@@ -0,0 +1,53 @@
+import { motion } from 'framer-motion'
+import { useEffect } from 'react'
+import type { CSSProperties, ReactNode } from 'react'
+import { useDeck } from './DeckContext'
+
+/* Click-build (the Slidev "v-click"): this element stays hidden until the
+ presenter advances to step `at` (1-based) on the current slide, then it
+ animates in. Reveal content in beats: … . The deck
+ advances builds before moving to the next slide.
+
+ In AUTO mode (Deck autoplay / the dock auto button / "A"), every build
+ instead animates in automatically on slide load, staggered by `at`. */
+export default function Build({
+ at = 1, y = 16, children, className, style,
+}: {
+ at?: number
+ y?: number
+ children: ReactNode
+ className?: string
+ style?: CSSProperties
+}) {
+ const { clicks, isStatic, auto, stagger = 0.16, registerMax } = useDeck()
+ useEffect(() => { registerMax?.(at) }, [at, registerMax])
+
+ if (isStatic) return {children}
+
+ if (auto) {
+ return (
+
+ {children}
+
+ )
+ }
+
+ const shown = clicks >= at
+ return (
+
+ {children}
+
+ )
+}
diff --git a/bolt-slides/src/deck/Deck.tsx b/bolt-slides/src/deck/Deck.tsx
new file mode 100644
index 00000000..90e1a7e1
--- /dev/null
+++ b/bolt-slides/src/deck/Deck.tsx
@@ -0,0 +1,240 @@
+import { Children, useCallback, useEffect, useMemo, useRef, useState } from 'react'
+import type { ReactElement, ReactNode } from 'react'
+import { MotionConfig } from 'framer-motion'
+import { DeckCtx } from './DeckContext'
+import Annotator, { type Stroke } from './Annotator'
+import { IconGrid, IconLeft, IconRight, IconPencil, IconExpand, IconShrink, IconPresent, IconClose, IconAuto } from './icons'
+
+/* ── The paged presentation engine + the Slidev-style chrome (dock + rail).
+ Wrap your //… in . Each top-level child is one slide.
+ → / ↓ / Space next (reveals the next , then the next slide)
+ ← / ↑ previous O overview F fullscreen
+ Home / End first / last D draw P presenter (new tab)
+ A auto-play H hide/show the UI
+ starts in auto mode; tunes the gap.
+ Copy verbatim; theme only via the :root tokens. ───────────────────────── */
+
+const fmt = (s: number) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
+
+function Thumb({ children }: { children: ReactNode }) {
+ const frameRef = useRef(null)
+ const [d, setD] = useState({ vw: 1280, vh: 720, scale: 0.15 })
+ useEffect(() => {
+ const el = frameRef.current
+ if (!el) return
+ const update = () => setD({ vw: window.innerWidth, vh: window.innerHeight, scale: el.clientWidth / window.innerWidth })
+ update()
+ const ro = new ResizeObserver(update)
+ ro.observe(el)
+ window.addEventListener('resize', update)
+ return () => { ro.disconnect(); window.removeEventListener('resize', update) }
+ }, [])
+ return (
+
+ )
+}
+
+export default function Deck({ children, autoplay = false, stagger = 0.16 }: { children: ReactNode; autoplay?: boolean; stagger?: number }) {
+ const slides = useMemo(() => Children.toArray(children) as ReactElement[], [children])
+ const total = slides.length
+ const isPresenter = useMemo(() => new URLSearchParams(window.location.search).has('presenter'), [])
+
+ const [slide, setSlide] = useState(() => {
+ const h = parseInt(window.location.hash.slice(1), 10)
+ return h >= 1 && h <= total ? h - 1 : 0
+ })
+ const [clicks, setClicks] = useState(0)
+ const [curMax, setCurMax] = useState(0)
+ const [auto, setAuto] = useState(autoplay)
+ const [railOpen, setRailOpen] = useState(false)
+ const [drawing, setDrawing] = useState(false)
+ const [elapsed, setElapsed] = useState(0)
+ const [fs, setFs] = useState(false)
+ const [uiHidden, setUiHidden] = useState(false)
+ const [nearDock, setNearDock] = useState(false)
+ const [cursorIdle, setCursorIdle] = useState(false)
+ const [noteOverrides, setNoteOverrides] = useState>(() => {
+ try { return JSON.parse(localStorage.getItem('deck:notes') || '{}') } catch { return {} }
+ })
+
+ // per-slide build maxima (so going back restores the right click state) and
+ // per-slide annotations (so drawings persist on the slide they were made).
+ const maxMap = useRef>({})
+ const annStore = useRef>({})
+ const slideRef = useRef(slide)
+ slideRef.current = slide
+
+ const registerMax = useCallback((at: number) => {
+ const m = maxMap.current
+ m[slideRef.current] = Math.max(m[slideRef.current] || 0, at)
+ setCurMax((c) => Math.max(c, at))
+ }, [])
+
+ const go = useCallback((i: number) => {
+ const n = Math.max(0, Math.min(total - 1, i))
+ setSlide(n); setClicks(0); setCurMax(maxMap.current[n] || 0)
+ }, [total])
+ const next = useCallback(() => {
+ if (!auto && clicks < curMax) { setClicks(clicks + 1); return }
+ if (slide < total - 1) { const n = slide + 1; setSlide(n); setClicks(0); setCurMax(maxMap.current[n] || 0) }
+ }, [auto, clicks, curMax, slide, total])
+ const prev = useCallback(() => {
+ if (!auto && clicks > 0) { setClicks(clicks - 1); return }
+ if (slide > 0) { const n = slide - 1; const m = auto ? 0 : (maxMap.current[n] || 0); setSlide(n); setClicks(m); setCurMax(m) }
+ }, [auto, clicks, slide])
+
+ const toggleFs = useCallback(() => {
+ if (document.fullscreenElement) document.exitFullscreen()
+ else document.documentElement.requestFullscreen?.()
+ }, [])
+ const toggleAuto = useCallback(() => setAuto((v) => !v), [])
+ const setNote = useCallback((text: string) => {
+ setNoteOverrides((prev) => {
+ const nextO = { ...prev, [slideRef.current]: text }
+ try { localStorage.setItem('deck:notes', JSON.stringify(nextO)) } catch { /* ignore */ }
+ return nextO
+ })
+ }, [])
+ const openPresenter = useCallback(() => {
+ if (isPresenter) return
+ const url = window.location.pathname + '?presenter=1' + window.location.hash
+ window.open(url, 'deck-presenter')
+ }, [isPresenter])
+
+ // keyboard
+ useEffect(() => {
+ const onKey = (e: KeyboardEvent) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return
+ const t = e.target as HTMLElement | null
+ if (t && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable)) return
+ switch (e.key) {
+ case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown': e.preventDefault(); next(); break
+ case 'ArrowLeft': case 'ArrowUp': case 'PageUp': e.preventDefault(); prev(); break
+ case 'Home': e.preventDefault(); go(0); break
+ case 'End': e.preventDefault(); go(total - 1); break
+ case 'o': case 'O': setRailOpen((v) => !v); break
+ case 'f': case 'F': toggleFs(); break
+ case 'd': case 'D': setDrawing((v) => !v); break
+ case 'p': case 'P': openPresenter(); break
+ case 'a': case 'A': toggleAuto(); break
+ case 'h': case 'H': setUiHidden((v) => !v); break
+ case 'Escape': setRailOpen(false); setDrawing(false); setUiHidden(false); break
+ }
+ }
+ window.addEventListener('keydown', onKey)
+ return () => window.removeEventListener('keydown', onKey)
+ }, [next, prev, go, total, toggleFs, toggleAuto, openPresenter])
+
+ // URL hash sync (initial slide comes from the hash via useState above)
+ useEffect(() => { const want = String(slide + 1); if (window.location.hash.slice(1) !== want) history.replaceState(null, '', '#' + want) }, [slide])
+ useEffect(() => {
+ const onHash = () => { const h = parseInt(window.location.hash.slice(1), 10); if (h >= 1 && h <= total && h - 1 !== slide) go(h - 1) }
+ window.addEventListener('hashchange', onHash)
+ return () => window.removeEventListener('hashchange', onHash)
+ }, [slide, total, go])
+
+ // cross-tab sync (audience ⇄ presenter), via BroadcastChannel
+ const chan = useRef(null)
+ const applyingRemote = useRef(false)
+ useEffect(() => {
+ const c = new BroadcastChannel('deck-sync')
+ chan.current = c
+ c.onmessage = (e) => {
+ if (e.data?.type === 'state') { applyingRemote.current = true; setSlide(e.data.slide); setClicks(e.data.clicks); setAuto(e.data.auto) }
+ }
+ return () => c.close()
+ }, [])
+ useEffect(() => {
+ if (applyingRemote.current) { applyingRemote.current = false; return }
+ chan.current?.postMessage({ type: 'state', slide, clicks, auto })
+ }, [slide, clicks, auto])
+
+ // fullscreen flag, presenter timer, idle auto-hide
+ useEffect(() => { const h = () => setFs(!!document.fullscreenElement); document.addEventListener('fullscreenchange', h); return () => document.removeEventListener('fullscreenchange', h) }, [])
+ useEffect(() => {
+ if (!isPresenter) return
+ setElapsed(0)
+ const t = setInterval(() => setElapsed((e) => e + 1), 1000)
+ return () => clearInterval(t)
+ }, [isPresenter])
+ // mouse-driven only: keyboard nav keeps the UI hidden; the dock returns when
+ // the pointer nears the bottom (where it lives); the cursor hides on idle.
+ useEffect(() => {
+ let t = 0
+ const onMove = (e: MouseEvent) => {
+ setCursorIdle(false)
+ setNearDock(e.clientY > window.innerHeight - 150)
+ clearTimeout(t); t = window.setTimeout(() => setCursorIdle(true), 2600)
+ }
+ window.addEventListener('mousemove', onMove)
+ return () => { clearTimeout(t); window.removeEventListener('mousemove', onMove) }
+ }, [])
+
+ const liveCtx = useMemo(() => ({ clicks, isStatic: false, auto, stagger, registerMax }), [clicks, auto, stagger, registerMax])
+ const hasPrev = auto ? slide > 0 : (slide > 0 || clicks > 0)
+ const hasNext = auto ? slide < total - 1 : (slide < total - 1 || clicks < curMax)
+ const notes = (slides[slide]?.props as { notes?: string } | undefined)?.notes
+ const noteText = noteOverrides[slide] ?? notes ?? ''
+ const nextSlide = slides[slide + 1]
+ const hideUI = uiHidden || (fs && !nearDock)
+ const cursorHidden = fs && cursorIdle && !drawing
+ const showAnnotator = drawing || (annStore.current[slide]?.length ?? 0) > 0
+
+ return (
+
+
+
+ {slides[slide]}
+
+
+ {showAnnotator &&
}
+
+
+
+ Slides
+ setRailOpen(false)}>
+
+
+ {railOpen && slides.map((s, i) => (
+ { go(i); setRailOpen(false) }}>
+ {i + 1}
+ {s}
+
+ ))}
+
+
+
+ {isPresenter && (
+
+
+ Presenter · {slide + 1} / {total}
+ {fmt(elapsed)}
+
+ {nextSlide &&
{nextSlide}
}
+
+ )}
+
+
+
+
setRailOpen((v) => !v)}>
+
+
+
{slide + 1} / {total}
+
+
+
+
setDrawing((v) => !v)}>
+
{fs ? : }
+
+
+
+
+
+ )
+}
diff --git a/bolt-slides/src/deck/DeckContext.ts b/bolt-slides/src/deck/DeckContext.ts
new file mode 100644
index 00000000..754eeb4e
--- /dev/null
+++ b/bolt-slides/src/deck/DeckContext.ts
@@ -0,0 +1,16 @@
+import { createContext, useContext } from 'react'
+
+/* Shared state for click-builds. The live slide gets {clicks, isStatic:false,
+ registerMax}; thumbnails/previews get {isStatic:true} so every build shows
+ at once with no animation. Default is static so components render fine even
+ outside a . */
+export type DeckCtxValue = {
+ clicks: number
+ isStatic: boolean
+ auto?: boolean // auto-play: reveal all builds in a stagger on slide load
+ stagger?: number // seconds between builds in auto mode
+ registerMax?: (at: number) => void
+}
+
+export const DeckCtx = createContext({ clicks: 9999, isStatic: true })
+export const useDeck = () => useContext(DeckCtx)
diff --git a/bolt-slides/src/deck/Reveal.tsx b/bolt-slides/src/deck/Reveal.tsx
new file mode 100644
index 00000000..f53266d7
--- /dev/null
+++ b/bolt-slides/src/deck/Reveal.tsx
@@ -0,0 +1,29 @@
+import { motion } from 'framer-motion'
+import type { CSSProperties, ReactNode } from 'react'
+import { useDeck } from './DeckContext'
+
+/* Entrance animation: content rises + fades in when its slide becomes active
+ (on mount). Wrap a headline, a grid, a card. Static in thumbnails. */
+export default function Reveal({
+ children, y = 26, delay = 0, className, style,
+}: {
+ children: ReactNode
+ y?: number
+ delay?: number
+ className?: string
+ style?: CSSProperties
+}) {
+ const { isStatic } = useDeck()
+ if (isStatic) return {children}
+ return (
+
+ {children}
+
+ )
+}
diff --git a/bolt-slides/src/deck/Slide.tsx b/bolt-slides/src/deck/Slide.tsx
new file mode 100644
index 00000000..accd70c3
--- /dev/null
+++ b/bolt-slides/src/deck/Slide.tsx
@@ -0,0 +1,25 @@
+import type { CSSProperties, ReactNode } from 'react'
+
+/* One full-viewport slide. Content REFLOWS responsively (no fixed canvas).
+ - center: centered + text-align center (cover, statement, quote, CTA)
+ - full: no padding, edge-to-edge (for split / full-bleed media)
+ - nav: label shown in the thumbnail rail
+ - notes: speaker notes shown in the presenter overlay
+ Non-full slides wrap content in a max-width .container. */
+export default function Slide({
+ children, center, full, className = '', style,
+}: {
+ children: ReactNode
+ center?: boolean
+ full?: boolean
+ nav?: string
+ notes?: string
+ className?: string
+ style?: CSSProperties
+}) {
+ return (
+
+ {full ? children :
{children}
}
+
+ )
+}
diff --git a/bolt-slides/src/deck/icons.tsx b/bolt-slides/src/deck/icons.tsx
new file mode 100644
index 00000000..ed86d7ac
--- /dev/null
+++ b/bolt-slides/src/deck/icons.tsx
@@ -0,0 +1,12 @@
+/* Minimal inline icons for the dock (no icon-font dependency). */
+const base = { width: 24, height: 24, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const }
+
+export const IconGrid = () => ( )
+export const IconLeft = () => ( )
+export const IconRight = () => ( )
+export const IconPencil = () => ( )
+export const IconExpand = () => ( )
+export const IconShrink = () => ( )
+export const IconPresent = () => ( )
+export const IconClose = () => ( )
+export const IconAuto = () => ( )
diff --git a/bolt-slides/src/deck/useInView.ts b/bolt-slides/src/deck/useInView.ts
new file mode 100644
index 00000000..ae0dc044
--- /dev/null
+++ b/bolt-slides/src/deck/useInView.ts
@@ -0,0 +1,23 @@
+import { useEffect, useRef, useState } from 'react'
+import { useDeck } from './DeckContext'
+
+/* Returns a ref + `inView` that flips true when the element scrolls into view —
+ the trigger for draw-in animations. In thumbnails (isStatic) it's true
+ immediately so previews render their final state. */
+export function useInView(threshold = 0.3) {
+ const { isStatic } = useDeck()
+ const ref = useRef(null)
+ const [inView, setInView] = useState(isStatic)
+ useEffect(() => {
+ if (isStatic) { setInView(true); return }
+ const el = ref.current
+ if (!el) return
+ const io = new IntersectionObserver(
+ (entries) => entries.forEach((e) => { if (e.isIntersecting) setInView(true) }),
+ { threshold },
+ )
+ io.observe(el)
+ return () => io.disconnect()
+ }, [isStatic, threshold])
+ return { ref, inView }
+}
diff --git a/bolt-slides/src/main.tsx b/bolt-slides/src/main.tsx
new file mode 100644
index 00000000..cebaf4df
--- /dev/null
+++ b/bolt-slides/src/main.tsx
@@ -0,0 +1,11 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import App from './App'
+import './styles/tokens.css'
+import './styles/base.css'
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/bolt-slides/src/styles/base.css b/bolt-slides/src/styles/base.css
new file mode 100644
index 00000000..c8789dd6
--- /dev/null
+++ b/bolt-slides/src/styles/base.css
@@ -0,0 +1,375 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
+
+/* ── Reset & page ───────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; }
+html, body, #root { height: 100%; margin: 0; }
+html { color-scheme: dark; } /* set "light" for light decks */
+body {
+ background: var(--bg);
+ color: var(--fg);
+ font-family: var(--font-body);
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ text-rendering: optimizeLegibility;
+ font-feature-settings: "kern" 1, "liga" 1, "ss01" 1;
+ letter-spacing: -0.011em;
+ overflow: hidden;
+}
+
+/* ── The deck: a fixed-viewport stage that PAGES (one slide at a time) ── */
+.deck { position: relative; width: 100vw; height: 100vh; height: 100svh; overflow: hidden; }
+.slide-stage { position: absolute; inset: 0; animation: slideIn 0.5s var(--ease) both; }
+@keyframes slideIn { from { opacity: 0; transform: scale(1.018); } to { opacity: 1; transform: scale(1); } }
+
+/* ── A slide = one full-viewport screen. Content is RESPONSIVE: it
+ reflows to the viewport (no fixed canvas, no scaling/clipping). ──── */
+.slide {
+ position: relative; width: 100%; height: 100%;
+ display: flex; flex-direction: column; justify-content: center;
+ padding: var(--gutter-y) var(--gutter);
+ overflow: hidden;
+}
+.slide.center { align-items: center; text-align: center; }
+.slide.full { padding: 0; }
+.slide > * { position: relative; z-index: 1; }
+.container { width: 100%; max-width: 1180px; margin-inline: auto; }
+
+/* Atmosphere — drifting spotlights + film grain + vignette, per slide */
+.slide { box-shadow: inset 0 0 220px 50px var(--vignette); }
+.slide::before {
+ content: ""; position: absolute; inset: -15%; pointer-events: none; z-index: 0;
+ background:
+ radial-gradient(44% 50% at 24% 16%, var(--bg-grad-1), transparent 60%),
+ radial-gradient(50% 56% at 82% 88%, var(--bg-grad-2), transparent 62%);
+ animation: drift 26s var(--ease) infinite alternate;
+}
+@keyframes drift { 0% { transform: translate3d(-2%,-1%,0) scale(1.06); } 100% { transform: translate3d(3%,2%,0) scale(1.14); } }
+.slide::after {
+ content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
+ opacity: var(--grain); mix-blend-mode: overlay;
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
+}
+
+/* ── Fluid type atoms ───────────────────────────────────────────── */
+h1, h2, h3 { font-family: var(--font-head); font-weight: 600; letter-spacing: -0.028em; line-height: 1.04; margin: 0; color: var(--fg); }
+p { margin: 0; line-height: 1.5; }
+strong { font-weight: 600; color: var(--fg); }
+.kicker { font-size: clamp(12px, 1.1vw, 15px); font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-faint); }
+.display { font-weight: 600; letter-spacing: -0.04em; line-height: 0.98; font-size: clamp(44px, 8vw, 116px); }
+.headline { font-weight: 600; letter-spacing: -0.03em; line-height: 1.04; font-size: clamp(32px, 5.2vw, 62px); max-width: 18ch; }
+.lead { font-size: clamp(17px, 2.1vw, 25px); color: var(--fg-muted); max-width: 42ch; line-height: 1.5; }
+.subhead { font-size: clamp(18px, 2.4vw, 30px); color: var(--fg-muted); max-width: 26ch; line-height: 1.35; }
+.foot { font-size: clamp(13px, 1.2vw, 16px); color: var(--fg-faint); }
+.center .lead, .center .subhead { margin-inline: auto; }
+.accent-text { background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
+.figure { font-weight: 600; letter-spacing: -0.05em; line-height: 0.9; font-size: clamp(88px, 16vw, 220px); font-feature-settings: "tnum" 1; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
+.rule { width: 96px; height: 1px; background: linear-gradient(90deg, var(--primary), transparent); }
+
+/* ── Bento — responsive asymmetric tile grid ────────────────────── */
+.bento { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: minmax(clamp(130px, 20vh, 230px), auto); gap: 16px; width: 100%; }
+.btile {
+ position: relative; overflow: hidden;
+ grid-column: span var(--c, 4); grid-row: span var(--r, 1);
+ display: flex; flex-direction: column; justify-content: space-between; gap: 14px;
+ padding: clamp(20px, 2vw, 28px); border-radius: var(--radius-lg);
+ background: linear-gradient(180deg, var(--surface-2), var(--surface));
+ border: 1px solid var(--hair);
+ transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
+}
+.btile:hover { border-color: color-mix(in srgb, var(--primary) 32%, transparent); transform: translateY(-3px); }
+.btile.accent { background: var(--accent); border-color: transparent; }
+.btile.accent .btile-fig, .btile.accent h3, .btile.accent p { color: var(--accent-ink); -webkit-text-fill-color: var(--accent-ink); }
+.btile.accent .btile-k { color: color-mix(in srgb, var(--accent-ink) 72%, transparent); }
+.btile.glow { box-shadow: var(--glow); border-color: color-mix(in srgb, var(--primary) 40%, transparent); }
+.btile-k { font-size: clamp(11px, 1vw, 13px); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-faint); }
+.btile-fig { font-size: clamp(40px, 4.4vw, 64px); font-weight: 600; letter-spacing: -0.045em; line-height: 0.92; font-feature-settings: "tnum" 1; color: var(--fg); }
+.btile-fig.grad { background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
+.btile h3 { font-size: clamp(18px, 2vw, 23px); }
+.btile p { font-size: clamp(14px, 1.4vw, 16px); color: var(--fg-muted); line-height: 1.4; }
+
+/* ── Split — full-bleed feature section ─────────────────────────── */
+.split { display: grid; grid-template-columns: 1.05fr 1fr; width: 100%; height: 100%; }
+.split-body { display: flex; flex-direction: column; justify-content: center; gap: clamp(14px, 2vh, 22px); padding: var(--gutter-y) clamp(28px, 5vw, 72px) var(--gutter-y) var(--gutter); }
+.split.flip .split-body { order: 2; padding: var(--gutter-y) var(--gutter) var(--gutter-y) clamp(28px, 5vw, 72px); }
+.split-media { position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
+.split-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
+
+/* ── Stat grid + card ───────────────────────────────────────────── */
+.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: clamp(14px, 1.6vw, 22px); width: 100%; }
+.stat-card { display: flex; flex-direction: column; gap: 8px; padding: clamp(24px, 2.4vw, 36px) clamp(22px, 2vw, 30px); border-radius: var(--radius); background: var(--surface); border: 1px solid var(--hair); text-align: left; transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
+.stat-card:hover { transform: translateY(-6px); border-color: color-mix(in srgb, var(--primary) 35%, transparent); box-shadow: var(--glow); }
+.stat-value { font-size: clamp(40px, 4.6vw, 58px); font-weight: 600; letter-spacing: -0.04em; line-height: 1; font-feature-settings: "tnum" 1; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
+.stat-label { font-size: clamp(16px, 1.6vw, 19px); font-weight: 600; color: var(--fg); margin-top: 8px; }
+.stat-caption { font-size: clamp(13px, 1.3vw, 15px); color: var(--fg-faint); line-height: 1.4; }
+
+/* ── Accordion ──────────────────────────────────────────────────── */
+.accordion { display: flex; flex-direction: column; gap: 12px; width: 100%; }
+.acc-item { border: 1px solid var(--hair); border-radius: var(--radius); background: var(--surface); overflow: hidden; transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease); }
+.acc-item.open { border-color: color-mix(in srgb, var(--primary) 38%, transparent); background: var(--surface-2); }
+.acc-head { display: flex; align-items: center; gap: 16px; width: 100%; padding: clamp(16px,1.8vw,22px) clamp(18px,2vw,26px); background: transparent; border: none; cursor: pointer; text-align: left; font-family: var(--font-body); color: var(--fg); }
+.acc-index { font-family: var(--font-mono); font-size: 13px; font-weight: 600; letter-spacing: 0.04em; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; flex-shrink: 0; }
+.acc-head h3 { flex: 1; font-size: clamp(17px,1.9vw,21px); font-weight: 600; letter-spacing: -0.02em; color: var(--fg); margin: 0; }
+.acc-chevron { display: grid; place-items: center; color: var(--fg-faint); transition: transform var(--dur) var(--ease), color var(--dur) var(--ease); flex-shrink: 0; }
+.acc-item.open .acc-chevron { transform: rotate(180deg); color: var(--primary); }
+.acc-chevron svg { width: 18px; height: 18px; }
+.acc-body { overflow: hidden; }
+.acc-body-inner { padding: 0 clamp(18px,2vw,26px) clamp(18px,2vw,24px) clamp(44px,5vw,58px); font-size: clamp(14px,1.5vw,17px); color: var(--fg-muted); line-height: 1.5; max-width: 62ch; }
+
+/* ═══ State-of-the-art sections ═════════════════════════════════════ */
+
+/* Comparison (us vs them) */
+.cmp { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
+.cmp-row { display: grid; grid-template-columns: 1.6fr repeat(var(--vcols, 2), 1fr); align-items: center; }
+.cmp-row + .cmp-row { border-top: 1px solid var(--hair-2); }
+.cmp-cell { padding: clamp(12px,1.5vw,18px) clamp(14px,1.6vw,22px); font-size: clamp(14px,1.5vw,17px); color: var(--fg-muted); display: flex; align-items: center; }
+.cmp-cell:not(.cmp-label):not(.cmp-h) { justify-content: center; }
+.cmp-head { background: var(--surface-2); }
+.cmp-h { font-weight: 600; color: var(--fg); }
+.cmp-label { color: var(--fg); font-weight: 500; }
+.cmp-cell.hl { background: color-mix(in srgb, var(--primary) 8%, transparent); }
+.cmp-h.hl { color: transparent; background: var(--accent); -webkit-background-clip: text; background-clip: text; }
+.cmp-ic { width: 22px; height: 22px; }
+.cmp-ic.ok { color: var(--primary); }
+.cmp-ic.no { color: var(--fg-faint); opacity: 0.55; }
+.cmp-val { font-feature-settings: "tnum" 1; }
+
+/* Tabs (sliding accent pill) */
+.tabs { width: 100%; }
+.tabs-bar { display: inline-flex; gap: 4px; padding: 5px; border-radius: 999px; background: var(--surface); border: 1px solid var(--hair); }
+.tab { position: relative; border: none; background: transparent; cursor: pointer; padding: 9px 18px; border-radius: 999px; font-family: var(--font-body); font-size: 15px; font-weight: 600; color: var(--fg-muted); transition: color var(--dur) var(--ease); }
+.tab.on { color: var(--accent-ink); }
+.tab-pill { position: absolute; inset: 0; border-radius: 999px; background: var(--accent); z-index: 0; }
+.tab-label { position: relative; z-index: 1; }
+.tabs-panel { margin-top: 22px; }
+
+/* Timeline */
+.tl { position: relative; }
+.tl-line { position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background: var(--hair); border-radius: 2px; overflow: hidden; }
+.tl-line-fill { position: absolute; inset: 0; background: var(--accent); transform-origin: top; }
+.tl-items { display: flex; flex-direction: column; gap: clamp(20px,3vh,34px); }
+.tl-item { position: relative; display: flex; gap: 22px; padding-left: 6px; }
+.tl-dot { position: relative; z-index: 1; flex-shrink: 0; width: 12px; height: 12px; margin-top: 5px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 4px var(--bg), var(--glow); }
+.tl-time { font-size: 12.5px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 4px; }
+.tl-content h3 { font-size: clamp(18px,2vw,23px); font-weight: 600; letter-spacing: -0.02em; margin: 0 0 5px; }
+.tl-content p { font-size: clamp(14px,1.5vw,16px); color: var(--fg-muted); line-height: 1.5; max-width: 52ch; margin: 0; }
+
+/* Code window */
+.cw { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius); overflow: hidden; background: var(--bg); box-shadow: var(--shadow); }
+.cw-bar { display: flex; align-items: center; gap: 12px; padding: 11px 16px; background: var(--surface-2); border-bottom: 1px solid var(--hair-2); }
+.cw-dots { display: flex; gap: 7px; }
+.cw-dots i { width: 11px; height: 11px; border-radius: 50%; background: var(--hair); }
+.cw-title { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-faint); }
+.cw-body { margin: 0; padding: 16px 0; overflow-x: auto; }
+.cw-body code { font-family: var(--font-mono); font-size: clamp(12px,1.3vw,14px); line-height: 1.65; }
+.cw-line { display: flex; padding: 0 16px; white-space: pre; }
+.cw-line.hl { background: color-mix(in srgb, var(--primary) 10%, transparent); box-shadow: inset 2px 0 0 var(--primary); }
+.cw-no { width: 30px; flex-shrink: 0; color: var(--fg-faint); opacity: 0.5; user-select: none; text-align: right; padding-right: 18px; }
+.cw-code { color: var(--fg); }
+.cw-kw { color: var(--primary); }
+.cw-str { color: color-mix(in srgb, var(--primary) 50%, var(--fg-muted)); }
+.cw-com { color: var(--fg-faint); font-style: italic; }
+
+/* Browser frame */
+.bf { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); box-shadow: var(--shadow); }
+.bf-bar { display: flex; align-items: center; gap: 14px; padding: 11px 16px; background: var(--surface-2); border-bottom: 1px solid var(--hair-2); }
+.bf-dots { display: flex; gap: 7px; }
+.bf-dots i { width: 11px; height: 11px; border-radius: 50%; background: var(--hair); }
+.bf-url { flex: 1; font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-faint); background: var(--bg); border: 1px solid var(--hair-2); border-radius: 999px; padding: 6px 14px; text-align: center; }
+.bf-body { background: var(--bg); }
+.bf-body img { display: block; width: 100%; }
+
+/* Charts */
+.ch-bars { display: flex; align-items: flex-end; gap: clamp(8px,1.4vw,16px); width: 100%; }
+.ch-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 9px; height: 100%; }
+.ch-bar-track { width: 100%; flex: 1; display: flex; align-items: flex-end; }
+.ch-bar { width: 100%; border-radius: 8px 8px 4px 4px; background: var(--accent); display: block; }
+.ch-x { font-size: 11.5px; color: var(--fg-faint); }
+.ch-line { width: 100%; display: block; }
+.ch-donut { width: var(--ch-size, 168px); display: flex; flex-direction: column; align-items: center; gap: 12px; }
+.ch-donut svg { width: 100%; height: auto; display: block; }
+.ch-donut-val { fill: var(--fg); font-family: var(--font-head); font-size: 30px; font-weight: 600; }
+.ch-donut-label { font-size: 14px; color: var(--fg-muted); font-weight: 500; }
+
+/* Spotlight card (cursor-follow glow) */
+.spot { position: relative; border: 1px solid var(--hair); border-radius: var(--radius-lg); background: var(--surface); padding: clamp(24px,2.6vw,36px); overflow: hidden; transition: border-color var(--dur) var(--ease); }
+.spot::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.3s var(--ease); background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--primary) 20%, transparent), transparent 72%); }
+.spot:hover { border-color: color-mix(in srgb, var(--primary) 35%, transparent); }
+.spot:hover::before { opacity: 1; }
+.spot > * { position: relative; z-index: 1; }
+
+/* ── Hover helpers ──────────────────────────────────────────────── */
+.lift { transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
+.lift:hover { transform: translateY(-6px); box-shadow: var(--shadow), var(--glow); }
+
+/* ── Responsive collapse ────────────────────────────────────────── */
+@media (max-width: 900px) {
+ .bento { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; }
+ .btile { grid-column: 1 / -1; grid-row: auto; min-height: clamp(140px, 24vh, 200px); }
+ .split { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }
+ .split.flip .split-body { order: 0; }
+ .split-media { order: 2; }
+}
+
+/* ═══════════════════════════════════════════════════════════════════
+ NAVIGATION CHROME — the Slidev-style floating glass dock + thumbnail
+ rail, ported to React. This is the UI to keep; restyle only via tokens.
+ ═══════════════════════════════════════════════════════════════════ */
+.noir-dock {
+ position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 90;
+ opacity: 0.35; transition: opacity 0.35s ease; font-family: var(--font-body);
+}
+.noir-dock:hover { opacity: 1; }
+.noir-bar {
+ display: flex; align-items: center; gap: 4px; padding: 7px 9px; border-radius: 999px;
+ background: rgba(28, 28, 30, 0.66); border: 1px solid rgba(255, 255, 255, 0.10);
+ backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
+ box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8);
+}
+.noir-icon-btn {
+ width: 38px; height: 38px; display: grid; place-items: center; border: none; border-radius: 999px;
+ background: transparent; color: #e8e8ea; font-size: 19px; cursor: pointer;
+ transition: background 0.2s ease, color 0.2s ease, opacity 0.2s ease;
+}
+.noir-icon-btn:hover { background: rgba(255, 255, 255, 0.12); }
+.noir-icon-btn:disabled { opacity: 0.28; cursor: default; }
+.noir-icon-btn:disabled:hover { background: transparent; }
+.noir-icon-btn.on { color: var(--accent-ink, #04140e); background: var(--accent, linear-gradient(115deg, #c8f56e, #34dcd8)); }
+.noir-icon-btn.sm { width: 30px; height: 30px; font-size: 16px; }
+.noir-icon-btn svg { width: 1em; height: 1em; }
+.noir-sep { width: 1px; height: 22px; background: rgba(255, 255, 255, 0.12); margin: 0 4px; }
+.noir-counter { display: flex; align-items: baseline; gap: 5px; padding: 0 12px; font-feature-settings: "tnum" 1; user-select: none; }
+.noir-counter-now { font-size: 16px; font-weight: 600; color: #f5f5f7; }
+.noir-counter-tot { font-size: 13px; color: #8e8e93; }
+
+/* Left thumbnail rail */
+.noir-rail {
+ position: fixed; top: 0; left: 0; bottom: 0; width: 248px; z-index: 95; padding: 18px 14px 26px;
+ overflow-y: auto; background: rgba(18, 18, 20, 0.82); border-right: 1px solid rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(26px) saturate(180%); -webkit-backdrop-filter: blur(26px) saturate(180%);
+ font-family: var(--font-body); transform: translateX(-100%); transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
+}
+.noir-rail.open { transform: translateX(0); }
+.noir-rail-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px 14px; }
+.noir-rail-title { font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #8e8e93; }
+.noir-rail-list { display: flex; flex-direction: column; gap: 12px; }
+.noir-thumb { display: flex; align-items: center; gap: 10px; padding: 0; border: none; background: transparent; cursor: pointer; text-align: left; }
+.noir-thumb-no { width: 18px; font-size: 12px; font-feature-settings: "tnum" 1; color: #6e6e73; flex-shrink: 0; }
+.noir-thumb-frame { flex: 1; border-radius: 9px; overflow: hidden; border: 1.5px solid transparent; transition: border-color 0.25s ease; line-height: 0; aspect-ratio: 16 / 9; position: relative; background: var(--bg); }
+.noir-thumb:hover .noir-thumb-frame { border-color: rgba(255, 255, 255, 0.22); }
+.noir-thumb.active .noir-thumb-no { color: var(--primary); }
+.noir-thumb.active .noir-thumb-frame { border-color: var(--primary); }
+/* the live slide rendered at true viewport size, then scaled into the frame
+ (so responsive vw/vh units stay faithful). width/height/transform set inline. */
+.noir-thumb-scale { position: absolute; top: 0; left: 0; transform-origin: top left; pointer-events: none; }
+.noir-rail::-webkit-scrollbar { width: 0; }
+.noir-rail { scrollbar-width: none; }
+
+/* Presenter overlay (notes + next + timer) */
+.noir-presenter {
+ position: fixed; right: 18px; bottom: 78px; z-index: 92; width: clamp(260px, 26vw, 360px);
+ background: rgba(18, 18, 20, 0.9); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px;
+ backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
+ box-shadow: 0 18px 50px -20px rgba(0,0,0,0.8); padding: 16px; font-family: var(--font-body); color: #e8e8ea;
+}
+.noir-presenter-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
+.noir-presenter-label { font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #8e8e93; }
+.noir-presenter-timer { font-size: 13px; font-feature-settings: "tnum" 1; color: var(--primary); }
+.noir-presenter-next { border-radius: 9px; overflow: hidden; border: 1px solid rgba(255,255,255,0.08); aspect-ratio: 16/9; position: relative; background: var(--bg); margin-bottom: 12px; }
+.noir-presenter-notes { width: 100%; min-height: 84px; max-height: 32vh; font-size: 13.5px; line-height: 1.5; color: #c7c7cc; background: transparent; border: none; resize: none; outline: none; font-family: var(--font-body); }
+.noir-presenter-notes:focus { color: #f0f0f2; }
+.noir-presenter-notes::placeholder { color: #6e6e73; }
+.noir-presenter-hint { margin-top: 8px; font-size: 11px; color: #6e6e73; }
+
+/* Annotation overlay + tool bar */
+.ann-canvas { position: fixed; inset: 0; z-index: 88; touch-action: none; }
+.ann-bar {
+ position: fixed; bottom: 78px; left: 50%; transform: translateX(-50%); z-index: 91;
+ display: flex; align-items: center; gap: 4px; padding: 7px 9px; border-radius: 999px; flex-wrap: wrap; max-width: 94vw; justify-content: center;
+ background: rgba(28, 28, 30, 0.72); border: 1px solid rgba(255, 255, 255, 0.10);
+ backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
+ box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8); font-family: var(--font-body);
+}
+.ann-btn { width: 34px; height: 34px; display: grid; place-items: center; border: none; border-radius: 999px; background: transparent; color: #e8e8ea; cursor: pointer; transition: background 0.2s ease, color 0.2s ease; }
+.ann-btn svg { width: 19px; height: 19px; }
+.ann-btn:hover { background: rgba(255, 255, 255, 0.12); }
+.ann-btn.on { color: var(--accent-ink); background: var(--accent); }
+.ann-sep { width: 1px; height: 22px; background: rgba(255, 255, 255, 0.12); margin: 0 4px; }
+.ann-color { width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); }
+.ann-color.on { border-color: #fff; transform: scale(1.08); }
+.ann-size { width: 30px; height: 34px; display: grid; place-items: center; border: none; background: transparent; cursor: pointer; border-radius: 10px; }
+.ann-size span { display: block; border-radius: 50%; background: #e8e8ea; }
+.ann-size.on { background: rgba(255, 255, 255, 0.12); }
+.ann-size.on span { background: var(--primary); }
+
+/* Tooltips for chrome controls (instant, styled) */
+[data-tip] { position: relative; }
+[data-tip]::after {
+ content: attr(data-tip); position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%) translateY(4px);
+ padding: 5px 9px; border-radius: 7px; white-space: nowrap; font-size: 11.5px; font-weight: 500; font-family: var(--font-body);
+ background: rgba(20, 20, 22, 0.96); color: #f5f5f7; border: 1px solid rgba(255, 255, 255, 0.10);
+ box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.7); opacity: 0; pointer-events: none;
+ transition: opacity 0.15s ease, transform 0.15s ease; z-index: 100;
+}
+[data-tip]:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); }
+
+/* Hide / dim the UI (H key, or auto on idle in fullscreen) */
+.noir-dock.hidden { opacity: 0 !important; pointer-events: none; transform: translateX(-50%) translateY(18px); }
+.deck.nocursor, .deck.nocursor * { cursor: none !important; }
+
+/* ── TiltCard / Marquee / visual frame (shared with the other skills) ─ */
+.tilt { position: relative; display: inline-block; transition: transform 0.5s var(--ease); transform-style: preserve-3d; will-change: transform; }
+.tilt-glare { position: absolute; inset: 0; pointer-events: none; border-radius: var(--radius-lg); background: radial-gradient(60% 60% at var(--gx,50%) var(--gy,50%), rgba(255,255,255,0.16), transparent 60%); opacity: 0; transition: opacity 0.4s var(--ease); }
+.tilt:hover .tilt-glare { opacity: 1; }
+.marquee { width: 100%; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
+.marquee-track { display: flex; gap: 56px; width: max-content; animation: marquee linear infinite; }
+.marquee-item { font-size: clamp(20px, 2.4vw, 28px); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; color: var(--fg-muted); opacity: 0.78; }
+@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
+.vframe { padding: 24px; border-radius: var(--radius-lg); background: linear-gradient(180deg, var(--surface-2), var(--surface)); border: 1px solid var(--hair); box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 16px; font-family: var(--font-body); }
+.vtitle { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; color: var(--fg); }
+.vmeta { font-size: 12.5px; color: var(--fg-faint); }
+.ddot { width: 9px; height: 9px; border-radius: 3px; background: var(--accent); }
+.dlive { margin-left: auto; display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--primary); padding: 5px 11px; border-radius: 999px; background: color-mix(in srgb, var(--primary) 10%, transparent); border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent); }
+.dlive span { width: 6px; height: 6px; border-radius: 50%; background: var(--primary); animation: pulse 1.6s ease-in-out infinite; }
+@keyframes pulse { 0%,100% { opacity: 0.35; } 50% { opacity: 1; } }
+.dkpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
+.dkpi { background: var(--surface); border: 1px solid var(--hair-2); border-radius: var(--radius-sm); padding: 14px 15px; }
+.dkpi-l { font-size: 12.5px; color: var(--fg-faint); }
+.dkpi-v { font-size: 25px; font-weight: 600; letter-spacing: -0.03em; color: var(--fg); margin: 4px 0 5px; font-feature-settings: "tnum" 1; }
+.dkpi-d { font-size: 12px; font-weight: 600; color: var(--primary); }
+.dbars { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; align-items: end; }
+.dbar { display: flex; flex-direction: column; align-items: center; gap: 7px; }
+.dbar-track { width: 100%; height: 44px; border-radius: 7px; background: var(--hair-2); display: flex; align-items: flex-end; overflow: hidden; }
+.dbar-track span { width: 100%; border-radius: 7px; background: var(--accent); display: block; transform: scaleY(0); transform-origin: bottom; }
+.dbar-k { font-size: 10.5px; color: var(--fg-faint); }
+.dline { stroke-dasharray: 1; stroke-dashoffset: 1; }
+.dfill-area { opacity: 0; transition: opacity 0.8s var(--ease) 0.5s; }
+.vframe.shown .dline { animation: dash 1.5s var(--ease) 0.15s forwards; }
+.vframe.shown .dfill-area { opacity: 1; }
+.vframe.shown .dbar-track span { animation: bargrow 0.8s var(--ease-spring) both; }
+@keyframes dash { to { stroke-dashoffset: 0; } }
+@keyframes bargrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
+
+/* ── Responsive controls — declutter the dock on touch / small screens ─
+ On touch there's no hover, so keep the dock fully visible; on phones,
+ shrink it and drop the presenter-only tools (draw + presenter overlay). */
+@media (hover: none) { .noir-dock { opacity: 1; } }
+@media (max-width: 640px) {
+ .noir-dock { opacity: 1; bottom: 14px; }
+ .noir-bar { gap: 2px; padding: 6px 7px; }
+ .noir-icon-btn { width: 34px; height: 34px; font-size: 17px; }
+ .noir-icon-btn.sm { width: 28px; height: 28px; }
+ .noir-counter { padding: 0 8px; }
+ .noir-counter-now { font-size: 15px; }
+ .noir-sep { margin: 0 2px; }
+ .noir-optional { display: none; }
+ .noir-rail { width: min(248px, 84vw); }
+}
+
+/* ── Reduced motion ─────────────────────────────────────────────── */
+@media (prefers-reduced-motion: reduce) {
+ .slide-stage, .slide::before, .lift, .marquee-track, .btile, .acc-item, .acc-chevron { animation: none !important; transition: none !important; }
+ .tilt { transform: none !important; }
+ .dline { stroke-dashoffset: 0; }
+ .dfill-area { opacity: 1; }
+ .dbar-track span { transform: scaleY(1); }
+}
diff --git a/bolt-slides/src/styles/tokens.css b/bolt-slides/src/styles/tokens.css
new file mode 100644
index 00000000..6994aca8
--- /dev/null
+++ b/bolt-slides/src/styles/tokens.css
@@ -0,0 +1,55 @@
+/* ===================================================================
+ THEME TOKENS — edit ONLY this :root block to re-theme the whole deck.
+ The SAME token vocabulary as the `slides` (Slidev) and `webdeck` skills,
+ so a brand looks identical across all of them. The values below are a
+ "dark product" default — REPLACE them to match the brief or the brand.
+ =================================================================== */
+:root {
+ /* Surfaces */
+ --bg: #05070a;
+ --bg-grad-1: rgba(79, 229, 176, 0.12);
+ --bg-grad-2: rgba(41, 192, 240, 0.08);
+ --surface: rgba(255, 255, 255, 0.045);
+ --surface-2: rgba(255, 255, 255, 0.07);
+
+ /* Ink */
+ --fg: #f4f7fa;
+ --fg-muted: #9aa4b2;
+ --fg-faint: #626d7d;
+
+ /* Hairlines */
+ --hair: rgba(255, 255, 255, 0.10);
+ --hair-2: rgba(255, 255, 255, 0.06);
+
+ /* Accent — ONE accent. May be a 2–4 stop gradient within ~80° of hue. */
+ --primary: #4fe5b0;
+ --accent: linear-gradient(115deg, #c8f56e 0%, #5aeeb0 42%, #34dcd8 74%, #29c0f0 100%);
+ --accent-ink: #04140e;
+
+ /* Radius */
+ --radius: 22px;
+ --radius-sm: 12px;
+ --radius-lg: 26px;
+
+ /* Depth */
+ --shadow: 0 50px 120px -50px rgba(0,0,0,0.9);
+ --glow: 0 30px 80px -40px rgba(79, 229, 176, 0.5);
+
+ /* Type */
+ --font-head: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
+ --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
+ --font-mono: 'JetBrains Mono', ui-monospace, monospace;
+
+ /* Motion */
+ --dur: 0.4s;
+ --ease: cubic-bezier(0.32, 0.72, 0, 1);
+ --ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
+
+ /* Atmosphere */
+ --grain: 0.035;
+ --vignette: rgba(0, 0, 0, 0.22);
+
+ /* Responsive gutter — fluid side / vertical padding */
+ --gutter: clamp(28px, 6vw, 120px);
+ --gutter-y: clamp(40px, 8vh, 96px);
+}
diff --git a/bolt-slides/tsconfig.json b/bolt-slides/tsconfig.json
new file mode 100644
index 00000000..17f43b17
--- /dev/null
+++ b/bolt-slides/tsconfig.json
@@ -0,0 +1,21 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
diff --git a/bolt-slides/tsconfig.node.json b/bolt-slides/tsconfig.node.json
new file mode 100644
index 00000000..97ede7ee
--- /dev/null
+++ b/bolt-slides/tsconfig.node.json
@@ -0,0 +1,11 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/bolt-slides/vite.config.ts b/bolt-slides/vite.config.ts
new file mode 100644
index 00000000..9ffcc675
--- /dev/null
+++ b/bolt-slides/vite.config.ts
@@ -0,0 +1,6 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+})
From 711e053e274a74c0a3ab4ee4e7e80e09efa212c2 Mon Sep 17 00:00:00 2001
From: xKevIsDev
Date: Thu, 2 Jul 2026 14:34:58 +0100
Subject: [PATCH 2/3] refactor: standardize code formatting across components
This commit updates the formatting of various TypeScript files in the bolt-slides project. Changes include consistent use of semicolons, improved indentation, and enhanced readability of JSX elements. The updates affect configuration files, component files, and main application files, ensuring a cleaner and more maintainable codebase.
---
bolt-slides/src/App.tsx | 698 +++++--
bolt-slides/src/components/Accordion.tsx | 53 +-
bolt-slides/src/components/Bento.tsx | 54 +-
bolt-slides/src/components/BrowserFrame.tsx | 18 +-
bolt-slides/src/components/Charts.tsx | 156 +-
bolt-slides/src/components/CodeWindow.tsx | 78 +-
bolt-slides/src/components/Comparison.tsx | 68 +-
bolt-slides/src/components/CountUp.tsx | 91 +-
bolt-slides/src/components/Marquee.tsx | 19 +-
bolt-slides/src/components/Split.tsx | 39 +-
bolt-slides/src/components/SpotlightCard.tsx | 20 +-
bolt-slides/src/components/StatGrid.tsx | 42 +-
bolt-slides/src/components/Tabs.tsx | 34 +-
bolt-slides/src/components/TiltCard.tsx | 28 +-
bolt-slides/src/components/Timeline.tsx | 25 +-
.../src/components/VisualDashboard.tsx | 122 +-
bolt-slides/src/deck/Annotator.tsx | 391 +++-
bolt-slides/src/deck/Build.tsx | 55 +-
bolt-slides/src/deck/Deck.tsx | 548 ++++--
bolt-slides/src/deck/DeckContext.ts | 21 +-
bolt-slides/src/deck/Reveal.tsx | 33 +-
bolt-slides/src/deck/Slide.tsx | 31 +-
bolt-slides/src/deck/icons.tsx | 69 +-
bolt-slides/src/deck/useInView.ts | 36 +-
bolt-slides/src/main.tsx | 14 +-
bolt-slides/src/styles/base.css | 1673 ++++++++++++++---
bolt-slides/src/styles/tokens.css | 38 +-
bolt-slides/vite.config.ts | 6 +-
28 files changed, 3529 insertions(+), 931 deletions(-)
diff --git a/bolt-slides/src/App.tsx b/bolt-slides/src/App.tsx
index f150ad15..e7769f2f 100644
--- a/bolt-slides/src/App.tsx
+++ b/bolt-slides/src/App.tsx
@@ -1,50 +1,81 @@
-import Deck from './deck/Deck'
-import Slide from './deck/Slide'
-import Build from './deck/Build'
-import Reveal from './deck/Reveal'
-import Bento from './components/Bento'
-import Split from './components/Split'
-import CountUp from './components/CountUp'
-import TiltCard from './components/TiltCard'
-import Marquee from './components/Marquee'
-import VisualDashboard from './components/VisualDashboard'
-import StatGrid from './components/StatGrid'
-import Accordion from './components/Accordion'
-import Comparison from './components/Comparison'
-import Tabs from './components/Tabs'
-import Timeline from './components/Timeline'
-import CodeWindow from './components/CodeWindow'
-import BrowserFrame from './components/BrowserFrame'
-import SpotlightCard from './components/SpotlightCard'
-import { BarChart, LineChart, DonutChart } from './components/Charts'
+import Deck from './deck/Deck';
+import Slide from './deck/Slide';
+import Build from './deck/Build';
+import Reveal from './deck/Reveal';
+import Bento from './components/Bento';
+import Split from './components/Split';
+import CountUp from './components/CountUp';
+import TiltCard from './components/TiltCard';
+import Marquee from './components/Marquee';
+import VisualDashboard from './components/VisualDashboard';
+import StatGrid from './components/StatGrid';
+import Accordion from './components/Accordion';
+import Comparison from './components/Comparison';
+import Tabs from './components/Tabs';
+import Timeline from './components/Timeline';
+import CodeWindow from './components/CodeWindow';
+import BrowserFrame from './components/BrowserFrame';
+import SpotlightCard from './components/SpotlightCard';
+import { BarChart, LineChart, DonutChart } from './components/Charts';
/* ══════════════════════════════════════════════════════════════════════
⚠️ THROWAWAY DEMO showing every component. DELETE these slides and AUTHOR
THE USER'S DECK. Each child of is one slide. Add speaker notes with
notes="…" on any slide (shown in presenter mode — press P). (A = auto-play.)
══════════════════════════════════════════════════════════════════════ */
-const panel = (extra = 0.22): React.CSSProperties => ({ position: 'absolute', inset: 0, background: `radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--primary) ${extra * 100}%, transparent), transparent 60%), var(--surface-2)` })
-const card: React.CSSProperties = { padding: 22, borderRadius: 'var(--radius)', background: 'var(--surface)', border: '1px solid var(--hair)' }
+const panel = (extra = 0.22): React.CSSProperties => ({
+ position: 'absolute',
+ inset: 0,
+ background: `radial-gradient(120% 100% at 30% 20%, color-mix(in srgb, var(--primary) ${
+ extra * 100
+ }%, transparent), transparent 60%), var(--surface-2)`,
+});
+const card: React.CSSProperties = {
+ padding: 22,
+ borderRadius: 'var(--radius)',
+ background: 'var(--surface)',
+ border: '1px solid var(--hair)',
+};
export default function App() {
return (
{/* Cover */}
-
+
- Helix · Component demo
- Helix
- A responsive React deck engine. Delete this and build the real one.
+
+ Helix · Component demo
+
+
+ Helix
+
+
+ A responsive React deck engine. Delete this and build the real one.
+
{/* Statement + click-build */}
-
-
- Dashboards are everywhere. Insight isn't.
+
+
+ Dashboards are everywhere.{' '}
+ Insight isn't.
- Helix turns raw events into answers — automatically.
+
+ Helix turns raw events into answers — automatically.
+
@@ -53,9 +84,22 @@ export default function App() {
nav="Realtime"
notes="Emphasize sub-second latency. Point at the live chart while you talk."
kicker="Realtime"
- title={<>Everything, as it happens. >}
+ title={
+ <>
+ Everything, as it happens.
+ >
+ }
body="Live metrics with sub-second latency — no pipelines to babysit."
- media={<>
>}
+ media={
+ <>
+
+
+
+
+
+
+ >
+ }
/>
{/* Bento */}
@@ -65,10 +109,26 @@ export default function App() {
kicker="One platform"
title="Everything in one place."
tiles={[
- { k: 'Throughput', fig: , body: 'events / min at peak.', c: 5, r: 2, variant: 'glow' },
- { k: 'Uptime', fig: , c: 4 },
+ {
+ k: 'Throughput',
+ fig: ,
+ body: 'events / min at peak.',
+ c: 5,
+ r: 2,
+ variant: 'glow',
+ },
+ {
+ k: 'Uptime',
+ fig: ,
+ c: 4,
+ },
{ k: 'Regions', fig: , c: 3, variant: 'accent' },
- { k: 'Connectors', title: '120+ native', body: 'Snowflake, Kafka, dbt…', c: 4 },
+ {
+ k: 'Connectors',
+ title: '120+ native',
+ body: 'Snowflake, Kafka, dbt…',
+ c: 4,
+ },
{ k: 'Compliance', title: 'SOC 2 · HIPAA', c: 3 },
]}
/>
@@ -80,17 +140,46 @@ export default function App() {
kicker="Traction"
title="Numbers that compound."
stats={[
- { value: , label: 'ARR', caption: 'up 3× year over year' },
- { value: , label: 'Net retention', caption: 'best in class' },
- { value: , label: 'Enterprise logos', caption: 'across six industries' },
+ {
+ value: ,
+ label: 'ARR',
+ caption: 'up 3× year over year',
+ },
+ {
+ value: ,
+ label: 'Net retention',
+ caption: 'best in class',
+ },
+ {
+ value: ,
+ label: 'Enterprise logos',
+ caption: 'across six industries',
+ },
]}
/>
{/* Comparison */}
-
+
- Why teams switch
- The honest comparison.
+
+ Why teams switch
+
+
+ The honest comparison.
+
@@ -100,7 +189,10 @@ export default function App() {
rows={[
{ label: 'Realtime by default', values: [true, false] },
{ label: 'Self-host option', values: [true, false] },
- { label: 'Time to first insight', values: ['5 min', '2 weeks'] },
+ {
+ label: 'Time to first insight',
+ values: ['5 min', '2 weeks'],
+ },
{ label: 'Starting price', values: ['$29', '$99'] },
]}
/>
@@ -109,17 +201,70 @@ export default function App() {
{/* Tabs */}
-
+
- One platform
- Built for every team.
+
+ One platform
+
+
+ Built for every team.
+
-
- Trace any request end-to-end, alert on anomalies, ship with confidence. },
- { label: 'Data', content:
},
- { label: 'Ops', content: One source of truth for uptime, cost, and capacity — no spreadsheets.
},
- ]} />
+
+
+ Trace any request end-to-end, alert on anomalies, ship with
+ confidence.
+
+ ),
+ },
+ {
+ label: 'Data',
+ content: (
+
+
+
+ ),
+ },
+ {
+ label: 'Ops',
+ content: (
+
+ One source of truth for uptime, cost, and capacity — no
+ spreadsheets.
+
+ ),
+ },
+ ]}
+ />
@@ -128,45 +273,157 @@ export default function App() {
nav="Developer-first"
notes="Three lines, no schema. If there's an engineer in the room, this is the slide for them."
kicker="Developer-first"
- title={<>Drop-in simple. >}
+ title={
+ <>
+ Drop-in simple.
+ >
+ }
body="Add it to your app in three lines. No SDK to learn, no schema to define."
- media={<>
-
+
+
+
-
>}
+})`}
+ />
+
+ >
+ }
/>
{/* Browser frame */}
-
+
- See it live
- Your data, one screen.
+
+ See it live
+
+
+ Your data, one screen.
+
-
-
-
Helix
- {['Overview', 'Events', 'Funnels', 'Cohorts', 'Settings'].map((n, i) => (
-
{n}
- ))}
+
+
+
+ Helix
+
+ {['Overview', 'Events', 'Funnels', 'Cohorts', 'Settings'].map(
+ (n, i) => (
+
+ {n}
+
+ )
+ )}
-
-
Overview
+
+
+ Overview
+
Last 30 days
-
- {[['Revenue', '$1.24M', '▲ 18.2%'], ['Active users', '48,210', '▲ 9.4%'], ['Churn', '1.9%', '▼ 0.6%']].map(([l, v, d]) => (
-
+
+ {[
+ ['Revenue', '$1.24M', '▲ 18.2%'],
+ ['Active users', '48,210', '▲ 9.4%'],
+ ['Churn', '1.9%', '▼ 0.6%'],
+ ].map(([l, v, d]) => (
+
+
+ {l}
+
+
+ {v}
+
+
+ {d}
+
+
))}
-
+
+
+
@@ -175,52 +432,199 @@ track('signup', {
{/* Charts */}
-
+
- The numbers
- Growth you can see.
+
+ The numbers
+
+
+ Growth you can see.
+
-
-
-
-
+
+
+
+ Weekly active
+
+
+
+
+
+
+
+
+
{/* Timeline */}
-
+
- Where we're going
- The roadmap.
+
+ Where we're going
+
+
+ The roadmap.
+
-
+
{/* Spotlight principles */}
-
+
- What we believe
- Three principles.
+
+ What we believe
+
+
+ Three principles.
+
-
+
{[
- { k: '01', t: 'Fast by default', d: 'Speed is a feature. Everything is realtime.' },
- { k: '02', t: 'Yours to own', d: 'Your data, your infra, no lock-in.' },
- { k: '03', t: 'Honest pricing', d: 'No per-seat tax. Scale without surprises.' },
+ {
+ k: '01',
+ t: 'Fast by default',
+ d: 'Speed is a feature. Everything is realtime.',
+ },
+ {
+ k: '02',
+ t: 'Yours to own',
+ d: 'Your data, your infra, no lock-in.',
+ },
+ {
+ k: '03',
+ t: 'Honest pricing',
+ d: 'No per-seat tax. Scale without surprises.',
+ },
].map((p) => (
- {p.k}
- {p.t}
- {p.d}
+
+ {p.k}
+
+
+ {p.t}
+
+
+ {p.d}
+
))}
@@ -228,45 +632,113 @@ track('signup', {
{/* Accordion — FAQ */}
-
+
- Common questions
- Frequently asked.
+
+ Common questions
+
+
+ Frequently asked.
+
{/* Logos */}
-
- Trusted by teams everywhere
-
+
+
+
+ Trusted by teams everywhere
+
+
+
{/* Quote */}
-
+
-
+
“We replaced four tools with Helix and never looked back.”
- — Dana Kim, VP Engineering
+
+ — Dana Kim, VP Engineering
+
{/* CTA */}
-
+
- Let's talk.
- helix.io · hello@helix.io
+
+ Let's talk.
+
+
+ helix.io · hello@helix.io
+
- )
+ );
}
diff --git a/bolt-slides/src/components/Accordion.tsx b/bolt-slides/src/components/Accordion.tsx
index 44175f04..9e4897cd 100644
--- a/bolt-slides/src/components/Accordion.tsx
+++ b/bolt-slides/src/components/Accordion.tsx
@@ -1,42 +1,59 @@
-import { useState, type ReactNode } from 'react'
-import { AnimatePresence, motion } from 'framer-motion'
+import { useState, type ReactNode } from 'react';
+import { AnimatePresence, motion } from 'framer-motion';
/* An expand/collapse accordion. Data-driven like Bento/StatGrid.
- single (default true): only one panel open at a time.
- defaultOpen: index open on mount (null = all closed). */
-export type AccordionItem = { title: string; body: ReactNode }
+export type AccordionItem = { title: string; body: ReactNode };
const Chevron = () => (
-
+
-)
+);
export default function Accordion({
- items, single = true, defaultOpen = 0,
+ items,
+ single = true,
+ defaultOpen = 0,
}: {
- items: AccordionItem[]
- single?: boolean
- defaultOpen?: number | null
+ items: AccordionItem[];
+ single?: boolean;
+ defaultOpen?: number | null;
}) {
- const [open, setOpen] = useState(defaultOpen == null ? [] : [defaultOpen])
- const isOpen = (i: number) => open.includes(i)
+ const [open, setOpen] = useState(
+ defaultOpen == null ? [] : [defaultOpen]
+ );
+ const isOpen = (i: number) => open.includes(i);
const toggle = (i: number) =>
setOpen((cur) => {
- const has = cur.includes(i)
- if (single) return has ? [] : [i]
- return has ? cur.filter((x) => x !== i) : [...cur, i]
- })
+ const has = cur.includes(i);
+ if (single) return has ? [] : [i];
+ return has ? cur.filter((x) => x !== i) : [...cur, i];
+ });
return (
{items.map((it, i) => (
-
toggle(i)} aria-expanded={isOpen(i)}>
+ toggle(i)}
+ aria-expanded={isOpen(i)}
+ >
{String(i + 1).padStart(2, '0')}
{it.title}
-
+
+
+
{isOpen(i) && (
@@ -54,5 +71,5 @@ export default function Accordion({
))}
- )
+ );
}
diff --git a/bolt-slides/src/components/Bento.tsx b/bolt-slides/src/components/Bento.tsx
index 6f455ccb..b9984966 100644
--- a/bolt-slides/src/components/Bento.tsx
+++ b/bolt-slides/src/components/Bento.tsx
@@ -1,23 +1,47 @@
-import type { CSSProperties, ReactNode } from 'react'
-import Reveal from '../deck/Reveal'
+import type { CSSProperties, ReactNode } from 'react';
+import Reveal from '../deck/Reveal';
/* A full-viewport slide laid out as an asymmetric bento grid. Spans via c
(columns of 12) and r (rows). Collapses to one column on narrow screens.
nav/notes are read by the engine (rail label / presenter notes). */
export type BentoTile = {
- k?: string; fig?: ReactNode; title?: string; body?: string
- c?: number; r?: number; variant?: 'accent' | 'glow'
-}
+ k?: string;
+ fig?: ReactNode;
+ title?: string;
+ body?: string;
+ c?: number;
+ r?: number;
+ variant?: 'accent' | 'glow';
+};
-export default function Bento({ kicker, title, tiles }: {
- kicker?: string; title?: string; tiles: BentoTile[]; nav?: string; notes?: string
+export default function Bento({
+ kicker,
+ title,
+ tiles,
+}: {
+ kicker?: string;
+ title?: string;
+ tiles: BentoTile[];
+ nav?: string;
+ notes?: string;
}) {
return (
- {kicker && {kicker}
}
- {title && {title} }
+ {kicker && (
+
+ {kicker}
+
+ )}
+ {title && (
+
+ {title}
+
+ )}
{tiles.map((t, i) => (
@@ -28,7 +52,15 @@ export default function Bento({ kicker, title, tiles }: {
>
{t.k && {t.k}
}
- {t.fig != null &&
{t.fig}
}
+ {t.fig != null && (
+
+ {t.fig}
+
+ )}
{t.title &&
{t.title} }
{t.body &&
{t.body}
}
@@ -37,5 +69,5 @@ export default function Bento({ kicker, title, tiles }: {
- )
+ );
}
diff --git a/bolt-slides/src/components/BrowserFrame.tsx b/bolt-slides/src/components/BrowserFrame.tsx
index 31834e13..ccb9ba44 100644
--- a/bolt-slides/src/components/BrowserFrame.tsx
+++ b/bolt-slides/src/components/BrowserFrame.tsx
@@ -1,15 +1,25 @@
-import type { ReactNode } from 'react'
+import type { ReactNode } from 'react';
/* Wrap a screenshot or UI mock in browser chrome. children fill the body —
an , a , anything. */
-export default function BrowserFrame({ url = 'app.acme.com', children }: { url?: string; children: ReactNode }) {
+export default function BrowserFrame({
+ url = 'app.acme.com',
+ children,
+}: {
+ url?: string;
+ children: ReactNode;
+}) {
return (
-
+
+
+
+
+
{url}
{children}
- )
+ );
}
diff --git a/bolt-slides/src/components/Charts.tsx b/bolt-slides/src/components/Charts.tsx
index 130be0e2..9cc1fa99 100644
--- a/bolt-slides/src/components/Charts.tsx
+++ b/bolt-slides/src/components/Charts.tsx
@@ -1,6 +1,6 @@
-import { motion } from 'framer-motion'
-import type { CSSProperties } from 'react'
-import { useInView } from '../deck/useInView'
+import { motion } from 'framer-motion';
+import type { CSSProperties } from 'react';
+import { useInView } from '../deck/useInView';
/* A small hand-built chart kit — bar, line/area, and donut. Each draws itself in
when scrolled into view. All token-driven (one accent), no chart library.
@@ -8,58 +8,150 @@ import { useInView } from '../deck/useInView'
*/
-export function BarChart({ data, height = 200 }: { data: { label: string; value: number }[]; height?: number }) {
- const { ref, inView } = useInView(0.3)
- const max = Math.max(...data.map((d) => d.value)) || 1
+export function BarChart({
+ data,
+ height = 200,
+}: {
+ data: { label: string; value: number }[];
+ height?: number;
+}) {
+ const { ref, inView } = useInView(0.3);
+ const max = Math.max(...data.map((d) => d.value)) || 1;
return (
{data.map((d, i) => (
))}
- )
+ );
}
-export function LineChart({ points, height = 200 }: { points: number[]; height?: number }) {
- const { ref, inView } = useInView(0.3)
- const w = 300, h = 120
- const max = Math.max(...points), min = Math.min(...points)
- const span = max - min || 1
- const coords = points.map((p, i) => [(i / (points.length - 1)) * w, h - ((p - min) / span) * (h - 10) - 5])
- const line = coords.map((c, i) => `${i === 0 ? 'M' : 'L'}${c[0].toFixed(1)},${c[1].toFixed(1)}`).join(' ')
- const area = `${line} L${w},${h} L0,${h} Z`
+export function LineChart({
+ points,
+ height = 200,
+}: {
+ points: number[];
+ height?: number;
+}) {
+ const { ref, inView } = useInView(0.3);
+ const w = 300,
+ h = 120;
+ const max = Math.max(...points),
+ min = Math.min(...points);
+ const span = max - min || 1;
+ const coords = points.map((p, i) => [
+ (i / (points.length - 1)) * w,
+ h - ((p - min) / span) * (h - 10) - 5,
+ ]);
+ const line = coords
+ .map(
+ (c, i) => `${i === 0 ? 'M' : 'L'}${c[0].toFixed(1)},${c[1].toFixed(1)}`
+ )
+ .join(' ');
+ const area = `${line} L${w},${h} L0,${h} Z`;
return (
-
+
-
+
+
+
+
-
-
+
+
- )
+ );
}
-export function DonutChart({ value, label, size = 168 }: { value: number; label?: string; size?: number }) {
- const { ref, inView } = useInView(0.4)
- const r = 54
- const circ = 2 * Math.PI * r
+export function DonutChart({
+ value,
+ label,
+ size = 168,
+}: {
+ value: number;
+ label?: string;
+ size?: number;
+}) {
+ const { ref, inView } = useInView(0.4);
+ const r = 54;
+ const circ = 2 * Math.PI * r;
return (
-
+
-
+
- {value}%
+
+ {value}%
+
{label &&
{label}
}
- )
+ );
}
diff --git a/bolt-slides/src/components/CodeWindow.tsx b/bolt-slides/src/components/CodeWindow.tsx
index 251b5ca8..fa8a20be 100644
--- a/bolt-slides/src/components/CodeWindow.tsx
+++ b/bolt-slides/src/components/CodeWindow.tsx
@@ -1,46 +1,80 @@
-import { Fragment, type ReactNode } from 'react'
+import { Fragment, type ReactNode } from 'react';
/* A macOS-style code window with line numbers, restrained syntax tinting, and
optional line highlighting. Highlighting is intentionally minimal (keywords,
strings, comments) to stay on-brand — one accent, no rainbow.
*/
-const KW = /\b(const|let|var|function|return|if|else|for|while|import|from|export|default|class|new|await|async|type|interface|extends|implements|public|private|true|false|null|undefined)\b/g
+const KW =
+ /\b(const|let|var|function|return|if|else|for|while|import|from|export|default|class|new|await|async|type|interface|extends|implements|public|private|true|false|null|undefined)\b/g;
function hl(line: string): ReactNode {
// split off a trailing line comment
- const ci = line.indexOf('//')
- const code = ci >= 0 ? line.slice(0, ci) : line
- const comment = ci >= 0 ? line.slice(ci) : ''
+ const ci = line.indexOf('//');
+ const code = ci >= 0 ? line.slice(0, ci) : line;
+ const comment = ci >= 0 ? line.slice(ci) : '';
// strings first (odd indices), then keywords inside the rest
- const segs = code.split(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g)
+ const segs = code.split(
+ /("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)/g
+ );
return (
<>
{segs.map((seg, i) =>
- i % 2 === 1
- ?
{seg}
- :
{seg.split(KW).map((s, j) => (j % 2 === 1 ? {s} : s))} ,
+ i % 2 === 1 ? (
+
+ {seg}
+
+ ) : (
+
+ {seg.split(KW).map((s, j) =>
+ j % 2 === 1 ? (
+
+ {s}
+
+ ) : (
+ s
+ )
+ )}
+
+ )
)}
{comment &&
{comment} }
>
- )
+ );
}
-export default function CodeWindow({ title = 'app.tsx', code, highlight = [] }: { title?: string; code: string; highlight?: number[] }) {
- const lines = code.replace(/\n$/, '').split('\n')
+export default function CodeWindow({
+ title = 'app.tsx',
+ code,
+ highlight = [],
+}: {
+ title?: string;
+ code: string;
+ highlight?: number[];
+}) {
+ const lines = code.replace(/\n$/, '').split('\n');
return (
-
+
+
+
+
+
{title}
-
- {lines.map((ln, i) => (
-
- {i + 1}
- {ln.length ? hl(ln) : ' '}
-
- ))}
-
+
+
+ {lines.map((ln, i) => (
+
+ {i + 1}
+ {ln.length ? hl(ln) : ' '}
+
+ ))}
+
+
- )
+ );
}
diff --git a/bolt-slides/src/components/Comparison.tsx b/bolt-slides/src/components/Comparison.tsx
index 1800e2eb..28c420a2 100644
--- a/bolt-slides/src/components/Comparison.tsx
+++ b/bolt-slides/src/components/Comparison.tsx
@@ -3,32 +3,80 @@
rows={[{ label: 'Realtime sync', values: [true, false] },
{ label: 'Price', values: ['$29', '$99'] }]} />
values are booleans (→ ✓/✗) or strings; `highlight` indexes the value columns. */
-import type { CSSProperties } from 'react'
+import type { CSSProperties } from 'react';
-export type CompRow = { label: string; values: (boolean | string)[] }
+export type CompRow = { label: string; values: (boolean | string)[] };
-const Check = () => (
)
-const Cross = () => (
)
+const Check = () => (
+
+
+
+);
+const Cross = () => (
+
+
+
+);
-export default function Comparison({ cols, rows, highlight = 0 }: { cols: string[]; rows: CompRow[]; highlight?: number }) {
- const vcols = cols.length - 1
+export default function Comparison({
+ cols,
+ rows,
+ highlight = 0,
+}: {
+ cols: string[];
+ rows: CompRow[];
+ highlight?: number;
+}) {
+ const vcols = cols.length - 1;
return (
{cols.map((c, i) => (
-
{c}
+
+ {c}
+
))}
{rows.map((r, ri) => (
{r.label}
{r.values.map((v, vi) => (
-
- {typeof v === 'boolean' ? (v ?
:
) :
{v} }
+
+ {typeof v === 'boolean' ? (
+ v ? (
+
+ ) : (
+
+ )
+ ) : (
+ {v}
+ )}
))}
))}
- )
+ );
}
diff --git a/bolt-slides/src/components/CountUp.tsx b/bolt-slides/src/components/CountUp.tsx
index e00be403..8362a4df 100644
--- a/bolt-slides/src/components/CountUp.tsx
+++ b/bolt-slides/src/components/CountUp.tsx
@@ -1,42 +1,77 @@
-import { useEffect, useRef, useState } from 'react'
-import { useDeck } from '../deck/DeckContext'
+import { useEffect, useRef, useState } from 'react';
+import { useDeck } from '../deck/DeckContext';
/* A number that animates 0 → to when its slide becomes active. Inherits color,
so inside a .figure / .stat-value it picks up the accent gradient. Renders
the final value instantly in thumbnails. */
export default function CountUp({
- to, from = 0, duration = 1500, decimals = 0, prefix = '', suffix = '',
+ to,
+ from = 0,
+ duration = 1500,
+ decimals = 0,
+ prefix = '',
+ suffix = '',
}: {
- to: number; from?: number; duration?: number; decimals?: number; prefix?: string; suffix?: string
+ to: number;
+ from?: number;
+ duration?: number;
+ decimals?: number;
+ prefix?: string;
+ suffix?: string;
}) {
- const { isStatic } = useDeck()
- const ref = useRef
(null)
- const [val, setVal] = useState(isStatic ? to : from)
- const started = useRef(false)
+ const { isStatic } = useDeck();
+ const ref = useRef(null);
+ const [val, setVal] = useState(isStatic ? to : from);
+ const started = useRef(false);
useEffect(() => {
- if (isStatic) { setVal(to); return }
- const el = ref.current
- if (!el) return
- let raf = 0
+ if (isStatic) {
+ setVal(to);
+ return;
+ }
+ const el = ref.current;
+ if (!el) return;
+ let raf = 0;
const run = () => {
- if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) { setVal(to); return }
- const t0 = performance.now()
- const tick = (now: number) => {
- const p = Math.min(1, (now - t0) / duration)
- setVal(from + (to - from) * (1 - Math.pow(1 - p, 3)))
- if (p < 1) raf = requestAnimationFrame(tick)
+ if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
+ setVal(to);
+ return;
}
- raf = requestAnimationFrame(tick)
- }
+ const t0 = performance.now();
+ const tick = (now: number) => {
+ const p = Math.min(1, (now - t0) / duration);
+ setVal(from + (to - from) * (1 - Math.pow(1 - p, 3)));
+ if (p < 1) raf = requestAnimationFrame(tick);
+ };
+ raf = requestAnimationFrame(tick);
+ };
const io = new IntersectionObserver(
- (entries) => entries.forEach((e) => { if (e.isIntersecting && !started.current) { started.current = true; run() } }),
- { threshold: 0.4 },
- )
- io.observe(el)
- return () => { io.disconnect(); cancelAnimationFrame(raf) }
- }, [isStatic, to, from, duration])
+ (entries) =>
+ entries.forEach((e) => {
+ if (e.isIntersecting && !started.current) {
+ started.current = true;
+ run();
+ }
+ }),
+ { threshold: 0.4 }
+ );
+ io.observe(el);
+ return () => {
+ io.disconnect();
+ cancelAnimationFrame(raf);
+ };
+ }, [isStatic, to, from, duration]);
- const text = prefix + val.toLocaleString('en-US', { minimumFractionDigits: decimals, maximumFractionDigits: decimals }) + suffix
- return {text}
+ const text =
+ prefix +
+ val.toLocaleString('en-US', {
+ minimumFractionDigits: decimals,
+ maximumFractionDigits: decimals,
+ }) +
+ suffix;
+ return (
+
+ {text}
+
+ );
}
diff --git a/bolt-slides/src/components/Marquee.tsx b/bolt-slides/src/components/Marquee.tsx
index 4336e4d3..8a1141c2 100644
--- a/bolt-slides/src/components/Marquee.tsx
+++ b/bolt-slides/src/components/Marquee.tsx
@@ -1,12 +1,23 @@
/* A continuously-scrolling strip — logo wall, value props, tech stack. */
-export default function Marquee({ items, duration = 26 }: { items: string[]; duration?: number }) {
+export default function Marquee({
+ items,
+ duration = 26,
+}: {
+ items: string[];
+ duration?: number;
+}) {
return (
-
+
{[...items, ...items].map((it, i) => (
- {it}
+
+ {it}
+
))}
- )
+ );
}
diff --git a/bolt-slides/src/components/Split.tsx b/bolt-slides/src/components/Split.tsx
index 6181cefe..a5d08afd 100644
--- a/bolt-slides/src/components/Split.tsx
+++ b/bolt-slides/src/components/Split.tsx
@@ -1,25 +1,48 @@
-import type { ReactNode } from 'react'
-import Reveal from '../deck/Reveal'
+import type { ReactNode } from 'react';
+import Reveal from '../deck/Reveal';
/* A full-bleed feature slide: text one side, edge-to-edge media the other.
`flip` swaps sides. Stacks vertically on narrow screens. */
export default function Split({
- kicker, title, body, media, flip,
+ kicker,
+ title,
+ body,
+ media,
+ flip,
}: {
- kicker?: string; title: ReactNode; body?: ReactNode; media: ReactNode; flip?: boolean; nav?: string; notes?: string
+ kicker?: string;
+ title: ReactNode;
+ body?: ReactNode;
+ media: ReactNode;
+ flip?: boolean;
+ nav?: string;
+ notes?: string;
}) {
return (
- {kicker && {kicker}
}
- {title}
- {body && {body}
}
+ {kicker && (
+
+ {kicker}
+
+ )}
+
+ {title}
+
+ {body && (
+
+ {body}
+
+ )}
{media}
- )
+ );
}
diff --git a/bolt-slides/src/components/SpotlightCard.tsx b/bolt-slides/src/components/SpotlightCard.tsx
index 46a6dc27..129627c1 100644
--- a/bolt-slides/src/components/SpotlightCard.tsx
+++ b/bolt-slides/src/components/SpotlightCard.tsx
@@ -1,16 +1,20 @@
-import { useRef, type ReactNode } from 'react'
+import { useRef, type ReactNode } from 'react';
/* A card with a soft accent glow that follows the cursor (the Linear/Vercel
hover). Wrap content; it provides the card shell + padding.
…
… …
*/
export default function SpotlightCard({ children }: { children: ReactNode }) {
- const ref = useRef
(null)
+ const ref = useRef(null);
function move(e: React.MouseEvent) {
- const el = ref.current
- if (!el) return
- const r = el.getBoundingClientRect()
- el.style.setProperty('--mx', `${e.clientX - r.left}px`)
- el.style.setProperty('--my', `${e.clientY - r.top}px`)
+ const el = ref.current;
+ if (!el) return;
+ const r = el.getBoundingClientRect();
+ el.style.setProperty('--mx', `${e.clientX - r.left}px`);
+ el.style.setProperty('--my', `${e.clientY - r.top}px`);
}
- return {children}
+ return (
+
+ {children}
+
+ );
}
diff --git a/bolt-slides/src/components/StatGrid.tsx b/bolt-slides/src/components/StatGrid.tsx
index 4d100e7c..a6cc3d3b 100644
--- a/bolt-slides/src/components/StatGrid.tsx
+++ b/bolt-slides/src/components/StatGrid.tsx
@@ -1,19 +1,45 @@
-import type { ReactNode } from 'react'
-import Reveal from '../deck/Reveal'
+import type { ReactNode } from 'react';
+import Reveal from '../deck/Reveal';
/* A full-viewport proof slide: responsive auto-fit stat cards. Pass a
as a stat `value` so figures animate in. */
-export type Stat = { value?: ReactNode; label: string; caption?: string }
+export type Stat = { value?: ReactNode; label: string; caption?: string };
-export default function StatGrid({ kicker, title, stats }: {
- kicker?: string; title?: string; stats: Stat[]; nav?: string; notes?: string
+export default function StatGrid({
+ kicker,
+ title,
+ stats,
+}: {
+ kicker?: string;
+ title?: string;
+ stats: Stat[];
+ nav?: string;
+ notes?: string;
}) {
return (
- {kicker && {kicker}
}
- {title && {title} }
+ {kicker && (
+
+ {kicker}
+
+ )}
+ {title && (
+
+ {title}
+
+ )}
{stats.map((s, i) => (
@@ -26,5 +52,5 @@ export default function StatGrid({ kicker, title, stats }: {
- )
+ );
}
diff --git a/bolt-slides/src/components/Tabs.tsx b/bolt-slides/src/components/Tabs.tsx
index f55892d7..309da5fa 100644
--- a/bolt-slides/src/components/Tabs.tsx
+++ b/bolt-slides/src/components/Tabs.tsx
@@ -1,18 +1,36 @@
-import { useState, type ReactNode } from 'react'
-import { AnimatePresence, motion } from 'framer-motion'
+import { useState, type ReactNode } from 'react';
+import { AnimatePresence, motion } from 'framer-motion';
/* Tabbed content with a sliding accent pill (framer layoutId) and a cross-fade
between panels. }, …]} /> */
-export type Tab = { label: string; content: ReactNode }
+export type Tab = { label: string; content: ReactNode };
-export default function Tabs({ tabs, defaultTab = 0 }: { tabs: Tab[]; defaultTab?: number }) {
- const [active, setActive] = useState(defaultTab)
+export default function Tabs({
+ tabs,
+ defaultTab = 0,
+}: {
+ tabs: Tab[];
+ defaultTab?: number;
+}) {
+ const [active, setActive] = useState(defaultTab);
return (
{tabs.map((t, i) => (
- setActive(i)} role="tab" aria-selected={i === active}>
- {i === active && }
+ setActive(i)}
+ role="tab"
+ aria-selected={i === active}
+ >
+ {i === active && (
+
+ )}
{t.label}
))}
@@ -31,5 +49,5 @@ export default function Tabs({ tabs, defaultTab = 0 }: { tabs: Tab[]; defaultTab
- )
+ );
}
diff --git a/bolt-slides/src/components/TiltCard.tsx b/bolt-slides/src/components/TiltCard.tsx
index 1b57ab7a..29681e72 100644
--- a/bolt-slides/src/components/TiltCard.tsx
+++ b/bolt-slides/src/components/TiltCard.tsx
@@ -1,25 +1,33 @@
-import { useRef, useState, type CSSProperties, type ReactNode } from 'react'
+import { useRef, useState, type CSSProperties, type ReactNode } from 'react';
/* Mouse-tracked 3D tilt + glare. Wrap one hero visual per slide. */
export default function TiltCard({ children }: { children: ReactNode }) {
- const ref = useRef(null)
- const [style, setStyle] = useState({})
+ const ref = useRef(null);
+ const [style, setStyle] = useState({});
function move(e: React.MouseEvent) {
- const r = ref.current!.getBoundingClientRect()
- const px = (e.clientX - r.left) / r.width - 0.5
- const py = (e.clientY - r.top) / r.height - 0.5
+ const r = ref.current!.getBoundingClientRect();
+ const px = (e.clientX - r.left) / r.width - 0.5;
+ const py = (e.clientY - r.top) / r.height - 0.5;
setStyle({
- transform: `perspective(1200px) rotateX(${(-py * 6).toFixed(2)}deg) rotateY(${(px * 9).toFixed(2)}deg)`,
+ transform: `perspective(1200px) rotateX(${(-py * 6).toFixed(
+ 2
+ )}deg) rotateY(${(px * 9).toFixed(2)}deg)`,
['--gx' as string]: `${((px + 0.5) * 100).toFixed(1)}%`,
['--gy' as string]: `${((py + 0.5) * 100).toFixed(1)}%`,
- } as CSSProperties)
+ } as CSSProperties);
}
return (
- setStyle({})}>
+
setStyle({})}
+ >
{children}
- )
+ );
}
diff --git a/bolt-slides/src/components/Timeline.tsx b/bolt-slides/src/components/Timeline.tsx
index 25a8d84b..05b825e6 100644
--- a/bolt-slides/src/components/Timeline.tsx
+++ b/bolt-slides/src/components/Timeline.tsx
@@ -1,18 +1,23 @@
-import { motion } from 'framer-motion'
-import type { ReactNode } from 'react'
-import { useInView } from '../deck/useInView'
+import { motion } from 'framer-motion';
+import type { ReactNode } from 'react';
+import { useInView } from '../deck/useInView';
/* A vertical timeline / roadmap. The connector line draws in and milestones
reveal in sequence when scrolled into view.
*/
-export type TimelineItem = { time: string; title: string; body?: ReactNode }
+export type TimelineItem = { time: string; title: string; body?: ReactNode };
export default function Timeline({ items }: { items: TimelineItem[] }) {
- const { ref, inView } = useInView
(0.2)
+ const { ref, inView } = useInView(0.2);
return (
-
+
{items.map((it, i) => (
@@ -21,7 +26,11 @@ export default function Timeline({ items }: { items: TimelineItem[] }) {
className="tl-item"
initial={{ opacity: 0, x: -14 }}
animate={inView ? { opacity: 1, x: 0 } : { opacity: 0, x: -14 }}
- transition={{ delay: 0.15 + i * 0.12, duration: 0.5, ease: [0.16, 1, 0.3, 1] }}
+ transition={{
+ delay: 0.15 + i * 0.12,
+ duration: 0.5,
+ ease: [0.16, 1, 0.3, 1],
+ }}
>
@@ -33,5 +42,5 @@ export default function Timeline({ items }: { items: TimelineItem[] }) {
))}
- )
+ );
}
diff --git a/bolt-slides/src/components/VisualDashboard.tsx b/bolt-slides/src/components/VisualDashboard.tsx
index af2a75ed..8295ca7f 100644
--- a/bolt-slides/src/components/VisualDashboard.tsx
+++ b/bolt-slides/src/components/VisualDashboard.tsx
@@ -1,53 +1,117 @@
-import { useEffect, useRef, useState } from 'react'
-import CountUp from './CountUp'
-import { useDeck } from '../deck/DeckContext'
+import { useEffect, useRef, useState } from 'react';
+import CountUp from './CountUp';
+import { useDeck } from '../deck/DeckContext';
/* Example analytics visual (a self-drawing dashboard mock). ONLY use it when
the deck is genuinely about data — otherwise build a topic-fit visual in the
same .vframe shell. Draws in when its slide becomes active. */
-const bars = [44, 62, 55, 78, 96, 70, 58]
-const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+const bars = [44, 62, 55, 78, 96, 70, 58];
+const days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
export default function VisualDashboard() {
- const { isStatic } = useDeck()
- const ref = useRef
(null)
- const [shown, setShown] = useState(isStatic)
+ const { isStatic } = useDeck();
+ const ref = useRef(null);
+ const [shown, setShown] = useState(isStatic);
useEffect(() => {
- if (isStatic) { setShown(true); return }
- const el = ref.current
- if (!el) return
+ if (isStatic) {
+ setShown(true);
+ return;
+ }
+ const el = ref.current;
+ if (!el) return;
const io = new IntersectionObserver(
- (entries) => entries.forEach((e) => { if (e.isIntersecting) setShown(true) }),
- { threshold: 0.3 },
- )
- io.observe(el)
- return () => io.disconnect()
- }, [isStatic])
+ (entries) =>
+ entries.forEach((e) => {
+ if (e.isIntersecting) setShown(true);
+ }),
+ { threshold: 0.3 }
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, [isStatic]);
return (
-
+
-
Overview · Last 30 days
-
Live
+
+
+ Overview · Last 30 days
+
+
+
+ Live
+
-
-
-
+
+
Revenue
+
+
+
+
▲ 18.2%
+
+
+
Active users
+
+
+
+
▲ 9.4%
+
+
-
+
-
+
+
+
+
-
-
+
+
{bars.map((h, i) => (
-
+
))}
- )
+ );
}
diff --git a/bolt-slides/src/deck/Annotator.tsx b/bolt-slides/src/deck/Annotator.tsx
index fa646102..d17fac14 100644
--- a/bolt-slides/src/deck/Annotator.tsx
+++ b/bolt-slides/src/deck/Annotator.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useRef, useState } from 'react'
+import { useEffect, useRef, useState } from 'react';
/* State-of-the-art annotation: a full-screen canvas + a floating tool bar.
Tools: pen, highlighter, line, arrow, rectangle, ellipse, eraser; a color
@@ -7,147 +7,358 @@ import { useEffect, useRef, useState } from 'react'
stroke-erase work cleanly. The bar + drawing are only interactive when
`active`; otherwise the canvas just displays the slide's saved annotations. */
-type Tool = 'pen' | 'highlighter' | 'line' | 'arrow' | 'rect' | 'ellipse' | 'eraser'
-type Pt = { x: number; y: number }
-export type Stroke = { tool: Tool; color: string; size: number; points: Pt[] }
+type Tool =
+ | 'pen'
+ | 'highlighter'
+ | 'line'
+ | 'arrow'
+ | 'rect'
+ | 'ellipse'
+ | 'eraser';
+type Pt = { x: number; y: number };
+export type Stroke = { tool: Tool; color: string; size: number; points: Pt[] };
const TOOLS: { id: Tool; label: string; path: string }[] = [
{ id: 'pen', label: 'Pen', path: 'M4 20h4L18 10a2 2 0 0 0-3-3L5 17z' },
- { id: 'highlighter', label: 'Highlighter', path: 'M4 20h5l8-8-4-4-9 9zM13 7l4 4' },
+ {
+ id: 'highlighter',
+ label: 'Highlighter',
+ path: 'M4 20h5l8-8-4-4-9 9zM13 7l4 4',
+ },
{ id: 'line', label: 'Line', path: 'M5 19L19 5' },
{ id: 'arrow', label: 'Arrow', path: 'M6 18L18 6M18 6h-6M18 6v6' },
{ id: 'rect', label: 'Rectangle', path: 'M4 6h16v12H4z' },
- { id: 'ellipse', label: 'Ellipse', path: 'M12 6c4.5 0 8 2.7 8 6s-3.5 6-8 6-8-2.7-8-6 3.5-6 8-6z' },
- { id: 'eraser', label: 'Eraser', path: 'M8 18l-4-4a2 2 0 0 1 0-3l7-7a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3l-7 7zM7 17h11' },
-]
-const COLORS = ['var(--primary)', '#ffffff', '#ef4444', '#f5b73a', '#4aa8ff']
-const SIZES = [3, 6, 11]
+ {
+ id: 'ellipse',
+ label: 'Ellipse',
+ path: 'M12 6c4.5 0 8 2.7 8 6s-3.5 6-8 6-8-2.7-8-6 3.5-6 8-6z',
+ },
+ {
+ id: 'eraser',
+ label: 'Eraser',
+ path: 'M8 18l-4-4a2 2 0 0 1 0-3l7-7a2 2 0 0 1 3 0l4 4a2 2 0 0 1 0 3l-7 7zM7 17h11',
+ },
+];
+const COLORS = ['var(--primary)', '#ffffff', '#ef4444', '#f5b73a', '#4aa8ff'];
+const SIZES = [3, 6, 11];
-const IconUndo = () => (
)
-const IconTrash = () => (
)
+const IconUndo = () => (
+
+
+
+
+);
+const IconTrash = () => (
+
+
+
+);
-function distToSeg(px: number, py: number, ax: number, ay: number, bx: number, by: number) {
- const dx = bx - ax, dy = by - ay
- const len = dx * dx + dy * dy || 1
- let t = ((px - ax) * dx + (py - ay) * dy) / len
- t = Math.max(0, Math.min(1, t))
- return Math.hypot(px - (ax + t * dx), py - (ay + t * dy))
+function distToSeg(
+ px: number,
+ py: number,
+ ax: number,
+ ay: number,
+ bx: number,
+ by: number
+) {
+ const dx = bx - ax,
+ dy = by - ay;
+ const len = dx * dx + dy * dy || 1;
+ let t = ((px - ax) * dx + (py - ay) * dy) / len;
+ t = Math.max(0, Math.min(1, t));
+ return Math.hypot(px - (ax + t * dx), py - (ay + t * dy));
}
function outline(s: Stroke): Pt[] {
- if (s.points.length < 2) return s.points
- const a = s.points[0], b = s.points[s.points.length - 1]
- if (s.tool === 'rect') return [a, { x: b.x, y: a.y }, b, { x: a.x, y: b.y }, a]
+ if (s.points.length < 2) return s.points;
+ const a = s.points[0],
+ b = s.points[s.points.length - 1];
+ if (s.tool === 'rect')
+ return [a, { x: b.x, y: a.y }, b, { x: a.x, y: b.y }, a];
if (s.tool === 'ellipse') {
- const cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2, rx = Math.abs(b.x - a.x) / 2, ry = Math.abs(b.y - a.y) / 2
- return Array.from({ length: 25 }, (_, i) => ({ x: cx + rx * Math.cos((i / 24) * 2 * Math.PI), y: cy + ry * Math.sin((i / 24) * 2 * Math.PI) }))
+ const cx = (a.x + b.x) / 2,
+ cy = (a.y + b.y) / 2,
+ rx = Math.abs(b.x - a.x) / 2,
+ ry = Math.abs(b.y - a.y) / 2;
+ return Array.from({ length: 25 }, (_, i) => ({
+ x: cx + rx * Math.cos((i / 24) * 2 * Math.PI),
+ y: cy + ry * Math.sin((i / 24) * 2 * Math.PI),
+ }));
}
- return s.points
+ return s.points;
}
function hits(s: Stroke, x: number, y: number, r: number) {
- const pts = outline(s)
- if (pts.length === 1) return Math.hypot(pts[0].x - x, pts[0].y - y) < r + s.size
- for (let i = 0; i < pts.length - 1; i++) if (distToSeg(x, y, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y) < r + s.size / 2) return true
- return false
+ const pts = outline(s);
+ if (pts.length === 1)
+ return Math.hypot(pts[0].x - x, pts[0].y - y) < r + s.size;
+ for (let i = 0; i < pts.length - 1; i++)
+ if (
+ distToSeg(x, y, pts[i].x, pts[i].y, pts[i + 1].x, pts[i + 1].y) <
+ r + s.size / 2
+ )
+ return true;
+ return false;
}
-export default function Annotator({ slide, store, active }: { slide: number; store: Record
; active: boolean }) {
- const canvasRef = useRef(null)
- if (!store[slide]) store[slide] = []
- const strokes = useRef(store[slide])
- const draft = useRef(null)
- const [tool, setTool] = useState('pen')
- const [color, setColor] = useState(COLORS[0])
- const [size, setSize] = useState(SIZES[1])
- const toolRef = useRef(tool), colorRef = useRef(color), sizeRef = useRef(size)
- toolRef.current = tool; colorRef.current = color; sizeRef.current = size
+export default function Annotator({
+ slide,
+ store,
+ active,
+}: {
+ slide: number;
+ store: Record;
+ active: boolean;
+}) {
+ const canvasRef = useRef(null);
+ if (!store[slide]) store[slide] = [];
+ const strokes = useRef(store[slide]);
+ const draft = useRef(null);
+ const [tool, setTool] = useState('pen');
+ const [color, setColor] = useState(COLORS[0]);
+ const [size, setSize] = useState(SIZES[1]);
+ const toolRef = useRef(tool),
+ colorRef = useRef(color),
+ sizeRef = useRef(size);
+ toolRef.current = tool;
+ colorRef.current = color;
+ sizeRef.current = size;
- const resolve = (c: string) => c.startsWith('var(') ? (getComputedStyle(document.documentElement).getPropertyValue('--primary').trim() || '#4fe5b0') : c
+ const resolve = (c: string) =>
+ c.startsWith('var(')
+ ? getComputedStyle(document.documentElement)
+ .getPropertyValue('--primary')
+ .trim() || '#4fe5b0'
+ : c;
function paint(ctx: CanvasRenderingContext2D, s: Stroke) {
- if (!s.points.length) return
- ctx.save()
- ctx.strokeStyle = resolve(s.color); ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.lineWidth = s.size
- if (s.tool === 'highlighter') { ctx.globalAlpha = 0.32; ctx.lineWidth = s.size * 3.2 }
- const p = s.points, a = p[0], b = p[p.length - 1]
- ctx.beginPath()
- if (s.tool === 'pen' || s.tool === 'highlighter') { ctx.moveTo(a.x, a.y); for (let i = 1; i < p.length; i++) ctx.lineTo(p[i].x, p[i].y); ctx.stroke() }
- else if (s.tool === 'line' || s.tool === 'arrow') {
- ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y); ctx.stroke()
+ if (!s.points.length) return;
+ ctx.save();
+ ctx.strokeStyle = resolve(s.color);
+ ctx.lineCap = 'round';
+ ctx.lineJoin = 'round';
+ ctx.lineWidth = s.size;
+ if (s.tool === 'highlighter') {
+ ctx.globalAlpha = 0.32;
+ ctx.lineWidth = s.size * 3.2;
+ }
+ const p = s.points,
+ a = p[0],
+ b = p[p.length - 1];
+ ctx.beginPath();
+ if (s.tool === 'pen' || s.tool === 'highlighter') {
+ ctx.moveTo(a.x, a.y);
+ for (let i = 1; i < p.length; i++) ctx.lineTo(p[i].x, p[i].y);
+ ctx.stroke();
+ } else if (s.tool === 'line' || s.tool === 'arrow') {
+ ctx.moveTo(a.x, a.y);
+ ctx.lineTo(b.x, b.y);
+ ctx.stroke();
if (s.tool === 'arrow') {
- const ang = Math.atan2(b.y - a.y, b.x - a.x), h = 8 + s.size * 1.8
- ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.lineTo(b.x - h * Math.cos(ang - 0.4), b.y - h * Math.sin(ang - 0.4))
- ctx.moveTo(b.x, b.y); ctx.lineTo(b.x - h * Math.cos(ang + 0.4), b.y - h * Math.sin(ang + 0.4)); ctx.stroke()
+ const ang = Math.atan2(b.y - a.y, b.x - a.x),
+ h = 8 + s.size * 1.8;
+ ctx.beginPath();
+ ctx.moveTo(b.x, b.y);
+ ctx.lineTo(
+ b.x - h * Math.cos(ang - 0.4),
+ b.y - h * Math.sin(ang - 0.4)
+ );
+ ctx.moveTo(b.x, b.y);
+ ctx.lineTo(
+ b.x - h * Math.cos(ang + 0.4),
+ b.y - h * Math.sin(ang + 0.4)
+ );
+ ctx.stroke();
}
- } else if (s.tool === 'rect') { ctx.strokeRect(Math.min(a.x, b.x), Math.min(a.y, b.y), Math.abs(b.x - a.x), Math.abs(b.y - a.y)) }
- else if (s.tool === 'ellipse') { ctx.ellipse((a.x + b.x) / 2, (a.y + b.y) / 2, Math.abs(b.x - a.x) / 2, Math.abs(b.y - a.y) / 2, 0, 0, Math.PI * 2); ctx.stroke() }
- ctx.restore()
+ } else if (s.tool === 'rect') {
+ ctx.strokeRect(
+ Math.min(a.x, b.x),
+ Math.min(a.y, b.y),
+ Math.abs(b.x - a.x),
+ Math.abs(b.y - a.y)
+ );
+ } else if (s.tool === 'ellipse') {
+ ctx.ellipse(
+ (a.x + b.x) / 2,
+ (a.y + b.y) / 2,
+ Math.abs(b.x - a.x) / 2,
+ Math.abs(b.y - a.y) / 2,
+ 0,
+ 0,
+ Math.PI * 2
+ );
+ ctx.stroke();
+ }
+ ctx.restore();
}
function redraw() {
- const cv = canvasRef.current; if (!cv) return
- const ctx = cv.getContext('2d'); if (!ctx) return
- const dpr = window.devicePixelRatio || 1
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0)
- ctx.clearRect(0, 0, cv.width / dpr, cv.height / dpr)
- for (const s of strokes.current) paint(ctx, s)
- if (draft.current) paint(ctx, draft.current)
+ const cv = canvasRef.current;
+ if (!cv) return;
+ const ctx = cv.getContext('2d');
+ if (!ctx) return;
+ const dpr = window.devicePixelRatio || 1;
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+ ctx.clearRect(0, 0, cv.width / dpr, cv.height / dpr);
+ for (const s of strokes.current) paint(ctx, s);
+ if (draft.current) paint(ctx, draft.current);
+ }
+ function commit() {
+ store[slide] = strokes.current;
+ redraw();
}
- function commit() { store[slide] = strokes.current; redraw() }
function erase(x: number, y: number) {
- const before = strokes.current.length
- strokes.current = strokes.current.filter((s) => !hits(s, x, y, 12))
- if (strokes.current.length !== before) commit()
+ const before = strokes.current.length;
+ strokes.current = strokes.current.filter((s) => !hits(s, x, y, 12));
+ if (strokes.current.length !== before) commit();
}
useEffect(() => {
- const cv = canvasRef.current; if (!cv) return
- const fit = () => { const dpr = window.devicePixelRatio || 1; cv.width = window.innerWidth * dpr; cv.height = window.innerHeight * dpr; cv.style.width = window.innerWidth + 'px'; cv.style.height = window.innerHeight + 'px'; redraw() }
- fit()
- const onUp = () => { if (draft.current) { strokes.current.push(draft.current); draft.current = null; commit() } }
- window.addEventListener('resize', fit); window.addEventListener('pointerup', onUp)
- return () => { window.removeEventListener('resize', fit); window.removeEventListener('pointerup', onUp) }
+ const cv = canvasRef.current;
+ if (!cv) return;
+ const fit = () => {
+ const dpr = window.devicePixelRatio || 1;
+ cv.width = window.innerWidth * dpr;
+ cv.height = window.innerHeight * dpr;
+ cv.style.width = window.innerWidth + 'px';
+ cv.style.height = window.innerHeight + 'px';
+ redraw();
+ };
+ fit();
+ const onUp = () => {
+ if (draft.current) {
+ strokes.current.push(draft.current);
+ draft.current = null;
+ commit();
+ }
+ };
+ window.addEventListener('resize', fit);
+ window.addEventListener('pointerup', onUp);
+ return () => {
+ window.removeEventListener('resize', fit);
+ window.removeEventListener('pointerup', onUp);
+ };
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [])
+ }, []);
function down(e: React.PointerEvent) {
- const x = e.clientX, y = e.clientY
- if (toolRef.current === 'eraser') { erase(x, y); return }
- draft.current = { tool: toolRef.current, color: colorRef.current, size: sizeRef.current, points: [{ x, y }] }
- redraw()
+ const x = e.clientX,
+ y = e.clientY;
+ if (toolRef.current === 'eraser') {
+ erase(x, y);
+ return;
+ }
+ draft.current = {
+ tool: toolRef.current,
+ color: colorRef.current,
+ size: sizeRef.current,
+ points: [{ x, y }],
+ };
+ redraw();
}
function move(e: React.PointerEvent) {
- const x = e.clientX, y = e.clientY
- if (toolRef.current === 'eraser') { if (e.buttons) erase(x, y); return }
- const d = draft.current; if (!d) return
- if (d.tool === 'pen' || d.tool === 'highlighter') d.points.push({ x, y })
- else d.points = [d.points[0], { x, y }]
- redraw()
+ const x = e.clientX,
+ y = e.clientY;
+ if (toolRef.current === 'eraser') {
+ if (e.buttons) erase(x, y);
+ return;
+ }
+ const d = draft.current;
+ if (!d) return;
+ if (d.tool === 'pen' || d.tool === 'highlighter') d.points.push({ x, y });
+ else d.points = [d.points[0], { x, y }];
+ redraw();
}
return (
<>
-
+
{active && (
{TOOLS.map((t) => (
-
setTool(t.id)}>
-
+ setTool(t.id)}
+ >
+
+
+
))}
{COLORS.map((c) => (
- setColor(c)} style={{ background: c }} />
+ setColor(c)}
+ style={{ background: c }}
+ />
))}
{SIZES.map((s) => (
- setSize(s)}>
+ setSize(s)}
+ >
))}
- { strokes.current = strokes.current.slice(0, -1); commit() }}>
- { strokes.current = []; commit() }}>
+ {
+ strokes.current = strokes.current.slice(0, -1);
+ commit();
+ }}
+ >
+
+
+ {
+ strokes.current = [];
+ commit();
+ }}
+ >
+
+
)}
>
- )
+ );
}
diff --git a/bolt-slides/src/deck/Build.tsx b/bolt-slides/src/deck/Build.tsx
index 50de6813..f93fdc87 100644
--- a/bolt-slides/src/deck/Build.tsx
+++ b/bolt-slides/src/deck/Build.tsx
@@ -1,7 +1,7 @@
-import { motion } from 'framer-motion'
-import { useEffect } from 'react'
-import type { CSSProperties, ReactNode } from 'react'
-import { useDeck } from './DeckContext'
+import { motion } from 'framer-motion';
+import { useEffect } from 'react';
+import type { CSSProperties, ReactNode } from 'react';
+import { useDeck } from './DeckContext';
/* Click-build (the Slidev "v-click"): this element stays hidden until the
presenter advances to step `at` (1-based) on the current slide, then it
@@ -11,18 +11,29 @@ import { useDeck } from './DeckContext'
In AUTO mode (Deck autoplay / the dock auto button / "A"), every build
instead animates in automatically on slide load, staggered by `at`. */
export default function Build({
- at = 1, y = 16, children, className, style,
+ at = 1,
+ y = 16,
+ children,
+ className,
+ style,
}: {
- at?: number
- y?: number
- children: ReactNode
- className?: string
- style?: CSSProperties
+ at?: number;
+ y?: number;
+ children: ReactNode;
+ className?: string;
+ style?: CSSProperties;
}) {
- const { clicks, isStatic, auto, stagger = 0.16, registerMax } = useDeck()
- useEffect(() => { registerMax?.(at) }, [at, registerMax])
+ const { clicks, isStatic, auto, stagger = 0.16, registerMax } = useDeck();
+ useEffect(() => {
+ registerMax?.(at);
+ }, [at, registerMax]);
- if (isStatic) return {children}
+ if (isStatic)
+ return (
+
+ {children}
+
+ );
if (auto) {
return (
@@ -31,23 +42,31 @@ export default function Build({
style={style}
initial={{ opacity: 0, y, filter: 'blur(3px)' }}
animate={{ opacity: 1, y: 0, filter: 'blur(0px)' }}
- transition={{ duration: 0.5, ease: [0.16, 1, 0.3, 1], delay: 0.12 + (at - 1) * stagger }}
+ transition={{
+ duration: 0.5,
+ ease: [0.16, 1, 0.3, 1],
+ delay: 0.12 + (at - 1) * stagger,
+ }}
>
{children}
- )
+ );
}
- const shown = clicks >= at
+ const shown = clicks >= at;
return (
{children}
- )
+ );
}
diff --git a/bolt-slides/src/deck/Deck.tsx b/bolt-slides/src/deck/Deck.tsx
index 90e1a7e1..216e0c65 100644
--- a/bolt-slides/src/deck/Deck.tsx
+++ b/bolt-slides/src/deck/Deck.tsx
@@ -1,9 +1,26 @@
-import { Children, useCallback, useEffect, useMemo, useRef, useState } from 'react'
-import type { ReactElement, ReactNode } from 'react'
-import { MotionConfig } from 'framer-motion'
-import { DeckCtx } from './DeckContext'
-import Annotator, { type Stroke } from './Annotator'
-import { IconGrid, IconLeft, IconRight, IconPencil, IconExpand, IconShrink, IconPresent, IconClose, IconAuto } from './icons'
+import {
+ Children,
+ useCallback,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+} from 'react';
+import type { ReactElement, ReactNode } from 'react';
+import { MotionConfig } from 'framer-motion';
+import { DeckCtx } from './DeckContext';
+import Annotator, { type Stroke } from './Annotator';
+import {
+ IconGrid,
+ IconLeft,
+ IconRight,
+ IconPencil,
+ IconExpand,
+ IconShrink,
+ IconPresent,
+ IconClose,
+ IconAuto,
+} from './icons';
/* ── The paged presentation engine + the Slidev-style chrome (dock + rail).
Wrap your //… in . Each top-level child is one slide.
@@ -14,227 +31,452 @@ import { IconGrid, IconLeft, IconRight, IconPencil, IconExpand, IconShrink, Icon
starts in auto mode; tunes the gap.
Copy verbatim; theme only via the :root tokens. ───────────────────────── */
-const fmt = (s: number) => `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
+const fmt = (s: number) =>
+ `${String(Math.floor(s / 60)).padStart(2, '0')}:${String(s % 60).padStart(
+ 2,
+ '0'
+ )}`;
function Thumb({ children }: { children: ReactNode }) {
- const frameRef = useRef(null)
- const [d, setD] = useState({ vw: 1280, vh: 720, scale: 0.15 })
+ const frameRef = useRef(null);
+ const [d, setD] = useState({ vw: 1280, vh: 720, scale: 0.15 });
useEffect(() => {
- const el = frameRef.current
- if (!el) return
- const update = () => setD({ vw: window.innerWidth, vh: window.innerHeight, scale: el.clientWidth / window.innerWidth })
- update()
- const ro = new ResizeObserver(update)
- ro.observe(el)
- window.addEventListener('resize', update)
- return () => { ro.disconnect(); window.removeEventListener('resize', update) }
- }, [])
+ const el = frameRef.current;
+ if (!el) return;
+ const update = () =>
+ setD({
+ vw: window.innerWidth,
+ vh: window.innerHeight,
+ scale: el.clientWidth / window.innerWidth,
+ });
+ update();
+ const ro = new ResizeObserver(update);
+ ro.observe(el);
+ window.addEventListener('resize', update);
+ return () => {
+ ro.disconnect();
+ window.removeEventListener('resize', update);
+ };
+ }, []);
return (
-
+
- {children}
+
+ {children}
+
- )
+ );
}
-export default function Deck({ children, autoplay = false, stagger = 0.16 }: { children: ReactNode; autoplay?: boolean; stagger?: number }) {
- const slides = useMemo(() => Children.toArray(children) as ReactElement[], [children])
- const total = slides.length
- const isPresenter = useMemo(() => new URLSearchParams(window.location.search).has('presenter'), [])
+export default function Deck({
+ children,
+ autoplay = false,
+ stagger = 0.16,
+}: {
+ children: ReactNode;
+ autoplay?: boolean;
+ stagger?: number;
+}) {
+ const slides = useMemo(
+ () => Children.toArray(children) as ReactElement[],
+ [children]
+ );
+ const total = slides.length;
+ const isPresenter = useMemo(
+ () => new URLSearchParams(window.location.search).has('presenter'),
+ []
+ );
const [slide, setSlide] = useState(() => {
- const h = parseInt(window.location.hash.slice(1), 10)
- return h >= 1 && h <= total ? h - 1 : 0
- })
- const [clicks, setClicks] = useState(0)
- const [curMax, setCurMax] = useState(0)
- const [auto, setAuto] = useState(autoplay)
- const [railOpen, setRailOpen] = useState(false)
- const [drawing, setDrawing] = useState(false)
- const [elapsed, setElapsed] = useState(0)
- const [fs, setFs] = useState(false)
- const [uiHidden, setUiHidden] = useState(false)
- const [nearDock, setNearDock] = useState(false)
- const [cursorIdle, setCursorIdle] = useState(false)
- const [noteOverrides, setNoteOverrides] = useState
>(() => {
- try { return JSON.parse(localStorage.getItem('deck:notes') || '{}') } catch { return {} }
- })
+ const h = parseInt(window.location.hash.slice(1), 10);
+ return h >= 1 && h <= total ? h - 1 : 0;
+ });
+ const [clicks, setClicks] = useState(0);
+ const [curMax, setCurMax] = useState(0);
+ const [auto, setAuto] = useState(autoplay);
+ const [railOpen, setRailOpen] = useState(false);
+ const [drawing, setDrawing] = useState(false);
+ const [elapsed, setElapsed] = useState(0);
+ const [fs, setFs] = useState(false);
+ const [uiHidden, setUiHidden] = useState(false);
+ const [nearDock, setNearDock] = useState(false);
+ const [cursorIdle, setCursorIdle] = useState(false);
+ const [noteOverrides, setNoteOverrides] = useState>(
+ () => {
+ try {
+ return JSON.parse(localStorage.getItem('deck:notes') || '{}');
+ } catch {
+ return {};
+ }
+ }
+ );
// per-slide build maxima (so going back restores the right click state) and
// per-slide annotations (so drawings persist on the slide they were made).
- const maxMap = useRef>({})
- const annStore = useRef>({})
- const slideRef = useRef(slide)
- slideRef.current = slide
+ const maxMap = useRef>({});
+ const annStore = useRef>({});
+ const slideRef = useRef(slide);
+ slideRef.current = slide;
const registerMax = useCallback((at: number) => {
- const m = maxMap.current
- m[slideRef.current] = Math.max(m[slideRef.current] || 0, at)
- setCurMax((c) => Math.max(c, at))
- }, [])
-
- const go = useCallback((i: number) => {
- const n = Math.max(0, Math.min(total - 1, i))
- setSlide(n); setClicks(0); setCurMax(maxMap.current[n] || 0)
- }, [total])
+ const m = maxMap.current;
+ m[slideRef.current] = Math.max(m[slideRef.current] || 0, at);
+ setCurMax((c) => Math.max(c, at));
+ }, []);
+
+ const go = useCallback(
+ (i: number) => {
+ const n = Math.max(0, Math.min(total - 1, i));
+ setSlide(n);
+ setClicks(0);
+ setCurMax(maxMap.current[n] || 0);
+ },
+ [total]
+ );
const next = useCallback(() => {
- if (!auto && clicks < curMax) { setClicks(clicks + 1); return }
- if (slide < total - 1) { const n = slide + 1; setSlide(n); setClicks(0); setCurMax(maxMap.current[n] || 0) }
- }, [auto, clicks, curMax, slide, total])
+ if (!auto && clicks < curMax) {
+ setClicks(clicks + 1);
+ return;
+ }
+ if (slide < total - 1) {
+ const n = slide + 1;
+ setSlide(n);
+ setClicks(0);
+ setCurMax(maxMap.current[n] || 0);
+ }
+ }, [auto, clicks, curMax, slide, total]);
const prev = useCallback(() => {
- if (!auto && clicks > 0) { setClicks(clicks - 1); return }
- if (slide > 0) { const n = slide - 1; const m = auto ? 0 : (maxMap.current[n] || 0); setSlide(n); setClicks(m); setCurMax(m) }
- }, [auto, clicks, slide])
+ if (!auto && clicks > 0) {
+ setClicks(clicks - 1);
+ return;
+ }
+ if (slide > 0) {
+ const n = slide - 1;
+ const m = auto ? 0 : maxMap.current[n] || 0;
+ setSlide(n);
+ setClicks(m);
+ setCurMax(m);
+ }
+ }, [auto, clicks, slide]);
const toggleFs = useCallback(() => {
- if (document.fullscreenElement) document.exitFullscreen()
- else document.documentElement.requestFullscreen?.()
- }, [])
- const toggleAuto = useCallback(() => setAuto((v) => !v), [])
+ if (document.fullscreenElement) document.exitFullscreen();
+ else document.documentElement.requestFullscreen?.();
+ }, []);
+ const toggleAuto = useCallback(() => setAuto((v) => !v), []);
const setNote = useCallback((text: string) => {
setNoteOverrides((prev) => {
- const nextO = { ...prev, [slideRef.current]: text }
- try { localStorage.setItem('deck:notes', JSON.stringify(nextO)) } catch { /* ignore */ }
- return nextO
- })
- }, [])
+ const nextO = { ...prev, [slideRef.current]: text };
+ try {
+ localStorage.setItem('deck:notes', JSON.stringify(nextO));
+ } catch {
+ /* ignore */
+ }
+ return nextO;
+ });
+ }, []);
const openPresenter = useCallback(() => {
- if (isPresenter) return
- const url = window.location.pathname + '?presenter=1' + window.location.hash
- window.open(url, 'deck-presenter')
- }, [isPresenter])
+ if (isPresenter) return;
+ const url =
+ window.location.pathname + '?presenter=1' + window.location.hash;
+ window.open(url, 'deck-presenter');
+ }, [isPresenter]);
// keyboard
useEffect(() => {
const onKey = (e: KeyboardEvent) => {
- if (e.metaKey || e.ctrlKey || e.altKey) return
- const t = e.target as HTMLElement | null
- if (t && (t.tagName === 'TEXTAREA' || t.tagName === 'INPUT' || t.isContentEditable)) return
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const t = e.target as HTMLElement | null;
+ if (
+ t &&
+ (t.tagName === 'TEXTAREA' ||
+ t.tagName === 'INPUT' ||
+ t.isContentEditable)
+ )
+ return;
switch (e.key) {
- case 'ArrowRight': case 'ArrowDown': case ' ': case 'PageDown': e.preventDefault(); next(); break
- case 'ArrowLeft': case 'ArrowUp': case 'PageUp': e.preventDefault(); prev(); break
- case 'Home': e.preventDefault(); go(0); break
- case 'End': e.preventDefault(); go(total - 1); break
- case 'o': case 'O': setRailOpen((v) => !v); break
- case 'f': case 'F': toggleFs(); break
- case 'd': case 'D': setDrawing((v) => !v); break
- case 'p': case 'P': openPresenter(); break
- case 'a': case 'A': toggleAuto(); break
- case 'h': case 'H': setUiHidden((v) => !v); break
- case 'Escape': setRailOpen(false); setDrawing(false); setUiHidden(false); break
+ case 'ArrowRight':
+ case 'ArrowDown':
+ case ' ':
+ case 'PageDown':
+ e.preventDefault();
+ next();
+ break;
+ case 'ArrowLeft':
+ case 'ArrowUp':
+ case 'PageUp':
+ e.preventDefault();
+ prev();
+ break;
+ case 'Home':
+ e.preventDefault();
+ go(0);
+ break;
+ case 'End':
+ e.preventDefault();
+ go(total - 1);
+ break;
+ case 'o':
+ case 'O':
+ setRailOpen((v) => !v);
+ break;
+ case 'f':
+ case 'F':
+ toggleFs();
+ break;
+ case 'd':
+ case 'D':
+ setDrawing((v) => !v);
+ break;
+ case 'p':
+ case 'P':
+ openPresenter();
+ break;
+ case 'a':
+ case 'A':
+ toggleAuto();
+ break;
+ case 'h':
+ case 'H':
+ setUiHidden((v) => !v);
+ break;
+ case 'Escape':
+ setRailOpen(false);
+ setDrawing(false);
+ setUiHidden(false);
+ break;
}
- }
- window.addEventListener('keydown', onKey)
- return () => window.removeEventListener('keydown', onKey)
- }, [next, prev, go, total, toggleFs, toggleAuto, openPresenter])
+ };
+ window.addEventListener('keydown', onKey);
+ return () => window.removeEventListener('keydown', onKey);
+ }, [next, prev, go, total, toggleFs, toggleAuto, openPresenter]);
// URL hash sync (initial slide comes from the hash via useState above)
- useEffect(() => { const want = String(slide + 1); if (window.location.hash.slice(1) !== want) history.replaceState(null, '', '#' + want) }, [slide])
useEffect(() => {
- const onHash = () => { const h = parseInt(window.location.hash.slice(1), 10); if (h >= 1 && h <= total && h - 1 !== slide) go(h - 1) }
- window.addEventListener('hashchange', onHash)
- return () => window.removeEventListener('hashchange', onHash)
- }, [slide, total, go])
+ const want = String(slide + 1);
+ if (window.location.hash.slice(1) !== want)
+ history.replaceState(null, '', '#' + want);
+ }, [slide]);
+ useEffect(() => {
+ const onHash = () => {
+ const h = parseInt(window.location.hash.slice(1), 10);
+ if (h >= 1 && h <= total && h - 1 !== slide) go(h - 1);
+ };
+ window.addEventListener('hashchange', onHash);
+ return () => window.removeEventListener('hashchange', onHash);
+ }, [slide, total, go]);
// cross-tab sync (audience ⇄ presenter), via BroadcastChannel
- const chan = useRef(null)
- const applyingRemote = useRef(false)
+ const chan = useRef(null);
+ const applyingRemote = useRef(false);
useEffect(() => {
- const c = new BroadcastChannel('deck-sync')
- chan.current = c
+ const c = new BroadcastChannel('deck-sync');
+ chan.current = c;
c.onmessage = (e) => {
- if (e.data?.type === 'state') { applyingRemote.current = true; setSlide(e.data.slide); setClicks(e.data.clicks); setAuto(e.data.auto) }
- }
- return () => c.close()
- }, [])
+ if (e.data?.type === 'state') {
+ applyingRemote.current = true;
+ setSlide(e.data.slide);
+ setClicks(e.data.clicks);
+ setAuto(e.data.auto);
+ }
+ };
+ return () => c.close();
+ }, []);
useEffect(() => {
- if (applyingRemote.current) { applyingRemote.current = false; return }
- chan.current?.postMessage({ type: 'state', slide, clicks, auto })
- }, [slide, clicks, auto])
+ if (applyingRemote.current) {
+ applyingRemote.current = false;
+ return;
+ }
+ chan.current?.postMessage({ type: 'state', slide, clicks, auto });
+ }, [slide, clicks, auto]);
// fullscreen flag, presenter timer, idle auto-hide
- useEffect(() => { const h = () => setFs(!!document.fullscreenElement); document.addEventListener('fullscreenchange', h); return () => document.removeEventListener('fullscreenchange', h) }, [])
useEffect(() => {
- if (!isPresenter) return
- setElapsed(0)
- const t = setInterval(() => setElapsed((e) => e + 1), 1000)
- return () => clearInterval(t)
- }, [isPresenter])
+ const h = () => setFs(!!document.fullscreenElement);
+ document.addEventListener('fullscreenchange', h);
+ return () => document.removeEventListener('fullscreenchange', h);
+ }, []);
+ useEffect(() => {
+ if (!isPresenter) return;
+ setElapsed(0);
+ const t = setInterval(() => setElapsed((e) => e + 1), 1000);
+ return () => clearInterval(t);
+ }, [isPresenter]);
// mouse-driven only: keyboard nav keeps the UI hidden; the dock returns when
// the pointer nears the bottom (where it lives); the cursor hides on idle.
useEffect(() => {
- let t = 0
+ let t = 0;
const onMove = (e: MouseEvent) => {
- setCursorIdle(false)
- setNearDock(e.clientY > window.innerHeight - 150)
- clearTimeout(t); t = window.setTimeout(() => setCursorIdle(true), 2600)
- }
- window.addEventListener('mousemove', onMove)
- return () => { clearTimeout(t); window.removeEventListener('mousemove', onMove) }
- }, [])
-
- const liveCtx = useMemo(() => ({ clicks, isStatic: false, auto, stagger, registerMax }), [clicks, auto, stagger, registerMax])
- const hasPrev = auto ? slide > 0 : (slide > 0 || clicks > 0)
- const hasNext = auto ? slide < total - 1 : (slide < total - 1 || clicks < curMax)
- const notes = (slides[slide]?.props as { notes?: string } | undefined)?.notes
- const noteText = noteOverrides[slide] ?? notes ?? ''
- const nextSlide = slides[slide + 1]
- const hideUI = uiHidden || (fs && !nearDock)
- const cursorHidden = fs && cursorIdle && !drawing
- const showAnnotator = drawing || (annStore.current[slide]?.length ?? 0) > 0
+ setCursorIdle(false);
+ setNearDock(e.clientY > window.innerHeight - 150);
+ clearTimeout(t);
+ t = window.setTimeout(() => setCursorIdle(true), 2600);
+ };
+ window.addEventListener('mousemove', onMove);
+ return () => {
+ clearTimeout(t);
+ window.removeEventListener('mousemove', onMove);
+ };
+ }, []);
+
+ const liveCtx = useMemo(
+ () => ({ clicks, isStatic: false, auto, stagger, registerMax }),
+ [clicks, auto, stagger, registerMax]
+ );
+ const hasPrev = auto ? slide > 0 : slide > 0 || clicks > 0;
+ const hasNext = auto
+ ? slide < total - 1
+ : slide < total - 1 || clicks < curMax;
+ const notes = (slides[slide]?.props as { notes?: string } | undefined)?.notes;
+ const noteText = noteOverrides[slide] ?? notes ?? '';
+ const nextSlide = slides[slide + 1];
+ const hideUI = uiHidden || (fs && !nearDock);
+ const cursorHidden = fs && cursorIdle && !drawing;
+ const showAnnotator = drawing || (annStore.current[slide]?.length ?? 0) > 0;
return (
- {slides[slide]}
+
+ {slides[slide]}
+
- {showAnnotator &&
}
+ {showAnnotator && (
+
+ )}
Slides
- setRailOpen(false)}>
+ setRailOpen(false)}
+ >
+
+
- {railOpen && slides.map((s, i) => (
- { go(i); setRailOpen(false) }}>
- {i + 1}
- {s}
-
- ))}
+ {railOpen &&
+ slides.map((s, i) => (
+ {
+ go(i);
+ setRailOpen(false);
+ }}
+ >
+ {i + 1}
+ {s}
+
+ ))}
{isPresenter && (
- Presenter · {slide + 1} / {total}
+
+ Presenter · {slide + 1} / {total}
+
{fmt(elapsed)}
- {nextSlide &&
{nextSlide}
}
-
)}
-
setRailOpen((v) => !v)}>
+
setRailOpen((v) => !v)}
+ >
+
+
-
-
{slide + 1} / {total}
-
+
+
+
+
+ {slide + 1}
+ / {total}
+
+
+
+
-
-
setDrawing((v) => !v)}>
-
{fs ? : }
-
+
+
+
+
setDrawing((v) => !v)}
+ >
+
+
+
+ {fs ? : }
+
+
+
+
- )
+ );
}
diff --git a/bolt-slides/src/deck/DeckContext.ts b/bolt-slides/src/deck/DeckContext.ts
index 754eeb4e..6cde9ae6 100644
--- a/bolt-slides/src/deck/DeckContext.ts
+++ b/bolt-slides/src/deck/DeckContext.ts
@@ -1,16 +1,19 @@
-import { createContext, useContext } from 'react'
+import { createContext, useContext } from 'react';
/* Shared state for click-builds. The live slide gets {clicks, isStatic:false,
registerMax}; thumbnails/previews get {isStatic:true} so every build shows
at once with no animation. Default is static so components render fine even
outside a . */
export type DeckCtxValue = {
- clicks: number
- isStatic: boolean
- auto?: boolean // auto-play: reveal all builds in a stagger on slide load
- stagger?: number // seconds between builds in auto mode
- registerMax?: (at: number) => void
-}
+ clicks: number;
+ isStatic: boolean;
+ auto?: boolean; // auto-play: reveal all builds in a stagger on slide load
+ stagger?: number; // seconds between builds in auto mode
+ registerMax?: (at: number) => void;
+};
-export const DeckCtx = createContext({ clicks: 9999, isStatic: true })
-export const useDeck = () => useContext(DeckCtx)
+export const DeckCtx = createContext({
+ clicks: 9999,
+ isStatic: true,
+});
+export const useDeck = () => useContext(DeckCtx);
diff --git a/bolt-slides/src/deck/Reveal.tsx b/bolt-slides/src/deck/Reveal.tsx
index f53266d7..6b40350a 100644
--- a/bolt-slides/src/deck/Reveal.tsx
+++ b/bolt-slides/src/deck/Reveal.tsx
@@ -1,20 +1,29 @@
-import { motion } from 'framer-motion'
-import type { CSSProperties, ReactNode } from 'react'
-import { useDeck } from './DeckContext'
+import { motion } from 'framer-motion';
+import type { CSSProperties, ReactNode } from 'react';
+import { useDeck } from './DeckContext';
/* Entrance animation: content rises + fades in when its slide becomes active
(on mount). Wrap a headline, a grid, a card. Static in thumbnails. */
export default function Reveal({
- children, y = 26, delay = 0, className, style,
+ children,
+ y = 26,
+ delay = 0,
+ className,
+ style,
}: {
- children: ReactNode
- y?: number
- delay?: number
- className?: string
- style?: CSSProperties
+ children: ReactNode;
+ y?: number;
+ delay?: number;
+ className?: string;
+ style?: CSSProperties;
}) {
- const { isStatic } = useDeck()
- if (isStatic) return {children}
+ const { isStatic } = useDeck();
+ if (isStatic)
+ return (
+
+ {children}
+
+ );
return (
{children}
- )
+ );
}
diff --git a/bolt-slides/src/deck/Slide.tsx b/bolt-slides/src/deck/Slide.tsx
index accd70c3..f5b2b685 100644
--- a/bolt-slides/src/deck/Slide.tsx
+++ b/bolt-slides/src/deck/Slide.tsx
@@ -1,4 +1,4 @@
-import type { CSSProperties, ReactNode } from 'react'
+import type { CSSProperties, ReactNode } from 'react';
/* One full-viewport slide. Content REFLOWS responsively (no fixed canvas).
- center: centered + text-align center (cover, statement, quote, CTA)
@@ -7,19 +7,28 @@ import type { CSSProperties, ReactNode } from 'react'
- notes: speaker notes shown in the presenter overlay
Non-full slides wrap content in a max-width .container. */
export default function Slide({
- children, center, full, className = '', style,
+ children,
+ center,
+ full,
+ className = '',
+ style,
}: {
- children: ReactNode
- center?: boolean
- full?: boolean
- nav?: string
- notes?: string
- className?: string
- style?: CSSProperties
+ children: ReactNode;
+ center?: boolean;
+ full?: boolean;
+ nav?: string;
+ notes?: string;
+ className?: string;
+ style?: CSSProperties;
}) {
return (
-
+
{full ? children :
{children}
}
- )
+ );
}
diff --git a/bolt-slides/src/deck/icons.tsx b/bolt-slides/src/deck/icons.tsx
index ed86d7ac..7a77b17e 100644
--- a/bolt-slides/src/deck/icons.tsx
+++ b/bolt-slides/src/deck/icons.tsx
@@ -1,12 +1,61 @@
/* Minimal inline icons for the dock (no icon-font dependency). */
-const base = { width: 24, height: 24, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.8, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const }
+const base = {
+ width: 24,
+ height: 24,
+ viewBox: '0 0 24 24',
+ fill: 'none',
+ stroke: 'currentColor',
+ strokeWidth: 1.8,
+ strokeLinecap: 'round' as const,
+ strokeLinejoin: 'round' as const,
+};
-export const IconGrid = () => (
)
-export const IconLeft = () => (
)
-export const IconRight = () => (
)
-export const IconPencil = () => (
)
-export const IconExpand = () => (
)
-export const IconShrink = () => (
)
-export const IconPresent = () => (
)
-export const IconClose = () => (
)
-export const IconAuto = () => (
)
+export const IconGrid = () => (
+
+
+
+
+
+
+);
+export const IconLeft = () => (
+
+
+
+);
+export const IconRight = () => (
+
+
+
+);
+export const IconPencil = () => (
+
+
+
+);
+export const IconExpand = () => (
+
+
+
+);
+export const IconShrink = () => (
+
+
+
+);
+export const IconPresent = () => (
+
+
+
+
+);
+export const IconClose = () => (
+
+
+
+);
+export const IconAuto = () => (
+
+
+
+);
diff --git a/bolt-slides/src/deck/useInView.ts b/bolt-slides/src/deck/useInView.ts
index ae0dc044..a31a00af 100644
--- a/bolt-slides/src/deck/useInView.ts
+++ b/bolt-slides/src/deck/useInView.ts
@@ -1,23 +1,29 @@
-import { useEffect, useRef, useState } from 'react'
-import { useDeck } from './DeckContext'
+import { useEffect, useRef, useState } from 'react';
+import { useDeck } from './DeckContext';
/* Returns a ref + `inView` that flips true when the element scrolls into view —
the trigger for draw-in animations. In thumbnails (isStatic) it's true
immediately so previews render their final state. */
export function useInView
(threshold = 0.3) {
- const { isStatic } = useDeck()
- const ref = useRef(null)
- const [inView, setInView] = useState(isStatic)
+ const { isStatic } = useDeck();
+ const ref = useRef(null);
+ const [inView, setInView] = useState(isStatic);
useEffect(() => {
- if (isStatic) { setInView(true); return }
- const el = ref.current
- if (!el) return
+ if (isStatic) {
+ setInView(true);
+ return;
+ }
+ const el = ref.current;
+ if (!el) return;
const io = new IntersectionObserver(
- (entries) => entries.forEach((e) => { if (e.isIntersecting) setInView(true) }),
- { threshold },
- )
- io.observe(el)
- return () => io.disconnect()
- }, [isStatic, threshold])
- return { ref, inView }
+ (entries) =>
+ entries.forEach((e) => {
+ if (e.isIntersecting) setInView(true);
+ }),
+ { threshold }
+ );
+ io.observe(el);
+ return () => io.disconnect();
+ }, [isStatic, threshold]);
+ return { ref, inView };
}
diff --git a/bolt-slides/src/main.tsx b/bolt-slides/src/main.tsx
index cebaf4df..342c2b6f 100644
--- a/bolt-slides/src/main.tsx
+++ b/bolt-slides/src/main.tsx
@@ -1,11 +1,11 @@
-import React from 'react'
-import ReactDOM from 'react-dom/client'
-import App from './App'
-import './styles/tokens.css'
-import './styles/base.css'
+import React from 'react';
+import ReactDOM from 'react-dom/client';
+import App from './App';
+import './styles/tokens.css';
+import './styles/base.css';
ReactDOM.createRoot(document.getElementById('root')!).render(
- ,
-)
+
+);
diff --git a/bolt-slides/src/styles/base.css b/bolt-slides/src/styles/base.css
index c8789dd6..58fc3c62 100644
--- a/bolt-slides/src/styles/base.css
+++ b/bolt-slides/src/styles/base.css
@@ -1,9 +1,20 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
/* ── Reset & page ───────────────────────────────────────────────── */
-*, *::before, *::after { box-sizing: border-box; }
-html, body, #root { height: 100%; margin: 0; }
-html { color-scheme: dark; } /* set "light" for light decks */
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+html,
+body,
+#root {
+ height: 100%;
+ margin: 0;
+}
+html {
+ color-scheme: dark;
+} /* set "light" for light decks */
body {
background: var(--bg);
color: var(--fg);
@@ -11,203 +22,832 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
- font-feature-settings: "kern" 1, "liga" 1, "ss01" 1;
+ font-feature-settings: 'kern' 1, 'liga' 1, 'ss01' 1;
letter-spacing: -0.011em;
overflow: hidden;
}
/* ── The deck: a fixed-viewport stage that PAGES (one slide at a time) ── */
-.deck { position: relative; width: 100vw; height: 100vh; height: 100svh; overflow: hidden; }
-.slide-stage { position: absolute; inset: 0; animation: slideIn 0.5s var(--ease) both; }
-@keyframes slideIn { from { opacity: 0; transform: scale(1.018); } to { opacity: 1; transform: scale(1); } }
+.deck {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+ height: 100svh;
+ overflow: hidden;
+}
+.slide-stage {
+ position: absolute;
+ inset: 0;
+ animation: slideIn 0.5s var(--ease) both;
+}
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: scale(1.018);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
/* ── A slide = one full-viewport screen. Content is RESPONSIVE: it
reflows to the viewport (no fixed canvas, no scaling/clipping). ──── */
.slide {
- position: relative; width: 100%; height: 100%;
- display: flex; flex-direction: column; justify-content: center;
+ position: relative;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
padding: var(--gutter-y) var(--gutter);
overflow: hidden;
}
-.slide.center { align-items: center; text-align: center; }
-.slide.full { padding: 0; }
-.slide > * { position: relative; z-index: 1; }
-.container { width: 100%; max-width: 1180px; margin-inline: auto; }
+.slide.center {
+ align-items: center;
+ text-align: center;
+}
+.slide.full {
+ padding: 0;
+}
+.slide > * {
+ position: relative;
+ z-index: 1;
+}
+.container {
+ width: 100%;
+ max-width: 1180px;
+ margin-inline: auto;
+}
/* Atmosphere — drifting spotlights + film grain + vignette, per slide */
-.slide { box-shadow: inset 0 0 220px 50px var(--vignette); }
+.slide {
+ box-shadow: inset 0 0 220px 50px var(--vignette);
+}
.slide::before {
- content: ""; position: absolute; inset: -15%; pointer-events: none; z-index: 0;
- background:
- radial-gradient(44% 50% at 24% 16%, var(--bg-grad-1), transparent 60%),
+ content: '';
+ position: absolute;
+ inset: -15%;
+ pointer-events: none;
+ z-index: 0;
+ background: radial-gradient(
+ 44% 50% at 24% 16%,
+ var(--bg-grad-1),
+ transparent 60%
+ ),
radial-gradient(50% 56% at 82% 88%, var(--bg-grad-2), transparent 62%);
animation: drift 26s var(--ease) infinite alternate;
}
-@keyframes drift { 0% { transform: translate3d(-2%,-1%,0) scale(1.06); } 100% { transform: translate3d(3%,2%,0) scale(1.14); } }
+@keyframes drift {
+ 0% {
+ transform: translate3d(-2%, -1%, 0) scale(1.06);
+ }
+ 100% {
+ transform: translate3d(3%, 2%, 0) scale(1.14);
+ }
+}
.slide::after {
- content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 2;
- opacity: var(--grain); mix-blend-mode: overlay;
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ z-index: 2;
+ opacity: var(--grain);
+ mix-blend-mode: overlay;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
}
/* ── Fluid type atoms ───────────────────────────────────────────── */
-h1, h2, h3 { font-family: var(--font-head); font-weight: 600; letter-spacing: -0.028em; line-height: 1.04; margin: 0; color: var(--fg); }
-p { margin: 0; line-height: 1.5; }
-strong { font-weight: 600; color: var(--fg); }
-.kicker { font-size: clamp(12px, 1.1vw, 15px); font-weight: 500; letter-spacing: 0.16em; text-transform: uppercase; color: var(--fg-faint); }
-.display { font-weight: 600; letter-spacing: -0.04em; line-height: 0.98; font-size: clamp(44px, 8vw, 116px); }
-.headline { font-weight: 600; letter-spacing: -0.03em; line-height: 1.04; font-size: clamp(32px, 5.2vw, 62px); max-width: 18ch; }
-.lead { font-size: clamp(17px, 2.1vw, 25px); color: var(--fg-muted); max-width: 42ch; line-height: 1.5; }
-.subhead { font-size: clamp(18px, 2.4vw, 30px); color: var(--fg-muted); max-width: 26ch; line-height: 1.35; }
-.foot { font-size: clamp(13px, 1.2vw, 16px); color: var(--fg-faint); }
-.center .lead, .center .subhead { margin-inline: auto; }
-.accent-text { background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
-.figure { font-weight: 600; letter-spacing: -0.05em; line-height: 0.9; font-size: clamp(88px, 16vw, 220px); font-feature-settings: "tnum" 1; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
-.rule { width: 96px; height: 1px; background: linear-gradient(90deg, var(--primary), transparent); }
+h1,
+h2,
+h3 {
+ font-family: var(--font-head);
+ font-weight: 600;
+ letter-spacing: -0.028em;
+ line-height: 1.04;
+ margin: 0;
+ color: var(--fg);
+}
+p {
+ margin: 0;
+ line-height: 1.5;
+}
+strong {
+ font-weight: 600;
+ color: var(--fg);
+}
+.kicker {
+ font-size: clamp(12px, 1.1vw, 15px);
+ font-weight: 500;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ color: var(--fg-faint);
+}
+.display {
+ font-weight: 600;
+ letter-spacing: -0.04em;
+ line-height: 0.98;
+ font-size: clamp(44px, 8vw, 116px);
+}
+.headline {
+ font-weight: 600;
+ letter-spacing: -0.03em;
+ line-height: 1.04;
+ font-size: clamp(32px, 5.2vw, 62px);
+ max-width: 18ch;
+}
+.lead {
+ font-size: clamp(17px, 2.1vw, 25px);
+ color: var(--fg-muted);
+ max-width: 42ch;
+ line-height: 1.5;
+}
+.subhead {
+ font-size: clamp(18px, 2.4vw, 30px);
+ color: var(--fg-muted);
+ max-width: 26ch;
+ line-height: 1.35;
+}
+.foot {
+ font-size: clamp(13px, 1.2vw, 16px);
+ color: var(--fg-faint);
+}
+.center .lead,
+.center .subhead {
+ margin-inline: auto;
+}
+.accent-text {
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.figure {
+ font-weight: 600;
+ letter-spacing: -0.05em;
+ line-height: 0.9;
+ font-size: clamp(88px, 16vw, 220px);
+ font-feature-settings: 'tnum' 1;
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.rule {
+ width: 96px;
+ height: 1px;
+ background: linear-gradient(90deg, var(--primary), transparent);
+}
/* ── Bento — responsive asymmetric tile grid ────────────────────── */
-.bento { display: grid; grid-template-columns: repeat(12, 1fr); grid-auto-rows: minmax(clamp(130px, 20vh, 230px), auto); gap: 16px; width: 100%; }
+.bento {
+ display: grid;
+ grid-template-columns: repeat(12, 1fr);
+ grid-auto-rows: minmax(clamp(130px, 20vh, 230px), auto);
+ gap: 16px;
+ width: 100%;
+}
.btile {
- position: relative; overflow: hidden;
- grid-column: span var(--c, 4); grid-row: span var(--r, 1);
- display: flex; flex-direction: column; justify-content: space-between; gap: 14px;
- padding: clamp(20px, 2vw, 28px); border-radius: var(--radius-lg);
+ position: relative;
+ overflow: hidden;
+ grid-column: span var(--c, 4);
+ grid-row: span var(--r, 1);
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ gap: 14px;
+ padding: clamp(20px, 2vw, 28px);
+ border-radius: var(--radius-lg);
background: linear-gradient(180deg, var(--surface-2), var(--surface));
border: 1px solid var(--hair);
- transition: border-color var(--dur) var(--ease), transform var(--dur) var(--ease);
-}
-.btile:hover { border-color: color-mix(in srgb, var(--primary) 32%, transparent); transform: translateY(-3px); }
-.btile.accent { background: var(--accent); border-color: transparent; }
-.btile.accent .btile-fig, .btile.accent h3, .btile.accent p { color: var(--accent-ink); -webkit-text-fill-color: var(--accent-ink); }
-.btile.accent .btile-k { color: color-mix(in srgb, var(--accent-ink) 72%, transparent); }
-.btile.glow { box-shadow: var(--glow); border-color: color-mix(in srgb, var(--primary) 40%, transparent); }
-.btile-k { font-size: clamp(11px, 1vw, 13px); font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: var(--fg-faint); }
-.btile-fig { font-size: clamp(40px, 4.4vw, 64px); font-weight: 600; letter-spacing: -0.045em; line-height: 0.92; font-feature-settings: "tnum" 1; color: var(--fg); }
-.btile-fig.grad { background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
-.btile h3 { font-size: clamp(18px, 2vw, 23px); }
-.btile p { font-size: clamp(14px, 1.4vw, 16px); color: var(--fg-muted); line-height: 1.4; }
+ transition: border-color var(--dur) var(--ease),
+ transform var(--dur) var(--ease);
+}
+.btile:hover {
+ border-color: color-mix(in srgb, var(--primary) 32%, transparent);
+ transform: translateY(-3px);
+}
+.btile.accent {
+ background: var(--accent);
+ border-color: transparent;
+}
+.btile.accent .btile-fig,
+.btile.accent h3,
+.btile.accent p {
+ color: var(--accent-ink);
+ -webkit-text-fill-color: var(--accent-ink);
+}
+.btile.accent .btile-k {
+ color: color-mix(in srgb, var(--accent-ink) 72%, transparent);
+}
+.btile.glow {
+ box-shadow: var(--glow);
+ border-color: color-mix(in srgb, var(--primary) 40%, transparent);
+}
+.btile-k {
+ font-size: clamp(11px, 1vw, 13px);
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: var(--fg-faint);
+}
+.btile-fig {
+ font-size: clamp(40px, 4.4vw, 64px);
+ font-weight: 600;
+ letter-spacing: -0.045em;
+ line-height: 0.92;
+ font-feature-settings: 'tnum' 1;
+ color: var(--fg);
+}
+.btile-fig.grad {
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.btile h3 {
+ font-size: clamp(18px, 2vw, 23px);
+}
+.btile p {
+ font-size: clamp(14px, 1.4vw, 16px);
+ color: var(--fg-muted);
+ line-height: 1.4;
+}
/* ── Split — full-bleed feature section ─────────────────────────── */
-.split { display: grid; grid-template-columns: 1.05fr 1fr; width: 100%; height: 100%; }
-.split-body { display: flex; flex-direction: column; justify-content: center; gap: clamp(14px, 2vh, 22px); padding: var(--gutter-y) clamp(28px, 5vw, 72px) var(--gutter-y) var(--gutter); }
-.split.flip .split-body { order: 2; padding: var(--gutter-y) var(--gutter) var(--gutter-y) clamp(28px, 5vw, 72px); }
-.split-media { position: relative; overflow: hidden; display: flex; align-items: center; justify-content: center; }
-.split-media img { width: 100%; height: 100%; object-fit: cover; display: block; }
+.split {
+ display: grid;
+ grid-template-columns: 1.05fr 1fr;
+ width: 100%;
+ height: 100%;
+}
+.split-body {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: clamp(14px, 2vh, 22px);
+ padding: var(--gutter-y) clamp(28px, 5vw, 72px) var(--gutter-y) var(--gutter);
+}
+.split.flip .split-body {
+ order: 2;
+ padding: var(--gutter-y) var(--gutter) var(--gutter-y) clamp(28px, 5vw, 72px);
+}
+.split-media {
+ position: relative;
+ overflow: hidden;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+.split-media img {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ display: block;
+}
/* ── Stat grid + card ───────────────────────────────────────────── */
-.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: clamp(14px, 1.6vw, 22px); width: 100%; }
-.stat-card { display: flex; flex-direction: column; gap: 8px; padding: clamp(24px, 2.4vw, 36px) clamp(22px, 2vw, 30px); border-radius: var(--radius); background: var(--surface); border: 1px solid var(--hair); text-align: left; transition: transform var(--dur) var(--ease), border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
-.stat-card:hover { transform: translateY(-6px); border-color: color-mix(in srgb, var(--primary) 35%, transparent); box-shadow: var(--glow); }
-.stat-value { font-size: clamp(40px, 4.6vw, 58px); font-weight: 600; letter-spacing: -0.04em; line-height: 1; font-feature-settings: "tnum" 1; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; }
-.stat-label { font-size: clamp(16px, 1.6vw, 19px); font-weight: 600; color: var(--fg); margin-top: 8px; }
-.stat-caption { font-size: clamp(13px, 1.3vw, 15px); color: var(--fg-faint); line-height: 1.4; }
+.stat-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: clamp(14px, 1.6vw, 22px);
+ width: 100%;
+}
+.stat-card {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding: clamp(24px, 2.4vw, 36px) clamp(22px, 2vw, 30px);
+ border-radius: var(--radius);
+ background: var(--surface);
+ border: 1px solid var(--hair);
+ text-align: left;
+ transition: transform var(--dur) var(--ease),
+ border-color var(--dur) var(--ease), box-shadow var(--dur) var(--ease);
+}
+.stat-card:hover {
+ transform: translateY(-6px);
+ border-color: color-mix(in srgb, var(--primary) 35%, transparent);
+ box-shadow: var(--glow);
+}
+.stat-value {
+ font-size: clamp(40px, 4.6vw, 58px);
+ font-weight: 600;
+ letter-spacing: -0.04em;
+ line-height: 1;
+ font-feature-settings: 'tnum' 1;
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+.stat-label {
+ font-size: clamp(16px, 1.6vw, 19px);
+ font-weight: 600;
+ color: var(--fg);
+ margin-top: 8px;
+}
+.stat-caption {
+ font-size: clamp(13px, 1.3vw, 15px);
+ color: var(--fg-faint);
+ line-height: 1.4;
+}
/* ── Accordion ──────────────────────────────────────────────────── */
-.accordion { display: flex; flex-direction: column; gap: 12px; width: 100%; }
-.acc-item { border: 1px solid var(--hair); border-radius: var(--radius); background: var(--surface); overflow: hidden; transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease); }
-.acc-item.open { border-color: color-mix(in srgb, var(--primary) 38%, transparent); background: var(--surface-2); }
-.acc-head { display: flex; align-items: center; gap: 16px; width: 100%; padding: clamp(16px,1.8vw,22px) clamp(18px,2vw,26px); background: transparent; border: none; cursor: pointer; text-align: left; font-family: var(--font-body); color: var(--fg); }
-.acc-index { font-family: var(--font-mono); font-size: 13px; font-weight: 600; letter-spacing: 0.04em; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; flex-shrink: 0; }
-.acc-head h3 { flex: 1; font-size: clamp(17px,1.9vw,21px); font-weight: 600; letter-spacing: -0.02em; color: var(--fg); margin: 0; }
-.acc-chevron { display: grid; place-items: center; color: var(--fg-faint); transition: transform var(--dur) var(--ease), color var(--dur) var(--ease); flex-shrink: 0; }
-.acc-item.open .acc-chevron { transform: rotate(180deg); color: var(--primary); }
-.acc-chevron svg { width: 18px; height: 18px; }
-.acc-body { overflow: hidden; }
-.acc-body-inner { padding: 0 clamp(18px,2vw,26px) clamp(18px,2vw,24px) clamp(44px,5vw,58px); font-size: clamp(14px,1.5vw,17px); color: var(--fg-muted); line-height: 1.5; max-width: 62ch; }
+.accordion {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ width: 100%;
+}
+.acc-item {
+ border: 1px solid var(--hair);
+ border-radius: var(--radius);
+ background: var(--surface);
+ overflow: hidden;
+ transition: border-color var(--dur) var(--ease),
+ background var(--dur) var(--ease);
+}
+.acc-item.open {
+ border-color: color-mix(in srgb, var(--primary) 38%, transparent);
+ background: var(--surface-2);
+}
+.acc-head {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ width: 100%;
+ padding: clamp(16px, 1.8vw, 22px) clamp(18px, 2vw, 26px);
+ background: transparent;
+ border: none;
+ cursor: pointer;
+ text-align: left;
+ font-family: var(--font-body);
+ color: var(--fg);
+}
+.acc-index {
+ font-family: var(--font-mono);
+ font-size: 13px;
+ font-weight: 600;
+ letter-spacing: 0.04em;
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ flex-shrink: 0;
+}
+.acc-head h3 {
+ flex: 1;
+ font-size: clamp(17px, 1.9vw, 21px);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--fg);
+ margin: 0;
+}
+.acc-chevron {
+ display: grid;
+ place-items: center;
+ color: var(--fg-faint);
+ transition: transform var(--dur) var(--ease), color var(--dur) var(--ease);
+ flex-shrink: 0;
+}
+.acc-item.open .acc-chevron {
+ transform: rotate(180deg);
+ color: var(--primary);
+}
+.acc-chevron svg {
+ width: 18px;
+ height: 18px;
+}
+.acc-body {
+ overflow: hidden;
+}
+.acc-body-inner {
+ padding: 0 clamp(18px, 2vw, 26px) clamp(18px, 2vw, 24px)
+ clamp(44px, 5vw, 58px);
+ font-size: clamp(14px, 1.5vw, 17px);
+ color: var(--fg-muted);
+ line-height: 1.5;
+ max-width: 62ch;
+}
/* ═══ State-of-the-art sections ═════════════════════════════════════ */
/* Comparison (us vs them) */
-.cmp { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius); overflow: hidden; background: var(--surface); }
-.cmp-row { display: grid; grid-template-columns: 1.6fr repeat(var(--vcols, 2), 1fr); align-items: center; }
-.cmp-row + .cmp-row { border-top: 1px solid var(--hair-2); }
-.cmp-cell { padding: clamp(12px,1.5vw,18px) clamp(14px,1.6vw,22px); font-size: clamp(14px,1.5vw,17px); color: var(--fg-muted); display: flex; align-items: center; }
-.cmp-cell:not(.cmp-label):not(.cmp-h) { justify-content: center; }
-.cmp-head { background: var(--surface-2); }
-.cmp-h { font-weight: 600; color: var(--fg); }
-.cmp-label { color: var(--fg); font-weight: 500; }
-.cmp-cell.hl { background: color-mix(in srgb, var(--primary) 8%, transparent); }
-.cmp-h.hl { color: transparent; background: var(--accent); -webkit-background-clip: text; background-clip: text; }
-.cmp-ic { width: 22px; height: 22px; }
-.cmp-ic.ok { color: var(--primary); }
-.cmp-ic.no { color: var(--fg-faint); opacity: 0.55; }
-.cmp-val { font-feature-settings: "tnum" 1; }
+.cmp {
+ width: 100%;
+ border: 1px solid var(--hair);
+ border-radius: var(--radius);
+ overflow: hidden;
+ background: var(--surface);
+}
+.cmp-row {
+ display: grid;
+ grid-template-columns: 1.6fr repeat(var(--vcols, 2), 1fr);
+ align-items: center;
+}
+.cmp-row + .cmp-row {
+ border-top: 1px solid var(--hair-2);
+}
+.cmp-cell {
+ padding: clamp(12px, 1.5vw, 18px) clamp(14px, 1.6vw, 22px);
+ font-size: clamp(14px, 1.5vw, 17px);
+ color: var(--fg-muted);
+ display: flex;
+ align-items: center;
+}
+.cmp-cell:not(.cmp-label):not(.cmp-h) {
+ justify-content: center;
+}
+.cmp-head {
+ background: var(--surface-2);
+}
+.cmp-h {
+ font-weight: 600;
+ color: var(--fg);
+}
+.cmp-label {
+ color: var(--fg);
+ font-weight: 500;
+}
+.cmp-cell.hl {
+ background: color-mix(in srgb, var(--primary) 8%, transparent);
+}
+.cmp-h.hl {
+ color: transparent;
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+}
+.cmp-ic {
+ width: 22px;
+ height: 22px;
+}
+.cmp-ic.ok {
+ color: var(--primary);
+}
+.cmp-ic.no {
+ color: var(--fg-faint);
+ opacity: 0.55;
+}
+.cmp-val {
+ font-feature-settings: 'tnum' 1;
+}
/* Tabs (sliding accent pill) */
-.tabs { width: 100%; }
-.tabs-bar { display: inline-flex; gap: 4px; padding: 5px; border-radius: 999px; background: var(--surface); border: 1px solid var(--hair); }
-.tab { position: relative; border: none; background: transparent; cursor: pointer; padding: 9px 18px; border-radius: 999px; font-family: var(--font-body); font-size: 15px; font-weight: 600; color: var(--fg-muted); transition: color var(--dur) var(--ease); }
-.tab.on { color: var(--accent-ink); }
-.tab-pill { position: absolute; inset: 0; border-radius: 999px; background: var(--accent); z-index: 0; }
-.tab-label { position: relative; z-index: 1; }
-.tabs-panel { margin-top: 22px; }
+.tabs {
+ width: 100%;
+}
+.tabs-bar {
+ display: inline-flex;
+ gap: 4px;
+ padding: 5px;
+ border-radius: 999px;
+ background: var(--surface);
+ border: 1px solid var(--hair);
+}
+.tab {
+ position: relative;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ padding: 9px 18px;
+ border-radius: 999px;
+ font-family: var(--font-body);
+ font-size: 15px;
+ font-weight: 600;
+ color: var(--fg-muted);
+ transition: color var(--dur) var(--ease);
+}
+.tab.on {
+ color: var(--accent-ink);
+}
+.tab-pill {
+ position: absolute;
+ inset: 0;
+ border-radius: 999px;
+ background: var(--accent);
+ z-index: 0;
+}
+.tab-label {
+ position: relative;
+ z-index: 1;
+}
+.tabs-panel {
+ margin-top: 22px;
+}
/* Timeline */
-.tl { position: relative; }
-.tl-line { position: absolute; left: 11px; top: 8px; bottom: 8px; width: 2px; background: var(--hair); border-radius: 2px; overflow: hidden; }
-.tl-line-fill { position: absolute; inset: 0; background: var(--accent); transform-origin: top; }
-.tl-items { display: flex; flex-direction: column; gap: clamp(20px,3vh,34px); }
-.tl-item { position: relative; display: flex; gap: 22px; padding-left: 6px; }
-.tl-dot { position: relative; z-index: 1; flex-shrink: 0; width: 12px; height: 12px; margin-top: 5px; border-radius: 50%; background: var(--primary); box-shadow: 0 0 0 4px var(--bg), var(--glow); }
-.tl-time { font-size: 12.5px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; background: var(--accent); -webkit-background-clip: text; background-clip: text; color: transparent; margin-bottom: 4px; }
-.tl-content h3 { font-size: clamp(18px,2vw,23px); font-weight: 600; letter-spacing: -0.02em; margin: 0 0 5px; }
-.tl-content p { font-size: clamp(14px,1.5vw,16px); color: var(--fg-muted); line-height: 1.5; max-width: 52ch; margin: 0; }
+.tl {
+ position: relative;
+}
+.tl-line {
+ position: absolute;
+ left: 11px;
+ top: 8px;
+ bottom: 8px;
+ width: 2px;
+ background: var(--hair);
+ border-radius: 2px;
+ overflow: hidden;
+}
+.tl-line-fill {
+ position: absolute;
+ inset: 0;
+ background: var(--accent);
+ transform-origin: top;
+}
+.tl-items {
+ display: flex;
+ flex-direction: column;
+ gap: clamp(20px, 3vh, 34px);
+}
+.tl-item {
+ position: relative;
+ display: flex;
+ gap: 22px;
+ padding-left: 6px;
+}
+.tl-dot {
+ position: relative;
+ z-index: 1;
+ flex-shrink: 0;
+ width: 12px;
+ height: 12px;
+ margin-top: 5px;
+ border-radius: 50%;
+ background: var(--primary);
+ box-shadow: 0 0 0 4px var(--bg), var(--glow);
+}
+.tl-time {
+ font-size: 12.5px;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ background: var(--accent);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+ margin-bottom: 4px;
+}
+.tl-content h3 {
+ font-size: clamp(18px, 2vw, 23px);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ margin: 0 0 5px;
+}
+.tl-content p {
+ font-size: clamp(14px, 1.5vw, 16px);
+ color: var(--fg-muted);
+ line-height: 1.5;
+ max-width: 52ch;
+ margin: 0;
+}
/* Code window */
-.cw { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius); overflow: hidden; background: var(--bg); box-shadow: var(--shadow); }
-.cw-bar { display: flex; align-items: center; gap: 12px; padding: 11px 16px; background: var(--surface-2); border-bottom: 1px solid var(--hair-2); }
-.cw-dots { display: flex; gap: 7px; }
-.cw-dots i { width: 11px; height: 11px; border-radius: 50%; background: var(--hair); }
-.cw-title { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-faint); }
-.cw-body { margin: 0; padding: 16px 0; overflow-x: auto; }
-.cw-body code { font-family: var(--font-mono); font-size: clamp(12px,1.3vw,14px); line-height: 1.65; }
-.cw-line { display: flex; padding: 0 16px; white-space: pre; }
-.cw-line.hl { background: color-mix(in srgb, var(--primary) 10%, transparent); box-shadow: inset 2px 0 0 var(--primary); }
-.cw-no { width: 30px; flex-shrink: 0; color: var(--fg-faint); opacity: 0.5; user-select: none; text-align: right; padding-right: 18px; }
-.cw-code { color: var(--fg); }
-.cw-kw { color: var(--primary); }
-.cw-str { color: color-mix(in srgb, var(--primary) 50%, var(--fg-muted)); }
-.cw-com { color: var(--fg-faint); font-style: italic; }
+.cw {
+ width: 100%;
+ border: 1px solid var(--hair);
+ border-radius: var(--radius);
+ overflow: hidden;
+ background: var(--bg);
+ box-shadow: var(--shadow);
+}
+.cw-bar {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 11px 16px;
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--hair-2);
+}
+.cw-dots {
+ display: flex;
+ gap: 7px;
+}
+.cw-dots i {
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ background: var(--hair);
+}
+.cw-title {
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ color: var(--fg-faint);
+}
+.cw-body {
+ margin: 0;
+ padding: 16px 0;
+ overflow-x: auto;
+}
+.cw-body code {
+ font-family: var(--font-mono);
+ font-size: clamp(12px, 1.3vw, 14px);
+ line-height: 1.65;
+}
+.cw-line {
+ display: flex;
+ padding: 0 16px;
+ white-space: pre;
+}
+.cw-line.hl {
+ background: color-mix(in srgb, var(--primary) 10%, transparent);
+ box-shadow: inset 2px 0 0 var(--primary);
+}
+.cw-no {
+ width: 30px;
+ flex-shrink: 0;
+ color: var(--fg-faint);
+ opacity: 0.5;
+ user-select: none;
+ text-align: right;
+ padding-right: 18px;
+}
+.cw-code {
+ color: var(--fg);
+}
+.cw-kw {
+ color: var(--primary);
+}
+.cw-str {
+ color: color-mix(in srgb, var(--primary) 50%, var(--fg-muted));
+}
+.cw-com {
+ color: var(--fg-faint);
+ font-style: italic;
+}
/* Browser frame */
-.bf { width: 100%; border: 1px solid var(--hair); border-radius: var(--radius-lg); overflow: hidden; background: var(--surface); box-shadow: var(--shadow); }
-.bf-bar { display: flex; align-items: center; gap: 14px; padding: 11px 16px; background: var(--surface-2); border-bottom: 1px solid var(--hair-2); }
-.bf-dots { display: flex; gap: 7px; }
-.bf-dots i { width: 11px; height: 11px; border-radius: 50%; background: var(--hair); }
-.bf-url { flex: 1; font-family: var(--font-mono); font-size: 12.5px; color: var(--fg-faint); background: var(--bg); border: 1px solid var(--hair-2); border-radius: 999px; padding: 6px 14px; text-align: center; }
-.bf-body { background: var(--bg); }
-.bf-body img { display: block; width: 100%; }
+.bf {
+ width: 100%;
+ border: 1px solid var(--hair);
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ background: var(--surface);
+ box-shadow: var(--shadow);
+}
+.bf-bar {
+ display: flex;
+ align-items: center;
+ gap: 14px;
+ padding: 11px 16px;
+ background: var(--surface-2);
+ border-bottom: 1px solid var(--hair-2);
+}
+.bf-dots {
+ display: flex;
+ gap: 7px;
+}
+.bf-dots i {
+ width: 11px;
+ height: 11px;
+ border-radius: 50%;
+ background: var(--hair);
+}
+.bf-url {
+ flex: 1;
+ font-family: var(--font-mono);
+ font-size: 12.5px;
+ color: var(--fg-faint);
+ background: var(--bg);
+ border: 1px solid var(--hair-2);
+ border-radius: 999px;
+ padding: 6px 14px;
+ text-align: center;
+}
+.bf-body {
+ background: var(--bg);
+}
+.bf-body img {
+ display: block;
+ width: 100%;
+}
/* Charts */
-.ch-bars { display: flex; align-items: flex-end; gap: clamp(8px,1.4vw,16px); width: 100%; }
-.ch-col { flex: 1; display: flex; flex-direction: column; align-items: center; gap: 9px; height: 100%; }
-.ch-bar-track { width: 100%; flex: 1; display: flex; align-items: flex-end; }
-.ch-bar { width: 100%; border-radius: 8px 8px 4px 4px; background: var(--accent); display: block; }
-.ch-x { font-size: 11.5px; color: var(--fg-faint); }
-.ch-line { width: 100%; display: block; }
-.ch-donut { width: var(--ch-size, 168px); display: flex; flex-direction: column; align-items: center; gap: 12px; }
-.ch-donut svg { width: 100%; height: auto; display: block; }
-.ch-donut-val { fill: var(--fg); font-family: var(--font-head); font-size: 30px; font-weight: 600; }
-.ch-donut-label { font-size: 14px; color: var(--fg-muted); font-weight: 500; }
+.ch-bars {
+ display: flex;
+ align-items: flex-end;
+ gap: clamp(8px, 1.4vw, 16px);
+ width: 100%;
+}
+.ch-col {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 9px;
+ height: 100%;
+}
+.ch-bar-track {
+ width: 100%;
+ flex: 1;
+ display: flex;
+ align-items: flex-end;
+}
+.ch-bar {
+ width: 100%;
+ border-radius: 8px 8px 4px 4px;
+ background: var(--accent);
+ display: block;
+}
+.ch-x {
+ font-size: 11.5px;
+ color: var(--fg-faint);
+}
+.ch-line {
+ width: 100%;
+ display: block;
+}
+.ch-donut {
+ width: var(--ch-size, 168px);
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 12px;
+}
+.ch-donut svg {
+ width: 100%;
+ height: auto;
+ display: block;
+}
+.ch-donut-val {
+ fill: var(--fg);
+ font-family: var(--font-head);
+ font-size: 30px;
+ font-weight: 600;
+}
+.ch-donut-label {
+ font-size: 14px;
+ color: var(--fg-muted);
+ font-weight: 500;
+}
/* Spotlight card (cursor-follow glow) */
-.spot { position: relative; border: 1px solid var(--hair); border-radius: var(--radius-lg); background: var(--surface); padding: clamp(24px,2.6vw,36px); overflow: hidden; transition: border-color var(--dur) var(--ease); }
-.spot::before { content: ""; position: absolute; inset: 0; pointer-events: none; opacity: 0; transition: opacity 0.3s var(--ease); background: radial-gradient(240px circle at var(--mx, 50%) var(--my, 50%), color-mix(in srgb, var(--primary) 20%, transparent), transparent 72%); }
-.spot:hover { border-color: color-mix(in srgb, var(--primary) 35%, transparent); }
-.spot:hover::before { opacity: 1; }
-.spot > * { position: relative; z-index: 1; }
+.spot {
+ position: relative;
+ border: 1px solid var(--hair);
+ border-radius: var(--radius-lg);
+ background: var(--surface);
+ padding: clamp(24px, 2.6vw, 36px);
+ overflow: hidden;
+ transition: border-color var(--dur) var(--ease);
+}
+.spot::before {
+ content: '';
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ opacity: 0;
+ transition: opacity 0.3s var(--ease);
+ background: radial-gradient(
+ 240px circle at var(--mx, 50%) var(--my, 50%),
+ color-mix(in srgb, var(--primary) 20%, transparent),
+ transparent 72%
+ );
+}
+.spot:hover {
+ border-color: color-mix(in srgb, var(--primary) 35%, transparent);
+}
+.spot:hover::before {
+ opacity: 1;
+}
+.spot > * {
+ position: relative;
+ z-index: 1;
+}
/* ── Hover helpers ──────────────────────────────────────────────── */
-.lift { transition: transform var(--dur) var(--ease), box-shadow var(--dur) var(--ease); }
-.lift:hover { transform: translateY(-6px); box-shadow: var(--shadow), var(--glow); }
+.lift {
+ transition: transform var(--dur) var(--ease),
+ box-shadow var(--dur) var(--ease);
+}
+.lift:hover {
+ transform: translateY(-6px);
+ box-shadow: var(--shadow), var(--glow);
+}
/* ── Responsive collapse ────────────────────────────────────────── */
@media (max-width: 900px) {
- .bento { grid-template-columns: repeat(2, 1fr); grid-auto-rows: auto; }
- .btile { grid-column: 1 / -1; grid-row: auto; min-height: clamp(140px, 24vh, 200px); }
- .split { grid-template-columns: 1fr; grid-template-rows: 1fr 1fr; }
- .split.flip .split-body { order: 0; }
- .split-media { order: 2; }
+ .bento {
+ grid-template-columns: repeat(2, 1fr);
+ grid-auto-rows: auto;
+ }
+ .btile {
+ grid-column: 1 / -1;
+ grid-row: auto;
+ min-height: clamp(140px, 24vh, 200px);
+ }
+ .split {
+ grid-template-columns: 1fr;
+ grid-template-rows: 1fr 1fr;
+ }
+ .split.flip .split-body {
+ order: 0;
+ }
+ .split-media {
+ order: 2;
+ }
}
/* ═══════════════════════════════════════════════════════════════════
@@ -215,161 +855,660 @@ strong { font-weight: 600; color: var(--fg); }
rail, ported to React. This is the UI to keep; restyle only via tokens.
═══════════════════════════════════════════════════════════════════ */
.noir-dock {
- position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 90;
- opacity: 0.35; transition: opacity 0.35s ease; font-family: var(--font-body);
+ position: fixed;
+ bottom: 22px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 90;
+ opacity: 0.35;
+ transition: opacity 0.35s ease;
+ font-family: var(--font-body);
+}
+.noir-dock:hover {
+ opacity: 1;
}
-.noir-dock:hover { opacity: 1; }
.noir-bar {
- display: flex; align-items: center; gap: 4px; padding: 7px 9px; border-radius: 999px;
- background: rgba(28, 28, 30, 0.66); border: 1px solid rgba(255, 255, 255, 0.10);
- backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 7px 9px;
+ border-radius: 999px;
+ background: rgba(28, 28, 30, 0.66);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(22px) saturate(180%);
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8);
}
.noir-icon-btn {
- width: 38px; height: 38px; display: grid; place-items: center; border: none; border-radius: 999px;
- background: transparent; color: #e8e8ea; font-size: 19px; cursor: pointer;
+ width: 38px;
+ height: 38px;
+ display: grid;
+ place-items: center;
+ border: none;
+ border-radius: 999px;
+ background: transparent;
+ color: #e8e8ea;
+ font-size: 19px;
+ cursor: pointer;
transition: background 0.2s ease, color 0.2s ease, opacity 0.2s ease;
}
-.noir-icon-btn:hover { background: rgba(255, 255, 255, 0.12); }
-.noir-icon-btn:disabled { opacity: 0.28; cursor: default; }
-.noir-icon-btn:disabled:hover { background: transparent; }
-.noir-icon-btn.on { color: var(--accent-ink, #04140e); background: var(--accent, linear-gradient(115deg, #c8f56e, #34dcd8)); }
-.noir-icon-btn.sm { width: 30px; height: 30px; font-size: 16px; }
-.noir-icon-btn svg { width: 1em; height: 1em; }
-.noir-sep { width: 1px; height: 22px; background: rgba(255, 255, 255, 0.12); margin: 0 4px; }
-.noir-counter { display: flex; align-items: baseline; gap: 5px; padding: 0 12px; font-feature-settings: "tnum" 1; user-select: none; }
-.noir-counter-now { font-size: 16px; font-weight: 600; color: #f5f5f7; }
-.noir-counter-tot { font-size: 13px; color: #8e8e93; }
+.noir-icon-btn:hover {
+ background: rgba(255, 255, 255, 0.12);
+}
+.noir-icon-btn:disabled {
+ opacity: 0.28;
+ cursor: default;
+}
+.noir-icon-btn:disabled:hover {
+ background: transparent;
+}
+.noir-icon-btn.on {
+ color: var(--accent-ink, #04140e);
+ background: var(--accent, linear-gradient(115deg, #c8f56e, #34dcd8));
+}
+.noir-icon-btn.sm {
+ width: 30px;
+ height: 30px;
+ font-size: 16px;
+}
+.noir-icon-btn svg {
+ width: 1em;
+ height: 1em;
+}
+.noir-sep {
+ width: 1px;
+ height: 22px;
+ background: rgba(255, 255, 255, 0.12);
+ margin: 0 4px;
+}
+.noir-counter {
+ display: flex;
+ align-items: baseline;
+ gap: 5px;
+ padding: 0 12px;
+ font-feature-settings: 'tnum' 1;
+ user-select: none;
+}
+.noir-counter-now {
+ font-size: 16px;
+ font-weight: 600;
+ color: #f5f5f7;
+}
+.noir-counter-tot {
+ font-size: 13px;
+ color: #8e8e93;
+}
/* Left thumbnail rail */
.noir-rail {
- position: fixed; top: 0; left: 0; bottom: 0; width: 248px; z-index: 95; padding: 18px 14px 26px;
- overflow-y: auto; background: rgba(18, 18, 20, 0.82); border-right: 1px solid rgba(255, 255, 255, 0.08);
- backdrop-filter: blur(26px) saturate(180%); -webkit-backdrop-filter: blur(26px) saturate(180%);
- font-family: var(--font-body); transform: translateX(-100%); transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
-}
-.noir-rail.open { transform: translateX(0); }
-.noir-rail-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 6px 14px; }
-.noir-rail-title { font-size: 12px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #8e8e93; }
-.noir-rail-list { display: flex; flex-direction: column; gap: 12px; }
-.noir-thumb { display: flex; align-items: center; gap: 10px; padding: 0; border: none; background: transparent; cursor: pointer; text-align: left; }
-.noir-thumb-no { width: 18px; font-size: 12px; font-feature-settings: "tnum" 1; color: #6e6e73; flex-shrink: 0; }
-.noir-thumb-frame { flex: 1; border-radius: 9px; overflow: hidden; border: 1.5px solid transparent; transition: border-color 0.25s ease; line-height: 0; aspect-ratio: 16 / 9; position: relative; background: var(--bg); }
-.noir-thumb:hover .noir-thumb-frame { border-color: rgba(255, 255, 255, 0.22); }
-.noir-thumb.active .noir-thumb-no { color: var(--primary); }
-.noir-thumb.active .noir-thumb-frame { border-color: var(--primary); }
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 248px;
+ z-index: 95;
+ padding: 18px 14px 26px;
+ overflow-y: auto;
+ background: rgba(18, 18, 20, 0.82);
+ border-right: 1px solid rgba(255, 255, 255, 0.08);
+ backdrop-filter: blur(26px) saturate(180%);
+ -webkit-backdrop-filter: blur(26px) saturate(180%);
+ font-family: var(--font-body);
+ transform: translateX(-100%);
+ transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1);
+}
+.noir-rail.open {
+ transform: translateX(0);
+}
+.noir-rail-head {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: 4px 6px 14px;
+}
+.noir-rail-title {
+ font-size: 12px;
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: #8e8e93;
+}
+.noir-rail-list {
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+}
+.noir-thumb {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ text-align: left;
+}
+.noir-thumb-no {
+ width: 18px;
+ font-size: 12px;
+ font-feature-settings: 'tnum' 1;
+ color: #6e6e73;
+ flex-shrink: 0;
+}
+.noir-thumb-frame {
+ flex: 1;
+ border-radius: 9px;
+ overflow: hidden;
+ border: 1.5px solid transparent;
+ transition: border-color 0.25s ease;
+ line-height: 0;
+ aspect-ratio: 16 / 9;
+ position: relative;
+ background: var(--bg);
+}
+.noir-thumb:hover .noir-thumb-frame {
+ border-color: rgba(255, 255, 255, 0.22);
+}
+.noir-thumb.active .noir-thumb-no {
+ color: var(--primary);
+}
+.noir-thumb.active .noir-thumb-frame {
+ border-color: var(--primary);
+}
/* the live slide rendered at true viewport size, then scaled into the frame
(so responsive vw/vh units stay faithful). width/height/transform set inline. */
-.noir-thumb-scale { position: absolute; top: 0; left: 0; transform-origin: top left; pointer-events: none; }
-.noir-rail::-webkit-scrollbar { width: 0; }
-.noir-rail { scrollbar-width: none; }
+.noir-thumb-scale {
+ position: absolute;
+ top: 0;
+ left: 0;
+ transform-origin: top left;
+ pointer-events: none;
+}
+.noir-rail::-webkit-scrollbar {
+ width: 0;
+}
+.noir-rail {
+ scrollbar-width: none;
+}
/* Presenter overlay (notes + next + timer) */
.noir-presenter {
- position: fixed; right: 18px; bottom: 78px; z-index: 92; width: clamp(260px, 26vw, 360px);
- background: rgba(18, 18, 20, 0.9); border: 1px solid rgba(255,255,255,0.1); border-radius: 16px;
- backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
- box-shadow: 0 18px 50px -20px rgba(0,0,0,0.8); padding: 16px; font-family: var(--font-body); color: #e8e8ea;
-}
-.noir-presenter-row { display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; }
-.noir-presenter-label { font-size: 11px; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; color: #8e8e93; }
-.noir-presenter-timer { font-size: 13px; font-feature-settings: "tnum" 1; color: var(--primary); }
-.noir-presenter-next { border-radius: 9px; overflow: hidden; border: 1px solid rgba(255,255,255,0.08); aspect-ratio: 16/9; position: relative; background: var(--bg); margin-bottom: 12px; }
-.noir-presenter-notes { width: 100%; min-height: 84px; max-height: 32vh; font-size: 13.5px; line-height: 1.5; color: #c7c7cc; background: transparent; border: none; resize: none; outline: none; font-family: var(--font-body); }
-.noir-presenter-notes:focus { color: #f0f0f2; }
-.noir-presenter-notes::placeholder { color: #6e6e73; }
-.noir-presenter-hint { margin-top: 8px; font-size: 11px; color: #6e6e73; }
+ position: fixed;
+ right: 18px;
+ bottom: 78px;
+ z-index: 92;
+ width: clamp(260px, 26vw, 360px);
+ background: rgba(18, 18, 20, 0.9);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ border-radius: 16px;
+ backdrop-filter: blur(22px) saturate(180%);
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
+ box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8);
+ padding: 16px;
+ font-family: var(--font-body);
+ color: #e8e8ea;
+}
+.noir-presenter-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ margin-bottom: 12px;
+}
+.noir-presenter-label {
+ font-size: 11px;
+ font-weight: 600;
+ letter-spacing: 0.14em;
+ text-transform: uppercase;
+ color: #8e8e93;
+}
+.noir-presenter-timer {
+ font-size: 13px;
+ font-feature-settings: 'tnum' 1;
+ color: var(--primary);
+}
+.noir-presenter-next {
+ border-radius: 9px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ aspect-ratio: 16/9;
+ position: relative;
+ background: var(--bg);
+ margin-bottom: 12px;
+}
+.noir-presenter-notes {
+ width: 100%;
+ min-height: 84px;
+ max-height: 32vh;
+ font-size: 13.5px;
+ line-height: 1.5;
+ color: #c7c7cc;
+ background: transparent;
+ border: none;
+ resize: none;
+ outline: none;
+ font-family: var(--font-body);
+}
+.noir-presenter-notes:focus {
+ color: #f0f0f2;
+}
+.noir-presenter-notes::placeholder {
+ color: #6e6e73;
+}
+.noir-presenter-hint {
+ margin-top: 8px;
+ font-size: 11px;
+ color: #6e6e73;
+}
/* Annotation overlay + tool bar */
-.ann-canvas { position: fixed; inset: 0; z-index: 88; touch-action: none; }
+.ann-canvas {
+ position: fixed;
+ inset: 0;
+ z-index: 88;
+ touch-action: none;
+}
.ann-bar {
- position: fixed; bottom: 78px; left: 50%; transform: translateX(-50%); z-index: 91;
- display: flex; align-items: center; gap: 4px; padding: 7px 9px; border-radius: 999px; flex-wrap: wrap; max-width: 94vw; justify-content: center;
- background: rgba(28, 28, 30, 0.72); border: 1px solid rgba(255, 255, 255, 0.10);
- backdrop-filter: blur(22px) saturate(180%); -webkit-backdrop-filter: blur(22px) saturate(180%);
- box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8); font-family: var(--font-body);
-}
-.ann-btn { width: 34px; height: 34px; display: grid; place-items: center; border: none; border-radius: 999px; background: transparent; color: #e8e8ea; cursor: pointer; transition: background 0.2s ease, color 0.2s ease; }
-.ann-btn svg { width: 19px; height: 19px; }
-.ann-btn:hover { background: rgba(255, 255, 255, 0.12); }
-.ann-btn.on { color: var(--accent-ink); background: var(--accent); }
-.ann-sep { width: 1px; height: 22px; background: rgba(255, 255, 255, 0.12); margin: 0 4px; }
-.ann-color { width: 22px; height: 22px; border-radius: 50%; border: 2px solid transparent; cursor: pointer; padding: 0; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); }
-.ann-color.on { border-color: #fff; transform: scale(1.08); }
-.ann-size { width: 30px; height: 34px; display: grid; place-items: center; border: none; background: transparent; cursor: pointer; border-radius: 10px; }
-.ann-size span { display: block; border-radius: 50%; background: #e8e8ea; }
-.ann-size.on { background: rgba(255, 255, 255, 0.12); }
-.ann-size.on span { background: var(--primary); }
+ position: fixed;
+ bottom: 78px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 91;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 7px 9px;
+ border-radius: 999px;
+ flex-wrap: wrap;
+ max-width: 94vw;
+ justify-content: center;
+ background: rgba(28, 28, 30, 0.72);
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ backdrop-filter: blur(22px) saturate(180%);
+ -webkit-backdrop-filter: blur(22px) saturate(180%);
+ box-shadow: 0 18px 50px -20px rgba(0, 0, 0, 0.8);
+ font-family: var(--font-body);
+}
+.ann-btn {
+ width: 34px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border: none;
+ border-radius: 999px;
+ background: transparent;
+ color: #e8e8ea;
+ cursor: pointer;
+ transition: background 0.2s ease, color 0.2s ease;
+}
+.ann-btn svg {
+ width: 19px;
+ height: 19px;
+}
+.ann-btn:hover {
+ background: rgba(255, 255, 255, 0.12);
+}
+.ann-btn.on {
+ color: var(--accent-ink);
+ background: var(--accent);
+}
+.ann-sep {
+ width: 1px;
+ height: 22px;
+ background: rgba(255, 255, 255, 0.12);
+ margin: 0 4px;
+}
+.ann-color {
+ width: 22px;
+ height: 22px;
+ border-radius: 50%;
+ border: 2px solid transparent;
+ cursor: pointer;
+ padding: 0;
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
+}
+.ann-color.on {
+ border-color: #fff;
+ transform: scale(1.08);
+}
+.ann-size {
+ width: 30px;
+ height: 34px;
+ display: grid;
+ place-items: center;
+ border: none;
+ background: transparent;
+ cursor: pointer;
+ border-radius: 10px;
+}
+.ann-size span {
+ display: block;
+ border-radius: 50%;
+ background: #e8e8ea;
+}
+.ann-size.on {
+ background: rgba(255, 255, 255, 0.12);
+}
+.ann-size.on span {
+ background: var(--primary);
+}
/* Tooltips for chrome controls (instant, styled) */
-[data-tip] { position: relative; }
+[data-tip] {
+ position: relative;
+}
[data-tip]::after {
- content: attr(data-tip); position: absolute; bottom: calc(100% + 9px); left: 50%; transform: translateX(-50%) translateY(4px);
- padding: 5px 9px; border-radius: 7px; white-space: nowrap; font-size: 11.5px; font-weight: 500; font-family: var(--font-body);
- background: rgba(20, 20, 22, 0.96); color: #f5f5f7; border: 1px solid rgba(255, 255, 255, 0.10);
- box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.7); opacity: 0; pointer-events: none;
- transition: opacity 0.15s ease, transform 0.15s ease; z-index: 100;
+ content: attr(data-tip);
+ position: absolute;
+ bottom: calc(100% + 9px);
+ left: 50%;
+ transform: translateX(-50%) translateY(4px);
+ padding: 5px 9px;
+ border-radius: 7px;
+ white-space: nowrap;
+ font-size: 11.5px;
+ font-weight: 500;
+ font-family: var(--font-body);
+ background: rgba(20, 20, 22, 0.96);
+ color: #f5f5f7;
+ border: 1px solid rgba(255, 255, 255, 0.1);
+ box-shadow: 0 8px 24px -8px rgba(0, 0, 0, 0.7);
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.15s ease, transform 0.15s ease;
+ z-index: 100;
+}
+[data-tip]:hover::after {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
}
-[data-tip]:hover::after { opacity: 1; transform: translateX(-50%) translateY(0); }
/* Hide / dim the UI (H key, or auto on idle in fullscreen) */
-.noir-dock.hidden { opacity: 0 !important; pointer-events: none; transform: translateX(-50%) translateY(18px); }
-.deck.nocursor, .deck.nocursor * { cursor: none !important; }
+.noir-dock.hidden {
+ opacity: 0 !important;
+ pointer-events: none;
+ transform: translateX(-50%) translateY(18px);
+}
+.deck.nocursor,
+.deck.nocursor * {
+ cursor: none !important;
+}
/* ── TiltCard / Marquee / visual frame (shared with the other skills) ─ */
-.tilt { position: relative; display: inline-block; transition: transform 0.5s var(--ease); transform-style: preserve-3d; will-change: transform; }
-.tilt-glare { position: absolute; inset: 0; pointer-events: none; border-radius: var(--radius-lg); background: radial-gradient(60% 60% at var(--gx,50%) var(--gy,50%), rgba(255,255,255,0.16), transparent 60%); opacity: 0; transition: opacity 0.4s var(--ease); }
-.tilt:hover .tilt-glare { opacity: 1; }
-.marquee { width: 100%; overflow: hidden; -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
-.marquee-track { display: flex; gap: 56px; width: max-content; animation: marquee linear infinite; }
-.marquee-item { font-size: clamp(20px, 2.4vw, 28px); font-weight: 600; letter-spacing: -0.02em; white-space: nowrap; color: var(--fg-muted); opacity: 0.78; }
-@keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
-.vframe { padding: 24px; border-radius: var(--radius-lg); background: linear-gradient(180deg, var(--surface-2), var(--surface)); border: 1px solid var(--hair); box-shadow: var(--shadow); display: flex; flex-direction: column; gap: 16px; font-family: var(--font-body); }
-.vtitle { font-size: 18px; font-weight: 600; letter-spacing: -0.02em; color: var(--fg); }
-.vmeta { font-size: 12.5px; color: var(--fg-faint); }
-.ddot { width: 9px; height: 9px; border-radius: 3px; background: var(--accent); }
-.dlive { margin-left: auto; display: flex; align-items: center; gap: 7px; font-size: 12px; color: var(--primary); padding: 5px 11px; border-radius: 999px; background: color-mix(in srgb, var(--primary) 10%, transparent); border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent); }
-.dlive span { width: 6px; height: 6px; border-radius: 50%; background: var(--primary); animation: pulse 1.6s ease-in-out infinite; }
-@keyframes pulse { 0%,100% { opacity: 0.35; } 50% { opacity: 1; } }
-.dkpis { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
-.dkpi { background: var(--surface); border: 1px solid var(--hair-2); border-radius: var(--radius-sm); padding: 14px 15px; }
-.dkpi-l { font-size: 12.5px; color: var(--fg-faint); }
-.dkpi-v { font-size: 25px; font-weight: 600; letter-spacing: -0.03em; color: var(--fg); margin: 4px 0 5px; font-feature-settings: "tnum" 1; }
-.dkpi-d { font-size: 12px; font-weight: 600; color: var(--primary); }
-.dbars { display: grid; grid-template-columns: repeat(7, 1fr); gap: 10px; align-items: end; }
-.dbar { display: flex; flex-direction: column; align-items: center; gap: 7px; }
-.dbar-track { width: 100%; height: 44px; border-radius: 7px; background: var(--hair-2); display: flex; align-items: flex-end; overflow: hidden; }
-.dbar-track span { width: 100%; border-radius: 7px; background: var(--accent); display: block; transform: scaleY(0); transform-origin: bottom; }
-.dbar-k { font-size: 10.5px; color: var(--fg-faint); }
-.dline { stroke-dasharray: 1; stroke-dashoffset: 1; }
-.dfill-area { opacity: 0; transition: opacity 0.8s var(--ease) 0.5s; }
-.vframe.shown .dline { animation: dash 1.5s var(--ease) 0.15s forwards; }
-.vframe.shown .dfill-area { opacity: 1; }
-.vframe.shown .dbar-track span { animation: bargrow 0.8s var(--ease-spring) both; }
-@keyframes dash { to { stroke-dashoffset: 0; } }
-@keyframes bargrow { from { transform: scaleY(0); } to { transform: scaleY(1); } }
+.tilt {
+ position: relative;
+ display: inline-block;
+ transition: transform 0.5s var(--ease);
+ transform-style: preserve-3d;
+ will-change: transform;
+}
+.tilt-glare {
+ position: absolute;
+ inset: 0;
+ pointer-events: none;
+ border-radius: var(--radius-lg);
+ background: radial-gradient(
+ 60% 60% at var(--gx, 50%) var(--gy, 50%),
+ rgba(255, 255, 255, 0.16),
+ transparent 60%
+ );
+ opacity: 0;
+ transition: opacity 0.4s var(--ease);
+}
+.tilt:hover .tilt-glare {
+ opacity: 1;
+}
+.marquee {
+ width: 100%;
+ overflow: hidden;
+ -webkit-mask-image: linear-gradient(
+ 90deg,
+ transparent,
+ #000 8%,
+ #000 92%,
+ transparent
+ );
+ mask-image: linear-gradient(
+ 90deg,
+ transparent,
+ #000 8%,
+ #000 92%,
+ transparent
+ );
+}
+.marquee-track {
+ display: flex;
+ gap: 56px;
+ width: max-content;
+ animation: marquee linear infinite;
+}
+.marquee-item {
+ font-size: clamp(20px, 2.4vw, 28px);
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ white-space: nowrap;
+ color: var(--fg-muted);
+ opacity: 0.78;
+}
+@keyframes marquee {
+ from {
+ transform: translateX(0);
+ }
+ to {
+ transform: translateX(-50%);
+ }
+}
+.vframe {
+ padding: 24px;
+ border-radius: var(--radius-lg);
+ background: linear-gradient(180deg, var(--surface-2), var(--surface));
+ border: 1px solid var(--hair);
+ box-shadow: var(--shadow);
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+ font-family: var(--font-body);
+}
+.vtitle {
+ font-size: 18px;
+ font-weight: 600;
+ letter-spacing: -0.02em;
+ color: var(--fg);
+}
+.vmeta {
+ font-size: 12.5px;
+ color: var(--fg-faint);
+}
+.ddot {
+ width: 9px;
+ height: 9px;
+ border-radius: 3px;
+ background: var(--accent);
+}
+.dlive {
+ margin-left: auto;
+ display: flex;
+ align-items: center;
+ gap: 7px;
+ font-size: 12px;
+ color: var(--primary);
+ padding: 5px 11px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--primary) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--primary) 28%, transparent);
+}
+.dlive span {
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: var(--primary);
+ animation: pulse 1.6s ease-in-out infinite;
+}
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 0.35;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+.dkpis {
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 12px;
+}
+.dkpi {
+ background: var(--surface);
+ border: 1px solid var(--hair-2);
+ border-radius: var(--radius-sm);
+ padding: 14px 15px;
+}
+.dkpi-l {
+ font-size: 12.5px;
+ color: var(--fg-faint);
+}
+.dkpi-v {
+ font-size: 25px;
+ font-weight: 600;
+ letter-spacing: -0.03em;
+ color: var(--fg);
+ margin: 4px 0 5px;
+ font-feature-settings: 'tnum' 1;
+}
+.dkpi-d {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--primary);
+}
+.dbars {
+ display: grid;
+ grid-template-columns: repeat(7, 1fr);
+ gap: 10px;
+ align-items: end;
+}
+.dbar {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 7px;
+}
+.dbar-track {
+ width: 100%;
+ height: 44px;
+ border-radius: 7px;
+ background: var(--hair-2);
+ display: flex;
+ align-items: flex-end;
+ overflow: hidden;
+}
+.dbar-track span {
+ width: 100%;
+ border-radius: 7px;
+ background: var(--accent);
+ display: block;
+ transform: scaleY(0);
+ transform-origin: bottom;
+}
+.dbar-k {
+ font-size: 10.5px;
+ color: var(--fg-faint);
+}
+.dline {
+ stroke-dasharray: 1;
+ stroke-dashoffset: 1;
+}
+.dfill-area {
+ opacity: 0;
+ transition: opacity 0.8s var(--ease) 0.5s;
+}
+.vframe.shown .dline {
+ animation: dash 1.5s var(--ease) 0.15s forwards;
+}
+.vframe.shown .dfill-area {
+ opacity: 1;
+}
+.vframe.shown .dbar-track span {
+ animation: bargrow 0.8s var(--ease-spring) both;
+}
+@keyframes dash {
+ to {
+ stroke-dashoffset: 0;
+ }
+}
+@keyframes bargrow {
+ from {
+ transform: scaleY(0);
+ }
+ to {
+ transform: scaleY(1);
+ }
+}
/* ── Responsive controls — declutter the dock on touch / small screens ─
On touch there's no hover, so keep the dock fully visible; on phones,
shrink it and drop the presenter-only tools (draw + presenter overlay). */
-@media (hover: none) { .noir-dock { opacity: 1; } }
+@media (hover: none) {
+ .noir-dock {
+ opacity: 1;
+ }
+}
@media (max-width: 640px) {
- .noir-dock { opacity: 1; bottom: 14px; }
- .noir-bar { gap: 2px; padding: 6px 7px; }
- .noir-icon-btn { width: 34px; height: 34px; font-size: 17px; }
- .noir-icon-btn.sm { width: 28px; height: 28px; }
- .noir-counter { padding: 0 8px; }
- .noir-counter-now { font-size: 15px; }
- .noir-sep { margin: 0 2px; }
- .noir-optional { display: none; }
- .noir-rail { width: min(248px, 84vw); }
+ .noir-dock {
+ opacity: 1;
+ bottom: 14px;
+ }
+ .noir-bar {
+ gap: 2px;
+ padding: 6px 7px;
+ }
+ .noir-icon-btn {
+ width: 34px;
+ height: 34px;
+ font-size: 17px;
+ }
+ .noir-icon-btn.sm {
+ width: 28px;
+ height: 28px;
+ }
+ .noir-counter {
+ padding: 0 8px;
+ }
+ .noir-counter-now {
+ font-size: 15px;
+ }
+ .noir-sep {
+ margin: 0 2px;
+ }
+ .noir-optional {
+ display: none;
+ }
+ .noir-rail {
+ width: min(248px, 84vw);
+ }
}
/* ── Reduced motion ─────────────────────────────────────────────── */
@media (prefers-reduced-motion: reduce) {
- .slide-stage, .slide::before, .lift, .marquee-track, .btile, .acc-item, .acc-chevron { animation: none !important; transition: none !important; }
- .tilt { transform: none !important; }
- .dline { stroke-dashoffset: 0; }
- .dfill-area { opacity: 1; }
- .dbar-track span { transform: scaleY(1); }
+ .slide-stage,
+ .slide::before,
+ .lift,
+ .marquee-track,
+ .btile,
+ .acc-item,
+ .acc-chevron {
+ animation: none !important;
+ transition: none !important;
+ }
+ .tilt {
+ transform: none !important;
+ }
+ .dline {
+ stroke-dashoffset: 0;
+ }
+ .dfill-area {
+ opacity: 1;
+ }
+ .dbar-track span {
+ transform: scaleY(1);
+ }
}
diff --git a/bolt-slides/src/styles/tokens.css b/bolt-slides/src/styles/tokens.css
index 6994aca8..a46a5639 100644
--- a/bolt-slides/src/styles/tokens.css
+++ b/bolt-slides/src/styles/tokens.css
@@ -6,50 +6,58 @@
=================================================================== */
:root {
/* Surfaces */
- --bg: #05070a;
+ --bg: #05070a;
--bg-grad-1: rgba(79, 229, 176, 0.12);
--bg-grad-2: rgba(41, 192, 240, 0.08);
- --surface: rgba(255, 255, 255, 0.045);
+ --surface: rgba(255, 255, 255, 0.045);
--surface-2: rgba(255, 255, 255, 0.07);
/* Ink */
- --fg: #f4f7fa;
+ --fg: #f4f7fa;
--fg-muted: #9aa4b2;
--fg-faint: #626d7d;
/* Hairlines */
- --hair: rgba(255, 255, 255, 0.10);
+ --hair: rgba(255, 255, 255, 0.1);
--hair-2: rgba(255, 255, 255, 0.06);
/* Accent — ONE accent. May be a 2–4 stop gradient within ~80° of hue. */
- --primary: #4fe5b0;
- --accent: linear-gradient(115deg, #c8f56e 0%, #5aeeb0 42%, #34dcd8 74%, #29c0f0 100%);
+ --primary: #4fe5b0;
+ --accent: linear-gradient(
+ 115deg,
+ #c8f56e 0%,
+ #5aeeb0 42%,
+ #34dcd8 74%,
+ #29c0f0 100%
+ );
--accent-ink: #04140e;
/* Radius */
- --radius: 22px;
+ --radius: 22px;
--radius-sm: 12px;
--radius-lg: 26px;
/* Depth */
- --shadow: 0 50px 120px -50px rgba(0,0,0,0.9);
- --glow: 0 30px 80px -40px rgba(79, 229, 176, 0.5);
+ --shadow: 0 50px 120px -50px rgba(0, 0, 0, 0.9);
+ --glow: 0 30px 80px -40px rgba(79, 229, 176, 0.5);
/* Type */
- --font-head: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif;
- --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
+ --font-head: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Display',
+ sans-serif;
+ --font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'SF Pro Text',
+ sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
/* Motion */
- --dur: 0.4s;
- --ease: cubic-bezier(0.32, 0.72, 0, 1);
+ --dur: 0.4s;
+ --ease: cubic-bezier(0.32, 0.72, 0, 1);
--ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
/* Atmosphere */
- --grain: 0.035;
+ --grain: 0.035;
--vignette: rgba(0, 0, 0, 0.22);
/* Responsive gutter — fluid side / vertical padding */
- --gutter: clamp(28px, 6vw, 120px);
+ --gutter: clamp(28px, 6vw, 120px);
--gutter-y: clamp(40px, 8vh, 96px);
}
diff --git a/bolt-slides/vite.config.ts b/bolt-slides/vite.config.ts
index 9ffcc675..0466183a 100644
--- a/bolt-slides/vite.config.ts
+++ b/bolt-slides/vite.config.ts
@@ -1,6 +1,6 @@
-import { defineConfig } from 'vite'
-import react from '@vitejs/plugin-react'
+import { defineConfig } from 'vite';
+import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
-})
+});
From d0ed6745b611398af64bf92198342fb68ab48663 Mon Sep 17 00:00:00 2001
From: xKevIsDev
Date: Thu, 2 Jul 2026 14:45:43 +0100
Subject: [PATCH 3/3] chore: add .gitignore and SKILL.md for slides
functionality
This commit introduces a .gitignore file to exclude unnecessary files and directories such as node_modules, dist, and environment files. Additionally, it adds SKILL.md, which outlines the usage and structure of a new responsive React presentation engine for Bolt, detailing the setup and authoring process for creating slide decks.
---
bolt-slides/.bolt/skills/slides/SKILL.md | 220 +++++++++++++++++++++++
bolt-slides/.gitignore | 5 +
2 files changed, 225 insertions(+)
create mode 100644 bolt-slides/.bolt/skills/slides/SKILL.md
create mode 100644 bolt-slides/.gitignore
diff --git a/bolt-slides/.bolt/skills/slides/SKILL.md b/bolt-slides/.bolt/skills/slides/SKILL.md
new file mode 100644
index 00000000..5acdd496
--- /dev/null
+++ b/bolt-slides/.bolt/skills/slides/SKILL.md
@@ -0,0 +1,220 @@
+---
+name: slides
+description: >-
+ Build a premium PRESENTED slide deck — classic one-slide-at-a-time slides you
+ advance with a clicker — as a Vite + React app. It keeps the Slidev navigation UI
+ (floating glass dock + thumbnail rail + click-builds + presenter) but slides are
+ RESPONSIVE React (reflow to any screen, no fixed canvas) and fully interactive.
+ Use this for a deck you'll PRESENT (talk over, projector, screen-share) when you
+ want web interactivity and responsiveness without Slidev's constraints. The repo
+ IS the running app (Vite + React) — you install it in place, theme the tokens, and
+ AUTHOR an original deck from the user's real topic/brand.
+---
+
+# Slides — a premium, responsive, React presentation engine for Bolt
+
+Classic **paged** slides (advance one at a time, present over them) — but rebuilt in
+**Vite + React** so each slide is a **responsive web layout** instead of a fixed
+1080×607 canvas that clips. It keeps the Slidev UI you liked (the floating dock +
+thumbnail rail + click-builds + presenter) and adds real web interactivity.
+
+Two halves — keep them separate:
+
+- **The engine + UI are pre-built** — this repo is a complete, runnable app: the paged
+ engine (`src/deck/`), the dock/rail chrome, the section components (`src/components/`),
+ the shared CSS. **Leave the engine as-is. Never regenerate it.** This is the part you
+ liked; it must look/behave identically.
+- **The content is authored fresh, every time** — the slides (topic, structure, copy,
+ visuals, theme) are designed from scratch for *this* request.
+
+> **Theme surface.** All color, type, radius, depth, and motion live in the `:root`
+> token vocabulary of `src/styles/tokens.css`. Theme a brand once, there.
+
+## ⛔ Two hard rules
+
+1. **Don't touch the engine** — leave `src/deck/` (the engine + chrome: `Deck`, `Slide`,
+ `Build`, `Reveal`, `DeckContext`, `icons`, `Annotator`, `useInView`) untouched. Don't
+ rewrite or paraphrase the engine or the chrome; author *around* it.
+2. **Author the deck from the user's REAL input — do not reskin the starter.**
+ `src/App.tsx` is a throwaway that only proves it runs. **Delete its slides and
+ write a new deck.** Never reuse its order, copy, placeholders, or fake names.
+
+---
+
+## Step 0 — Ground the deck in the user's real input
+
+Use the user's real topic, brand, document, facts. Never fabricate a placeholder
+company, logo, or quote for a real subject. If a URL/brand is given, the theme comes
+from that brand — fetch the page for real colors/font/logo, or use the brand's known
+palette, or **STOP and ask**. Report which colors/fonts you used and where from.
+
+---
+
+## Step 1 — Run it in place (the repo is the app)
+
+The repo root is already a complete Vite + React app — no scaffolding or copying. Its
+layout:
+
+```
+package.json vite.config.ts tsconfig*.json index.html src/main.tsx
+src/App.tsx ← THROWAWAY. delete its slides; author the real deck.
+src/styles/ tokens.css (edit :root ONLY) · base.css ← don't edit base.css
+src/deck/ Deck Slide Build Reveal DeckContext useInView icons Annotator ← engine + UI. LOCKED.
+src/components/ CountUp TiltCard Marquee Bento Split StatGrid VisualDashboard Accordion
+ Comparison Tabs Timeline CodeWindow BrowserFrame SpotlightCard Charts
+```
+
+`npm install && npm run dev` runs the deck at `/`. Verify the dock / thumbnail rail /
+click-builds work, then delete the starter slides and author the real deck in
+`App.tsx`.
+
+---
+
+## Step 2 — Theme it (edit only the `:root` block)
+
+All color, type, radius, depth, motion live in `src/styles/tokens.css` `:root`.
+**Change values, never variable names.** Nine ready-made theme families to pull from
+(dark product, editorial luxury, Swiss, dark technical, warm minimal,
+fintech, aurora glass, cinematic, paper editorial). One accent, used sparingly.
+Dark vs light: set `html { color-scheme }` in base.css and pick `--bg`/`--fg`
+accordingly. Set fonts in `--font-head`/`--font-body` and the `@import` at the top
+of `base.css`. Derive from the brand when given.
+
+---
+
+## Step 3 — Author slides (each child of `` is one slide)
+
+Compose slides in `App.tsx`. The building blocks:
+
+- **``** — one slide. `center` for cover/statement/quote/CTA; `full` for
+ edge-to-edge; `nav="Label"`; `notes="…"` (editable in the presenter overlay).
+- **``** — text + edge-to-edge media (`flip` swaps). media = ` `, a color
+ panel, a ``, or ` `.
+- **``** — asymmetric tile grid; tiles take `c`/`r` spans + `variant`.
+- **``** — responsive proof cards; pass a `` as a stat `value`.
+- **``** — us-vs-them feature matrix; one column highlighted in the accent.
+- **``** — tabbed content with a sliding accent pill.
+- **``** — expand/collapse panels (FAQ, feature detail).
+- **``** — vertical roadmap that draws its connector + milestones in.
+- **``** — macOS code window with line numbers + line highlight.
+- **``** — browser chrome around a screenshot / full-bleed app mock
+ (fill it edge-to-edge — a real-looking app screen, not a floating card).
+- **``** — card with a cursor-follow accent glow (Linear/Vercel hover).
+- **Charts** — `` / `` / ``, all draw-in on view.
+- **Atoms** (CSS classes): `.display .headline .lead .subhead .kicker .figure
+ .accent-text .rule`. All fluid (`clamp()`).
+
+**Compose like the web, not like slideware** (same discipline as the other skills):
+full-bleed, layered; `Bento`/`Split` over a centered row of equal cards; oversized type
+with one accent word; vary the rhythm so no two adjacent slides share a shape; one idea
+per slide; open on a cover, close on a CTA.
+
+> **Centering rule:** left-aligned/asymmetric layouts need a **side visual** (a `Split`,
+> an image, a `BrowserFrame`). A **text-only** section anchored left reads as off-center
+> — center those: a centered heading over a centered content block (e.g.
+> `marginInline:'auto'`). `Comparison`, `Tabs`, `Timeline`, `Accordion`, `StatGrid` look
+> best centered unless paired with a side visual.
+
+### Interactivity: click-builds (the signature)
+Reveal content in beats with **``** — it stays hidden until you advance
+to step `n` on that slide, then animates in. Advancing (→ / space / Next) reveals the
+next build, then moves to the next slide. Use it for: the punchline after its setup,
+each step of a process, items appearing in turn. Use **``** for an on-enter
+entrance (no click needed) on headlines/grids.
+
+**Auto-play mode.** The same ``s can instead reveal **automatically, staggered,
+on slide load** — no clicking. Toggle it live with the dock's auto button or the `A`
+key, or start the deck in auto mode with **``** (tune the gap with
+``, seconds). Ideal for a self-running deck or mobile viewing;
+click-mode stays the default.
+
+```tsx
+
+ Three things changed.
+ First, the data got bigger.
+ Then, the tools got faster.
+ Now, anyone can ship.
+
+```
+
+---
+
+## Step 4 — Responsive, not fixed (no clipping)
+
+Each slide is a **full-viewport responsive layout**, not a fixed canvas — it reflows
+to the screen, so nothing scales-and-clips:
+
+- **Fluid sizing.** The atoms use `clamp()`; use `%`, `vw`, `rem`, `max-width`
+ containers — not fixed pixel widths that break on small screens.
+- **One idea per slide**, sized to fill ~one screen with deliberate negative space.
+- **Check a narrow viewport** — `Bento`/`Split` stack (built in); make sure headlines
+ don't overflow and nothing needs scrolling. (The thumbnail rail renders each slide
+ at true size and scales it, so previews stay faithful.)
+- **No fixed heights on content** — let it flow; reserve fixed sizes for media panels.
+
+---
+
+## Step 5 — Visuals & imagery
+
+Visuals must fit the topic (same rules as the other skills): data/SaaS → a `.vframe`
+mock (`VisualDashboard` is an *example* — build topic-fit ones with real data);
+brand/product/editorial/real-world → **generate images** into `public/`, one
+consistent style, used as `Split` media or full-bleed slide backgrounds under a
+gradient scrim (no text in images). A `Split` or full-bleed image beats a floating card.
+
+---
+
+## Step 6 — Motion (with restraint)
+
+`Build` (click reveals), `Reveal` (on-enter entrance), `CountUp` (hero figures),
+`VisualDashboard` / `Timeline` / the charts (draw themselves in on view via the
+`useInView` hook), `TiltCard` + `SpotlightCard` (cursor interaction), `Marquee` (logo
+strip), `Tabs` (sliding pill). The ambient background (drifting spotlights + grain +
+vignette) and the slide-change transition are automatic. **One or two motion ideas per
+slide**, never a circus. All honors `prefers-reduced-motion`.
+
+---
+
+## Extend the system — invent new slides, components & visuals
+
+The kit is a **floor, not a ceiling.** The bundled components cover a lot (comparison,
+tabs, timeline, charts, code/browser frames, accordion, bento, split…) — but author
+new ones for the topic when nothing fits: a `` table, a Gantt, a device/phone
+mock, a chat or kanban mock, a map. Only the token *names* and `src/deck/` (engine +
+chrome) are off-limits to rewrite; **adding** components/visuals is encouraged. Every
+new piece must: use `var(--…)` tokens only (no raw hex), compose like a web section, be
+responsive (work on mobile), animate with `Reveal`/`Build`/`useInView` + honor
+reduced-motion, use tabular figures, and add **no new dependencies** (plain React +
+CSS + SVG).
+
+---
+
+## Step 7 — Structure & writing
+
+Pick an arc that fits the deck type (pitch, launch, brand, teaching, report) —
+structure follows content. Open on a cover, close on a CTA. ~8–16 slides sized to the
+material. Headlines short, declarative, specific (sentence case); body 1–3 tight
+sentences; 1–3 word kickers; one idea per slide. Use the user's real numbers; never
+invent numbers for a real brand. Zero lorem, zero placeholder names. Add `notes` for
+talking points where useful.
+
+---
+
+## Definition of done (self-check)
+
+- [ ] The engine + chrome in `src/deck/` are **left untouched**; the
+ dock + thumbnail rail appear, arrow keys advance AND step back through builds,
+ auto-play / fullscreen / overview work, annotation (D) has full tools and
+ persists per slide, presenter (P) opens a synced new tab, `H` hides the UI, and
+ the URL hash tracks the slide.
+- [ ] The deck is **authored, not reskinned** — topic, structure, copy, names are the
+ user's, with no starter leftovers (no "Title"/"Northwind").
+- [ ] If a brand/URL was given, `--primary`, fonts, and logo come from that brand.
+- [ ] Only the `:root` block was edited for the theme; editing `--primary` recolors
+ everything incl. the dock.
+- [ ] Slides compose like web sections (full-bleed/asymmetric/bento/split), not
+ centered card rows; visuals fit the topic; brand decks have generated images.
+- [ ] **Responsive:** looks right narrow + wide — sections stack, nothing clips or
+ needs scrolling. Builds reveal in the intended order.
+- [ ] Motion is restrained; reduced-motion respected.
+- [ ] `npm install && npm run dev` runs with no console errors; `npm run build` passes.
diff --git a/bolt-slides/.gitignore b/bolt-slides/.gitignore
new file mode 100644
index 00000000..da431f1c
--- /dev/null
+++ b/bolt-slides/.gitignore
@@ -0,0 +1,5 @@
+node_modules/
+dist/
+.vite/
+.DS_Store
+.env