Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a29279f0f | ||
|
|
61cc2383fc | ||
|
|
08befffdeb | ||
|
|
87dc305496 | ||
|
|
9c4e09e420 |
No files matched your search
@@ -0,0 +1,12 @@
|
|||||||
|
# Copy to .env style settings on your host. Never commit real values.
|
||||||
|
ADMIN_EMAIL=rana.krunal7558@gmail.com
|
||||||
|
ADMIN_PASSWORD=pick-a-long-password-here
|
||||||
|
# Where new tickets are emailed (this is already the default)
|
||||||
|
TICKET_TO=rana.krunal7558@gmail.com
|
||||||
|
# Gmail sending: use an "App Password", not your normal password
|
||||||
|
SMTP_HOST=smtp.gmail.com
|
||||||
|
SMTP_PORT=465
|
||||||
|
SMTP_USER=rana.krunal7558@gmail.com
|
||||||
|
SMTP_PASS=your-16-letter-app-password
|
||||||
|
# Set to 1 when hosted behind a proxy (most hosts)
|
||||||
|
TRUST_PROXY=1
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
server/data
|
||||||
|
.env
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Deploy checklist (Hostinger)
|
||||||
|
|
||||||
|
## A. Before you start
|
||||||
|
- [ ] Plan is Business Web Hosting or a Cloud plan (Node.js Web App supported)
|
||||||
|
- [ ] Gmail app password created (myaccount.google.com > Security > 2-Step Verification > App passwords)
|
||||||
|
|
||||||
|
## B. In hPanel: Websites > Add website > Node.js Web App > Import Git repository
|
||||||
|
- [ ] Repo: keithrana/cycle-atlas
|
||||||
|
- [ ] Branch: claude/sleepy-goodall-rykli1 (NOT main)
|
||||||
|
- [ ] Root/app directory: it-support (if no such box, ask Claude to split into its own repo)
|
||||||
|
- [ ] Node version: 22.x or 24.x
|
||||||
|
- [ ] Build command: npm run build
|
||||||
|
- [ ] Start command: npm start (entry file, if asked: server/index.js)
|
||||||
|
|
||||||
|
## C. Environment variables (type them only into Hostinger, never into chat or Git)
|
||||||
|
ADMIN_EMAIL, ADMIN_PASSWORD (10+ chars), SMTP_HOST=smtp.gmail.com, SMTP_PORT=465,
|
||||||
|
SMTP_USER, SMTP_PASS (Gmail app password), TRUST_PROXY=1, DATA_DIR=/home/<your-username>/helpdesk-data
|
||||||
|
|
||||||
|
## D. Deploy, then test in order
|
||||||
|
1. [ ] Page loads over https:// and the video plays
|
||||||
|
2. [ ] Search "wifi" shows answers; send a test ticket
|
||||||
|
3. [ ] Email arrives at rana.krunal7558@gmail.com (check Spam)
|
||||||
|
4. [ ] /#/staff login works and the ticket is listed
|
||||||
|
5. [ ] Add a test FAQ, redeploy, confirm it is still there (proves data is kept)
|
||||||
|
6. [ ] Log out; /#/staff asks for login again
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# IT Support Site: PRD (plan)
|
||||||
|
|
||||||
|
Restaurant analogy: the FAQ database is the **kitchen's recipe book**. The website is the **dining room**.
|
||||||
|
|
||||||
|
## Core features
|
||||||
|
1. Cinematic hero: looping video, glass buttons (exactly as specified)
|
||||||
|
2. Search bar in the hero: type a problem, get matching answers
|
||||||
|
3. Category chips: browse the 11 topics (Access, Network, Hardware...)
|
||||||
|
4. FAQ answer cards: question, answer, optional "official guide" link
|
||||||
|
5. Footer social icons
|
||||||
|
|
||||||
|
## Phased roadmap
|
||||||
|
- **Phase 1 – Food stand (TODAY):** hero + search + categories + 87 FAQs from the database, baked in as a JSON file. No server.
|
||||||
|
- **Phase 2 – Small restaurant (DONE, browser-only storage):** "Was this helpful?" votes, "Contact IT" ticket form, shareable links per question.
|
||||||
|
- **Phase 3 – Full restaurant (DONE):** small back-of-house server + real database, staff login, admin page to edit FAQs, ticket tracking.
|
||||||
|
|
||||||
|
## Test ladder
|
||||||
|
1. Page loads, no console errors 2. Buttons/search work 3. Real data/links work 4. Security (no raw HTML injection, safe links)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
# HelpDesk IT Support
|
||||||
|
|
||||||
|
Run locally (needs Node 22+):
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='a-long-password' npm start
|
||||||
|
Open http://localhost:3001 (staff login: http://localhost:3001/#/staff)
|
||||||
|
|
||||||
|
For development with live reload: run `npm run server` and `npm run dev` in two terminals.
|
||||||
|
|
||||||
|
Settings are listed in `.env.example`. Data lives in `server/data/helpdesk.db`: back it up, and keep it on a persistent disk when hosting.
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>HelpDesk | IT Support</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+3528
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"name": "it-support",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"server": "node server/index.js",
|
||||||
|
"start": "NODE_ENV=production node server/index.js"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"express": "^4.22.3",
|
||||||
|
"lucide-react": "^0.460.0",
|
||||||
|
"nodemailer": "^10.0.13",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@vitejs/plugin-react": "^4.3.3",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.15",
|
||||||
|
"typescript": "~5.6.2",
|
||||||
|
"vite": "^5.4.11"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default { plugins: { tailwindcss: {}, autoprefixer: {} } }
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { scryptSync, randomBytes, timingSafeEqual, createHash } from 'node:crypto'
|
||||||
|
import { db } from './db.js'
|
||||||
|
|
||||||
|
export const SESSION_DAYS = 7
|
||||||
|
export const hashPassword = (pw) => {
|
||||||
|
const salt = randomBytes(16)
|
||||||
|
return `${salt.toString('hex')}:${scryptSync(pw, salt, 64).toString('hex')}`
|
||||||
|
}
|
||||||
|
export const verifyPassword = (pw, stored) => {
|
||||||
|
const [salt, hash] = stored.split(':')
|
||||||
|
const a = Buffer.from(hash, 'hex')
|
||||||
|
const b = scryptSync(pw, Buffer.from(salt, 'hex'), 64)
|
||||||
|
return a.length === b.length && timingSafeEqual(a, b)
|
||||||
|
}
|
||||||
|
const sha = (t) => createHash('sha256').update(t).digest('hex')
|
||||||
|
|
||||||
|
export function createSession(staffId) {
|
||||||
|
const token = randomBytes(32).toString('hex')
|
||||||
|
db.prepare('INSERT INTO sessions VALUES (?,?,?)').run(sha(token), staffId, Date.now() + SESSION_DAYS * 864e5)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
export const destroySession = (token) => db.prepare('DELETE FROM sessions WHERE token_hash=?').run(sha(token))
|
||||||
|
|
||||||
|
function cookieToken(req) {
|
||||||
|
const m = /(?:^|;\s*)session=([a-f0-9]{64})/.exec(req.headers.cookie || '')
|
||||||
|
return m ? m[1] : null
|
||||||
|
}
|
||||||
|
export { cookieToken }
|
||||||
|
|
||||||
|
export function requireStaff(req, res, next) {
|
||||||
|
const token = cookieToken(req)
|
||||||
|
const row = token && db.prepare('SELECT s.id, s.email, s.name, x.expires FROM sessions x JOIN staff s ON s.id=x.staff_id WHERE x.token_hash=?').get(sha(token))
|
||||||
|
if (!row || row.expires < Date.now()) return res.status(401).json({ error: 'Please log in.' })
|
||||||
|
req.staff = row
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tiny in-memory rate limiter: `max` hits per `windowMs` per IP+bucket.
|
||||||
|
const hits = new Map()
|
||||||
|
export const rateLimit = (bucket, max, windowMs) => (req, res, next) => {
|
||||||
|
const key = `${bucket}:${req.ip}`
|
||||||
|
const now = Date.now()
|
||||||
|
const recent = (hits.get(key) || []).filter((t) => now - t < windowMs)
|
||||||
|
if (recent.length >= max) return res.status(429).json({ error: 'Too many tries. Please wait a bit.' })
|
||||||
|
recent.push(now)
|
||||||
|
hits.set(key, recent)
|
||||||
|
next()
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { DatabaseSync } from 'node:sqlite'
|
||||||
|
import { readFileSync, mkdirSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
|
||||||
|
const here = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const dataDir = process.env.DATA_DIR || join(here, 'data')
|
||||||
|
mkdirSync(dataDir, { recursive: true })
|
||||||
|
|
||||||
|
export const db = new DatabaseSync(join(dataDir, 'helpdesk.db'))
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS faqs (id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, question TEXT NOT NULL, answer TEXT NOT NULL, link TEXT NOT NULL DEFAULT '', link_label TEXT NOT NULL DEFAULT '');
|
||||||
|
CREATE TABLE IF NOT EXISTS tickets (id TEXT PRIMARY KEY, name TEXT NOT NULL, email TEXT NOT NULL, topic TEXT NOT NULL, message TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'open', emailed INTEGER NOT NULL DEFAULT 0, created TEXT NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS votes (id INTEGER PRIMARY KEY AUTOINCREMENT, faq_id INTEGER NOT NULL, vote TEXT NOT NULL, created TEXT NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS staff (id INTEGER PRIMARY KEY AUTOINCREMENT, email TEXT NOT NULL UNIQUE, name TEXT NOT NULL, password_hash TEXT NOT NULL, created TEXT NOT NULL);
|
||||||
|
CREATE TABLE IF NOT EXISTS sessions (token_hash TEXT PRIMARY KEY, staff_id INTEGER NOT NULL, expires INTEGER NOT NULL);
|
||||||
|
`)
|
||||||
|
|
||||||
|
// First run: copy the 87 FAQs from the bundled file into the real database.
|
||||||
|
if (db.prepare('SELECT COUNT(*) AS n FROM faqs').get().n === 0) {
|
||||||
|
const seed = JSON.parse(readFileSync(join(here, '..', 'src', 'data', 'faqs.json'), 'utf8'))
|
||||||
|
const ins = db.prepare('INSERT INTO faqs (id, category, question, answer, link, link_label) VALUES (?,?,?,?,?,?)')
|
||||||
|
for (const f of seed) ins.run(f.id, f.category, f.question, f.answer, f.link, f.linkLabel)
|
||||||
|
}
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import express from 'express'
|
||||||
|
import { existsSync } from 'node:fs'
|
||||||
|
import { dirname, join } from 'node:path'
|
||||||
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { randomBytes } from 'node:crypto'
|
||||||
|
import { db } from './db.js'
|
||||||
|
import { hashPassword, verifyPassword, createSession, destroySession, cookieToken, requireStaff, rateLimit, SESSION_DAYS } from './auth.js'
|
||||||
|
import { sendTicketEmail, emailConfigured } from './mail.js'
|
||||||
|
|
||||||
|
const app = express()
|
||||||
|
const prod = process.env.NODE_ENV === 'production'
|
||||||
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
const clean = (s, max) => String(s ?? '').replace(/[\r\n]+/g, ' ').trim().slice(0, max)
|
||||||
|
const cleanBlock = (s, max) => String(s ?? '').trim().slice(0, max)
|
||||||
|
const isHttpUrl = (u) => { try { return ['http:', 'https:'].includes(new URL(u).protocol) } catch { return false } }
|
||||||
|
|
||||||
|
app.set('trust proxy', process.env.TRUST_PROXY ? 1 : false)
|
||||||
|
app.disable('x-powered-by')
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
res.set({
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
|
'X-Frame-Options': 'DENY',
|
||||||
|
'Referrer-Policy': 'same-origin',
|
||||||
|
'Content-Security-Policy': "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; media-src https://d8j0ntlcm91z4.cloudfront.net; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'",
|
||||||
|
})
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
app.use(express.json({ limit: '20kb' }))
|
||||||
|
// Block cross-site writes: browsers always send Origin on POST/PUT/PATCH/DELETE from other sites.
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (['GET', 'HEAD'].includes(req.method)) return next()
|
||||||
|
const origin = req.headers.origin
|
||||||
|
if (origin && new URL(origin).host !== req.headers.host) return res.status(403).json({ error: 'Bad origin.' })
|
||||||
|
next()
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- Public ----------
|
||||||
|
const faqOut = (r) => ({ id: r.id, category: r.category, question: r.question, answer: r.answer, link: r.link, linkLabel: r.link_label })
|
||||||
|
app.get('/api/faqs', (_req, res) => res.json(db.prepare('SELECT * FROM faqs ORDER BY id').all().map(faqOut)))
|
||||||
|
|
||||||
|
app.post('/api/tickets', rateLimit('ticket', 5, 3600e3), async (req, res) => {
|
||||||
|
const t = {
|
||||||
|
id: `IT-${Date.now().toString(36).toUpperCase()}${randomBytes(2).toString('hex').toUpperCase()}`,
|
||||||
|
name: clean(req.body.name, 100), email: clean(req.body.email, 200),
|
||||||
|
topic: clean(req.body.topic, 100), message: cleanBlock(req.body.message, 2000),
|
||||||
|
}
|
||||||
|
if (!t.name) return res.status(400).json({ error: 'Please enter your name.' })
|
||||||
|
if (!EMAIL_RE.test(t.email)) return res.status(400).json({ error: 'Please enter a valid email.' })
|
||||||
|
if (t.message.length < 10) return res.status(400).json({ error: 'Please describe the problem (at least 10 characters).' })
|
||||||
|
db.prepare('INSERT INTO tickets (id,name,email,topic,message,created) VALUES (?,?,?,?,?,?)').run(t.id, t.name, t.email, t.topic, t.message, new Date().toISOString())
|
||||||
|
const emailed = await sendTicketEmail(t)
|
||||||
|
if (emailed) db.prepare('UPDATE tickets SET emailed=1 WHERE id=?').run(t.id)
|
||||||
|
res.status(201).json({ id: t.id })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post('/api/votes', rateLimit('vote', 30, 3600e3), (req, res) => {
|
||||||
|
const faqId = Number(req.body.faqId)
|
||||||
|
const vote = req.body.vote
|
||||||
|
if (!['up', 'down'].includes(vote) || !db.prepare('SELECT 1 FROM faqs WHERE id=?').get(faqId)) return res.status(400).json({ error: 'Bad vote.' })
|
||||||
|
db.prepare('INSERT INTO votes (faq_id,vote,created) VALUES (?,?,?)').run(faqId, vote, new Date().toISOString())
|
||||||
|
res.status(201).json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------- Staff login ----------
|
||||||
|
app.post('/api/login', rateLimit('login', 10, 15 * 60e3), (req, res) => {
|
||||||
|
const row = db.prepare('SELECT * FROM staff WHERE email=?').get(clean(req.body.email, 200).toLowerCase())
|
||||||
|
// Same message either way so attackers can't learn which emails exist.
|
||||||
|
if (!row || !verifyPassword(String(req.body.password ?? ''), row.password_hash)) return res.status(401).json({ error: 'Wrong email or password.' })
|
||||||
|
const token = createSession(row.id)
|
||||||
|
res.cookie('session', token, { httpOnly: true, sameSite: 'strict', secure: prod, maxAge: SESSION_DAYS * 864e5, path: '/' })
|
||||||
|
res.json({ name: row.name, email: row.email })
|
||||||
|
})
|
||||||
|
app.post('/api/logout', (req, res) => {
|
||||||
|
const t = cookieToken(req)
|
||||||
|
if (t) destroySession(t)
|
||||||
|
res.clearCookie('session', { path: '/' })
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
app.get('/api/me', requireStaff, (req, res) => res.json({ name: req.staff.name, email: req.staff.email }))
|
||||||
|
|
||||||
|
// ---------- Staff only ----------
|
||||||
|
app.get('/api/staff/status', requireStaff, (_req, res) => res.json({ emailConfigured }))
|
||||||
|
app.get('/api/staff/tickets', requireStaff, (_req, res) => res.json(db.prepare('SELECT * FROM tickets ORDER BY created DESC').all()))
|
||||||
|
app.patch('/api/staff/tickets/:id', requireStaff, (req, res) => {
|
||||||
|
if (!['open', 'in_progress', 'closed'].includes(req.body.status)) return res.status(400).json({ error: 'Bad status.' })
|
||||||
|
db.prepare('UPDATE tickets SET status=? WHERE id=?').run(req.body.status, req.params.id)
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/staff/faqs', requireStaff, (_req, res) => {
|
||||||
|
const votes = db.prepare("SELECT faq_id, SUM(vote='up') AS up, SUM(vote='down') AS down FROM votes GROUP BY faq_id").all()
|
||||||
|
const byId = new Map(votes.map((v) => [v.faq_id, v]))
|
||||||
|
res.json(db.prepare('SELECT * FROM faqs ORDER BY id').all().map((r) => ({ ...faqOut(r), up: byId.get(r.id)?.up ?? 0, down: byId.get(r.id)?.down ?? 0 })))
|
||||||
|
})
|
||||||
|
function faqFields(body) {
|
||||||
|
const f = { category: clean(body.category, 100), question: clean(body.question, 300), answer: cleanBlock(body.answer, 3000), link: clean(body.link, 500), link_label: clean(body.linkLabel, 200) }
|
||||||
|
if (!f.category || !f.question || !f.answer) return { error: 'Category, question and answer are required.' }
|
||||||
|
if (f.link && !isHttpUrl(f.link)) return { error: 'Link must start with http:// or https://' }
|
||||||
|
return { f }
|
||||||
|
}
|
||||||
|
app.post('/api/staff/faqs', requireStaff, (req, res) => {
|
||||||
|
const { f, error } = faqFields(req.body)
|
||||||
|
if (error) return res.status(400).json({ error })
|
||||||
|
const r = db.prepare('INSERT INTO faqs (category,question,answer,link,link_label) VALUES (?,?,?,?,?)').run(f.category, f.question, f.answer, f.link, f.link_label)
|
||||||
|
res.status(201).json({ id: Number(r.lastInsertRowid) })
|
||||||
|
})
|
||||||
|
app.put('/api/staff/faqs/:id', requireStaff, (req, res) => {
|
||||||
|
const { f, error } = faqFields(req.body)
|
||||||
|
if (error) return res.status(400).json({ error })
|
||||||
|
db.prepare('UPDATE faqs SET category=?,question=?,answer=?,link=?,link_label=? WHERE id=?').run(f.category, f.question, f.answer, f.link, f.link_label, req.params.id)
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
app.delete('/api/staff/faqs/:id', requireStaff, (req, res) => {
|
||||||
|
db.prepare('DELETE FROM faqs WHERE id=?').run(req.params.id)
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.get('/api/staff/team', requireStaff, (_req, res) => res.json(db.prepare('SELECT id,email,name,created FROM staff ORDER BY id').all()))
|
||||||
|
app.post('/api/staff/team', requireStaff, (req, res) => {
|
||||||
|
const email = clean(req.body.email, 200).toLowerCase(), name = clean(req.body.name, 100), pw = String(req.body.password ?? '')
|
||||||
|
if (!EMAIL_RE.test(email) || !name) return res.status(400).json({ error: 'Name and a valid email are required.' })
|
||||||
|
if (pw.length < 10) return res.status(400).json({ error: 'Password must be at least 10 characters.' })
|
||||||
|
if (db.prepare('SELECT 1 FROM staff WHERE email=?').get(email)) return res.status(409).json({ error: 'That email is already on the team.' })
|
||||||
|
db.prepare('INSERT INTO staff (email,name,password_hash,created) VALUES (?,?,?,?)').run(email, name, hashPassword(pw), new Date().toISOString())
|
||||||
|
res.status(201).json({ ok: true })
|
||||||
|
})
|
||||||
|
app.delete('/api/staff/team/:id', requireStaff, (req, res) => {
|
||||||
|
if (Number(req.params.id) === req.staff.id) return res.status(400).json({ error: "You can't remove yourself." })
|
||||||
|
db.prepare('DELETE FROM sessions WHERE staff_id=?').run(req.params.id)
|
||||||
|
db.prepare('DELETE FROM staff WHERE id=?').run(req.params.id)
|
||||||
|
res.json({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use('/api', (_req, res) => res.status(404).json({ error: 'Not found.' }))
|
||||||
|
|
||||||
|
// ---------- First staff member ----------
|
||||||
|
if (db.prepare('SELECT COUNT(*) AS n FROM staff').get().n === 0) {
|
||||||
|
const { ADMIN_EMAIL, ADMIN_PASSWORD } = process.env
|
||||||
|
if (ADMIN_EMAIL && ADMIN_PASSWORD && ADMIN_PASSWORD.length >= 10) {
|
||||||
|
db.prepare('INSERT INTO staff (email,name,password_hash,created) VALUES (?,?,?,?)').run(ADMIN_EMAIL.toLowerCase(), 'Admin', hashPassword(ADMIN_PASSWORD), new Date().toISOString())
|
||||||
|
console.log(`Created first staff login: ${ADMIN_EMAIL}`)
|
||||||
|
} else console.warn('No staff yet. Set ADMIN_EMAIL and ADMIN_PASSWORD (10+ characters) and restart.')
|
||||||
|
}
|
||||||
|
if (!emailConfigured) console.warn('Email not configured (SMTP_HOST/SMTP_USER/SMTP_PASS). Tickets are saved but not emailed.')
|
||||||
|
|
||||||
|
// ---------- Serve the website (production) ----------
|
||||||
|
const dist = join(dirname(fileURLToPath(import.meta.url)), '..', 'dist')
|
||||||
|
if (existsSync(dist)) app.use(express.static(dist))
|
||||||
|
|
||||||
|
const port = Number(process.env.PORT || 3001)
|
||||||
|
app.listen(port, () => console.log(`HelpDesk server on http://localhost:${port}`))
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import nodemailer from 'nodemailer'
|
||||||
|
|
||||||
|
const { SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, TICKET_TO = 'rana.krunal7558@gmail.com' } = process.env
|
||||||
|
const transporter = SMTP_HOST && SMTP_USER && SMTP_PASS
|
||||||
|
? nodemailer.createTransport({ host: SMTP_HOST, port: Number(SMTP_PORT || 465), secure: Number(SMTP_PORT || 465) === 465, auth: { user: SMTP_USER, pass: SMTP_PASS } })
|
||||||
|
: null
|
||||||
|
|
||||||
|
export const emailConfigured = !!transporter
|
||||||
|
|
||||||
|
export async function sendTicketEmail(t) {
|
||||||
|
if (!transporter) return false
|
||||||
|
try {
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: SMTP_USER,
|
||||||
|
to: TICKET_TO,
|
||||||
|
replyTo: t.email,
|
||||||
|
subject: `[${t.id}] ${t.topic}: new IT request from ${t.name}`,
|
||||||
|
text: `Ticket: ${t.id}\nFrom: ${t.name} <${t.email}>\nTopic: ${t.topic}\n\n${t.message}\n`,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Email failed:', e.message)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import { ArrowRight, Globe, Instagram, Twitter } from 'lucide-react'
|
||||||
|
import VideoBackground from './VideoBackground'
|
||||||
|
import FaqList from './Faq'
|
||||||
|
import ContactForm from './ContactForm'
|
||||||
|
import type { Faq } from './types'
|
||||||
|
import Staff from './Staff'
|
||||||
|
import faqData from './data/faqs.json'
|
||||||
|
|
||||||
|
const bundledFaqs = faqData as Faq[]
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [faqs, setFaqs] = useState<Faq[]>(bundledFaqs)
|
||||||
|
const [isStaff, setIsStaff] = useState(location.hash.startsWith('#/staff'))
|
||||||
|
const categories = useMemo(() => Array.from(new Set(faqs.map((f) => f.category))), [faqs])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch('/api/faqs').then((r) => (r.ok ? r.json() : Promise.reject())).then(setFaqs).catch(() => {})
|
||||||
|
const onHash = () => setIsStaff(location.hash.startsWith('#/staff'))
|
||||||
|
window.addEventListener('hashchange', onHash)
|
||||||
|
return () => window.removeEventListener('hashchange', onHash)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const [query, setQuery] = useState('')
|
||||||
|
const [category, setCategory] = useState<string | null>(null)
|
||||||
|
const answersRef = useRef<HTMLElement>(null)
|
||||||
|
|
||||||
|
const results = useMemo(() => {
|
||||||
|
const words = query.toLowerCase().split(/\s+/).filter(Boolean)
|
||||||
|
return faqs.filter((f) => {
|
||||||
|
if (category && f.category !== category) return false
|
||||||
|
const text = `${f.question} ${f.answer} ${f.category}`.toLowerCase()
|
||||||
|
return words.every((w) => text.includes(w))
|
||||||
|
})
|
||||||
|
}, [query, category, faqs])
|
||||||
|
|
||||||
|
if (isStaff) return <Staff />
|
||||||
|
|
||||||
|
const scrollToAnswers = () => answersRef.current?.scrollIntoView({ behavior: 'smooth' })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-black">
|
||||||
|
<div className="min-h-screen bg-black overflow-hidden relative flex flex-col">
|
||||||
|
<VideoBackground />
|
||||||
|
|
||||||
|
<nav className="relative z-20 pl-6 pr-6 py-6">
|
||||||
|
<div className="liquid-glass rounded-full px-6 py-3 flex items-center justify-between max-w-5xl mx-auto">
|
||||||
|
<div className="flex items-center gap-8">
|
||||||
|
<div className="flex items-center gap-2 text-white font-semibold text-lg">
|
||||||
|
<Globe size={24} />
|
||||||
|
<span>HelpDesk</span>
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:flex items-center gap-8">
|
||||||
|
{[
|
||||||
|
['Topics', '#answers'],
|
||||||
|
['Search', '#top'],
|
||||||
|
['Contact', '#contact'],
|
||||||
|
].map(([label, href]) => (
|
||||||
|
<a key={label} href={href} className="text-white/80 hover:text-white transition-colors text-sm font-medium">
|
||||||
|
{label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<a href="#contact" className="text-white text-sm font-medium">Contact IT</a>
|
||||||
|
<a href="#/staff" className="liquid-glass rounded-full px-6 py-2 text-white text-sm font-medium">Staff login</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="top"
|
||||||
|
className="relative z-10 flex-1 flex flex-col items-center justify-center px-6 py-12 text-center -translate-y-[20%]"
|
||||||
|
>
|
||||||
|
<h1
|
||||||
|
className="text-5xl md:text-6xl lg:text-7xl text-white mb-8 tracking-tight whitespace-nowrap"
|
||||||
|
style={{ fontFamily: "'Instrument Serif', serif" }}
|
||||||
|
>
|
||||||
|
IT help, <em>made simple</em>
|
||||||
|
</h1>
|
||||||
|
<div className="max-w-xl w-full space-y-4">
|
||||||
|
<form
|
||||||
|
className="liquid-glass rounded-full pl-6 pr-2 py-2 flex items-center gap-3"
|
||||||
|
onSubmit={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
scrollToAnswers()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={query}
|
||||||
|
maxLength={200}
|
||||||
|
onChange={(e) => setQuery(e.target.value)}
|
||||||
|
placeholder="Describe your problem"
|
||||||
|
aria-label="Search IT help"
|
||||||
|
className="bg-transparent outline-none flex-1 text-white placeholder:text-white/40 text-base"
|
||||||
|
/>
|
||||||
|
<button type="submit" aria-label="Search" className="bg-white rounded-full p-3 text-black">
|
||||||
|
<ArrowRight size={20} />
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<p className="text-white text-sm leading-relaxed px-4">
|
||||||
|
Locked out? Wi-Fi down? Printer acting up? Search {faqs.length} plain-English answers to the most common IT problems.
|
||||||
|
</p>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<button
|
||||||
|
onClick={scrollToAnswers}
|
||||||
|
className="liquid-glass rounded-full px-8 py-3 text-white text-sm font-medium hover:bg-white/5 transition-colors"
|
||||||
|
>
|
||||||
|
Browse all topics
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative z-10 flex justify-center gap-4 pb-12">
|
||||||
|
{[
|
||||||
|
{ Icon: Instagram, label: 'Instagram' },
|
||||||
|
{ Icon: Twitter, label: 'Twitter' },
|
||||||
|
{ Icon: Globe, label: 'Website' },
|
||||||
|
].map(({ Icon, label }) => (
|
||||||
|
<button
|
||||||
|
key={label}
|
||||||
|
aria-label={label}
|
||||||
|
className="liquid-glass rounded-full p-4 text-white/80 hover:text-white hover:bg-white/5 transition-all"
|
||||||
|
>
|
||||||
|
<Icon size={20} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="answers" ref={answersRef} className="max-w-3xl mx-auto px-6 py-20">
|
||||||
|
<h2 className="text-4xl text-white mb-2" style={{ fontFamily: "'Instrument Serif', serif" }}>
|
||||||
|
Answers
|
||||||
|
</h2>
|
||||||
|
<p className="text-white/60 text-sm mb-6" aria-live="polite">
|
||||||
|
Showing {results.length} of {faqs.length}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-8">
|
||||||
|
{[null, ...categories].map((c) => (
|
||||||
|
<button
|
||||||
|
key={c ?? 'all'}
|
||||||
|
onClick={() => setCategory(c)}
|
||||||
|
aria-pressed={category === c}
|
||||||
|
className={`liquid-glass rounded-full px-4 py-2 text-sm transition-colors ${
|
||||||
|
category === c ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{c ?? 'All'}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<FaqList items={results} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ContactForm topics={categories} />
|
||||||
|
|
||||||
|
<footer id="about" className="max-w-3xl mx-auto px-6 pb-16 text-center text-white/50 text-sm">
|
||||||
|
General guidance only. For account-specific problems, contact your organisation's IT service desk.
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import { useState } from 'react'
|
||||||
|
import { Send } from 'lucide-react'
|
||||||
|
|
||||||
|
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||||
|
const inputCls = 'w-full bg-transparent outline-none text-white placeholder:text-white/40 text-base'
|
||||||
|
|
||||||
|
export default function ContactForm({ topics }: { topics: string[] }) {
|
||||||
|
const [form, setForm] = useState({ name: '', email: '', topic: topics[0] ?? '', message: '' })
|
||||||
|
const [errors, setErrors] = useState<Record<string, string>>({})
|
||||||
|
const [ticketId, setTicketId] = useState<string | null>(null)
|
||||||
|
const [sending, setSending] = useState(false)
|
||||||
|
const [serverError, setServerError] = useState('')
|
||||||
|
|
||||||
|
const set = (k: keyof typeof form) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
|
||||||
|
setForm({ ...form, [k]: e.target.value })
|
||||||
|
|
||||||
|
const submit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
const errs: Record<string, string> = {}
|
||||||
|
if (!form.name.trim()) errs.name = 'Please enter your name.'
|
||||||
|
if (!EMAIL_RE.test(form.email.trim())) errs.email = 'Please enter a valid email.'
|
||||||
|
if (form.message.trim().length < 10) errs.message = 'Please describe the problem (at least 10 characters).'
|
||||||
|
setErrors(errs)
|
||||||
|
setServerError('')
|
||||||
|
if (Object.keys(errs).length) return
|
||||||
|
setSending(true)
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/tickets', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(form) })
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
if (!res.ok) throw new Error(data.error || 'Something went wrong. Please try again.')
|
||||||
|
setTicketId(data.id)
|
||||||
|
setForm({ name: '', email: '', topic: topics[0] ?? '', message: '' })
|
||||||
|
} catch (err) {
|
||||||
|
setServerError(err instanceof Error ? err.message : 'Could not reach the server.')
|
||||||
|
} finally {
|
||||||
|
setSending(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const field = 'liquid-glass rounded-3xl px-6 py-4'
|
||||||
|
const err = (k: string) => errors[k] && <p role="alert" className="text-red-300 text-sm mt-1 px-2">{errors[k]}</p>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section id="contact" className="max-w-3xl mx-auto px-6 pb-20">
|
||||||
|
<h2 className="text-4xl text-white mb-2" style={{ fontFamily: "'Instrument Serif', serif" }}>Contact IT</h2>
|
||||||
|
<p className="text-white/60 text-sm mb-6">Can't find your answer? Tell us what's wrong.</p>
|
||||||
|
{ticketId ? (
|
||||||
|
<div className="liquid-glass rounded-3xl p-8 text-white" role="status">
|
||||||
|
<p className="text-xl mb-2">Request sent. Your ticket number is <strong>{ticketId}</strong>.</p>
|
||||||
|
<p className="text-white/70 text-sm mb-4">Our IT team has been notified. Keep this number if you follow up.</p>
|
||||||
|
<button onClick={() => setTicketId(null)} className="liquid-glass rounded-full px-6 py-2 text-sm hover:bg-white/5">Send another</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<form onSubmit={submit} noValidate className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<div className={field}><input className={inputCls} placeholder="Your name" aria-label="Your name" maxLength={100} value={form.name} onChange={set('name')} /></div>
|
||||||
|
{err('name')}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={field}><input className={inputCls} type="email" placeholder="Your email" aria-label="Your email" maxLength={200} value={form.email} onChange={set('email')} /></div>
|
||||||
|
{err('email')}
|
||||||
|
</div>
|
||||||
|
<div className={field}>
|
||||||
|
<select className={`${inputCls} [&>option]:text-black`} aria-label="Topic" value={form.topic} onChange={set('topic')}>
|
||||||
|
{topics.map((t) => <option key={t}>{t}</option>)}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className={field}><textarea className={`${inputCls} resize-none`} rows={5} placeholder="What's the problem?" aria-label="Problem description" maxLength={2000} value={form.message} onChange={set('message')} /></div>
|
||||||
|
{err('message')}
|
||||||
|
</div>
|
||||||
|
{serverError && <p role="alert" className="text-red-300 text-sm px-2">{serverError}</p>}
|
||||||
|
<button type="submit" disabled={sending} className="bg-white text-black rounded-full px-8 py-3 text-sm font-medium inline-flex items-center gap-2 disabled:opacity-60">
|
||||||
|
<Send size={16} /> {sending ? 'Sending...' : 'Send request'}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import { Check, ExternalLink, Link2, SearchX, ThumbsDown, ThumbsUp } from 'lucide-react'
|
||||||
|
import type { Faq } from './types'
|
||||||
|
import { load, save } from './storage'
|
||||||
|
|
||||||
|
type Votes = Record<number, 'up' | 'down'>
|
||||||
|
|
||||||
|
function safeUrl(url: string): string | null {
|
||||||
|
try {
|
||||||
|
const u = new URL(url)
|
||||||
|
return u.protocol === 'https:' || u.protocol === 'http:' ? u.href : null
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function FaqCard({ f, forceOpen, vote, onVote }: { f: Faq; forceOpen: boolean; vote?: 'up' | 'down'; onVote: (v: 'up' | 'down') => void }) {
|
||||||
|
const [copied, setCopied] = useState(false)
|
||||||
|
const href = f.link ? safeUrl(f.link) : null
|
||||||
|
|
||||||
|
const copyLink = async () => {
|
||||||
|
const url = `${location.origin}${location.pathname}#faq-${f.id}`
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(url)
|
||||||
|
setCopied(true)
|
||||||
|
window.setTimeout(() => setCopied(false), 2000)
|
||||||
|
} catch {
|
||||||
|
location.hash = `faq-${f.id}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const voteBtn = (v: 'up' | 'down', label: string, Icon: typeof ThumbsUp) => (
|
||||||
|
<button
|
||||||
|
onClick={() => onVote(v)}
|
||||||
|
aria-pressed={vote === v}
|
||||||
|
className={`liquid-glass rounded-full px-4 py-2 inline-flex items-center gap-2 text-sm transition-colors ${
|
||||||
|
vote === v ? 'bg-white/20 text-white' : 'text-white/70 hover:text-white hover:bg-white/5'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon size={14} /> {label}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li id={`faq-${f.id}`} className="scroll-mt-6">
|
||||||
|
<details className="liquid-glass rounded-3xl group" open={forceOpen || undefined}>
|
||||||
|
<summary className="cursor-pointer list-none px-6 py-5 flex items-start justify-between gap-4 text-white">
|
||||||
|
<span>
|
||||||
|
<span className="block text-xs uppercase tracking-wider text-white/50 mb-1">{f.category}</span>
|
||||||
|
<span className="text-lg">{f.question}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-white/60 transition-transform group-open:rotate-45 text-2xl leading-none">+</span>
|
||||||
|
</summary>
|
||||||
|
<div className="px-6 pb-6 text-white/80 text-sm leading-relaxed">
|
||||||
|
<p>{f.answer}</p>
|
||||||
|
{href && (
|
||||||
|
<a href={href} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-2 mt-4 text-white hover:underline">
|
||||||
|
{f.linkLabel || 'Official guide'} <ExternalLink size={14} />
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
<div className="mt-5 flex flex-wrap items-center gap-2">
|
||||||
|
<span className="text-white/50 mr-1">Was this helpful?</span>
|
||||||
|
{voteBtn('up', 'Yes', ThumbsUp)}
|
||||||
|
{voteBtn('down', 'No', ThumbsDown)}
|
||||||
|
<button
|
||||||
|
onClick={copyLink}
|
||||||
|
className="liquid-glass rounded-full px-4 py-2 inline-flex items-center gap-2 text-sm text-white/70 hover:text-white hover:bg-white/5 transition-colors ml-auto"
|
||||||
|
>
|
||||||
|
{copied ? <Check size={14} /> : <Link2 size={14} />} {copied ? 'Link copied' : 'Copy link'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{vote === 'down' && (
|
||||||
|
<p className="mt-3 text-white/60">
|
||||||
|
Sorry about that. <a href="#contact" className="text-white underline">Contact IT</a> and we'll help directly.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function FaqList({ items }: { items: Faq[] }) {
|
||||||
|
const [votes, setVotes] = useState<Votes>(() => load<Votes>('faq-votes', {}))
|
||||||
|
const [hashId, setHashId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const read = () => {
|
||||||
|
const m = /^#faq-(\d+)$/.exec(location.hash)
|
||||||
|
setHashId(m ? Number(m[1]) : null)
|
||||||
|
if (m) window.setTimeout(() => document.getElementById(`faq-${m[1]}`)?.scrollIntoView({ behavior: 'smooth' }), 50)
|
||||||
|
}
|
||||||
|
read()
|
||||||
|
window.addEventListener('hashchange', read)
|
||||||
|
return () => window.removeEventListener('hashchange', read)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onVote = (id: number, v: 'up' | 'down') => {
|
||||||
|
const next = { ...votes, [id]: v }
|
||||||
|
setVotes(next)
|
||||||
|
save('faq-votes', next)
|
||||||
|
fetch('/api/votes', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ faqId: id, vote: v }) }).catch(() => {})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="liquid-glass rounded-3xl p-10 text-center text-white/70">
|
||||||
|
<SearchX className="mx-auto mb-3" size={28} />
|
||||||
|
<p>No answers found. Try different words, or <a href="#contact" className="text-white underline">contact IT</a>.</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{items.map((f) => (
|
||||||
|
<FaqCard key={f.id} f={f} forceOpen={hashId === f.id} vote={votes[f.id]} onVote={(v) => onVote(f.id, v)} />
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react'
|
||||||
|
import { LogOut } from 'lucide-react'
|
||||||
|
|
||||||
|
interface Ticket { id: string; name: string; email: string; topic: string; message: string; status: string; emailed: number; created: string }
|
||||||
|
interface StaffFaq { id: number; category: string; question: string; answer: string; link: string; linkLabel: string; up: number; down: number }
|
||||||
|
interface Member { id: number; email: string; name: string }
|
||||||
|
type Tab = 'tickets' | 'faqs' | 'team'
|
||||||
|
|
||||||
|
async function api<T = unknown>(path: string, method = 'GET', body?: unknown): Promise<T> {
|
||||||
|
const res = await fetch(`/api${path}`, { method, headers: body ? { 'Content-Type': 'application/json' } : undefined, body: body ? JSON.stringify(body) : undefined })
|
||||||
|
const data = await res.json().catch(() => ({}))
|
||||||
|
if (!res.ok) throw Object.assign(new Error(data.error || 'Request failed'), { status: res.status })
|
||||||
|
return data as T
|
||||||
|
}
|
||||||
|
|
||||||
|
const box = 'liquid-glass rounded-3xl p-6 text-white'
|
||||||
|
const input = 'w-full bg-white/5 rounded-2xl px-4 py-3 text-white placeholder:text-white/40 outline-none'
|
||||||
|
const btn = 'liquid-glass rounded-full px-5 py-2 text-sm text-white hover:bg-white/5 transition-colors'
|
||||||
|
|
||||||
|
function Login({ onDone }: { onDone: () => void }) {
|
||||||
|
const [email, setEmail] = useState('')
|
||||||
|
const [password, setPassword] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const submit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try { await api('/login', 'POST', { email, password }); onDone() } catch (err) { setError((err as Error).message) }
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<form onSubmit={submit} className={`${box} max-w-sm mx-auto space-y-3`}>
|
||||||
|
<h1 className="text-3xl mb-2" style={{ fontFamily: "'Instrument Serif', serif" }}>Staff login</h1>
|
||||||
|
<input className={input} type="email" placeholder="Email" aria-label="Email" value={email} onChange={(e) => setEmail(e.target.value)} autoComplete="username" />
|
||||||
|
<input className={input} type="password" placeholder="Password" aria-label="Password" value={password} onChange={(e) => setPassword(e.target.value)} autoComplete="current-password" />
|
||||||
|
{error && <p role="alert" className="text-red-300 text-sm">{error}</p>}
|
||||||
|
<button type="submit" className="bg-white text-black rounded-full px-6 py-2 text-sm font-medium">Log in</button>
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Tickets() {
|
||||||
|
const [rows, setRows] = useState<Ticket[]>([])
|
||||||
|
const load = useCallback(() => api<Ticket[]>('/staff/tickets').then(setRows), [])
|
||||||
|
useEffect(() => { load() }, [load])
|
||||||
|
const setStatus = async (id: string, status: string) => { await api(`/staff/tickets/${id}`, 'PATCH', { status }); load() }
|
||||||
|
if (!rows.length) return <p className="text-white/60">No tickets yet.</p>
|
||||||
|
return (
|
||||||
|
<ul className="space-y-3">
|
||||||
|
{rows.map((t) => (
|
||||||
|
<li key={t.id} className={box}>
|
||||||
|
<div className="flex flex-wrap justify-between gap-2 text-sm text-white/60">
|
||||||
|
<span>{t.id} · {t.topic} · {new Date(t.created).toLocaleString()}</span>
|
||||||
|
<span>{t.emailed ? 'emailed' : 'not emailed'}</span>
|
||||||
|
</div>
|
||||||
|
<p className="mt-2 whitespace-pre-wrap">{t.message}</p>
|
||||||
|
<p className="mt-2 text-sm text-white/70">{t.name} · <a className="underline" href={`mailto:${encodeURIComponent(t.email)}`}>{t.email}</a></p>
|
||||||
|
<select aria-label="Status" value={t.status} onChange={(e) => setStatus(t.id, e.target.value)} className="mt-3 bg-white/10 rounded-full px-4 py-2 text-sm [&>option]:text-black">
|
||||||
|
<option value="open">Open</option><option value="in_progress">In progress</option><option value="closed">Closed</option>
|
||||||
|
</select>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const emptyFaq = { category: '', question: '', answer: '', link: '', linkLabel: '' }
|
||||||
|
function Faqs() {
|
||||||
|
const [rows, setRows] = useState<StaffFaq[]>([])
|
||||||
|
const [editing, setEditing] = useState<(typeof emptyFaq & { id?: number }) | null>(null)
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const load = useCallback(() => api<StaffFaq[]>('/staff/faqs').then(setRows), [])
|
||||||
|
useEffect(() => { load() }, [load])
|
||||||
|
const save = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
if (!editing) return
|
||||||
|
try {
|
||||||
|
if (editing.id) await api(`/staff/faqs/${editing.id}`, 'PUT', editing)
|
||||||
|
else await api('/staff/faqs', 'POST', editing)
|
||||||
|
setEditing(null); setError(''); load()
|
||||||
|
} catch (err) { setError((err as Error).message) }
|
||||||
|
}
|
||||||
|
const remove = async (f: StaffFaq) => { if (confirm(`Delete "${f.question}"?`)) { await api(`/staff/faqs/${f.id}`, 'DELETE'); load() } }
|
||||||
|
const set = (k: keyof typeof emptyFaq) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => editing && setEditing({ ...editing, [k]: e.target.value })
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{editing ? (
|
||||||
|
<form onSubmit={save} className={`${box} space-y-3`}>
|
||||||
|
<input className={input} placeholder="Category" aria-label="Category" value={editing.category} onChange={set('category')} />
|
||||||
|
<input className={input} placeholder="Question" aria-label="Question" value={editing.question} onChange={set('question')} />
|
||||||
|
<textarea className={input} rows={5} placeholder="Answer" aria-label="Answer" value={editing.answer} onChange={set('answer')} />
|
||||||
|
<input className={input} placeholder="Link (optional, https://...)" aria-label="Link" value={editing.link} onChange={set('link')} />
|
||||||
|
<input className={input} placeholder="Link label (optional)" aria-label="Link label" value={editing.linkLabel} onChange={set('linkLabel')} />
|
||||||
|
{error && <p role="alert" className="text-red-300 text-sm">{error}</p>}
|
||||||
|
<div className="flex gap-2"><button type="submit" className="bg-white text-black rounded-full px-6 py-2 text-sm font-medium">Save</button><button type="button" className={btn} onClick={() => { setEditing(null); setError('') }}>Cancel</button></div>
|
||||||
|
</form>
|
||||||
|
) : <button className={btn} onClick={() => setEditing({ ...emptyFaq })}>+ Add FAQ</button>}
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{rows.map((f) => (
|
||||||
|
<li key={f.id} className={`${box} flex flex-wrap items-center justify-between gap-3 !py-4`}>
|
||||||
|
<span><span className="block text-xs text-white/50">{f.category}</span>{f.question}</span>
|
||||||
|
<span className="flex items-center gap-3 text-sm text-white/70">
|
||||||
|
👍 {f.up} 👎 {f.down}
|
||||||
|
<button className={btn} onClick={() => setEditing(f)}>Edit</button>
|
||||||
|
<button className={btn} onClick={() => remove(f)}>Delete</button>
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Team({ me }: { me: string }) {
|
||||||
|
const [rows, setRows] = useState<Member[]>([])
|
||||||
|
const [f, setF] = useState({ name: '', email: '', password: '' })
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const load = useCallback(() => api<Member[]>('/staff/team').then(setRows), [])
|
||||||
|
useEffect(() => { load() }, [load])
|
||||||
|
const add = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
try { await api('/staff/team', 'POST', f); setF({ name: '', email: '', password: '' }); setError(''); load() } catch (err) { setError((err as Error).message) }
|
||||||
|
}
|
||||||
|
const remove = async (m: Member) => { if (confirm(`Remove ${m.name}?`)) { try { await api(`/staff/team/${m.id}`, 'DELETE'); load() } catch (err) { setError((err as Error).message) } } }
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{rows.map((m) => (
|
||||||
|
<li key={m.id} className={`${box} flex items-center justify-between !py-4`}>
|
||||||
|
<span>{m.name} <span className="text-white/60 text-sm">· {m.email}</span></span>
|
||||||
|
{m.email !== me && <button className={btn} onClick={() => remove(m)}>Remove</button>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<form onSubmit={add} className={`${box} space-y-3`}>
|
||||||
|
<h2 className="text-xl">Add a team member</h2>
|
||||||
|
<input className={input} placeholder="Name" aria-label="Name" value={f.name} onChange={(e) => setF({ ...f, name: e.target.value })} />
|
||||||
|
<input className={input} type="email" placeholder="Email" aria-label="Email" value={f.email} onChange={(e) => setF({ ...f, email: e.target.value })} />
|
||||||
|
<input className={input} type="password" placeholder="Temporary password (10+ characters)" aria-label="Password" value={f.password} onChange={(e) => setF({ ...f, password: e.target.value })} autoComplete="new-password" />
|
||||||
|
{error && <p role="alert" className="text-red-300 text-sm">{error}</p>}
|
||||||
|
<button type="submit" className="bg-white text-black rounded-full px-6 py-2 text-sm font-medium">Add</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Staff() {
|
||||||
|
const [me, setMe] = useState<{ name: string; email: string } | null | undefined>(undefined)
|
||||||
|
const [emailOk, setEmailOk] = useState(true)
|
||||||
|
const [tab, setTab] = useState<Tab>('tickets')
|
||||||
|
const check = useCallback(() => {
|
||||||
|
api<{ name: string; email: string }>('/me').then((m) => { setMe(m); api<{ emailConfigured: boolean }>('/staff/status').then((s) => setEmailOk(s.emailConfigured)) }).catch(() => setMe(null))
|
||||||
|
}, [])
|
||||||
|
useEffect(() => { check() }, [check])
|
||||||
|
const logout = async () => { await api('/logout', 'POST'); setMe(null) }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-black text-white px-6 py-10">
|
||||||
|
<div className="max-w-3xl mx-auto">
|
||||||
|
<div className="flex items-center justify-between mb-8">
|
||||||
|
<a href="#" className="text-white/70 hover:text-white text-sm">← Back to site</a>
|
||||||
|
{me && <button onClick={logout} className={`${btn} inline-flex items-center gap-2`}><LogOut size={14} /> Log out ({me.name})</button>}
|
||||||
|
</div>
|
||||||
|
{me === undefined ? <p className="text-white/60">Loading...</p> : me === null ? <Login onDone={check} /> : (
|
||||||
|
<>
|
||||||
|
{!emailOk && <p className="mb-4 text-amber-300 text-sm">Email is not set up on the server yet, so new tickets are saved here but not emailed.</p>}
|
||||||
|
<div className="flex gap-2 mb-6">
|
||||||
|
{(['tickets', 'faqs', 'team'] as Tab[]).map((t) => (
|
||||||
|
<button key={t} onClick={() => setTab(t)} aria-pressed={tab === t} className={`${btn} capitalize ${tab === t ? 'bg-white/20' : ''}`}>{t === 'faqs' ? 'FAQs' : t}</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{tab === 'tickets' && <Tickets />}{tab === 'faqs' && <Faqs />}{tab === 'team' && <Team me={me.email} />}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
|
const VIDEO_URL =
|
||||||
|
'https://d8j0ntlcm91z4.cloudfront.net/user_38xzZboKViGWJOttwIXH07lWA1P/hf_20260328_115001_bcdaa3b4-03de-47e7-ad63-ae3e392c32d4.mp4'
|
||||||
|
const FADE_MS = 500
|
||||||
|
const FADE_OUT_REMAINING = 0.55
|
||||||
|
|
||||||
|
export default function VideoBackground() {
|
||||||
|
const videoRef = useRef<HTMLVideoElement>(null)
|
||||||
|
const rafRef = useRef<number | null>(null)
|
||||||
|
const fadingOutRef = useRef(false)
|
||||||
|
|
||||||
|
const fadeTo = useCallback((target: number) => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
|
||||||
|
const startOpacity = parseFloat(video.style.opacity || '0')
|
||||||
|
const startTime = performance.now()
|
||||||
|
const step = (now: number) => {
|
||||||
|
const t = Math.min((now - startTime) / FADE_MS, 1)
|
||||||
|
video.style.opacity = String(startOpacity + (target - startOpacity) * t)
|
||||||
|
rafRef.current = t < 1 ? requestAnimationFrame(step) : null
|
||||||
|
}
|
||||||
|
rafRef.current = requestAnimationFrame(step)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => () => {
|
||||||
|
if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const handleTimeUpdate = () => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video || !video.duration || fadingOutRef.current) return
|
||||||
|
if (video.duration - video.currentTime <= FADE_OUT_REMAINING) {
|
||||||
|
fadingOutRef.current = true
|
||||||
|
fadeTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEnded = () => {
|
||||||
|
const video = videoRef.current
|
||||||
|
if (!video) return
|
||||||
|
video.style.opacity = '0'
|
||||||
|
window.setTimeout(() => {
|
||||||
|
video.currentTime = 0
|
||||||
|
void video.play().catch(() => {})
|
||||||
|
fadingOutRef.current = false
|
||||||
|
fadeTo(1)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video
|
||||||
|
ref={videoRef}
|
||||||
|
src={VIDEO_URL}
|
||||||
|
autoPlay
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
style={{ opacity: 0 }}
|
||||||
|
className="absolute inset-0 w-full h-full object-cover translate-y-[17%]"
|
||||||
|
onLoadedData={() => fadeTo(1)}
|
||||||
|
onTimeUpdate={handleTimeUpdate}
|
||||||
|
onEnded={handleEnded}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,698 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "I forgot my password",
|
||||||
|
"answer": "Most organisations have a self-service reset link on the sign-in page, usually 'Forgot password' or 'Can't access your account'. You'll be asked to verify your identity, normally with a code sent to a backup email or phone number you registered earlier. If there's no self-service option, or verification fails, your IT service desk can reset it after confirming your identity another way. Keep any backup codes you were given when MFA was set up somewhere safe, they're often the fastest way back in.",
|
||||||
|
"link": "https://support.microsoft.com/en-us/account-billing/how-to-reset-your-microsoft-account-password-eff4f067-5042-c1a3-fe72-b04d60556c37",
|
||||||
|
"linkLabel": "Microsoft: reset your password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "I'm locked out after too many failed attempts",
|
||||||
|
"answer": "This is a security feature, not a fault, it stops someone guessing your password repeatedly. Most systems clear the lockout automatically after a set time, often 15 to 30 minutes, so waiting is sometimes the fastest fix. If it's urgent, or the lockout doesn't clear itself, your IT service desk can unlock the account manually once they've verified it's really you.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "My multi-factor login (authenticator app) isn't working",
|
||||||
|
"answer": "If you've lost your phone or the authenticator app stopped generating valid codes, try a saved backup code first, most systems issue a set of one-time codes when MFA is first set up. If you don't have one, contact IT to re-register your MFA device, this normally requires an identity check (staff or student ID, or a manager confirming it's you) since it's effectively resetting your account security.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "My account is disabled or expired after returning from leave",
|
||||||
|
"answer": "Accounts are sometimes automatically disabled after a period of inactivity, or a leave period that wasn't flagged as 'returning' in the system. This is an account status issue in the directory (Active Directory or similar), not a password problem, ask IT to check whether the account itself is disabled or expired rather than resetting the password again.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "Single sign-on (SSO) keeps redirecting in a loop",
|
||||||
|
"answer": "An SSO loop, bouncing between your organisation's login page and the app without ever landing you in, is usually a cookie or session problem rather than your password. Try a private or incognito browser window first, if that works cleanly, clear cookies for your organisation's login domain. If it still loops, the connection between your identity provider and that specific app may need IT to check its configuration.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 6,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "A guest or contractor account isn't working",
|
||||||
|
"answer": "Guest and contractor accounts usually go through a separate approval process, often needing an internal sponsor, and typically expire automatically after a set period for security. If a guest account stops working, check with the person who sponsored the access first, then IT if they confirm it should still be active.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 7,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "My security questions aren't being accepted during recovery",
|
||||||
|
"answer": "Recovery answers need to match exactly what was originally entered, including capitalisation in some systems, so a common cause is simply misremembering the exact wording. If you've genuinely forgotten the answers, look for a 'try another way' option instead of repeatedly guessing, most recovery tools offer an alternative path such as an email or SMS code.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 8,
|
||||||
|
"category": "Access & logins",
|
||||||
|
"question": "A new password keeps being rejected as too weak or previously used",
|
||||||
|
"answer": "Organisations enforce password rules (minimum length, a mix of character types, and often a block on reusing recent passwords) to reduce the risk of a compromised account. A longer passphrase, three or four random unrelated words plus a number and symbol, is both stronger and easier to remember than a short complex one, and tends to pass these checks more easily. Avoid names, birthdays or the organisation's own name.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 9,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "My wifi keeps dropping",
|
||||||
|
"answer": "Try moving closer to the access point, restarting your device's wifi, and checking if other devices nearby have the same problem, that tells you whether it's your device or the network. On a corporate or school network with certificate-based login (802.1x), an expired certificate is a common cause, IT can reissue it. A network adapter driver that's out of date after a Windows update is another very common cause worth ruling out.",
|
||||||
|
"link": "https://support.microsoft.com/en-us/windows/fix-wi-fi-connection-issues-in-windows-9424a1f7-6a3b-65a6-4d78-7f07eee84d2c",
|
||||||
|
"linkLabel": "Microsoft: fix Wi-Fi connection issues"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 10,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "The VPN won't connect",
|
||||||
|
"answer": "Check your normal internet connection works first. Then fully close and reopen the VPN app rather than just reconnecting, a half-open session is a common cause. Public wifi (cafes, hotels) sometimes blocks VPN traffic entirely until you've accepted its own login page in a browser first.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 11,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "I can't reach a shared drive or folder",
|
||||||
|
"answer": "Confirm you're connected to the VPN or office network, shared drives usually don't work from the open internet. If it still fails, the folder permissions may have changed, ask IT to check your access rather than repeatedly trying to reconnect.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 12,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "I'm getting an IP conflict or 'limited connectivity' message",
|
||||||
|
"answer": "This usually means your device didn't get a valid address from the network's DHCP server, often after being moved between networks or waking from sleep. Disconnecting and reconnecting to wifi, or running `ipconfig /release` then `ipconfig /renew` on Windows, fixes most cases. If it keeps happening on the same device, report it, it can indicate a DHCP address pool running low.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 13,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "A captive portal (hotel or cafe wifi login page) won't load",
|
||||||
|
"answer": "If the login page doesn't appear automatically, try opening a plain, non-secure website, some networks only trigger the portal on unencrypted traffic. A corporate VPN client often blocks the portal from appearing at all, disconnect the VPN first, log in to the wifi, then reconnect the VPN.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 14,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "Wifi shows connected but the internet is very slow",
|
||||||
|
"answer": "Being 'connected' only confirms a local network link, not that everything beyond it is fast. Run a speed test, then check whether it's isolated to one device (a device or driver issue) or affects everyone nearby, which usually points to the access point, the internet link itself, or a bandwidth-heavy device worth reporting to IT.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 15,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "A mapped network drive keeps failing after a password change",
|
||||||
|
"answer": "A mapped drive often stores the old credentials separately, so it can keep failing silently even though you can log in to everything else fine. Disconnect the mapped drive and reconnect it fresh, entering the new password when prompted, rather than relying on the saved ones.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 16,
|
||||||
|
"category": "Network & wifi",
|
||||||
|
"question": "A Bluetooth device won't pair",
|
||||||
|
"answer": "Turn Bluetooth off and on again on both devices, and make sure the device is actually in pairing mode. If it was previously paired with a different computer, 'forget' or remove the old pairing on both ends first, most Bluetooth accessories only hold a limited number of paired connections at once.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 17,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "The printer won't print",
|
||||||
|
"answer": "Check it's powered on, has paper and toner, and that you picked the right printer name in the print dialog. Network printers can show as 'offline' if they've lost wifi, turning them off and on again often fixes this.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 18,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "An external monitor isn't detected",
|
||||||
|
"answer": "Check the cable is firmly in at both ends and try a different port or cable if you have one. Then open your display settings and click 'Detect'. A driver or graphics update can also help on older laptops.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 19,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "A USB device isn't recognised",
|
||||||
|
"answer": "Try a different USB port, and a different cable if it's detachable. If it still doesn't show up, check Device Manager (Windows) or System Report (Mac) for a warning icon next to the device.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 20,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "A laptop won't turn on at all",
|
||||||
|
"answer": "First rule out a flat battery, plug in the charger, check for a charging light, then leave it 15 to 30 minutes before trying again, some laptops won't start with a completely dead battery even while charging. If there's a charging light but still nothing on screen, try an external monitor, that tells you whether it's a screen problem or the laptop genuinely isn't starting.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 21,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "The battery is draining fast or won't charge",
|
||||||
|
"answer": "Test the charger and cable on a different device or outlet first, to rule out a faulty charger rather than the laptop itself. Battery health does genuinely fade over 2 to 4 years of daily use, if the laptop is older and the drop was gradual, a battery replacement through IT asset management is often the real fix rather than a software setting.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 22,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "The keyboard or trackpad isn't responding",
|
||||||
|
"answer": "Restart the device first, a stuck driver is a common and simple cause. If only specific keys are affected, check for anything physically stuck underneath, common after spills or crumbs, and try an external USB keyboard or mouse to confirm whether it's the built-in hardware or a wider fault.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 23,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "A docking station isn't detecting monitors or peripherals",
|
||||||
|
"answer": "Confirm the dock itself has power, many need their own power cable, not just the laptop cable, and try unplugging the laptop from the dock and reconnecting it firmly. Docks that connect via USB-C or Thunderbolt can be picky about cable quality, swap the cable before assuming the dock has failed.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 24,
|
||||||
|
"category": "Hardware",
|
||||||
|
"question": "The webcam isn't detected by any app",
|
||||||
|
"answer": "Some laptops have a physical privacy shutter or a function-key toggle that disables the camera entirely, check for both before assuming it's broken. Checking Device Manager (Windows) or System Information (Mac) shows whether the operating system sees the camera at all, if it's not listed there, it's a driver or hardware fault rather than an app setting.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 25,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "My computer is running very slow",
|
||||||
|
"answer": "Restart it first, this clears a surprising number of slowdowns. After that, check how much storage is free and close programs you're not using that start automatically. If it's still slow, let IT know, it could be a background update or a bigger issue.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 26,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "An app keeps crashing or won't open",
|
||||||
|
"answer": "Close and reopen it, then restart your computer if that doesn't help. Check whether an update is available. If it's an approved work app that IT installed for you, they may need to reinstall it rather than you doing it yourself.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 27,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "My email isn't syncing or I'm missing messages",
|
||||||
|
"answer": "Check your internet connection and your junk or spam folder. Also check your mailbox isn't full, a full mailbox silently stops receiving mail in some systems. Outlook and Gmail both have a 'repair account' option worth trying.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 28,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "My camera isn't working in Microsoft Teams",
|
||||||
|
"answer": "Close other apps that might already be using the camera (like Zoom or FaceTime), unplug and reconnect it if it's external, and restart the device. Make a test call from Teams' device settings to check camera, mic and speaker together in one go.",
|
||||||
|
"link": "https://support.microsoft.com/en-US/teams/meetings/my-camera-isn-t-working-in-microsoft-teams",
|
||||||
|
"linkLabel": "Microsoft: Teams camera troubleshooting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 29,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "Zoom audio or video isn't working",
|
||||||
|
"answer": "Check the correct microphone, speaker and camera are selected inside Zoom's own settings, not just that your device isn't muted at the operating system level. If it's a browser-based meeting, the browser also needs camera and microphone permission granted specifically for that site.",
|
||||||
|
"link": "https://support.zoom.us/",
|
||||||
|
"linkLabel": "Zoom support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 30,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "Windows Update is stuck or won't install",
|
||||||
|
"answer": "Restart the device once and let it sit for at least 30 minutes, some updates genuinely take a long time on the 'working on updates' screen, and interrupting them can cause more problems than waiting. If it's still stuck well beyond that, or fails repeatedly, note the exact error code for IT, the built-in update troubleshooter resolves a lot of common stuck updates on its own.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 31,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "Office or Microsoft 365 shows 'unlicensed product' or won't activate",
|
||||||
|
"answer": "This usually means the app can't confirm your licence with Microsoft's servers, often because you're signed in with the wrong account, or you've been offline too long since the last successful check-in. Sign out and back in with your work or school account specifically, not a personal Microsoft account, and make sure you're online or on VPN when it tries to reactivate.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 32,
|
||||||
|
"category": "Software & apps",
|
||||||
|
"question": "'Access denied' or 'permission denied' opening a file",
|
||||||
|
"answer": "This is almost always a permissions setting on the file or folder itself, not a fault with your computer. Confirm you're opening it from the right location, a synced local copy can behave differently to the shared original, and if you believe you should have access, ask the file's owner or IT to check the permission list rather than trying to force it open.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 33,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "I received a suspicious email, is it phishing?",
|
||||||
|
"answer": "Don't click links or open attachments. Check the sender's actual email address, not just the display name, and look for urgency or requests for passwords or gift cards, common phishing signs. Report it to your IT security team rather than deleting it, they may need to warn others across the organisation.",
|
||||||
|
"link": "https://www.cyber.gov.au/report-and-recover",
|
||||||
|
"linkLabel": "ACSC: report a cybercrime (ReportCyber)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 34,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "My computer crashed or showed a blue screen",
|
||||||
|
"answer": "Note down any error code or message shown before it restarts, a photo on your phone is fine. Restart the computer. If it happens more than once, that error code is exactly what IT will need to diagnose it.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 35,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "I think I've been hit by ransomware",
|
||||||
|
"answer": "If files suddenly become inaccessible with strange new extensions, or a ransom note appears, disconnect the device from the network immediately (unplug the cable or turn off wifi) without shutting it down, this can stop it spreading before you power off. Don't pay anything and don't try to remove it yourself, contact IT security immediately, this is treated as a priority incident.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 36,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "I suspect my account has been compromised",
|
||||||
|
"answer": "Change your password immediately from a device you trust, and check for any MFA devices, mail forwarding rules, or app connections you don't recognise, attackers often quietly add a forwarding rule or a second MFA device to keep access even after the password changes. Report it to IT security even after you've changed the password, they may need to check for wider impact.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 37,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "What actually makes a strong password or passphrase",
|
||||||
|
"answer": "Length matters more than complexity, three or four random unrelated words make a passphrase that's both stronger and easier to remember than a short complex one. Never reuse a password across personal and work accounts, and use a password manager rather than trying to remember unique passwords for everything, most organisations provide or approve one.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 38,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "How to report a data breach or accidental data exposure",
|
||||||
|
"answer": "If personal or sensitive information was sent to the wrong person, uploaded somewhere public by mistake, or a device holding such data is lost or stolen, report it to IT and your privacy contact as soon as possible. Timing genuinely matters for breach response, and in government and other regulated sectors there can be mandatory notification obligations attached.",
|
||||||
|
"link": "https://www.cyber.gov.au/report-and-recover",
|
||||||
|
"linkLabel": "ACSC: report and recover"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 39,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "Windows is asking for a BitLocker recovery key",
|
||||||
|
"answer": "This usually appears after a hardware or firmware change, like a BIOS update, a new battery, or a changed boot order, that the encryption chip treats as suspicious, it's the protection working as intended, not a sign of a hack. The 48-digit recovery key is normally stored in your organisation's directory (Azure AD/Intune) or was saved separately when the device was set up, IT can retrieve it if you don't have it yourself. Microsoft itself cannot recover a lost key, so if neither you nor IT can locate it, the drive has to be reset.",
|
||||||
|
"link": "https://support.microsoft.com/en-us/help/4026181",
|
||||||
|
"linkLabel": "Microsoft: find your BitLocker recovery key"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 40,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "A Mac is asking for a FileVault recovery key",
|
||||||
|
"answer": "Similar to Windows BitLocker, this appears after certain hardware or firmware changes and is the encryption protecting your data working as designed. The recovery key is usually escrowed with your organisation's MDM (Jamf or similar), IT can retrieve it if it wasn't saved when the device was set up.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 41,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "Antivirus or endpoint protection just quarantined a file or blocked something",
|
||||||
|
"answer": "Don't try to restore or override it yourself, even if you're confident the file is safe, false positives do happen but confirming that is IT security's job, not yours. Report exactly what you were doing and what the alert said, that detail is what determines whether it's a false positive or something needing wider action.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 42,
|
||||||
|
"category": "Security",
|
||||||
|
"question": "A USB drive or device is blocked by policy",
|
||||||
|
"answer": "Many organisations block USB storage devices entirely as a data-loss-prevention measure, this is deliberate policy rather than a fault, and it usually can't be worked around with a different port or drive. If you have a genuine business need to transfer files this way, request an exception through IT rather than trying another device.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 43,
|
||||||
|
"category": "Sector notes",
|
||||||
|
"question": "Why are some settings locked on my device?",
|
||||||
|
"answer": "Government, school and many corporate devices are deliberately locked down for security, this is policy, not a fault. Australian government agencies generally follow the ACSC's Essential Eight security controls, which is why things like installing your own software or changing certain settings are restricted to IT staff.",
|
||||||
|
"link": "https://www.cyber.gov.au/resources-business-and-government/essential-cybersecurity/essential-eight",
|
||||||
|
"linkLabel": "ACSC: Essential Eight explained"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 44,
|
||||||
|
"category": "Sector notes",
|
||||||
|
"question": "School: why is a legitimate website or resource blocked?",
|
||||||
|
"answer": "Australian schools typically apply internet content filtering as required by state and territory education department policy, and manage student devices through a mobile device management (MDM) system. If a legitimate educational site is blocked, that's a filtering rule, not a fault, report it to the school's IT support so they can review or whitelist it rather than trying to work around the filter.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 45,
|
||||||
|
"category": "Sector notes",
|
||||||
|
"question": "Government: why can't I install software myself?",
|
||||||
|
"answer": "Restricting who can install software (application control) is one of the Essential Eight strategies Australian government agencies are expected to implement, it stops unapproved or malicious software running on government devices. If you need software for a work purpose, the usual path is a request to IT for review and installation through an approved, managed process.",
|
||||||
|
"link": "https://www.cyber.gov.au/resources-business-and-government/essential-cybersecurity/essential-eight",
|
||||||
|
"linkLabel": "ACSC: Essential Eight explained"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 46,
|
||||||
|
"category": "Sector notes",
|
||||||
|
"question": "Corporate: why can't I do everything on my personal (BYOD) device?",
|
||||||
|
"answer": "A personal device enrolled for work access (BYOD) is usually only given a limited, contained work profile rather than full management of the whole device, protecting both your personal data and the organisation's. If a work app or file won't open, or copy-paste out of a work app is blocked, that's often a deliberate BYOD policy restriction rather than a technical fault.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 47,
|
||||||
|
"category": "How IT support works",
|
||||||
|
"question": "What do Level 1, 2, 3 (and 4) support actually mean?",
|
||||||
|
"answer": "Level 1 is the frontline help desk, handling common self-service issues like this page's Access, Wifi and Hardware topics, usually resolved in one call or ticket. Level 2 is more technical staff who handle things needing deeper diagnosis, like the Network Hardware topics here. Level 3 is specialist or senior engineers, often involved in design and complex incidents, and Level 4 is the vendor's own support (Cisco TAC, Fortinet support), brought in when the issue is inside the product itself, not the configuration.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 48,
|
||||||
|
"category": "How IT support works",
|
||||||
|
"question": "When should something be escalated to the next level?",
|
||||||
|
"answer": "As a general rule, escalate when the fix needs a permission, tool, or specialist knowledge the current level doesn't have, not simply because an issue is taking a long time. A good escalation includes what's already been tried, exact error messages, and which users or systems are affected, that context is what actually speeds up the next level's diagnosis.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 49,
|
||||||
|
"category": "How IT support works",
|
||||||
|
"question": "What's the difference between an incident, a request, and a problem?",
|
||||||
|
"answer": "An incident is something broken right now (the printer isn't printing, the VPN is down), the goal is to restore service quickly. A request is asking for something new (new software, more storage, account access), it follows an approval process rather than a fix. A problem is the underlying root cause behind repeated incidents, if the same printer fails every Monday, that pattern gets investigated as a problem so it stops recurring, rather than just fixed each time it happens.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 50,
|
||||||
|
"category": "How IT support works",
|
||||||
|
"question": "Where does a change request fit in?",
|
||||||
|
"answer": "A change request is a planned, deliberate modification to a system, like a firmware upgrade or a new firewall rule, done through an approval process because unplanned changes are one of the most common causes of new incidents. This is why network hardware changes especially are usually scheduled for a maintenance window rather than made on the spot.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 51,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "A phone or tablet won't enrol in the company's mobile device management (MDM)",
|
||||||
|
"answer": "Confirm you're using the exact enrolment method IT specified (a QR code, an enrolment URL, or a specific app store link), a generic 'add work account' option in phone settings sometimes skips steps the proper enrolment method includes. Also check the device's operating system version meets what IT requires, MDM enrolment is commonly blocked on outdated OS versions for security reasons.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 52,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "Work email or apps stopped working on a personal phone after an update",
|
||||||
|
"answer": "A phone OS update can reset app permissions or trigger a compliance re-check by the organisation's MDM, temporarily blocking access until the device is confirmed to still meet security requirements like having a passcode set. Check for a compliance or 'action needed' notification from the company portal app before assuming it's broken.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 53,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "A mobile VPN drops every time the screen locks",
|
||||||
|
"answer": "Many phones aggressively kill background network connections to save battery, including VPN apps, unless the app is specifically excluded from battery optimisation. Check the phone's battery settings for an 'unrestricted' or 'no optimisation' option for that VPN app specifically.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 54,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "A remote desktop (RDP) session freezes or won't connect",
|
||||||
|
"answer": "Confirm you're connected to the VPN first if the remote machine is inside a private network, RDP almost never works over the open internet directly, for good security reasons. If the VPN is connected but it still fails, check whether the remote machine is actually powered on and not asleep, a surprisingly common cause.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 55,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "A VDI session (Citrix, Horizon) is slow or won't launch",
|
||||||
|
"answer": "Test your own internet connection's latency and stability first, VDI performance is very sensitive to a poor home connection in a way a locally installed app isn't. If your connection is fine, try a different VDI client version or a browser-based login as a workaround while reporting the failure to IT with the exact error shown.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 56,
|
||||||
|
"category": "Mobile & remote work",
|
||||||
|
"question": "Working from home, the VPN connects but internal apps still time out",
|
||||||
|
"answer": "Some VPN setups use 'split tunnelling', where only traffic for specific internal systems goes through the VPN and everything else uses your home internet directly, if a newer internal app isn't yet included in that list, it will time out even with the VPN connected. That's a configuration detail for IT to add, not something you can usually fix from your end.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 57,
|
||||||
|
"category": "Cloud storage & sync",
|
||||||
|
"question": "OneDrive is stuck on 'processing changes' or won't sync",
|
||||||
|
"answer": "Pause and resume syncing from the OneDrive icon in the system tray, this resets a stuck sync engine more often than you'd expect. A very long file path, a file open in another program, or special characters in a file name are the next most common causes of a stuck sync. Moving the affected file out of the synced folder and back in again can also clear a single stuck item without resetting the whole sync.",
|
||||||
|
"link": "https://support.microsoft.com/en-us/onedrive/fix-onedrive-sync-problems",
|
||||||
|
"linkLabel": "Microsoft: fix OneDrive sync problems"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 58,
|
||||||
|
"category": "Cloud storage & sync",
|
||||||
|
"question": "Google Drive shows a red exclamation mark on a file",
|
||||||
|
"answer": "This means that specific file failed to sync, usually because it's open in another application, too large for your remaining storage quota, or has a file type or name Drive can't handle. Hover over the icon for the exact reason, then close the file elsewhere or check your storage quota before retrying.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 59,
|
||||||
|
"category": "Cloud storage & sync",
|
||||||
|
"question": "Cloud storage quota is full",
|
||||||
|
"answer": "Check what's actually taking up space first, large video files and old email attachments are common causes, rather than immediately requesting more storage. If you genuinely need more for legitimate work purposes, that's a request rather than an incident (see 'How IT support works'), so it goes through IT's normal approval process.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 60,
|
||||||
|
"category": "Cloud storage & sync",
|
||||||
|
"question": "A shared file link says 'access denied' for someone outside the organisation",
|
||||||
|
"answer": "Many organisations restrict external sharing by default for security, so a link that works fine internally can fail for an outside recipient. Check the specific sharing settings on that file or folder, external sharing is often something that needs to be deliberately enabled per-file or per-site, rather than being on by default.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 61,
|
||||||
|
"category": "Cloud storage & sync",
|
||||||
|
"question": "Files show as 'online-only' and won't open without internet",
|
||||||
|
"answer": "Cloud sync clients often keep infrequently used files as a placeholder to save local disk space, downloading the real content only when opened, which fails without an internet connection. Right-click the specific file or folder you need offline and choose 'always keep on this device' before you lose connectivity, before a flight or a site visit, for example.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 62,
|
||||||
|
"category": "macOS specific",
|
||||||
|
"question": "A Mac update is stuck or won't install",
|
||||||
|
"answer": "Confirm there's enough free disk space, macOS updates need significant free space to stage the install and fail silently without it. Restart once and try again from System Settings rather than clicking install repeatedly, which can leave the updater in a confused state.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 63,
|
||||||
|
"category": "macOS specific",
|
||||||
|
"question": "Time Machine backup keeps failing",
|
||||||
|
"answer": "Check the backup drive isn't full and is actually connected and mounted, an intermittent external drive connection is the most common cause. If backing up to a network location, confirm you can still reach that server or NAS at all, a backup failure is often really a network or permissions issue in disguise.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 64,
|
||||||
|
"category": "macOS specific",
|
||||||
|
"question": "A Mac keeps asking for the keychain password unexpectedly",
|
||||||
|
"answer": "This usually happens after a password change that the login keychain doesn't yet know about, since it's a separate password store from your account login. Go to Keychain Access and update or reset the login keychain's password to match your new account password, rather than repeatedly dismissing the prompt. Resetting the keychain does clear its saved passwords, so only do this if you can't recover the old one.",
|
||||||
|
"link": "https://support.apple.com/guide/keychain-access/kyca2429/mac",
|
||||||
|
"linkLabel": "Apple: update your keychain password"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 65,
|
||||||
|
"category": "macOS specific",
|
||||||
|
"question": "A Mac won't join a corporate wifi network with certificate login",
|
||||||
|
"answer": "Confirm the required certificate is actually installed in the Mac's keychain, it's normally pushed automatically by MDM (Jamf or similar) but can fail to install if the Mac hasn't checked in with MDM recently. Manually forcing an MDM check-in (System Settings > general profile management) before retrying the wifi connection resolves this in many cases.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 66,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A Cisco switch port shows no link light",
|
||||||
|
"answer": "Start at the physical layer: reseat the cable, try a different port, and check for a bad SFP. Then confirm the port isn't administratively shut down with `show interfaces status`, if it is, `no shutdown` brings it back up.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 67,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A Cisco port went err-disabled",
|
||||||
|
"answer": "This means the switch shut the port down itself, usually after a security violation, a BPDU guard trip, or a duplex mismatch. Check the reason with `show interfaces status err-disabled`, fix the underlying cause, then cycle the port with `shutdown` then `no shutdown`.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 68,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Devices can't reach each other across a Cisco VLAN or trunk",
|
||||||
|
"answer": "Confirm the VLAN exists on both switches with `show vlan brief`, then check the trunk with `show interfaces trunk`. A mismatched native VLAN or a VLAN missing from the allowed list is the most common cause.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 69,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A Cisco router or Layer 3 switch can't reach a remote subnet",
|
||||||
|
"answer": "Check `show ip interface brief` to confirm the interface is up, then `show ip route` to confirm a route exists. `ping` and `traceroute` will show exactly where the path breaks.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 70,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A Cisco device is running hot (high CPU)",
|
||||||
|
"answer": "Check `show processes cpu sorted` to see what's using it. A spanning-tree loop or broadcast storm is a common cause on switches, `show spanning-tree` will show repeated topology changes if that's happening.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 71,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiGate isn't passing traffic that looks like it should be allowed",
|
||||||
|
"answer": "Confirm a firewall policy actually matches the source, destination and service, then use `diagnose sniffer packet any 'host x.x.x.x' 4` to see if traffic is arriving, and `diagnose debug flow` to see how the FortiGate is treating it.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 72,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiGate site-to-site IPsec VPN won't come up",
|
||||||
|
"answer": "Phase 1 and Phase 2 settings (encryption, PSK or certificate, proposals) must match exactly on both ends. `diagnose vpn ike log-filter` with `diagnose debug application ike -1` shows exactly where negotiation is failing.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 73,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiGate is slow or unresponsive",
|
||||||
|
"answer": "Check `get system performance status` for CPU, memory and session count, and `diagnose sys top` to see which process is responsible. A nearly-full session table is a common cause on busy units.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 74,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Can't reach the FortiGate admin GUI from a particular network",
|
||||||
|
"answer": "FortiGate can restrict admin access to trusted hosts and specific interfaces. Check the admin account's trusted host settings and confirm the interface you're connecting through has HTTPS/SSH admin access enabled.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 75,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "How to safely back up a Cisco device's configuration",
|
||||||
|
"answer": "Before any change, save the running config to the startup config with `copy running-config startup-config`, then copy it off the device via TFTP or SCP so there's an external copy too. Always confirm the backup completed before making changes.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 76,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "How to safely upgrade Cisco IOS firmware",
|
||||||
|
"answer": "Confirm the new image is supported on the exact hardware model and that there's enough flash and RAM. Copy the image to flash, verify its checksum, set the new boot variable, save the config, then reload in a maintenance window in case it needs rolling back.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 77,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Lost the password to a Cisco device",
|
||||||
|
"answer": "Password recovery needs physical console access and a reboot into ROMMON mode to bypass the startup config temporarily. This is a standard, well-documented process, but the exact key combination differs by model, check the vendor documentation for your specific device first.",
|
||||||
|
"link": "https://www.cisco.com/c/en/us/support/index.html",
|
||||||
|
"linkLabel": "Cisco Support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 78,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A wireless access point won't join the Cisco WLC",
|
||||||
|
"answer": "Check the AP can discover the controller (DHCP option 43, DNS, or same-subnet broadcast), and that the AP's firmware is compatible with the controller's software version, a mismatch is a common cause of a failed join.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 79,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Traffic is blocked on a Cisco ASA despite a matching ACL",
|
||||||
|
"answer": "Check NAT rules aren't exempting or redirecting the traffic unexpectedly, and confirm the ACL is applied in the right direction on the right interface. `packet-tracer` simulates a specific flow and shows exactly which rule drops it.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 80,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "One FortiGate in an HA cluster won't sync",
|
||||||
|
"answer": "Check both units are running the identical firmware version first, a mismatch is the most common HA sync blocker. `diagnose sys ha status` shows the current sync state and any errors.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 81,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiSwitch isn't showing up under FortiGate management",
|
||||||
|
"answer": "FortiGate manages FortiSwitches over FortiLink, confirm the FortiLink interface is correctly configured and that the switch port used for FortiLink hasn't been repurposed for something else.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 82,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiAP won't join its FortiGate",
|
||||||
|
"answer": "Confirm the AP can discover the FortiGate (same subnet, or DHCP option 138/CAPWAP discovery across routed networks) and that the AP and FortiGate firmware are a supported combination.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 83,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "SD-WAN traffic is taking the wrong path on a FortiGate",
|
||||||
|
"answer": "Check the SD-WAN rule order, first match wins, and whether the SLA health checks for that link are actually passing, a failed health check silently removes a path from consideration. `diagnose sys sdwan` shows live link status.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 84,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "A FortiGate says its licence or support contract has expired",
|
||||||
|
"answer": "An expired contract stops signature and firmware updates but doesn't usually stop the firewall passing traffic. Check the exact status with `get system status`, then renew through your reseller or Fortinet, running without updates for long is a real security risk.",
|
||||||
|
"link": "",
|
||||||
|
"linkLabel": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 85,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Where to find full Cisco documentation and command syntax",
|
||||||
|
"answer": "Always confirm exact command syntax against Cisco's own docs, it changes between switch/router models and IOS versions.",
|
||||||
|
"link": "https://www.cisco.com/c/en/us/support/index.html",
|
||||||
|
"linkLabel": "Cisco Support"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 86,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "Where to find full Fortinet documentation and command syntax",
|
||||||
|
"answer": "FortiOS CLI commands and defaults vary by firmware version, the official Fortinet Document Library is the source of truth.",
|
||||||
|
"link": "https://docs.fortinet.com/",
|
||||||
|
"linkLabel": "Fortinet Document Library"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 87,
|
||||||
|
"category": "Network hardware (IT staff)",
|
||||||
|
"question": "An IPsec tunnel looks correctly configured but still won't come up",
|
||||||
|
"answer": "When the config looks right on paper, checking the live IKE negotiation logs tells you more than re-checking the configuration a third time. Fortinet's own support community has a detailed walkthrough of the exact `diagnose` commands and log filters their engineers use for exactly this scenario.",
|
||||||
|
"link": "https://community.fortinet.com/t5/FortiGate/Troubleshooting-Tip-IPsec-VPN-tunnels/ta-p/195955",
|
||||||
|
"linkLabel": "Fortinet Community: IPsec VPN troubleshooting"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&display=swap');
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.liquid-glass {
|
||||||
|
background: rgba(255, 255, 255, 0.01);
|
||||||
|
background-blend-mode: luminosity;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
-webkit-backdrop-filter: blur(4px);
|
||||||
|
border: none;
|
||||||
|
box-shadow: inset 0 1px 1px rgba(255, 255, 255, 0.1);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.liquid-glass::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: inherit;
|
||||||
|
padding: 1.4px;
|
||||||
|
background: linear-gradient(
|
||||||
|
180deg,
|
||||||
|
rgba(255, 255, 255, 0.45) 0%,
|
||||||
|
rgba(255, 255, 255, 0.15) 20%,
|
||||||
|
rgba(255, 255, 255, 0) 40%,
|
||||||
|
rgba(255, 255, 255, 0) 60%,
|
||||||
|
rgba(255, 255, 255, 0.15) 80%,
|
||||||
|
rgba(255, 255, 255, 0.45) 100%
|
||||||
|
);
|
||||||
|
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||||
|
-webkit-mask-composite: xor;
|
||||||
|
mask-composite: exclude;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import App from './App'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<App />
|
||||||
|
</React.StrictMode>
|
||||||
|
)
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Safe wrappers: localStorage can be blocked (private mode), so never let it crash the page.
|
||||||
|
export function load<T>(key: string, fallback: T): T {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(key)
|
||||||
|
return raw ? (JSON.parse(raw) as T) : fallback
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function save(key: string, value: unknown): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(key, JSON.stringify(value))
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface Faq {
|
||||||
|
id: number
|
||||||
|
category: string
|
||||||
|
question: string
|
||||||
|
answer: string
|
||||||
|
link: string
|
||||||
|
linkLabel: string
|
||||||
|
}
|
||||||
Vendored
+1
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{ts,tsx}'],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"isolatedModules": true
|
||||||
|
},
|
||||||
|
"include": ["src", "vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
{"root":["./src/App.tsx","./src/ContactForm.tsx","./src/Faq.tsx","./src/Staff.tsx","./src/VideoBackground.tsx","./src/main.tsx","./src/storage.ts","./src/types.ts","./src/vite-env.d.ts","./vite.config.ts"],"version":"5.6.3"}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
base: './',
|
||||||
|
server: { proxy: { '/api': 'http://localhost:3001' } },
|
||||||
|
})
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "cycle-atlas-root",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Root wrapper so hosts can build and start the IT Support site in it-support/",
|
||||||
|
"engines": { "node": ">=22.13" },
|
||||||
|
"scripts": {
|
||||||
|
"build": "npm --prefix it-support install --include=dev && npm --prefix it-support run build",
|
||||||
|
"start": "NODE_ENV=production node it-support/server/index.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
.hyperframes/artifacts/
|
|
||||||
node_modules/
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
<!--
|
|
||||||
Daisy Days — caption skin (preset-local source).
|
|
||||||
|
|
||||||
This is the preset's own lower-third karaoke caption look. product-launch-video's
|
|
||||||
Step 2 copies it into the project as caption-skin.html; captions.mjs then fills the
|
|
||||||
three reserved holes, wraps the fragment in a <template>, and writes
|
|
||||||
compositions/captions.html:
|
|
||||||
· the empty GROUPS array → the engine's scene-aware word groups
|
|
||||||
· the 0 duration + 0 width/height → real total + canvas size
|
|
||||||
· the empty data-brand-tokens style → :root tokens derived from the project's frame.md
|
|
||||||
|
|
||||||
Token contract — reference ONLY this fixed vocab (captions.mjs injects it from
|
|
||||||
frame.md, so the Step-2 brand overlay flows through; each var has a daisy-days literal
|
|
||||||
fallback for standalone preview):
|
|
||||||
--cap-ink · --cap-canvas · --cap-accent · --cap-accent-2 · --font-display ·
|
|
||||||
--font-body · --cap-band-top · --cap-band-height
|
|
||||||
Hooks (do not rename): .caption-group / .caption-word + states .is-active / .is-spoken,
|
|
||||||
root data-composition-id="captions", timeline at window.__timelines["captions"].
|
|
||||||
State changes use gsap.set({className}) (applied on the engine's frame-by-frame seek);
|
|
||||||
never tl.call() callbacks (seek does not fire them → wrong state on render).
|
|
||||||
|
|
||||||
Visual: a cheerful picture-book card — a cream surface inside a 3px charcoal outline
|
|
||||||
with a hard 6px charcoal offset shadow (no blur) and a generous 24px radius, the
|
|
||||||
sticker-on-paper signature. Fredoka One glyphs, sentence case, charcoal ink. Upcoming
|
|
||||||
words sit in muted charcoal; the current word pops into a rounded sunny-pastel candy
|
|
||||||
block (coral fill, 3px charcoal outline, hard 3px offset shadow, 12px radius) with a
|
|
||||||
bouncy scale pop; spoken words settle to solid charcoal, the candy block cleared.
|
|
||||||
-->
|
|
||||||
<script src="assets/vendor/gsap.min.js"></script>
|
|
||||||
|
|
||||||
<!-- captions.mjs injects the project's :root tokens here -->
|
|
||||||
<style data-brand-tokens></style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Fredoka One"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#captions-root {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.caption-layer {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 20;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
|
||||||
.caption-stage {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: var(--cap-band-top, 900px);
|
|
||||||
height: var(--cap-band-height, 180px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.caption-group {
|
|
||||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
|
||||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* THE picture-book card — cream surface, 3px charcoal outline, generous 24px radius,
|
|
||||||
a hard 6px charcoal offset shadow (no blur): the sticker-on-paper signature */
|
|
||||||
.caption-pill {
|
|
||||||
max-width: 80%;
|
|
||||||
padding: 18px 40px 22px;
|
|
||||||
background: var(--cap-canvas, #f5f0e6);
|
|
||||||
border: 3px solid var(--cap-ink, #2d2d2d);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 6px 6px 0 var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
.caption-line {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.18em 0.32em;
|
|
||||||
font-family: var(--font-display, "Fredoka"), sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: clamp(38px, 4.2vw, 56px);
|
|
||||||
line-height: 1.22;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* upcoming — muted charcoal, no candy block */
|
|
||||||
.caption-word {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.02em 0.16em;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 52%, var(--cap-canvas, #f5f0e6));
|
|
||||||
}
|
|
||||||
/* current word — THE sunny-pastel candy block: a rounded coral chip with a 3px
|
|
||||||
charcoal outline + a hard 3px charcoal offset shadow (no blur), charcoal on top */
|
|
||||||
.caption-word.is-active {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: var(--cap-accent, #f8635f);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 3px var(--cap-ink, #2d2d2d),
|
|
||||||
3px 5px 0 0 var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
/* already spoken — solid charcoal, candy block cleared */
|
|
||||||
.caption-word.is-spoken {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
|
||||||
/* brand / key term — a butter candy chip, the cheerful second pastel */
|
|
||||||
.caption-word.cap-brand {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: var(--cap-accent-2, #fde68a);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 0 0 3px var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
/* numerals / data — Quicksand, slightly recessed */
|
|
||||||
.caption-word.cap-num {
|
|
||||||
font-family: var(--font-body, "Quicksand"), system-ui, sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 78%, var(--cap-canvas, #f5f0e6));
|
|
||||||
}
|
|
||||||
/* emphasis — a butter underline rule (the second sunny pastel) */
|
|
||||||
.caption-word.cap-cta {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-thickness: 3px;
|
|
||||||
text-decoration-color: var(--cap-accent-2, #fde68a);
|
|
||||||
text-underline-offset: 0.12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-aspect-ratio: 9/16) {
|
|
||||||
.caption-pill {
|
|
||||||
max-width: 92%;
|
|
||||||
}
|
|
||||||
.caption-line {
|
|
||||||
font-size: clamp(32px, 5.2vw, 46px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="captions-root"
|
|
||||||
data-composition-id="captions"
|
|
||||||
data-timeline-locked
|
|
||||||
data-start="0"
|
|
||||||
data-duration="0"
|
|
||||||
data-fps="30"
|
|
||||||
data-width="0"
|
|
||||||
data-height="0"
|
|
||||||
>
|
|
||||||
<div class="caption-layer" aria-hidden="true">
|
|
||||||
<div id="caption-stage" class="caption-stage"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// captions.mjs injects the engine's scene-aware groups + real duration here.
|
|
||||||
var GROUPS = [];
|
|
||||||
var DURATION = 0;
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
var stage = document.getElementById("caption-stage");
|
|
||||||
|
|
||||||
// build DOM: one .caption-group per group, .caption-word per word
|
|
||||||
GROUPS.forEach(function (group, g) {
|
|
||||||
var groupEl = document.createElement("div");
|
|
||||||
groupEl.className = "caption-group";
|
|
||||||
groupEl.id = "caption-group-" + g;
|
|
||||||
var pill = document.createElement("div");
|
|
||||||
pill.className = "caption-pill";
|
|
||||||
var line = document.createElement("div");
|
|
||||||
line.className = "caption-line";
|
|
||||||
(group.words || []).forEach(function (w, i) {
|
|
||||||
var span = document.createElement("span");
|
|
||||||
span.className = "caption-word";
|
|
||||||
span.id = "caption-word-" + g + "-" + i;
|
|
||||||
span.textContent = String(w.text);
|
|
||||||
line.appendChild(span);
|
|
||||||
});
|
|
||||||
pill.appendChild(line);
|
|
||||||
groupEl.appendChild(pill);
|
|
||||||
stage.appendChild(groupEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
var tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
GROUPS.forEach(function (group, g) {
|
|
||||||
var groupEl = document.getElementById("caption-group-" + g);
|
|
||||||
var words = group.words || [];
|
|
||||||
var next = GROUPS[g + 1];
|
|
||||||
var isLast = g === GROUPS.length - 1;
|
|
||||||
var start = Math.max(0, Number(group.start));
|
|
||||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
|
||||||
if (end <= start) end = start + 0.01;
|
|
||||||
|
|
||||||
// group on/off — exactly one group visible at a time
|
|
||||||
tl.set(groupEl, { opacity: 1 }, start);
|
|
||||||
tl.set(groupEl, { opacity: 0 }, end);
|
|
||||||
|
|
||||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
|
||||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
|
||||||
words.forEach(function (w, i) {
|
|
||||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
|
||||||
var at = Math.max(start, Number(w.start));
|
|
||||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
|
||||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
|
||||||
tl.fromTo(el, { scale: 0.82 }, { scale: 1, duration: 0.2, ease: "back.out(2.6)" }, at);
|
|
||||||
if (i + 1 < words.length) {
|
|
||||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
|
||||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (words.length) {
|
|
||||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
|
||||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
|
||||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// full-span anchor so the sub-comp timeline spans the whole video
|
|
||||||
tl.to({}, { duration: DURATION }, 0);
|
|
||||||
window.__timelines["captions"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
635d4927-f2e8-44dd-9cbf-1b5d4f285f53
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":1,"paths":["renders/video.mp4"]}
|
|
||||||
@@ -1,939 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"historyId": "635d4927-f2e8-44dd-9cbf-1b5d4f285f53",
|
|
||||||
"currentRevision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"pendingChanges": false,
|
|
||||||
"files": {
|
|
||||||
".media/manifest.jsonl": {
|
|
||||||
"hash": "0619ff69adc91e800fdbfe4cd508b67d21791a1ef4eb8b043518158c0ce3483e",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/06/0619ff69adc91e800fdbfe4cd508b67d21791a1ef4eb8b043518158c0ce3483e"
|
|
||||||
},
|
|
||||||
"AGENTS.md": {
|
|
||||||
"hash": "1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1d/1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5"
|
|
||||||
},
|
|
||||||
"BRIEF.md": {
|
|
||||||
"hash": "14d649b0556246e716324f80fc2b65b27aaa4a25d5f508c9e6fceb702f29ebb4",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/14/14d649b0556246e716324f80fc2b65b27aaa4a25d5f508c9e6fceb702f29ebb4"
|
|
||||||
},
|
|
||||||
"CLAUDE.md": {
|
|
||||||
"hash": "1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1d/1d4f0c8ca2507b94fd7c9df709fff01416adbd51a0f922b9d6ed2f40129147d5"
|
|
||||||
},
|
|
||||||
"SCRIPT.md": {
|
|
||||||
"hash": "5fbae06748238e1717948353b077a8776b7f88221790923652363834c49fad9a",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/5f/5fbae06748238e1717948353b077a8776b7f88221790923652363834c49fad9a"
|
|
||||||
},
|
|
||||||
"STORYBOARD.md": {
|
|
||||||
"hash": "34f5c1242e964e76cf330da278bb80d9174ba52197439863a9c32f6a2bf23383",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/34/34f5c1242e964e76cf330da278bb80d9174ba52197439863a9c32f6a2bf23383"
|
|
||||||
},
|
|
||||||
"assets/fonts/fredoka-600.woff2": {
|
|
||||||
"hash": "b62f2898de150f019961f4884a49f83dcd2ce74375be5e0992c594ebb8f30294",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/b6/b62f2898de150f019961f4884a49f83dcd2ce74375be5e0992c594ebb8f30294"
|
|
||||||
},
|
|
||||||
"assets/fonts/quicksand-var.woff2": {
|
|
||||||
"hash": "2add7d60b1cd2ab84c9967e23d5ec08eb3fc9635c46855b17d59404dec6b410e",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/2a/2add7d60b1cd2ab84c9967e23d5ec08eb3fc9635c46855b17d59404dec6b410e"
|
|
||||||
},
|
|
||||||
"assets/voice/01.wav": {
|
|
||||||
"hash": "ef67616aef5fe0312c5a5a833eaba38438d7a1248057dcdd00b4d7053b064e98",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/ef/ef67616aef5fe0312c5a5a833eaba38438d7a1248057dcdd00b4d7053b064e98"
|
|
||||||
},
|
|
||||||
"assets/voice/02.wav": {
|
|
||||||
"hash": "c4b74e279af592152edbe412bc477e2fe8da05cb5add99bdbd55ed536d80cc1b",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c4/c4b74e279af592152edbe412bc477e2fe8da05cb5add99bdbd55ed536d80cc1b"
|
|
||||||
},
|
|
||||||
"assets/voice/03.wav": {
|
|
||||||
"hash": "e8537998055079e160467161206dcc3c51d8cc98411f89f42e478f3672fe9630",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/e8/e8537998055079e160467161206dcc3c51d8cc98411f89f42e478f3672fe9630"
|
|
||||||
},
|
|
||||||
"assets/voice/04.wav": {
|
|
||||||
"hash": "e9e118731fb136fab2751e5392c47a1b89598acac4bee763962ba0e6f3ede377",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/e9/e9e118731fb136fab2751e5392c47a1b89598acac4bee763962ba0e6f3ede377"
|
|
||||||
},
|
|
||||||
"assets/voice/05.wav": {
|
|
||||||
"hash": "70c9e8aad9f6f0167e2d9e31d830a27e0aa313e1307085b73e858c8108dc83e8",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/70/70c9e8aad9f6f0167e2d9e31d830a27e0aa313e1307085b73e858c8108dc83e8"
|
|
||||||
},
|
|
||||||
"assets/voice/06.wav": {
|
|
||||||
"hash": "9441a5b1472967c809efb6ff246e25800e74fd9e6372a748c73568e4450108c3",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/94/9441a5b1472967c809efb6ff246e25800e74fd9e6372a748c73568e4450108c3"
|
|
||||||
},
|
|
||||||
"assets/voice/07.wav": {
|
|
||||||
"hash": "4fbc575c406ead86d77a06c79dcd680303c7a935147208936c83969d0b4121b3",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4f/4fbc575c406ead86d77a06c79dcd680303c7a935147208936c83969d0b4121b3"
|
|
||||||
},
|
|
||||||
"assets/voice/08.wav": {
|
|
||||||
"hash": "7859a7ded9d7d2fe821b93162b97790c70b6929ff07211f230f9cc0f75764e95",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/78/7859a7ded9d7d2fe821b93162b97790c70b6929ff07211f230f9cc0f75764e95"
|
|
||||||
},
|
|
||||||
"assets/voice/09.wav": {
|
|
||||||
"hash": "0864df842c3d65cfb43aafd936de8bc963cd91aca56248475a8eb2c968fd4a55",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/08/0864df842c3d65cfb43aafd936de8bc963cd91aca56248475a8eb2c968fd4a55"
|
|
||||||
},
|
|
||||||
"audio_engine_meta.json": {
|
|
||||||
"hash": "4f2da6bf20a3982fb9ed326e7a457cac7e7128ef7e5e08d585f58bbf47a8c397",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4f/4f2da6bf20a3982fb9ed326e7a457cac7e7128ef7e5e08d585f58bbf47a8c397"
|
|
||||||
},
|
|
||||||
"audio_meta.json": {
|
|
||||||
"hash": "ecc4980e89d34d9873cdc8a92f954b38dcf094112a5bab89fde7fe98a321b624",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/ec/ecc4980e89d34d9873cdc8a92f954b38dcf094112a5bab89fde7fe98a321b624"
|
|
||||||
},
|
|
||||||
"audio_request.json": {
|
|
||||||
"hash": "76765154cffa684843af5b4bc6492f4e22d7a6aa9d2b6a716351eceab241a464",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/76/76765154cffa684843af5b4bc6492f4e22d7a6aa9d2b6a716351eceab241a464"
|
|
||||||
},
|
|
||||||
"caption-overrides.json": {
|
|
||||||
"hash": "37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/37/37517e5f3dc66819f61f5a7bb8ace1921282415f10551d2defa5c3eb0985b570"
|
|
||||||
},
|
|
||||||
"caption_groups.json": {
|
|
||||||
"hash": "8b53722d14f329a000bcd109192eaedf8f098e16f4e553381c1a139843f8fb94",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/8b/8b53722d14f329a000bcd109192eaedf8f098e16f4e553381c1a139843f8fb94"
|
|
||||||
},
|
|
||||||
"capture/extracted/asset-descriptions.md": {
|
|
||||||
"hash": "92c2cfe647902a93e595e21cebab23e9af1a2fefbec14cf22d0a5af437dc6964",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/92/92c2cfe647902a93e595e21cebab23e9af1a2fefbec14cf22d0a5af437dc6964"
|
|
||||||
},
|
|
||||||
"capture/extracted/tokens.json": {
|
|
||||||
"hash": "2f533a1d991bdf84ad653ceb0061fbefd42a19f7b4dc22fe9b37f7156ce79c32",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/2f/2f533a1d991bdf84ad653ceb0061fbefd42a19f7b4dc22fe9b37f7156ce79c32"
|
|
||||||
},
|
|
||||||
"capture/extracted/visible-text.txt": {
|
|
||||||
"hash": "39c044a9ee527c4a857cb65b5f0dc3f855fca4a7d17beb8ffb2cf24089f5d54f",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/39/39c044a9ee527c4a857cb65b5f0dc3f855fca4a7d17beb8ffb2cf24089f5d54f"
|
|
||||||
},
|
|
||||||
"compositions/captions.html": {
|
|
||||||
"hash": "c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c7/c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8"
|
|
||||||
},
|
|
||||||
"compositions/frames/01-hook.html": {
|
|
||||||
"hash": "48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/48/48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b"
|
|
||||||
},
|
|
||||||
"compositions/frames/02-worry.html": {
|
|
||||||
"hash": "36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/36/36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94"
|
|
||||||
},
|
|
||||||
"compositions/frames/03-meet.html": {
|
|
||||||
"hash": "33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/33/33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67"
|
|
||||||
},
|
|
||||||
"compositions/frames/04-limits.html": {
|
|
||||||
"hash": "fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/fd/fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37"
|
|
||||||
},
|
|
||||||
"compositions/frames/05-apps.html": {
|
|
||||||
"hash": "5da142d1be64b6d51d5c707ac913fdf819a0419288696974c2775b1f35e3654e",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/5d/5da142d1be64b6d51d5c707ac913fdf819a0419288696974c2775b1f35e3654e"
|
|
||||||
},
|
|
||||||
"compositions/frames/06-location.html": {
|
|
||||||
"hash": "199c8b0642ea141b105b1eb7f62ff545b4b3f77dcbf1767f6581cdceac9ac019",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/19/199c8b0642ea141b105b1eb7f62ff545b4b3f77dcbf1767f6581cdceac9ac019"
|
|
||||||
},
|
|
||||||
"compositions/frames/07-balance.html": {
|
|
||||||
"hash": "6ed5fc2fb15ca1573c5ff92fc4d6c73aa89808776f0236d1934f0537b3e42a8e",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/6e/6ed5fc2fb15ca1573c5ff92fc4d6c73aa89808776f0236d1934f0537b3e42a8e"
|
|
||||||
},
|
|
||||||
"compositions/frames/08-platforms.html": {
|
|
||||||
"hash": "b0a5b3c9b1d4bd94c0b3f9f7b71e7137632d8e400add3f5e93958acb7036d99a",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/b0/b0a5b3c9b1d4bd94c0b3f9f7b71e7137632d8e400add3f5e93958acb7036d99a"
|
|
||||||
},
|
|
||||||
"compositions/frames/09-cta.html": {
|
|
||||||
"hash": "d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07"
|
|
||||||
},
|
|
||||||
"frame.md": {
|
|
||||||
"hash": "42d827dbd279990052e171d04380b5e6bbc111ae391dc283deb7a452e70b9b97",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/42/42d827dbd279990052e171d04380b5e6bbc111ae391dc283deb7a452e70b9b97"
|
|
||||||
},
|
|
||||||
"hyperframes.json": {
|
|
||||||
"hash": "44d4e8dcefebce0a13f5afa82aa270b0b19a1d3f82e434f92616010cee775dc4",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/44/44d4e8dcefebce0a13f5afa82aa270b0b19a1d3f82e434f92616010cee775dc4"
|
|
||||||
},
|
|
||||||
"index.html": {
|
|
||||||
"hash": "7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/7f/7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32"
|
|
||||||
},
|
|
||||||
"meta.json": {
|
|
||||||
"hash": "1497903acd6ced7f19d4cf6c76f104f5656593c484427d89aea883c8246ad541",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/14/1497903acd6ced7f19d4cf6c76f104f5656593c484427d89aea883c8246ad541"
|
|
||||||
},
|
|
||||||
"package.json": {
|
|
||||||
"hash": "6873ac1737fc27fd1046b92a493604be2ebc328b232d134f04f3b65c07200b4f",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/68/6873ac1737fc27fd1046b92a493604be2ebc328b232d134f04f3b65c07200b4f"
|
|
||||||
},
|
|
||||||
"assets/vendor/gsap.min.js": {
|
|
||||||
"hash": "c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280",
|
|
||||||
"content": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c1/c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"revisions": {
|
|
||||||
"ad80a38d-f38a-4cc1-9042-d179e2b0e6f3": {
|
|
||||||
"actor": {
|
|
||||||
"kind": "outside",
|
|
||||||
"name": "Outside"
|
|
||||||
},
|
|
||||||
"label": "Changed outside the app",
|
|
||||||
"startedAt": 1792016103184,
|
|
||||||
"endedAt": 1792016103225,
|
|
||||||
"files": {
|
|
||||||
"compositions/frames/03-meet.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/96/96f8c3849528fd883b0c70f66c0fd291aa3030adaaa346a8ebdf3c95aeb38d7e",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/33/33f5f3cc5d827610300ff1198268b19f675d3adc26898e76ed11f7a89be5ef67"
|
|
||||||
},
|
|
||||||
"compositions/frames/09-cta.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/a9/a924e8f0e68f92423d70d4d8e086d81f0596cec3d6ad1120d1c7e930fd086fad",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/35/3566b01fb9bb64e4d22e86877179c7b7cbee0a98f4bb3c354c97db2b85b13b15"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"2748be21-fde2-40ba-a2c0-fddfb7e47431": {
|
|
||||||
"actor": {
|
|
||||||
"kind": "outside",
|
|
||||||
"name": "Outside"
|
|
||||||
},
|
|
||||||
"label": "Changed outside the app",
|
|
||||||
"startedAt": 1792016129829,
|
|
||||||
"endedAt": 1792016129859,
|
|
||||||
"files": {
|
|
||||||
"assets/vendor/gsap.min.js": {
|
|
||||||
"before": null,
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c1/c174bfce53a729418d57a8ad8625e7247c793a22fef8e2851e3cfa3de9cd8280"
|
|
||||||
},
|
|
||||||
"index.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/cb/cb8df64b81d4e2ad46e13002a5697224c5e21c3c04e925d3302f8c4ceb0a58c3",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/7f/7f9f086ba7fb700d1308e5f9efee068c11034983a04dfc6baed7b8096034ea32"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": {
|
|
||||||
"actor": {
|
|
||||||
"kind": "outside",
|
|
||||||
"name": "Outside"
|
|
||||||
},
|
|
||||||
"label": "Changed outside the app",
|
|
||||||
"startedAt": 1792016156924,
|
|
||||||
"endedAt": 1792016156944,
|
|
||||||
"files": {
|
|
||||||
"compositions/captions.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9ed837d5d7a9893cf38400b360adc8d5636489ede0eaffb3f4e341faab51c04",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/c7/c7a724d68db626674aa77f2e4b9967b63cb606d3dd1156796d0a319e23f1e0e8"
|
|
||||||
},
|
|
||||||
"compositions/frames/01-hook.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/1f/1f8e5283549f95e82cfa7324067517b68a5c852ee0400e39a4b8eeadb8d2afdb",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/f4/f4e6a9d0f2414e3f78e0b7ec499f3adf397e480ad3e3cbfc13110bd9ee6553f3"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": {
|
|
||||||
"actor": {
|
|
||||||
"kind": "outside",
|
|
||||||
"name": "Outside"
|
|
||||||
},
|
|
||||||
"label": "Changed outside the app",
|
|
||||||
"startedAt": 1792016208793,
|
|
||||||
"endedAt": 1792016208815,
|
|
||||||
"files": {
|
|
||||||
"compositions/frames/01-hook.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/f4/f4e6a9d0f2414e3f78e0b7ec499f3adf397e480ad3e3cbfc13110bd9ee6553f3",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/48/48852887a870cc3daa0c98e65504ee207475b7add2b4eecb79fe304b0539441b"
|
|
||||||
},
|
|
||||||
"compositions/frames/02-worry.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/4d/4df905219fb45cf58401646096425b66025ec56d32b12fca340e88e5a07ac264",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/36/36c53f5f5bdcbf9ebb6a7c73dc22017e80dfc64bd98470d78af8ef7846bb1d94"
|
|
||||||
},
|
|
||||||
"compositions/frames/04-limits.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/0b/0b852cb30390684e6002b762165405fabb56cad4955fc8287e84d595bd1891d3",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/fd/fd4173fd65eff6f6ca43680ba4824545ae6bb57eb8710780a035c9dd120ebc37"
|
|
||||||
},
|
|
||||||
"compositions/frames/09-cta.html": {
|
|
||||||
"before": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/35/3566b01fb9bb64e4d22e86877179c7b7cbee0a98f4bb3c354c97db2b85b13b15",
|
|
||||||
"after": "/root/.cache/hyperframes/history/635d4927-f2e8-44dd-9cbf-1b5d4f285f53/blobs/d9/d9e539932cec2431c43f6125a26bb452abc66a2f7c616b1d1e26da5911e80e07"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"checks": {
|
|
||||||
"latest": {
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"id": "31e798e4-ad68-444f-b4c3-170cece3aeb1"
|
|
||||||
},
|
|
||||||
"revisions": {
|
|
||||||
"ad80a38d-f38a-4cc1-9042-d179e2b0e6f3": [
|
|
||||||
{
|
|
||||||
"id": "bdef997d-ad31-4453-8fb8-c432364aadaf",
|
|
||||||
"scope": "project",
|
|
||||||
"metadata": {
|
|
||||||
"ok": false,
|
|
||||||
"browserSkipped": false,
|
|
||||||
"options": {
|
|
||||||
"samples": 9,
|
|
||||||
"atTransitions": false,
|
|
||||||
"maxIssues": 80,
|
|
||||||
"collapseStatic": true,
|
|
||||||
"tolerance": 2,
|
|
||||||
"timeout": 3000,
|
|
||||||
"contrast": true,
|
|
||||||
"strict": false,
|
|
||||||
"snapshots": false,
|
|
||||||
"browserGpuMode": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"createdAt": 1792016113510,
|
|
||||||
"contentHash": "cbaddad6d2047ae01a30682fc6f678b633adb8e61cc6d7b2cd135829f2f6a67d",
|
|
||||||
"revision": "ad80a38d-f38a-4cc1-9042-d179e2b0e6f3",
|
|
||||||
"inputFingerprint": "69a931e0c93bee3efd6531a54ac48f71e88a2fc05106c9891ccb8713064b92ee",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"2748be21-fde2-40ba-a2c0-fddfb7e47431": [
|
|
||||||
{
|
|
||||||
"id": "0e229a06-9c97-4ec5-badd-59e38d09bb91",
|
|
||||||
"scope": "project",
|
|
||||||
"metadata": {
|
|
||||||
"ok": false,
|
|
||||||
"browserSkipped": false,
|
|
||||||
"options": {
|
|
||||||
"samples": 9,
|
|
||||||
"atTransitions": false,
|
|
||||||
"maxIssues": 80,
|
|
||||||
"collapseStatic": true,
|
|
||||||
"tolerance": 2,
|
|
||||||
"timeout": 3000,
|
|
||||||
"contrast": true,
|
|
||||||
"strict": false,
|
|
||||||
"snapshots": false,
|
|
||||||
"browserGpuMode": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"createdAt": 1792016142676,
|
|
||||||
"contentHash": "cd9b82c350369ca166d5210b7192e5c1f0394570ec246f30f0a3134ed89d3e9a",
|
|
||||||
"revision": "2748be21-fde2-40ba-a2c0-fddfb7e47431",
|
|
||||||
"inputFingerprint": "86db889a4aed182e0d1dc77ef05552d96c77a15f49c3c15e3fb33051e7f719e6",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": [
|
|
||||||
{
|
|
||||||
"id": "2e9e2e50-ea4c-4ce0-ae6d-feb42f2db77c",
|
|
||||||
"scope": "project",
|
|
||||||
"metadata": {
|
|
||||||
"ok": true,
|
|
||||||
"browserSkipped": false,
|
|
||||||
"options": {
|
|
||||||
"samples": 9,
|
|
||||||
"atTransitions": false,
|
|
||||||
"maxIssues": 80,
|
|
||||||
"collapseStatic": true,
|
|
||||||
"tolerance": 2,
|
|
||||||
"timeout": 3000,
|
|
||||||
"contrast": true,
|
|
||||||
"strict": false,
|
|
||||||
"snapshots": false,
|
|
||||||
"browserGpuMode": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"createdAt": 1792016169247,
|
|
||||||
"contentHash": "3c336e043480b32bbb2ecf2a89aa5ad157bf0b6a1243806c47aeb6fba4bb5858",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": [
|
|
||||||
{
|
|
||||||
"id": "31e798e4-ad68-444f-b4c3-170cece3aeb1",
|
|
||||||
"scope": "project",
|
|
||||||
"report": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/checks/31e798e4-ad68-444f-b4c3-170cece3aeb1.json",
|
|
||||||
"metadata": {
|
|
||||||
"ok": true,
|
|
||||||
"browserSkipped": false,
|
|
||||||
"options": {
|
|
||||||
"samples": 9,
|
|
||||||
"atTransitions": false,
|
|
||||||
"maxIssues": 80,
|
|
||||||
"collapseStatic": true,
|
|
||||||
"tolerance": 2,
|
|
||||||
"timeout": 3000,
|
|
||||||
"contrast": true,
|
|
||||||
"strict": false,
|
|
||||||
"snapshots": false,
|
|
||||||
"browserGpuMode": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"createdAt": 1792016221309,
|
|
||||||
"contentHash": "e4b290d5513ee41c22f25665bfab73ab61f26f0c137a9fddd861279b15f805d4",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unversioned": [
|
|
||||||
{
|
|
||||||
"id": "b81768cb-66dc-4b7d-8159-85d988ebc321",
|
|
||||||
"scope": "project",
|
|
||||||
"report": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/checks/b81768cb-66dc-4b7d-8159-85d988ebc321.json",
|
|
||||||
"metadata": {
|
|
||||||
"ok": false,
|
|
||||||
"browserSkipped": false,
|
|
||||||
"options": {
|
|
||||||
"samples": 9,
|
|
||||||
"atTransitions": false,
|
|
||||||
"maxIssues": 80,
|
|
||||||
"collapseStatic": true,
|
|
||||||
"tolerance": 2,
|
|
||||||
"timeout": 3000,
|
|
||||||
"contrast": true,
|
|
||||||
"strict": false,
|
|
||||||
"snapshots": false,
|
|
||||||
"browserGpuMode": "auto"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"createdAt": 1792016095244,
|
|
||||||
"contentHash": "aab572c254c1e2e2a0d41cb3e69f19c969c52ea74d52d8a7b70fa33ca1a3d2e6",
|
|
||||||
"revision": null,
|
|
||||||
"inputFingerprint": "3b3a22e794c378dd99ccd728175edebfae98c1e343c6ca06027a0679d088604a",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "unversioned"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"snapshots": {
|
|
||||||
"latest": {
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-4"
|
|
||||||
},
|
|
||||||
"revisions": {
|
|
||||||
"f155d8de-7fb6-4e12-9291-60b3874a47cc": [
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-0",
|
|
||||||
"time": 1.5,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "b9d7d37046d37a8418b0e35047fa3c528818b9f28f321fd8c163b7d821b7b2f8",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-1",
|
|
||||||
"time": 4.8,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "8bfcfe4356e344034a588fb8eaa48d8c6a17fd1f00921172ac0452a80b1b71e4",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-2",
|
|
||||||
"time": 9,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "d7c0b4142c09d36f84c8a5b82705bd6cf46522dc372d88cef51f43192b37e42d",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-3",
|
|
||||||
"time": 12.3,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "90ca4efc8f9645e29abd5e9c1c6900404f7a53ac80294f7c6069b847fa1bccdd",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-4",
|
|
||||||
"time": 16,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "bc7a6a0347049f51793185e2867b34f9200ff93d40a06f0bffa6965696338d8c",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-5",
|
|
||||||
"time": 20.2,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "c86c03f4c948e4b51a67e7012bf1eb5898e381c173080a13261b212b5c8ce25a",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-6",
|
|
||||||
"time": 24,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "e2b216d57db98ddf973fd0134008417ff00f646888ace8fc5d3e7ce2ddef9bd0",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-7",
|
|
||||||
"time": 27.3,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "48b1ae5f592678b077b5b5ea2c166090dfc9e84c6baaabb2b1bba5f41642a735",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-8",
|
|
||||||
"time": 31,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "6b3edca25c8d8c0e7be74e31690404694ecb679d0d4348a05823c96bf891e492",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-9",
|
|
||||||
"time": 33,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182293,
|
|
||||||
"contentHash": "228695183d4db7398381a0d6d7f502ad5755dfe8601c9c68d7c73b45b44a210c",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-10",
|
|
||||||
"time": 36,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "d8a1ec155678c44ce8e68cde3085859a1ff031b78febf7ea5b6f4d5e45c4d0d6",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-11",
|
|
||||||
"time": 39.5,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "d814f09e5d907213260a03a8b6b0129ed31848abbd29de93417b6deaa5ff8871",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-12",
|
|
||||||
"time": 42,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "acb9108624fc79f1dcc4ecb6ddf74ee6f2c46be2c53604d8a8243b886779f9e7",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-13",
|
|
||||||
"time": 45.3,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "73edeff4a0be0e45c4d82b20caafd7eb154d496c5252af5c351d9349871a2dcd",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-14",
|
|
||||||
"time": 47.5,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "5f425b67e37447223fb533746a74d4994827e00945958068d3c6b6213f7a4938",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-15",
|
|
||||||
"time": 49,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "73b125a618712fb0d3e5daecffb66f3704e0b160f5844e4759971551a98c1c76",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-16",
|
|
||||||
"time": 51,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "83be74004821d6907afaaa93dd798169144c976855840ebaa8082069d1d7ce34",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-17",
|
|
||||||
"time": 54.2,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "a4293fac697e7669476d2d8668abf92a9c4aec2ed6f11672ad53f15074b43cf6",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "f15403c5-4592-4d35-ace8-a22740b42e6f-18",
|
|
||||||
"time": 53.091,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016182294,
|
|
||||||
"contentHash": "882bf50bb17bf4324156c5719de60e94835d1af72402f7af984a845c7684fd3a",
|
|
||||||
"revision": "f155d8de-7fb6-4e12-9291-60b3874a47cc",
|
|
||||||
"inputFingerprint": "9bc516d75c695a9fbcd85911f32ded85e7af79a7c0e6ef3b740f22c5908e924b",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision",
|
|
||||||
"status": "cleaned",
|
|
||||||
"cleanedAfterRender": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e": [
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-0",
|
|
||||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-00-at-1.5s.png",
|
|
||||||
"time": 1.5,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016226995,
|
|
||||||
"contentHash": "79be7032c63e7a191b47eaceb152a02bf809936a7e76071b68c388a49038f28a",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-1",
|
|
||||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-01-at-9s.png",
|
|
||||||
"time": 9,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016226996,
|
|
||||||
"contentHash": "1a9105cec34e2127ce91c60aab69a567895822f45267710285d03b2b99a1bf84",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-2",
|
|
||||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-02-at-26.5s.png",
|
|
||||||
"time": 26.5,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016226996,
|
|
||||||
"contentHash": "9132f797582916f355f507d87da8843d0fdd23f1b9c5d7f6473eb859b3fd3392",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-3",
|
|
||||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-03-at-53s.png",
|
|
||||||
"time": 53,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016226996,
|
|
||||||
"contentHash": "c8714c376f4e7f375d0c2c11766a2717127224c0927372e4ff67ee41c4d52a72",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"scope": "full",
|
|
||||||
"id": "a82e082b-e292-496f-a873-daced29d123f-4",
|
|
||||||
"image": "/home/user/cycle-atlas/videos/lookatu-promo/.hyperframes/artifacts/snapshots/f0662448-af98-40ff-b6fd-626156d4a694/frame-04-at-53.091s.png",
|
|
||||||
"time": 53.091,
|
|
||||||
"metadata": {
|
|
||||||
"width": 1080,
|
|
||||||
"height": 1920,
|
|
||||||
"zoomScale": 1,
|
|
||||||
"browserGpuMode": "auto",
|
|
||||||
"warnings": [],
|
|
||||||
"partial": false
|
|
||||||
},
|
|
||||||
"createdAt": 1792016226996,
|
|
||||||
"contentHash": "5605b5c311601f318545227a90787eb6f2950613dea019cf8e0daf81353d7946",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e",
|
|
||||||
"inputFingerprint": "4f091d26af72cc80221f64652e53f7b6154f8b8d94c2c43bddbdaa7e7bacc2a8",
|
|
||||||
"inputScope": "local-project-files",
|
|
||||||
"attribution": "recorded-revision"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"unversioned": []
|
|
||||||
},
|
|
||||||
"renders": {
|
|
||||||
"latest": {
|
|
||||||
"id": "690e4ff7-bd4a-49ec-94b8-f18083c79bc3",
|
|
||||||
"completedAt": 1792016445275,
|
|
||||||
"output": "/home/user/cycle-atlas/videos/lookatu-promo/renders/video.mp4",
|
|
||||||
"status": "completed",
|
|
||||||
"revision": "6999d82e-2d3b-4026-b89c-d3107735bf7e"
|
|
||||||
},
|
|
||||||
"retainedRevisions": [
|
|
||||||
"6999d82e-2d3b-4026-b89c-d3107735bf7e"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"pendingArtifactCleanup": []
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
# .media · 9 assets
|
|
||||||
|
|
||||||
id type dur dims path description
|
|
||||||
voice_010 voice 3.8s — assets/voice/01.wav Where are they right now? And what are they really doing on that phone?
|
|
||||||
voice_011 voice 5.6s — assets/voice/02.wav Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.
|
|
||||||
voice_012 voice 6.1s — assets/voice/03.wav There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.
|
|
||||||
voice_013 voice 5.1s — assets/voice/04.wav Set screen time limits that actually stick. When the time is up, the phone knows.
|
|
||||||
voice_014 voice 4.2s — assets/voice/05.wav Choose which apps are okay. Allow the good ones. Block the rest.
|
|
||||||
voice_015 voice 5s — assets/voice/06.wav And when it really matters, see where they are. Location, only when you need it.
|
|
||||||
voice_016 voice 4s — assets/voice/07.wav Because kids deserve some freedom, and parents deserve some calm.
|
|
||||||
voice_017 voice 2.3s — assets/voice/08.wav Works on Android and on iPhone.
|
|
||||||
voice_018 voice 3.1s — assets/voice/09.wav lookatu. Look out for them. Download it today.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{"id":"voice_001","type":"voice","path":"assets/voice/01.wav","source":"generated","description":"Where are they right now? And what are they really doing on that phone?","duration":3.3,"provenance":{"provider":"kokoro","prompt":"Where are they right now? And what are they really doing on that phone?"}}
|
|
||||||
{"id":"voice_002","type":"voice","path":"assets/voice/02.wav","source":"generated","description":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.","duration":4.9,"provenance":{"provider":"kokoro","prompt":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."}}
|
|
||||||
{"id":"voice_003","type":"voice","path":"assets/voice/03.wav","source":"generated","description":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.","duration":5.4,"provenance":{"provider":"kokoro","prompt":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."}}
|
|
||||||
{"id":"voice_004","type":"voice","path":"assets/voice/04.wav","source":"generated","description":"Set screen time limits that actually stick. When the time is up, the phone knows.","duration":4.5,"provenance":{"provider":"kokoro","prompt":"Set screen time limits that actually stick. When the time is up, the phone knows."}}
|
|
||||||
{"id":"voice_005","type":"voice","path":"assets/voice/05.wav","source":"generated","description":"Choose which apps are okay. Allow the good ones. Block the rest.","duration":3.7,"provenance":{"provider":"kokoro","prompt":"Choose which apps are okay. Allow the good ones. Block the rest."}}
|
|
||||||
{"id":"voice_006","type":"voice","path":"assets/voice/06.wav","source":"generated","description":"And when it really matters, see where they are. Location, only when you need it.","duration":4.4,"provenance":{"provider":"kokoro","prompt":"And when it really matters, see where they are. Location, only when you need it."}}
|
|
||||||
{"id":"voice_007","type":"voice","path":"assets/voice/07.wav","source":"generated","description":"Because kids deserve some freedom, and parents deserve some calm.","duration":3.6,"provenance":{"provider":"kokoro","prompt":"Because kids deserve some freedom, and parents deserve some calm."}}
|
|
||||||
{"id":"voice_008","type":"voice","path":"assets/voice/08.wav","source":"generated","description":"Works on Android and on iPhone.","duration":2,"provenance":{"provider":"kokoro","prompt":"Works on Android and on iPhone."}}
|
|
||||||
{"id":"voice_009","type":"voice","path":"assets/voice/09.wav","source":"generated","description":"lookatu. Look out for them. Download it today.","duration":2.7,"provenance":{"provider":"kokoro","prompt":"lookatu. Look out for them. Download it today."}}
|
|
||||||
{"id":"voice_010","type":"voice","path":"assets/voice/01.wav","source":"generated","description":"Where are they right now? And what are they really doing on that phone?","duration":3.8,"provenance":{"provider":"kokoro","prompt":"Where are they right now? And what are they really doing on that phone?"}}
|
|
||||||
{"id":"voice_011","type":"voice","path":"assets/voice/02.wav","source":"generated","description":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.","duration":5.6,"provenance":{"provider":"kokoro","prompt":"Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."}}
|
|
||||||
{"id":"voice_012","type":"voice","path":"assets/voice/03.wav","source":"generated","description":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.","duration":6.1,"provenance":{"provider":"kokoro","prompt":"There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."}}
|
|
||||||
{"id":"voice_013","type":"voice","path":"assets/voice/04.wav","source":"generated","description":"Set screen time limits that actually stick. When the time is up, the phone knows.","duration":5.1,"provenance":{"provider":"kokoro","prompt":"Set screen time limits that actually stick. When the time is up, the phone knows."}}
|
|
||||||
{"id":"voice_014","type":"voice","path":"assets/voice/05.wav","source":"generated","description":"Choose which apps are okay. Allow the good ones. Block the rest.","duration":4.2,"provenance":{"provider":"kokoro","prompt":"Choose which apps are okay. Allow the good ones. Block the rest."}}
|
|
||||||
{"id":"voice_015","type":"voice","path":"assets/voice/06.wav","source":"generated","description":"And when it really matters, see where they are. Location, only when you need it.","duration":5,"provenance":{"provider":"kokoro","prompt":"And when it really matters, see where they are. Location, only when you need it."}}
|
|
||||||
{"id":"voice_016","type":"voice","path":"assets/voice/07.wav","source":"generated","description":"Because kids deserve some freedom, and parents deserve some calm.","duration":4,"provenance":{"provider":"kokoro","prompt":"Because kids deserve some freedom, and parents deserve some calm."}}
|
|
||||||
{"id":"voice_017","type":"voice","path":"assets/voice/08.wav","source":"generated","description":"Works on Android and on iPhone.","duration":2.3,"provenance":{"provider":"kokoro","prompt":"Works on Android and on iPhone."}}
|
|
||||||
{"id":"voice_018","type":"voice","path":"assets/voice/09.wav","source":"generated","description":"lookatu. Look out for them. Download it today.","duration":3.1,"provenance":{"provider":"kokoro","prompt":"lookatu. Look out for them. Download it today."}}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"version": 1,
|
|
||||||
"preferences": {
|
|
||||||
"style_preset.product-launch-video": {
|
|
||||||
"value": "daisy-days",
|
|
||||||
"confirmed_in": [
|
|
||||||
"lookatu-promo"
|
|
||||||
],
|
|
||||||
"updated_at": "2026-10-14T22:05:31.928Z"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sightings": {}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# HyperFrames Composition Project
|
|
||||||
|
|
||||||
## Skills — USE THESE FIRST
|
|
||||||
|
|
||||||
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
|
||||||
|
|
||||||
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
|
|
||||||
|
|
||||||
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
|
|
||||||
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
|
||||||
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
|
||||||
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
|
||||||
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
|
||||||
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
|
||||||
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
|
|
||||||
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
|
|
||||||
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
|
|
||||||
|
|
||||||
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
|
||||||
|
|
||||||
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
|
|
||||||
|
|
||||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
|
||||||
|
|
||||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
|
||||||
|
|
||||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
|
||||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
|
||||||
> the plugin manager, not the standalone commands below.
|
|
||||||
>
|
|
||||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
|
||||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
|
||||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
|
||||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
|
||||||
> newly installed skills load.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev # human-operated foreground preview (blocks until stopped)
|
|
||||||
npx hyperframes preview --background # agent-safe persistent Studio preview
|
|
||||||
npx hyperframes preview --status # verify the persistent preview is listening
|
|
||||||
npx hyperframes preview --stop # stop it when review is finished
|
|
||||||
npm run check # lint + runtime + layout + motion + contrast (one command)
|
|
||||||
npm run render # render to MP4
|
|
||||||
npm run publish # publish and get a shareable link
|
|
||||||
npx hyperframes lint --verbose # include info-level findings
|
|
||||||
npx hyperframes lint --json # machine-readable output for CI
|
|
||||||
npx hyperframes docs <topic> # reference docs in terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
|
|
||||||
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
|
|
||||||
> remains owned by the invoking session and can disappear while the browser stays open,
|
|
||||||
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
|
|
||||||
> alive through review, and stop it explicitly with `preview --stop` afterward.
|
|
||||||
|
|
||||||
> **Back from the desktop app.** Once this project was opened in the HyperFrames desktop app, run `npx hyperframes catch-up` before your next change here: it lists what the person asked Framey in the app and which files changed since.
|
|
||||||
|
|
||||||
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
**For quick reference**, use the local CLI docs command (no network required):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx hyperframes docs <topic>
|
|
||||||
```
|
|
||||||
|
|
||||||
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
|
|
||||||
|
|
||||||
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://hyperframes.heygen.com/llms.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `index.html` — main composition (root timeline)
|
|
||||||
- `compositions/` — sub-compositions referenced via `data-composition-src`
|
|
||||||
- `meta.json` — project metadata (id, name)
|
|
||||||
- `transcript.json` — whisper word-level transcript (if generated)
|
|
||||||
|
|
||||||
## Linting — ALWAYS RUN AFTER CHANGES
|
|
||||||
|
|
||||||
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run check
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
|
|
||||||
|
|
||||||
## Key Rules
|
|
||||||
|
|
||||||
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
|
|
||||||
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
|
|
||||||
3. Register one paused root timeline per composition on `window.__timelines`:
|
|
||||||
```js
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
|
|
||||||
```
|
|
||||||
Scene timelines manually added to this root must not be paused. A paused
|
|
||||||
child does not advance when the root is seeked. The runtime activates
|
|
||||||
registered composition siblings, not arbitrary nested scene timelines.
|
|
||||||
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
|
|
||||||
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
|
|
||||||
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
workflow: product-launch-video
|
|
||||||
flow: automation
|
|
||||||
storyboard: no
|
|
||||||
message: "Keep your kids safe on their phones, with peace of mind for parents"
|
|
||||||
destination: tiktok-reels-shorts
|
|
||||||
aspect: 1080x1920
|
|
||||||
language: en
|
|
||||||
audience: parents of children with smartphones
|
|
||||||
length: 60s
|
|
||||||
angle: parent peace of mind
|
|
||||||
---
|
|
||||||
|
|
||||||
## Intent
|
|
||||||
|
|
||||||
Promo for "lookatu", an Android and iOS child-safety app. Parents control how
|
|
||||||
much their kids use the phone (time limits, app control) and can track location
|
|
||||||
only when needed. Story shape: start with a worried parent, then lookatu brings
|
|
||||||
calm and safety. Warm, reassuring, trustworthy. Not scary, not spying.
|
|
||||||
|
|
||||||
## Assets
|
|
||||||
|
|
||||||
- None supplied. No website, logo, or screenshots yet. Use invented phone UI
|
|
||||||
mock-ups and a typographic "lookatu" wordmark. Real screens come in Phase 2.
|
|
||||||
|
|
||||||
## Customizations
|
|
||||||
|
|
||||||
- No-capture mode (nothing to scrape). A shipped preset supplies the design.
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
|
|
||||||
- User is not a coder: explain steps in plain language.
|
|
||||||
- Only claim features the user described: usage control, location tracking
|
|
||||||
when needed, Android and iOS. No invented stats, awards, or user counts.
|
|
||||||
- Frame tracking as parent-led and transparent (no secret spying language).
|
|
||||||
- Phase 2: swap in real logo and screenshots, add real voice. Phase 3: other sizes.
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
# HyperFrames Composition Project
|
|
||||||
|
|
||||||
## Skills — USE THESE FIRST
|
|
||||||
|
|
||||||
**Always invoke the relevant skill before writing or modifying compositions.** Skills encode framework-specific patterns (e.g., `window.__timelines` registration, `data-*` attribute semantics, shader-compatible CSS rules) that are NOT in generic web docs. Skipping them produces broken compositions.
|
|
||||||
|
|
||||||
**Doing anything with HyperFrames?** Start at `/hyperframes` — it tells you what HyperFrames can do and which skill or workflow handles your intent (make a video, TTS / BGM, prep footage, author / animate, render, install blocks), confirms your brief up front (the intent layer), and routes every "make me a…" request (a video, a deck, a composition port) to the right workflow. Read it first, especially when there's no project context to orient you. The workflows it routes to:
|
|
||||||
|
|
||||||
- `/product-launch-video` — any **website** URL or brief / script → a product launch / SaaS / promo video, or a site tour / showcase featuring the site's own captured visuals.
|
|
||||||
- `/faceless-explainer` — arbitrary text (topic / article / notes), **no URL, no website capture** → 60-90s faceless explainer.
|
|
||||||
- `/embedded-captions` — an existing talking-head video (MP4) → the same footage with captions / subtitles added (rail + embed, or pure-cinematic embed); the footage itself is untouched.
|
|
||||||
- `/talking-head-recut` — an existing talking-head / interview / podcast video (MP4) → the same footage **packaged with designed graphic overlays** (kinetic titles, lower-thirds, data callouts, pull-quotes, side panels, pip) synced to the transcript; the clip plays unchanged underneath. (Plain captions/subtitles → `/embedded-captions`.)
|
|
||||||
- `/pr-to-video` — a GitHub PR (URL / `owner/repo#N` / "this PR") → 30-90s code-change explainer (changelog / feature reveal / fix / refactor).
|
|
||||||
- `/motion-graphics` — a short (typically under 10s) design-led **motion graphic**, motion-is-the-message, no narration: kinetic type, a stat / number count-up, a chart, a logo sting, a lower-third / overlay, or an animated tweet / headline / captured-page highlight; rendered to MP4 or a transparent overlay. Longer / narrated / custom → `/general-video`.
|
|
||||||
- `/music-to-video` — a **music track** (audio file, video to pull audio from, or one generated from a mood brief) → beat-synced video (lyric / slideshow / kinetic promo). Music drives pacing; user-supplied images / videos are cut onto the same beat grid.
|
|
||||||
- `/slideshow` — a **presentation / pitch deck / interactive deck** — discrete slides, fragment reveals, branching, hotspot navigation, presenter mode. Output is a navigable deck, not a rendered video.
|
|
||||||
- `/general-video` — fallback for any other video (title card, longer brand / sizzle reel, multi-scene montage, static loop, custom composition) and the home of **companion mode** — co-create with the full HyperFrames toolbox; the original hyperframes authoring flow, any length.
|
|
||||||
|
|
||||||
**Porting an existing composition?** `/remotion-to-hyperframes` translates a Remotion (React) composition into HyperFrames HTML — a source migration, separate from the creation workflows above.
|
|
||||||
|
|
||||||
The domain skills (`/hyperframes-core`, `/hyperframes-animation`, `/hyperframes-keyframes`, `/hyperframes-creative`, `/hyperframes-cli`, `/media-use`, `/hyperframes-audio`, `/hyperframes-registry`, `/figma`) and the full capability map live inside `/hyperframes` — it is the single source of truth for which skill handles which intent.
|
|
||||||
|
|
||||||
**Changing how real footage or images look or reveal?** Load `/media-use` and read its `references/media-treatments.md` before editing, even when the request only says dark, flat, boring, retro, private, or “make the reveal cooler.” It governs how footage is treated, never whether media may be used. Use canonical media treatments and seek-safe motion; do not improvise equivalent CSS/SVG filters or overlays.
|
|
||||||
|
|
||||||
> **Tailwind v4 projects** (`hyperframes init --tailwind`): see `/hyperframes-core` → `references/tailwind.md`.
|
|
||||||
|
|
||||||
> **Using a HyperFrames plugin?** Load skills from that installed bundle and follow
|
|
||||||
> its `hyperframes/references/plugin-installation.md` execution rules. Update via
|
|
||||||
> the plugin manager, not the standalone commands below.
|
|
||||||
>
|
|
||||||
> **Standalone skill missing or stale?** Run `npx hyperframes skills update <name>` to install/refresh
|
|
||||||
> the specific skill you need (the `/hyperframes` router does this automatically before
|
|
||||||
> entering a workflow), or bare `npx hyperframes skills update` to refresh the core set plus
|
|
||||||
> everything already installed — neither pulls the full set. Restart the agent session so
|
|
||||||
> newly installed skills load.
|
|
||||||
|
|
||||||
## Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev # human-operated foreground preview (blocks until stopped)
|
|
||||||
npx hyperframes preview --background # agent-safe persistent Studio preview
|
|
||||||
npx hyperframes preview --status # verify the persistent preview is listening
|
|
||||||
npx hyperframes preview --stop # stop it when review is finished
|
|
||||||
npm run check # lint + runtime + layout + motion + contrast (one command)
|
|
||||||
npm run render # render to MP4
|
|
||||||
npm run publish # publish and get a shareable link
|
|
||||||
npx hyperframes lint --verbose # include info-level findings
|
|
||||||
npx hyperframes lint --json # machine-readable output for CI
|
|
||||||
npx hyperframes docs <topic> # reference docs in terminal
|
|
||||||
```
|
|
||||||
|
|
||||||
> **Agents must use `npx hyperframes preview --background` for Studio handoff.** Do not rely
|
|
||||||
> on a shell/tool `run_in_background` wrapper around `npm run dev`: that foreground process
|
|
||||||
> remains owned by the invoking session and can disappear while the browser stays open,
|
|
||||||
> leaving refreshes at `ERR_CONNECTION_TIMED_OUT`. Verify with `preview --status`, keep it
|
|
||||||
> alive through review, and stop it explicitly with `preview --stop` afterward.
|
|
||||||
|
|
||||||
> **Back from the desktop app.** Once this project was opened in the HyperFrames desktop app, run `npx hyperframes catch-up` before your next change here: it lists what the person asked Framey in the app and which files changed since.
|
|
||||||
|
|
||||||
> **Pinned CLI version.** These scripts pin an exact `hyperframes@X.Y.Z` so this project re-renders identically over time. Weeks later that pin lags fixes shipped since. To move up: `npx hyperframes@latest upgrade --project . --check` (shows the delta), then `npx hyperframes@latest upgrade --project .` to rewrite the pins. Always unpinned — the pinned script re-runs the old version against itself.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
**For quick reference**, use the local CLI docs command (no network required):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npx hyperframes docs <topic>
|
|
||||||
```
|
|
||||||
|
|
||||||
Topics: `data-attributes`, `gsap`, `compositions`, `rendering`, `examples`, `troubleshooting`
|
|
||||||
|
|
||||||
**For full documentation**, discover pages via the machine-readable index — do NOT guess URLs:
|
|
||||||
|
|
||||||
```
|
|
||||||
https://hyperframes.heygen.com/llms.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
- `index.html` — main composition (root timeline)
|
|
||||||
- `compositions/` — sub-compositions referenced via `data-composition-src`
|
|
||||||
- `meta.json` — project metadata (id, name)
|
|
||||||
- `transcript.json` — whisper word-level transcript (if generated)
|
|
||||||
|
|
||||||
## Linting — ALWAYS RUN AFTER CHANGES
|
|
||||||
|
|
||||||
After creating or editing any `.html` composition, **always** run the full check before considering the task complete:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run check
|
|
||||||
```
|
|
||||||
|
|
||||||
Fix all errors before presenting the result. Warnings should be reviewed before rendering.
|
|
||||||
|
|
||||||
## Key Rules
|
|
||||||
|
|
||||||
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
|
|
||||||
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
|
|
||||||
3. Register one paused root timeline per composition on `window.__timelines`:
|
|
||||||
```js
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["composition-id"] = gsap.timeline({ paused: true });
|
|
||||||
```
|
|
||||||
Scene timelines manually added to this root must not be paused. A paused
|
|
||||||
child does not advance when the root is seeked. The runtime activates
|
|
||||||
registered composition siblings, not arbitrary nested scene timelines.
|
|
||||||
4. A video with sound keeps it on the `<video>` (`data-has-audio="true"`, no `muted`). Use a separate `<audio>` for music, voiceover, replacement audio, J/L cuts, or audio detached in Studio. Silent footage and b-roll: `muted`.
|
|
||||||
5. Sub-compositions use `data-composition-src="compositions/file.html"` to reference other HTML files
|
|
||||||
6. Only deterministic logic — no `Date.now()`, no `Math.random()`, no network fetches
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
# SCRIPT — lookatu-promo
|
|
||||||
|
|
||||||
**Voice:** af_heart (Kokoro, offline)
|
|
||||||
**Voice settings:** default
|
|
||||||
**Voice direction:** Warm, calm, reassuring. Like a friend who is also a parent.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Line 1 — Where are they (Frame 1)
|
|
||||||
|
|
||||||
**Time:** 0.0 – 6.0s
|
|
||||||
**Delivery:** Gentle worry, then curious.
|
|
||||||
|
|
||||||
Where are they right now? And what are they really doing on that phone?
|
|
||||||
|
|
||||||
## Line 2 — The quiet worry (Frame 2)
|
|
||||||
|
|
||||||
**Time:** 6.0 – 13.0s
|
|
||||||
**Delivery:** Slow, a little tired, honest.
|
|
||||||
|
|
||||||
Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud.
|
|
||||||
|
|
||||||
## Line 3 — Meet lookatu (Frame 3)
|
|
||||||
|
|
||||||
**Time:** 13.0 – 22.0s
|
|
||||||
**Delivery:** A breath, then relief and warmth on the name.
|
|
||||||
|
|
||||||
There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind.
|
|
||||||
|
|
||||||
## Line 4 — Screen time limits (Frame 4)
|
|
||||||
|
|
||||||
**Time:** 22.0 – 30.0s
|
|
||||||
**Delivery:** Confident and clear.
|
|
||||||
|
|
||||||
Set screen time limits that actually stick. When the time is up, the phone knows.
|
|
||||||
|
|
||||||
## Line 5 — App control (Frame 5)
|
|
||||||
|
|
||||||
**Time:** 30.0 – 37.0s
|
|
||||||
**Delivery:** Crisp, three short beats.
|
|
||||||
|
|
||||||
Choose which apps are okay. Allow the good ones. Block the rest.
|
|
||||||
|
|
||||||
## Line 6 — Location when needed (Frame 6)
|
|
||||||
|
|
||||||
**Time:** 37.0 – 45.0s
|
|
||||||
**Delivery:** Soft and reassuring.
|
|
||||||
|
|
||||||
And when it really matters, see where they are. Location, only when you need it.
|
|
||||||
|
|
||||||
## Line 7 — Freedom and calm (Frame 7)
|
|
||||||
|
|
||||||
**Time:** 45.0 – 51.0s
|
|
||||||
**Delivery:** Warm, almost a smile.
|
|
||||||
|
|
||||||
Because kids deserve some freedom, and parents deserve some calm.
|
|
||||||
|
|
||||||
## Line 8 — Android and iPhone (Frame 8)
|
|
||||||
|
|
||||||
**Time:** 51.0 – 55.0s
|
|
||||||
**Delivery:** Simple and friendly.
|
|
||||||
|
|
||||||
Works on Android and on iPhone.
|
|
||||||
|
|
||||||
## Line 9 — Look out for them (Frame 9)
|
|
||||||
|
|
||||||
**Time:** 55.0 – 61.0s
|
|
||||||
**Delivery:** Warm sign-off, land on the name.
|
|
||||||
|
|
||||||
lookatu. Look out for them. Download it today.
|
|
||||||
@@ -1,169 +0,0 @@
|
|||||||
---
|
|
||||||
format: 1080x1920
|
|
||||||
duration: 60s
|
|
||||||
message: "Keep your kids safe on their phones, with peace of mind for parents"
|
|
||||||
arc: Hook → Pain → Product → Time limits → App control → Location → Balance → Platforms → CTA
|
|
||||||
audience: parents of children with smartphones
|
|
||||||
mode: autonomous
|
|
||||||
music: none
|
|
||||||
---
|
|
||||||
|
|
||||||
## Video direction
|
|
||||||
|
|
||||||
Daisy Days picture-book look: cream and pastel grounds, 3px charcoal outlines, hard offset shadows,
|
|
||||||
hand-drawn ornaments (daisy, star, sun, cloud) filling the corners. Warm, reassuring, never scary.
|
|
||||||
Motion is soft and bouncy (back.out, power3.out), each piece lands when the voice names it. Phone
|
|
||||||
mock-ups are invented generic screens with no real brand logos. Captions sit in the bottom band.
|
|
||||||
|
|
||||||
## Frame 1 — Where are they
|
|
||||||
|
|
||||||
- scene: Two big question bubbles pop in over a sky-blue ground, a small phone silhouette wobbles
|
|
||||||
- voiceover: "Where are they right now? And what are they really doing on that phone?"
|
|
||||||
- duration: 5.64s
|
|
||||||
- transition_in: crossfade
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/01-hook.html
|
|
||||||
- type: hook
|
|
||||||
- persuasion: Pain validation
|
|
||||||
- beat: worry
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Opens on the parent's silent question in outcome language.
|
|
||||||
keyMessage: Parents worry about where their kids are and what the phone is doing.
|
|
||||||
|
|
||||||
## Frame 2 — The quiet worry
|
|
||||||
|
|
||||||
- scene: Three sticker words stack in one by one: SCROLLING, ARGUING, WORRYING
|
|
||||||
- voiceover: "Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."
|
|
||||||
- duration: 7.111s
|
|
||||||
- transition_in: crossfade
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/02-worry.html
|
|
||||||
- type: pain_point
|
|
||||||
- persuasion: Pain agitation
|
|
||||||
- beat: tension
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Names the everyday pain so the product feels like relief.
|
|
||||||
keyMessage: The phone brings daily friction and worry.
|
|
||||||
|
|
||||||
## Frame 3 — Meet lookatu
|
|
||||||
|
|
||||||
- scene: The lookatu wordmark builds on a turquoise ground with a shield badge and a calm sun
|
|
||||||
- voiceover: "There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."
|
|
||||||
- duration: 8.144s
|
|
||||||
- transition_in: zoom-through
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/03-meet.html
|
|
||||||
- type: product_intro
|
|
||||||
- persuasion: Future pacing
|
|
||||||
- beat: relief
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Lands the promise (the message) by beat 3 and names the product.
|
|
||||||
keyMessage: lookatu gives parents peace of mind.
|
|
||||||
|
|
||||||
## Frame 4 — Screen time limits
|
|
||||||
|
|
||||||
- scene: A phone mock-up with a timer ring that counts down and locks with a gentle lock icon
|
|
||||||
- voiceover: "Set screen time limits that actually stick. When the time is up, the phone knows."
|
|
||||||
- duration: 6.599s
|
|
||||||
- transition_in: push-slide LEFT
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/04-limits.html
|
|
||||||
- type: feature_showcase
|
|
||||||
- persuasion: Feature-to-benefit translation
|
|
||||||
- beat: control
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Feature 1 shown working, with the benefit of limits that hold.
|
|
||||||
keyMessage: Parents set the phone usage limits.
|
|
||||||
|
|
||||||
## Frame 5 — App control
|
|
||||||
|
|
||||||
- scene: A phone mock-up with a list of generic app tiles and toggles; good apps switch on, others switch off
|
|
||||||
- voiceover: "Choose which apps are okay. Allow the good ones. Block the rest."
|
|
||||||
- duration: 5.703s
|
|
||||||
- transition_in: push-slide LEFT
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/05-apps.html
|
|
||||||
- type: feature_showcase
|
|
||||||
- persuasion: Show-don't-tell proof
|
|
||||||
- beat: control
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Feature 2, parents decide which apps are allowed.
|
|
||||||
keyMessage: Parents allow or block apps.
|
|
||||||
|
|
||||||
## Frame 6 — Location when needed
|
|
||||||
|
|
||||||
- scene: A soft map card with a pin that drops and pulses once, sitting inside a phone mock-up
|
|
||||||
- voiceover: "And when it really matters, see where they are. Location, only when you need it."
|
|
||||||
- duration: 6.513s
|
|
||||||
- transition_in: push-slide LEFT
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/06-location.html
|
|
||||||
- type: feature_showcase
|
|
||||||
- persuasion: Risk reversal
|
|
||||||
- beat: reassurance
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Feature 3, location tracking available when needed.
|
|
||||||
keyMessage: Parents can check location when needed.
|
|
||||||
|
|
||||||
## Frame 7 — Freedom and calm
|
|
||||||
|
|
||||||
- scene: Two big words on a pink ground, FREEDOM and CALM, each in a sticker card joined by a heart
|
|
||||||
- voiceover: "Because kids deserve some freedom, and parents deserve some calm."
|
|
||||||
- duration: 5.468s
|
|
||||||
- transition_in: crossfade
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/07-balance.html
|
|
||||||
- type: benefit_highlight
|
|
||||||
- persuasion: Rule of two
|
|
||||||
- beat: peace of mind
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: The emotional payoff that ties the features together.
|
|
||||||
keyMessage: Safety without a fight.
|
|
||||||
|
|
||||||
## Frame 8 — Android and iPhone
|
|
||||||
|
|
||||||
- scene: Two pill badges, ANDROID and iOS, pop in side by side on a butter ground
|
|
||||||
- voiceover: "Works on Android and on iPhone."
|
|
||||||
- duration: 3.783s
|
|
||||||
- transition_in: crossfade
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/08-platforms.html
|
|
||||||
- type: benefit_highlight
|
|
||||||
- persuasion: Friction reduction
|
|
||||||
- beat: ease
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Removes the "will it work on my phone" question.
|
|
||||||
keyMessage: Available for both Android and iOS.
|
|
||||||
|
|
||||||
## Frame 9 — Look out for them
|
|
||||||
|
|
||||||
- scene: Lavender ground, lookatu wordmark with the tagline, ornament wreath, gentle settle
|
|
||||||
- voiceover: "lookatu. Look out for them. Download it today."
|
|
||||||
- duration: 5.772s
|
|
||||||
- transition_in: zoom-through
|
|
||||||
- status: animated
|
|
||||||
- src: compositions/frames/09-cta.html
|
|
||||||
- type: cta
|
|
||||||
- persuasion: Clear call to action
|
|
||||||
- beat: confidence
|
|
||||||
- blueprint: compose
|
|
||||||
- asset_candidates:
|
|
||||||
|
|
||||||
narrativeRole: Closing lockup and call to action.
|
|
||||||
keyMessage: Download lookatu.
|
|
||||||
Binary file not shown.
Binary file not shown.
-11
@@ -1,11 +0,0 @@
|
|||||||
/*!
|
|
||||||
* GSAP 3.14.2
|
|
||||||
* https://gsap.com
|
|
||||||
*
|
|
||||||
* @license Copyright 2025, GreenSock. All rights reserved.
|
|
||||||
* Subject to the terms at https://gsap.com/standard-license.
|
|
||||||
* @author: Jack Doyle, jack@greensock.com
|
|
||||||
*/
|
|
||||||
|
|
||||||
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Xt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function oa(){var t,e,r=pt.length,i=pt.slice(0);for(_t={},t=pt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function pa(t){return!!(t._initted||t._startAt||t.add)}function qa(t,e,r,i){pt.length&&!I&&oa(),t.render(e,r,i||!!(I&&e<0&&pa(t))),pt.length&&!I&&oa()}function ra(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(ot).length<2?e:r(t)?t.trim():t}function sa(t){return t}function ta(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function wa(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?wa(t[r]||(t[r]={}),e[r]):e[r]);return t}function xa(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function ya(t){var e=t.parent||L,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}($(t.keyframes)):ta;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function Aa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(I?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||q)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ka(t),r._dirty||Da(r,t)),t}function Ma(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ja(t.rawTime(),e),(!e._dur||Mt(0,e.totalDuration(),r)-e._tTime>q)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-q}}function Na(e,r,i,n){return r.parent&&Ca(r),r._start=la((t(i)?i:i||e!==L?Ot(e,i,r):e._time)+r._delay),r._end=la(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),Aa(e,r,"_first","_last",e._sort?"_start":0),xt(r)||(e._recent=r),n||Ma(e,r),e._ts<0&&La(e,e._tTime),e}function Oa(t,e){return(ht.ScrollTrigger||S("scrollTrigger",e))&&ht.ScrollTrigger.create(e,t)}function Pa(t,e,r,i,n){return Qt(t,e,n),t._initted?!r&&t._pt&&!I&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==It.frame?(pt.push(t),t._lazy=[n,i],1):void 0:1}function Ua(t,e,r,i){var n=t._repeat,a=la(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:la(a*(n+1)+t._rDelay*n):a,0<s&&!i&&La(t,t._tTime=t._tDur*s),t.parent&&Ka(t),r||Da(t.parent,t),t}function Va(t){return t instanceof Zt?Da(t)Line truncated
|
|
||||||
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,71 +0,0 @@
|
|||||||
{
|
|
||||||
"tts_provider": "kokoro",
|
|
||||||
"voice_id": "af_heart",
|
|
||||||
"bgm": null,
|
|
||||||
"bgm_pending": false,
|
|
||||||
"bgm_provider": null,
|
|
||||||
"bgm_pid": null,
|
|
||||||
"bgm_log": null,
|
|
||||||
"bgm_mode": null,
|
|
||||||
"bgm_target_duration_s": null,
|
|
||||||
"bgm_seed_duration_s": null,
|
|
||||||
"bgm_loop_count": null,
|
|
||||||
"voices": [
|
|
||||||
{
|
|
||||||
"id": "01",
|
|
||||||
"path": "assets/voice/01.wav",
|
|
||||||
"duration_s": 3.84,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "02",
|
|
||||||
"path": "assets/voice/02.wav",
|
|
||||||
"duration_s": 5.611,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "03",
|
|
||||||
"path": "assets/voice/03.wav",
|
|
||||||
"duration_s": 6.144,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "04",
|
|
||||||
"path": "assets/voice/04.wav",
|
|
||||||
"duration_s": 5.099,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "05",
|
|
||||||
"path": "assets/voice/05.wav",
|
|
||||||
"duration_s": 4.203,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "06",
|
|
||||||
"path": "assets/voice/06.wav",
|
|
||||||
"duration_s": 5.013,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "07",
|
|
||||||
"path": "assets/voice/07.wav",
|
|
||||||
"duration_s": 3.968,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "08",
|
|
||||||
"path": "assets/voice/08.wav",
|
|
||||||
"duration_s": 2.283,
|
|
||||||
"words": []
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "09",
|
|
||||||
"path": "assets/voice/09.wav",
|
|
||||||
"duration_s": 3.072,
|
|
||||||
"words": []
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sfx": [],
|
|
||||||
"total_duration_s": 39.233
|
|
||||||
}
|
|
||||||
@@ -1,635 +0,0 @@
|
|||||||
{
|
|
||||||
"bgm": null,
|
|
||||||
"bgm_pending": false,
|
|
||||||
"voices": [
|
|
||||||
{
|
|
||||||
"frame": 1,
|
|
||||||
"path": "assets/voice/01.wav",
|
|
||||||
"duration_s": 5.64,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Where",
|
|
||||||
"start": 0.8,
|
|
||||||
"end": 1.058
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "are",
|
|
||||||
"start": 1.08,
|
|
||||||
"end": 1.264
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "they",
|
|
||||||
"start": 1.28,
|
|
||||||
"end": 1.501
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "right",
|
|
||||||
"start": 1.52,
|
|
||||||
"end": 1.778
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "now",
|
|
||||||
"start": 1.8,
|
|
||||||
"end": 2.205
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "And",
|
|
||||||
"start": 2.24,
|
|
||||||
"end": 2.424
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "what",
|
|
||||||
"start": 2.44,
|
|
||||||
"end": 2.661
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "are",
|
|
||||||
"start": 2.68,
|
|
||||||
"end": 2.864
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "they",
|
|
||||||
"start": 2.88,
|
|
||||||
"end": 3.101
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "really",
|
|
||||||
"start": 3.12,
|
|
||||||
"end": 3.414
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "doing",
|
|
||||||
"start": 3.44,
|
|
||||||
"end": 3.698
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "on",
|
|
||||||
"start": 3.72,
|
|
||||||
"end": 3.867
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "that",
|
|
||||||
"start": 3.88,
|
|
||||||
"end": 4.101
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "phone",
|
|
||||||
"start": 4.12,
|
|
||||||
"end": 4.598
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 2,
|
|
||||||
"path": "assets/voice/02.wav",
|
|
||||||
"duration_s": 7.111,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Endless",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.908
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "scrolling",
|
|
||||||
"start": 0.943,
|
|
||||||
"end": 1.713
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Arguments",
|
|
||||||
"start": 1.78,
|
|
||||||
"end": 2.278
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "at",
|
|
||||||
"start": 2.321,
|
|
||||||
"end": 2.502
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "dinner",
|
|
||||||
"start": 2.518,
|
|
||||||
"end": 3.152
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "And",
|
|
||||||
"start": 3.207,
|
|
||||||
"end": 3.433
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "that",
|
|
||||||
"start": 3.453,
|
|
||||||
"end": 3.725
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "quiet",
|
|
||||||
"start": 3.748,
|
|
||||||
"end": 4.065
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "worry",
|
|
||||||
"start": 4.093,
|
|
||||||
"end": 4.41
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "you",
|
|
||||||
"start": 4.438,
|
|
||||||
"end": 4.664
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "never",
|
|
||||||
"start": 4.684,
|
|
||||||
"end": 5.001
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "say",
|
|
||||||
"start": 5.028,
|
|
||||||
"end": 5.255
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "out",
|
|
||||||
"start": 5.274,
|
|
||||||
"end": 5.501
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "loud",
|
|
||||||
"start": 5.52,
|
|
||||||
"end": 6.064
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 3,
|
|
||||||
"path": "assets/voice/03.wav",
|
|
||||||
"duration_s": 8.144,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "There",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.807
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "has",
|
|
||||||
"start": 0.833,
|
|
||||||
"end": 1.052
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "to",
|
|
||||||
"start": 1.072,
|
|
||||||
"end": 1.247
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "be",
|
|
||||||
"start": 1.262,
|
|
||||||
"end": 1.437
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "a",
|
|
||||||
"start": 1.453,
|
|
||||||
"end": 1.584
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "better",
|
|
||||||
"start": 1.595,
|
|
||||||
"end": 1.946
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "way",
|
|
||||||
"start": 1.976,
|
|
||||||
"end": 2.458
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Meet",
|
|
||||||
"start": 2.5,
|
|
||||||
"end": 2.763
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "lookatu",
|
|
||||||
"start": 2.786,
|
|
||||||
"end": 3.443
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "The",
|
|
||||||
"start": 3.501,
|
|
||||||
"end": 3.72
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "child",
|
|
||||||
"start": 3.739,
|
|
||||||
"end": 4.045
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "safety",
|
|
||||||
"start": 4.072,
|
|
||||||
"end": 4.423
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "app",
|
|
||||||
"start": 4.453,
|
|
||||||
"end": 4.672
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "that",
|
|
||||||
"start": 4.691,
|
|
||||||
"end": 4.954
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "gives",
|
|
||||||
"start": 4.977,
|
|
||||||
"end": 5.284
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "you",
|
|
||||||
"start": 5.31,
|
|
||||||
"end": 5.53
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "peace",
|
|
||||||
"start": 5.549,
|
|
||||||
"end": 5.855
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "of",
|
|
||||||
"start": 5.882,
|
|
||||||
"end": 6.057
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "mind",
|
|
||||||
"start": 6.072,
|
|
||||||
"end": 6.598
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 4,
|
|
||||||
"path": "assets/voice/04.wav",
|
|
||||||
"duration_s": 6.599,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Set",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.715
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "screen",
|
|
||||||
"start": 0.734,
|
|
||||||
"end": 1.078
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "time",
|
|
||||||
"start": 1.108,
|
|
||||||
"end": 1.366
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "limits",
|
|
||||||
"start": 1.389,
|
|
||||||
"end": 1.733
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "that",
|
|
||||||
"start": 1.763,
|
|
||||||
"end": 2.021
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "actually",
|
|
||||||
"start": 2.044,
|
|
||||||
"end": 2.474
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "stick",
|
|
||||||
"start": 2.512,
|
|
||||||
"end": 3.071
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "When",
|
|
||||||
"start": 3.12,
|
|
||||||
"end": 3.378
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "the",
|
|
||||||
"start": 3.4,
|
|
||||||
"end": 3.616
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "time",
|
|
||||||
"start": 3.634,
|
|
||||||
"end": 3.892
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "is",
|
|
||||||
"start": 3.915,
|
|
||||||
"end": 4.087
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "up",
|
|
||||||
"start": 4.102,
|
|
||||||
"end": 4.403
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "the",
|
|
||||||
"start": 4.43,
|
|
||||||
"end": 4.645
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "phone",
|
|
||||||
"start": 4.663,
|
|
||||||
"end": 4.965
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "knows",
|
|
||||||
"start": 4.991,
|
|
||||||
"end": 5.55
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 5,
|
|
||||||
"path": "assets/voice/05.wav",
|
|
||||||
"duration_s": 5.703,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Choose",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.836
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "which",
|
|
||||||
"start": 0.865,
|
|
||||||
"end": 1.16
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "apps",
|
|
||||||
"start": 1.185,
|
|
||||||
"end": 1.437
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "are",
|
|
||||||
"start": 1.459,
|
|
||||||
"end": 1.67
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "okay",
|
|
||||||
"start": 1.688,
|
|
||||||
"end": 2.192
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Allow",
|
|
||||||
"start": 2.236,
|
|
||||||
"end": 2.53
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "the",
|
|
||||||
"start": 2.556,
|
|
||||||
"end": 2.766
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "good",
|
|
||||||
"start": 2.784,
|
|
||||||
"end": 3.036
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "ones",
|
|
||||||
"start": 3.058,
|
|
||||||
"end": 3.563
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Block",
|
|
||||||
"start": 3.607,
|
|
||||||
"end": 3.901
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "the",
|
|
||||||
"start": 3.926,
|
|
||||||
"end": 4.137
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "rest",
|
|
||||||
"start": 4.155,
|
|
||||||
"end": 4.659
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 6,
|
|
||||||
"path": "assets/voice/06.wav",
|
|
||||||
"duration_s": 6.513,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "And",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.71
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "when",
|
|
||||||
"start": 0.728,
|
|
||||||
"end": 0.979
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "it",
|
|
||||||
"start": 1.001,
|
|
||||||
"end": 1.169
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "really",
|
|
||||||
"start": 1.184,
|
|
||||||
"end": 1.519
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "matters",
|
|
||||||
"start": 1.548,
|
|
||||||
"end": 2.051
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "see",
|
|
||||||
"start": 2.095,
|
|
||||||
"end": 2.305
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "where",
|
|
||||||
"start": 2.323,
|
|
||||||
"end": 2.616
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "they",
|
|
||||||
"start": 2.642,
|
|
||||||
"end": 2.893
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "are",
|
|
||||||
"start": 2.915,
|
|
||||||
"end": 3.377
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Location",
|
|
||||||
"start": 3.417,
|
|
||||||
"end": 3.962
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "only",
|
|
||||||
"start": 4.009,
|
|
||||||
"end": 4.261
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "when",
|
|
||||||
"start": 4.283,
|
|
||||||
"end": 4.534
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "you",
|
|
||||||
"start": 4.556,
|
|
||||||
"end": 4.766
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "need",
|
|
||||||
"start": 4.784,
|
|
||||||
"end": 5.035
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "it",
|
|
||||||
"start": 5.057,
|
|
||||||
"end": 5.477
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 7,
|
|
||||||
"path": "assets/voice/07.wav",
|
|
||||||
"duration_s": 5.468,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Because",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.896
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "kids",
|
|
||||||
"start": 0.93,
|
|
||||||
"end": 1.194
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "deserve",
|
|
||||||
"start": 1.217,
|
|
||||||
"end": 1.613
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "some",
|
|
||||||
"start": 1.647,
|
|
||||||
"end": 1.911
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "freedom",
|
|
||||||
"start": 1.934,
|
|
||||||
"end": 2.462
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "and",
|
|
||||||
"start": 2.508,
|
|
||||||
"end": 2.728
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "parents",
|
|
||||||
"start": 2.747,
|
|
||||||
"end": 3.143
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "deserve",
|
|
||||||
"start": 3.177,
|
|
||||||
"end": 3.573
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "some",
|
|
||||||
"start": 3.607,
|
|
||||||
"end": 3.871
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "calm",
|
|
||||||
"start": 3.894,
|
|
||||||
"end": 4.422
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 8,
|
|
||||||
"path": "assets/voice/08.wav",
|
|
||||||
"duration_s": 3.783,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "Works",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 0.842
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "on",
|
|
||||||
"start": 0.872,
|
|
||||||
"end": 1.067
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Android",
|
|
||||||
"start": 1.084,
|
|
||||||
"end": 1.524
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "and",
|
|
||||||
"start": 1.562,
|
|
||||||
"end": 1.806
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "on",
|
|
||||||
"start": 1.827,
|
|
||||||
"end": 2.023
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "iPhone",
|
|
||||||
"start": 2.04,
|
|
||||||
"end": 2.724
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"frame": 9,
|
|
||||||
"path": "assets/voice/09.wav",
|
|
||||||
"duration_s": 5.772,
|
|
||||||
"words": [
|
|
||||||
{
|
|
||||||
"text": "lookatu",
|
|
||||||
"start": 0.5,
|
|
||||||
"end": 1.106
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Look",
|
|
||||||
"start": 1.158,
|
|
||||||
"end": 1.401
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "out",
|
|
||||||
"start": 1.422,
|
|
||||||
"end": 1.623
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "for",
|
|
||||||
"start": 1.641,
|
|
||||||
"end": 1.843
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "them",
|
|
||||||
"start": 1.86,
|
|
||||||
"end": 2.345
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "Download",
|
|
||||||
"start": 2.387,
|
|
||||||
"end": 2.791
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "it",
|
|
||||||
"start": 2.826,
|
|
||||||
"end": 2.987
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"text": "today",
|
|
||||||
"start": 3.001,
|
|
||||||
"end": 3.526
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sfx": []
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
{
|
|
||||||
"provider": "kokoro",
|
|
||||||
"speed": "0.85",
|
|
||||||
"lines": [
|
|
||||||
{
|
|
||||||
"id": "01",
|
|
||||||
"text": "Where are they right now? And what are they really doing on that phone?"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "02",
|
|
||||||
"text": "Endless scrolling. Arguments at dinner. And that quiet worry you never say out loud."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "03",
|
|
||||||
"text": "There has to be a better way. Meet lookatu. The child safety app that gives you peace of mind."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "04",
|
|
||||||
"text": "Set screen time limits that actually stick. When the time is up, the phone knows."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "05",
|
|
||||||
"text": "Choose which apps are okay. Allow the good ones. Block the rest."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "06",
|
|
||||||
"text": "And when it really matters, see where they are. Location, only when you need it."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "07",
|
|
||||||
"text": "Because kids deserve some freedom, and parents deserve some calm."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "08",
|
|
||||||
"text": "Works on Android and on iPhone."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": "09",
|
|
||||||
"text": "lookatu. Look out for them. Download it today."
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"bgm": {
|
|
||||||
"mode": "none"
|
|
||||||
},
|
|
||||||
"voice": "af_heart"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1 +0,0 @@
|
|||||||
No assets were captured. No website, logo or screenshots supplied. Use invented phone UI mock-ups and a typographic lookatu wordmark.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{ "title": "lookatu", "description": "Child-safety app (Android and iOS) that lets parents control kids' phone usage and track location when needed.", "colors": [], "fonts": [] }
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
lookatu - child safety app for Android and iOS.
|
|
||||||
Parents control how their kids use the phone (time limits, app control).
|
|
||||||
Parents can track location when needed.
|
|
||||||
Message: keep your kids safe on their phones, with peace of mind for parents.
|
|
||||||
@@ -1,235 +0,0 @@
|
|||||||
<template id="captions-template" data-composition-id="captions" data-width="1080" data-height="1920">
|
|
||||||
<script src="assets/vendor/gsap.min.js"></script>
|
|
||||||
|
|
||||||
|
|
||||||
<style data-brand-tokens>
|
|
||||||
@font-face { font-family: 'Quicksand'; src: url('assets/fonts/quicksand-var.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: block; }
|
|
||||||
:root {
|
|
||||||
--cream: #F5F0E6;
|
|
||||||
--turquoise: #7ECDC0;
|
|
||||||
--soft-pink: #F7C8D4;
|
|
||||||
--butter: #FDE68A;
|
|
||||||
--mint: #A8E6CF;
|
|
||||||
--lavender: #D4A5E8;
|
|
||||||
--peach: #FFCBA4;
|
|
||||||
--sky: #A8D8F0;
|
|
||||||
--coral: #F8635F;
|
|
||||||
--text-dark: #2D2D2D;
|
|
||||||
--text-muted: #6B6B6B;
|
|
||||||
--white: #FFFFFF;
|
|
||||||
--cap-ink: #2D2D2D;
|
|
||||||
--cap-canvas: #F5F0E6;
|
|
||||||
--cap-accent: #F8635F;
|
|
||||||
--cap-accent-2: #FDE68A;
|
|
||||||
--font-display: "Fredoka One", system-ui, serif;
|
|
||||||
--font-body: "Quicksand", system-ui, sans-serif;
|
|
||||||
--cap-band-top: 1600px;
|
|
||||||
--cap-band-height: 320px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Fredoka One"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#captions-root {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
.caption-layer {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 20;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
/* the keep-out band, from captionBand(H) — frame content stays above it */
|
|
||||||
.caption-stage {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
top: var(--cap-band-top, 900px);
|
|
||||||
height: var(--cap-band-height, 180px);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
.caption-group {
|
|
||||||
/* every group is absolutely stacked over the SAME band — exactly one shown at a
|
|
||||||
time via opacity. Must be absolute: in-flow groups would lay out side-by-side. */
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* THE picture-book card — cream surface, 3px charcoal outline, generous 24px radius,
|
|
||||||
a hard 6px charcoal offset shadow (no blur): the sticker-on-paper signature */
|
|
||||||
.caption-pill {
|
|
||||||
max-width: 80%;
|
|
||||||
padding: 18px 40px 22px;
|
|
||||||
background: var(--cap-canvas, #f5f0e6);
|
|
||||||
border: 3px solid var(--cap-ink, #2d2d2d);
|
|
||||||
border-radius: 24px;
|
|
||||||
box-shadow: 6px 6px 0 var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
.caption-line {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.18em 0.32em;
|
|
||||||
font-family: "Fredoka One", sans-serif;
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: clamp(38px, 4.2vw, 56px);
|
|
||||||
line-height: 1.22;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* upcoming — muted charcoal, no candy block */
|
|
||||||
.caption-word {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.02em 0.16em;
|
|
||||||
border-radius: 12px;
|
|
||||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 52%, var(--cap-canvas, #f5f0e6));
|
|
||||||
}
|
|
||||||
/* current word — THE sunny-pastel candy block: a rounded coral chip with a 3px
|
|
||||||
charcoal outline + a hard 3px charcoal offset shadow (no blur), charcoal on top */
|
|
||||||
.caption-word.is-active {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: var(--cap-accent, #f8635f);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 3px var(--cap-ink, #2d2d2d),
|
|
||||||
3px 5px 0 0 var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
/* already spoken — solid charcoal, candy block cleared */
|
|
||||||
.caption-word.is-spoken {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: transparent;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* optional per-word accents (dormant until a word-classer tags them) */
|
|
||||||
/* brand / key term — a butter candy chip, the cheerful second pastel */
|
|
||||||
.caption-word.cap-brand {
|
|
||||||
color: var(--cap-ink, #2d2d2d);
|
|
||||||
background: var(--cap-accent-2, #fde68a);
|
|
||||||
border-radius: 12px;
|
|
||||||
box-shadow: 0 0 0 3px var(--cap-ink, #2d2d2d);
|
|
||||||
}
|
|
||||||
/* numerals / data — Quicksand, slightly recessed */
|
|
||||||
.caption-word.cap-num {
|
|
||||||
font-family: "Quicksand", system-ui, sans-serif;
|
|
||||||
font-weight: 700;
|
|
||||||
color: color-mix(in srgb, var(--cap-ink, #2d2d2d) 78%, var(--cap-canvas, #f5f0e6));
|
|
||||||
}
|
|
||||||
/* emphasis — a butter underline rule (the second sunny pastel) */
|
|
||||||
.caption-word.cap-cta {
|
|
||||||
text-decoration: underline;
|
|
||||||
text-decoration-thickness: 3px;
|
|
||||||
text-decoration-color: var(--cap-accent-2, #fde68a);
|
|
||||||
text-underline-offset: 0.12em;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-aspect-ratio: 9/16) {
|
|
||||||
.caption-pill {
|
|
||||||
max-width: 92%;
|
|
||||||
}
|
|
||||||
.caption-line {
|
|
||||||
font-size: clamp(32px, 5.2vw, 46px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="captions-root"
|
|
||||||
data-composition-id="captions"
|
|
||||||
data-timeline-locked
|
|
||||||
data-start="0"
|
|
||||||
data-duration="54.733"
|
|
||||||
data-fps="30"
|
|
||||||
data-width="1080"
|
|
||||||
data-height="1920"
|
|
||||||
>
|
|
||||||
<div class="caption-layer" aria-hidden="true">
|
|
||||||
<div id="caption-stage" class="caption-stage"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// captions.mjs injects the engine's scene-aware groups + real duration here.
|
|
||||||
var GROUPS = [{"id":"caption-group-0","frame":1,"start":0.8,"end":1.28,"text":"Where are","words":[{"id":"caption-word-0-0","text":"Where","start":0.8,"end":1.058},{"id":"caption-word-0-1","text":"are","start":1.08,"end":1.264}]},{"id":"caption-group-1","frame":1,"start":1.28,"end":1.8,"text":"they right","words":[{"id":"caption-word-1-0","text":"they","start":1.28,"end":1.501},{"id":"caption-word-1-1","text":"right","start":1.52,"end":1.778}]},{"id":"caption-group-2","frame":1,"start":1.8,"end":2.44,"text":"now And","words":[{"id":"caption-word-2-0","text":"now","start":1.8,"end":2.205},{"id":"caption-word-2-1","text":"And","start":2.24,"end":2.424}]},{"id":"caption-group-3","frame":1,"start":2.44,"end":2.88,"text":"what are","words":[{"id":"caption-word-3-0","text":"what","start":2.44,"end":2.661},{"id":"caption-word-3-1","text":"are","start":2.68,"end":2.864}]},{"id":"caption-group-4","frame":1,"start":2.88,"end":3.44,"text":"they really","words":[{"id":"caption-word-4-0","text":"they","start":2.88,"end":3.101},{"id":"caption-word-4-1","text":"really","start":3.12,"end":3.414}]},{"id":"caption-group-5","frame":1,"start":3.44,"end":3.88,"text":"doing on","words":[{"id":"caption-word-5-0","text":"doing","start":3.44,"end":3.698},{"id":"caption-word-5-1","text":"on","start":3.72,"end":3.867}]},{"id":"caption-group-6","frame":1,"start":3.88,"end":4.718,"text":"that phone","words":[{"id":"caption-word-6-0","text":"that","start":3.88,"end":4.101},{"id":"caption-word-6-1","text":"phone","start":4.12,"end":4.598}]},{"id":"caption-group-7","frame":2,"start":6.14,"end":8.158,"text":"Endless scrolling Arguments at","words":[{"id":"caption-word-7-0","text":"Endless","start":6.14,"end":6.548},{"id":"caption-word-7-1","text":"scrolling","start":6.583,"end":7.353},{"id":"caption-word-7-2","text":"Arguments","start":7.42,"end":7.918},{"id":"caption-word-7-3","text":"at","start":7.961,"end":8.142}]},{"id":"caption-group-8","frame":2,"start":8.158,"end":9.388,"text":"dinner And that","words":[{"id":"caption-word-8-0","text":"dinner","start":8.158,"end":8.792},{"id":"caption-word-8-1","text":"And","start":8.847,"end":9.073},{"id":"caption-word-8-2","text":"that","start":9.093,"end":9.365}]},{"id":"caption-group-9","frame":2,"start":9.388,"end":10.078,"text":"quiet worry","words":[{"id":"caption-word-9-0","text":"quiet","start":9.388,"end":9.705},{"id":"caption-word-9-1","text":"worry","start":9.733,"end":10.05}]},{"id":"caption-group-10","frame":2,"start":10.078,"end":10.668,"text":"you never","words":[{"id":"caption-word-10-0","text":"you","start":10.078,"end":10.304},{"id":"caption-word-10-1","text":"never","start":10.324,"end":10.641}]},{"id":"caption-group-11","frame":2,"start":10.668,"end":11.824,"text":"say out loud","words":[{"id":"caption-word-11-0","text":"say","start":10.668,"end":10.895},{"id":"caption-word-11-1","text":"out","start":10.914,"end":11.141},{"id":"caption-word-11-2","text":"loud","start":11.16,"end":11.704}]},{"id":"caption-group-12","frame":3,"start":13.251,"end":13.823,"text":"There has","words":[{"id":"caption-word-12-0","text":"There","start":13.251,"end":13.558},{"id":"caption-word-12-1","text":"has","start":13.584,"end":13.803}]},{"id":"caption-group-13","frame":3,"start":13.823,"end":14.204,"text":"to be","words":[{"id":"caption-word-13-0","text":"to","start":13.823,"end":13.998},{"id":"caption-word-13-1","text":"be","start":14.013,"end":14.188}]},{"id":"caption-group-14","frame":3,"start":14.204,"end":15.251,"text":"a better way","words":[{"id":"caption-word-14-0","text":"a","start":14.204,"end":14.335},{"id":"caption-word-14-1","text":"better","start":14.346,"end":14.697},{"id":"caption-word-14-2","text":"way","start":14.727,"end":15.209}]},{"id":"caption-group-15","frame":3,"start":15.251,"end":16.823,"text":"Meet lookatu The child","words":[{"id":"caption-word-15-0","text":"Meet","start":15.251,"end":15.514},{"id":"caption-word-15-1","text":"lookatu","start":15.537,"end":16.194},{"id":"caption-word-15-2","text":"The","start":16.252,"end":16.471},{"id":"caption-word-15-3","text":"child","start":16.49,"end":16.796}]},{"id":"caption-group-16","frame":3,"start":16.823,"end":17.442,"text":"safety app","words":[{"id":"caption-word-16-0","text":"safety","start":16.823,"end":17.174},{"id":"caption-word-16-1","text":"app","start":17.204,"end":17.423}]},{"id":"caption-group-17","frame":3,"start":17.442,"end":18.061,"text":"that gives","words":[{"id":"caption-word-17-0","text":"that","start":17.442,"end":17.705},{"id":"caption-word-17-1","text":"gives","start":17.728,"end":18.035}]},{"id":"caption-group-18","frame":3,"start":18.061,"end":18.633,"text":"you peace","words":[{"id":"caption-word-18-0","text":"you","start":18.061,"end":18.281},{"id":"caption-word-18-1","text":"peace","start":18.3,"end":18.606}]},{"id":"caption-group-19","frame":3,"start":18.633,"end":19.469,"text":"of mind","words":[{"id":"caption-word-19-0","text":"of","start":18.633,"end":18.808},{"id":"caption-word-19-1","text":"mLine truncated
|
|
||||||
var DURATION = 54.733;
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
var stage = document.getElementById("caption-stage");
|
|
||||||
|
|
||||||
// build DOM: one .caption-group per group, .caption-word per word
|
|
||||||
GROUPS.forEach(function (group, g) {
|
|
||||||
var groupEl = document.createElement("div");
|
|
||||||
groupEl.className = "caption-group";
|
|
||||||
groupEl.id = "caption-group-" + g;
|
|
||||||
var pill = document.createElement("div");
|
|
||||||
pill.className = "caption-pill";
|
|
||||||
var line = document.createElement("div");
|
|
||||||
line.className = "caption-line";
|
|
||||||
(group.words || []).forEach(function (w, i) {
|
|
||||||
var span = document.createElement("span");
|
|
||||||
span.className = "caption-word";
|
|
||||||
span.id = "caption-word-" + g + "-" + i;
|
|
||||||
span.textContent = String(w.text);
|
|
||||||
line.appendChild(span);
|
|
||||||
});
|
|
||||||
pill.appendChild(line);
|
|
||||||
groupEl.appendChild(pill);
|
|
||||||
stage.appendChild(groupEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
var tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
GROUPS.forEach(function (group, g) {
|
|
||||||
var groupEl = document.getElementById("caption-group-" + g);
|
|
||||||
var words = group.words || [];
|
|
||||||
var next = GROUPS[g + 1];
|
|
||||||
var isLast = g === GROUPS.length - 1;
|
|
||||||
var start = Math.max(0, Number(group.start));
|
|
||||||
var end = isLast ? DURATION : Math.min(Number(next.start), Number(group.end) + 0.3);
|
|
||||||
if (end <= start) end = start + 0.01;
|
|
||||||
|
|
||||||
// group on/off — exactly one group visible at a time
|
|
||||||
tl.set(groupEl, { opacity: 1 }, start);
|
|
||||||
tl.set(groupEl, { opacity: 0 }, end);
|
|
||||||
|
|
||||||
// 3-state karaoke via className SETS — seek-safe (gsap.set applies on the
|
|
||||||
// engine's frame-by-frame seek; .call callbacks would NOT fire).
|
|
||||||
words.forEach(function (w, i) {
|
|
||||||
var el = document.getElementById("caption-word-" + g + "-" + i);
|
|
||||||
var at = Math.max(start, Number(w.start));
|
|
||||||
tl.set(el, { className: "caption-word" }, start); // upcoming (also resets on reverse-seek)
|
|
||||||
tl.set(el, { className: "caption-word is-active" }, at); // current
|
|
||||||
tl.fromTo(el, { scale: 0.82 }, { scale: 1, duration: 0.2, ease: "back.out(2.6)" }, at);
|
|
||||||
if (i + 1 < words.length) {
|
|
||||||
var nextAt = Math.max(start, Number(words[i + 1].start));
|
|
||||||
tl.set(el, { className: "caption-word is-spoken" }, nextAt); // demote when next activates
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (words.length) {
|
|
||||||
var lastEl = document.getElementById("caption-word-" + g + "-" + (words.length - 1));
|
|
||||||
var lastSpoken = Math.min(end, Number(words[words.length - 1].end) + 0.1);
|
|
||||||
tl.set(lastEl, { className: "caption-word is-spoken" }, lastSpoken);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// full-span anchor so the sub-comp timeline spans the whole video
|
|
||||||
tl.to({}, { duration: DURATION }, 0);
|
|
||||||
window.__timelines["captions"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.caption-line { line-height: 1.1 !important; }
|
|
||||||
</style>
|
|
||||||
</template>
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f1-bg { position: absolute; inset: 0; }
|
|
||||||
.f1-orn { position: absolute; z-index: 1; }
|
|
||||||
.f1-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f1-z { position: absolute; z-index: 2; }
|
|
||||||
.f1-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f1-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f1-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f1-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f1-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f1-bub { position: absolute; z-index: 2; display: flex; align-items: center; justify-content: center; border-radius: 70px; }
|
|
||||||
.f1-bub span { font-size: 168px; color: #2D2D2D; }
|
|
||||||
.f1-b1 { left: 80px; top: 250px; width: 760px; height: 360px; }
|
|
||||||
.f1-b2 { left: 240px; top: 700px; width: 760px; height: 360px; background: #FDE68A; }
|
|
||||||
.f1-tail1 { position: absolute; z-index: 3; left: 190px; top: 584px; width: 70px; height: 70px; background: #FFFFFF; border-right: 5px solid #2D2D2D; border-bottom: 5px solid #2D2D2D; transform: rotate(45deg); }
|
|
||||||
.f1-tail2 { position: absolute; z-index: 3; left: 820px; top: 1034px; width: 70px; height: 70px; background: #FDE68A; border-right: 5px solid #2D2D2D; border-bottom: 5px solid #2D2D2D; transform: rotate(45deg); }
|
|
||||||
.f1-ph { left: 390px; top: 1160px; width: 300px; height: 420px; background: #2D2D2D; border: 6px solid #2D2D2D; border-radius: 56px; box-shadow: 12px 12px 0 rgba(45,45,45,0.25); display: flex; align-items: center; justify-content: center; }
|
|
||||||
.f1-ph b { font-family: "Fredoka", sans-serif; font-weight: 600; font-size: 170px; color: #FDE68A; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="01-hook" data-width="1080" data-height="1920" data-duration="6.14">
|
|
||||||
<div id="scene1-bg" class="clip f1-bg" data-start="0" data-duration="6.14" data-track-index="0" style="background:#A8D8F0"></div>
|
|
||||||
<div class="f1-orn" style="left:-70px;top:90px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(-4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f1-orn" style="left:800px;top:40px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f1-orn" style="left:900px;top:1150px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f1-orn" style="left:-50px;top:1330px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f1-orn" style="left:760px;top:1380px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(5deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f1-bub f1-card f1-b1"><span class="f1-d">WHERE?</span></div>
|
|
||||||
<div class="f1-tail1"></div>
|
|
||||||
<div class="f1-bub f1-card f1-b2"><span class="f1-d">WHAT?</span></div>
|
|
||||||
<div class="f1-tail2"></div>
|
|
||||||
<div class="f1-z f1-ph"><b>?</b></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 5.64;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f1-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f1-b1", { scale: 0.2, opacity: 0, rotation: -8 }, { scale: 1, opacity: 1, rotation: -2, duration: 0.55, ease: "back.out(1.9)" }, 0.3);
|
|
||||||
tl.fromTo(".f1-tail1", { opacity: 0 }, { opacity: 1, duration: 0.2 }, 0.6499999999999999);
|
|
||||||
tl.fromTo(".f1-b2", { scale: 0.2, opacity: 0, rotation: 8 }, { scale: 1, opacity: 1, rotation: 2, duration: 0.55, ease: "back.out(1.9)" }, 2.12);
|
|
||||||
tl.fromTo(".f1-tail2", { opacity: 0 }, { opacity: 1, duration: 0.2 }, 2.47);
|
|
||||||
tl.fromTo(".f1-ph", { scale: 0, rotation: 12 }, { scale: 1, rotation: 0, duration: 0.6, ease: "back.out(1.7)" }, 3);
|
|
||||||
tl.to(".f1-ph", { rotation: -5, duration: 0.18, ease: "sine.inOut" }, 3.6)
|
|
||||||
.to(".f1-ph", { rotation: 5, duration: 0.3, ease: "sine.inOut" }, ">")
|
|
||||||
.to(".f1-ph", { rotation: -3, duration: 0.3, ease: "sine.inOut" }, ">")
|
|
||||||
.to(".f1-ph", { rotation: 0, duration: 0.25, ease: "sine.inOut" }, ">");
|
|
||||||
window.__timelines["01-hook"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f2-bg { position: absolute; inset: 0; }
|
|
||||||
.f2-orn { position: absolute; z-index: 1; }
|
|
||||||
.f2-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f2-z { position: absolute; z-index: 2; }
|
|
||||||
.f2-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f2-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f2-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f2-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f2-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f2-row { position: absolute; z-index: 2; left: 100px; width: 880px; height: 320px; display: flex; align-items: center; padding: 0 44px; gap: 34px; }
|
|
||||||
.f2-row svg { width: 130px; height: 130px; flex: none; background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 50%; padding: 22px; box-sizing: border-box; }
|
|
||||||
.f2-row span { font-size: 92px; color: #2D2D2D; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="02-worry" data-width="1080" data-height="1920" data-duration="7.511">
|
|
||||||
<div id="scene2-bg" class="clip f2-bg" data-start="0" data-duration="7.511" data-track-index="0" style="background:#F5F0E6"></div>
|
|
||||||
<div class="f2-orn" style="left:880px;top:90px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f2-orn" style="left:-60px;top:150px;width:220px;height:220px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f2-orn" style="left:780px;top:1360px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(-6deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f2-orn" style="left:50px;top:1420px;width:130px;height:130px"><svg viewBox="0 0 100 100" style="transform:rotate(-14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f2-orn" style="left:900px;top:700px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(20deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f2-row f2-card f2-r0" style="top:250px;background:#FDE68A"><svg viewBox="0 0 100 100"><path d="M20 34 L50 62 L80 34" fill="none" stroke="#2D2D2D" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/><path d="M20 12 L50 40 L80 12" fill="none" stroke="#2D2D2D" stroke-width="9" stroke-linecap="round" stroke-linejoin="round"/></svg><span class="f2-d">SCROLLING</span></div>
|
|
||||||
<div class="f2-row f2-card f2-r1" style="top:650px;background:#F7C8D4"><svg viewBox="0 0 100 100"><polygon points="58,6 24,54 48,54 40,94 76,42 52,42" fill="#F8635F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/></svg><span class="f2-d">ARGUING</span></div>
|
|
||||||
<div class="f2-row f2-card f2-r2" style="top:1050px;background:#D4A5E8"><svg viewBox="0 0 100 100"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/></svg><span class="f2-d">WORRYING</span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 7.111;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f2-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
tl.fromTo(".f2-r0", { x: -260, opacity: 0, rotation: -12 }, { x: 0, opacity: 1, rotation: -3, duration: 0.6, ease: "back.out(1.5)" }, 0.38);
|
|
||||||
tl.fromTo(".f2-r1", { x: 260, opacity: 0, rotation: 10 }, { x: 0, opacity: 1, rotation: 2.5, duration: 0.6, ease: "back.out(1.5)" }, 1.66);
|
|
||||||
tl.fromTo(".f2-r2", { x: -260, opacity: 0, rotation: -8 }, { x: 0, opacity: 1, rotation: -2, duration: 0.6, ease: "back.out(1.5)" }, 3.09);
|
|
||||||
window.__timelines["02-worry"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f3-bg { position: absolute; inset: 0; }
|
|
||||||
.f3-orn { position: absolute; z-index: 1; }
|
|
||||||
.f3-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f3-z { position: absolute; z-index: 2; }
|
|
||||||
.f3-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f3-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f3-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f3-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f3-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f3-eye { display: block; position: relative; width: 128.8px; height: 128.8px; border-radius: 50%; background: #FFFFFF; border: 6.8999999999999995px solid #2D2D2D; box-sizing: border-box; text-shadow: none; }
|
|
||||||
.f3-pup { position: absolute; left: 50%; top: 50%; width: 50.6px; height: 50.6px; margin: -25.3px 0 0 -25.3px; border-radius: 50%; background: #2D2D2D; }
|
|
||||||
.f3-badge { left: 50%; top: 250px; margin-left: -330px; width: 660px; justify-content: center; background: #FDE68A; }
|
|
||||||
.f3-badge span { font-size: 62px; }
|
|
||||||
.f3-wmwrap { left: 0; right: 0; top: 700px; }
|
|
||||||
.f3-shield { left: 50%; top: 380px; margin-left: -115px; width: 230px; height: 260px; }
|
|
||||||
.f3-peace { left: 50%; top: 1130px; margin-left: -400px; width: 800px; height: 200px; display: flex; align-items: center; justify-content: center; gap: 36px; }
|
|
||||||
.f3-peace span { font-size: 78px; }
|
|
||||||
.f3-peace svg { width: 100px; height: 100px; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="03-meet" data-width="1080" data-height="1920" data-duration="8.644">
|
|
||||||
<div id="scene3-bg" class="clip f3-bg" data-start="0" data-duration="8.644" data-track-index="0" style="background:#7ECDC0"></div>
|
|
||||||
<div class="f3-orn" style="left:790px;top:60px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f3-orn" style="left:-80px;top:200px;width:330px;height:330px"><svg viewBox="0 0 100 100" style="transform:rotate(-3deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f3-orn" style="left:-40px;top:1250px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f3-orn" style="left:820px;top:1330px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f3-orn" style="left:130px;top:1010px;width:120px;height:120px"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f3-orn" style="left:830px;top:1020px;width:140px;height:140px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f3-z f3-pill f3-badge"><span class="f3-d">A BETTER WAY</span></div>
|
|
||||||
<svg class="f3-z f3-shield" viewBox="0 0 100 112"><path d="M50 4 L92 18 V54 C92 82 70 100 50 108 C30 100 8 82 8 54 V18 Z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="4.5" stroke-linejoin="round"/><path d="M30 56 L45 71 L72 40" fill="none" stroke="#F8635F" stroke-width="10" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
||||||
<div class="f3-z f3-wmwrap">
|
|
||||||
<div class="f3-wm f3-d" style="display:flex;align-items:flex-end;justify-content:center;font-size:230px;color:#2D2D2D;text-shadow:8px 8px 0 #FFFFFF;line-height:1">
|
|
||||||
<span>l</span>
|
|
||||||
<span class="f3-eye" style="margin:0 6.8999999999999995px 13.799999999999999px 11.5px"><i class="f3-pup"></i></span>
|
|
||||||
<span class="f3-eye" style="margin:0 11.5px 13.799999999999999px 6.8999999999999995px"><i class="f3-pup"></i></span>
|
|
||||||
<span>katu</span>
|
|
||||||
</div></div>
|
|
||||||
<div class="f3-z f3-card f3-peace"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M50 86 C8 56 10 18 32 18 C42 18 48 25 50 32 C52 25 58 18 68 18 C90 18 92 56 50 86 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg><span class="f3-d">PEACE OF MIND</span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 8.144;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f3-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f3-badge", { y: -80, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.55, ease: "back.out(1.8)" }, 0.38);
|
|
||||||
tl.fromTo(".f3-shield", { scale: 0, rotation: -20, opacity: 0 }, { scale: 1, rotation: 0, opacity: 1, duration: 0.65, ease: "back.out(1.9)" }, 1.48);
|
|
||||||
tl.fromTo(".f3-wm", { scale: 0.3, opacity: 0, y: 60 }, { scale: 1, opacity: 1, y: 0, duration: 0.75, ease: "back.out(1.6)" }, 2.38);
|
|
||||||
|
|
||||||
const pups = document.querySelectorAll(".f3-pup"), r = 23;
|
|
||||||
tl.fromTo(pups, { x: 0, y: 0 }, { x: -r, y: -r * 0.4, duration: 0.35, ease: "power2.out" }, 2.9699999999999998)
|
|
||||||
.to(pups, { x: r, y: r * 0.3, duration: 0.5, ease: "power2.inOut" }, 3.57)
|
|
||||||
.to(pups, { x: 0, y: 0, duration: 0.35, ease: "power2.out" }, 4.32);
|
|
||||||
tl.fromTo(".f3-peace", { y: 100, opacity: 0, rotation: -3 }, { y: 0, opacity: 1, rotation: 0, duration: 0.6, ease: "back.out(1.6)" }, 5.43);
|
|
||||||
tl.to(".f3-peace svg", { scale: 1.2, duration: 0.3, ease: "sine.inOut" }, 6.13).to(".f3-peace svg", { scale: 1, duration: 0.3, ease: "sine.inOut" }, ">");
|
|
||||||
window.__timelines["03-meet"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f4-bg { position: absolute; inset: 0; }
|
|
||||||
.f4-orn { position: absolute; z-index: 1; }
|
|
||||||
.f4-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f4-z { position: absolute; z-index: 2; }
|
|
||||||
.f4-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f4-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f4-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f4-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f4-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f4-label { position: absolute; left: 0; right: 0; top: 130px; text-align: center; }
|
|
||||||
.f4-label .f4-pill { background: #FDE68A; padding: 14px 44px; } .f4-label span { font-size: 56px; }
|
|
||||||
.f4-ringbox { position: absolute; left: 90px; top: 250px; width: 380px; height: 380px; }
|
|
||||||
.f4-ringbox svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
||||||
.f4-time { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; font-size: 104px; }
|
|
||||||
.f4-lock { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; }
|
|
||||||
.f4-lock svg { position: static; width: 170px; height: 170px; }
|
|
||||||
.f4-state { position: absolute; left: 50px; top: 740px; width: 460px; text-align: center; }
|
|
||||||
.f4-state .f4-pill { padding: 14px 0; width: 100%; justify-content: center; box-shadow: 6px 6px 0 #2D2D2D; } .f4-state span { font-size: 56px; }
|
|
||||||
.f4-s2 { position: absolute; left: 0; top: 0; background: #F8635F; color: #FFFFFF; opacity: 0; }
|
|
||||||
.f4-bar { position: absolute; left: 70px; top: 930px; width: 420px; height: 26px; border: 4px solid #2D2D2D; border-radius: 20px; background: #F5F0E6; overflow: hidden; }
|
|
||||||
.f4-bar i { display: block; height: 100%; width: 100%; background: #A8E6CF; transform-origin: left center; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="04-limits" data-width="1080" data-height="1920" data-duration="7.099">
|
|
||||||
<div id="scene4-bg" class="clip f4-bg" data-start="0" data-duration="7.099" data-track-index="0" style="background:#F5F0E6"></div>
|
|
||||||
<div class="f4-orn" style="left:-70px;top:120px;width:240px;height:240px"><svg viewBox="0 0 100 100" style="transform:rotate(5deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f4-orn" style="left:840px;top:90px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f4-orn" style="left:30px;top:1050px;width:140px;height:140px"><svg viewBox="0 0 100 100" style="transform:rotate(-8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f4-orn" style="left:770px;top:1180px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f4-orn" style="left:40px;top:1400px;width:190px;height:190px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f4-label f4-z"><span class="f4-pill"><span class="f4-d">SCREEN TIME</span></span></div>
|
|
||||||
<div class="f4-phone">
|
|
||||||
<div class="f4-notch"></div>
|
|
||||||
<div class="f4-ringbox">
|
|
||||||
<svg viewBox="0 0 380 380"><circle cx="190" cy="190" r="150" fill="none" stroke="#E8E0D4" stroke-width="40"/><circle class="f4-arc" cx="190" cy="190" r="150" fill="none" stroke="#7ECDC0" stroke-width="40" stroke-linecap="round" stroke-dasharray="942.5" stroke-dashoffset="0" transform="rotate(-90 190 190)"/></svg>
|
|
||||||
<div class="f4-time f4-d">1:00</div>
|
|
||||||
<div class="f4-lock"><svg viewBox="0 0 100 100"><rect x="20" y="42" width="60" height="46" rx="10" fill="#F8635F" stroke="#2D2D2D" stroke-width="5"/><path d="M33 42 V30 a17 17 0 0 1 34 0 V42" fill="none" stroke="#2D2D2D" stroke-width="6" stroke-linecap="round"/><circle cx="50" cy="65" r="6" fill="#2D2D2D"/></svg></div>
|
|
||||||
</div>
|
|
||||||
<div class="f4-state"><div class="f4-pill f4-s1" style="background:#A8E6CF"><span class="f4-d">LIMIT SET</span></div><div class="f4-pill f4-s2"><span class="f4-d">TIME'S UP</span></div></div>
|
|
||||||
<div class="f4-bar"><i></i></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 6.599;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f4-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
const t0 = 0.38, tEnd = 3.79;
|
|
||||||
tl.fromTo(".f4-label", { y: -60, opacity: 0 }, { y: 0, opacity: 1, duration: 0.5, ease: "back.out(1.6)" }, t0);
|
|
||||||
tl.fromTo(".f4-phone", { y: 160, opacity: 0, rotation: 4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, t0);
|
|
||||||
tl.set(".f4-lock", { opacity: 0 }, 0);
|
|
||||||
tl.fromTo(".f4-state", { scale: 0.6, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.45, ease: "back.out(1.8)" }, 0.61);
|
|
||||||
tl.to(".f4-arc", { strokeDashoffset: 942.5, duration: tEnd - 0.99, ease: "none" }, 0.99);
|
|
||||||
tl.to(".f4-bar i", { scaleX: 0, duration: tEnd - 0.99, ease: "none" }, 0.99);
|
|
||||||
const cd = { v: 60 }, tm = document.querySelector(".f4-time");
|
|
||||||
tl.to(cd, { v: 0, duration: tEnd - 0.99, ease: "none", onUpdate: () => { const s = Math.ceil(cd.v); tm.textContent = Math.floor(s / 60) + ":" + String(s % 60).padStart(2, "0"); } }, 0.99);
|
|
||||||
tl.to(".f4-time", { opacity: 0, duration: 0.2 }, tEnd);
|
|
||||||
tl.to(".f4-arc", { opacity: 0, duration: 0.1 }, tEnd - 0.05);
|
|
||||||
tl.fromTo(".f4-lock", { opacity: 0, scale: 0.3, rotation: -25 }, { opacity: 1, scale: 1, rotation: 0, duration: 0.55, ease: "back.out(2)" }, tEnd);
|
|
||||||
tl.to(".f4-s1", { opacity: 0, duration: 0.15 }, tEnd).to(".f4-s2", { opacity: 1, duration: 0.2 }, tEnd);
|
|
||||||
tl.to(".f4-phone", { rotation: -2, duration: 0.12, ease: "sine.inOut" }, tEnd + 0.1).to(".f4-phone", { rotation: 2, duration: 0.16, ease: "sine.inOut" }, ">").to(".f4-phone", { rotation: 0, duration: 0.14, ease: "sine.inOut" }, ">");
|
|
||||||
window.__timelines["04-limits"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f5-bg { position: absolute; inset: 0; }
|
|
||||||
.f5-orn { position: absolute; z-index: 1; }
|
|
||||||
.f5-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f5-z { position: absolute; z-index: 2; }
|
|
||||||
.f5-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f5-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f5-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f5-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f5-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f5-row { position: absolute; left: 36px; width: 488px; height: 170px; display: flex; align-items: center; gap: 22px; padding: 0 24px; background: #FFFFFF; border: 4px solid #2D2D2D; border-radius: 34px; box-shadow: 6px 6px 0 #2D2D2D; }
|
|
||||||
.f5-ic { flex: none; width: 92px; height: 92px; border-radius: 50%; border: 4px solid #2D2D2D; display: flex; align-items: center; justify-content: center; font-size: 52px; }
|
|
||||||
.f5-nm { font-size: 46px; flex: 1; }
|
|
||||||
.f5-tg { flex: none; position: relative; width: 120px; height: 66px; border-radius: 40px; border: 4px solid #2D2D2D; background: #D9D4C7; }
|
|
||||||
.f5-kn { position: absolute; left: 4px; top: 4px; width: 50px; height: 50px; border-radius: 50%; background: #FFFFFF; border: 4px solid #2D2D2D; box-sizing: border-box; width: 50px; }
|
|
||||||
.f5-blk { position: absolute; right: -12px; top: -22px; background: #F8635F; color: #FFFFFF; border: 4px solid #2D2D2D; border-radius: 30px; padding: 4px 16px; font-size: 30px; opacity: 0; }
|
|
||||||
.f5-head { position: absolute; left: 0; right: 0; top: 80px; text-align: center; font-size: 56px; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="05-apps" data-width="1080" data-height="1920" data-duration="6.203">
|
|
||||||
<div id="scene5-bg" class="clip f5-bg" data-start="0" data-duration="6.203" data-track-index="0" style="background:#A8D8F0"></div>
|
|
||||||
<div class="f5-orn" style="left:-90px;top:90px;width:320px;height:320px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f5-orn" style="left:880px;top:150px;width:170px;height:170px"><svg viewBox="0 0 100 100" style="transform:rotate(14deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f5-orn" style="left:-50px;top:1180px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f5-orn" style="left:800px;top:1250px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f5-orn" style="left:900px;top:760px;width:110px;height:110px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f5-phone">
|
|
||||||
<div class="f5-notch"></div>
|
|
||||||
<div class="f5-head f5-d">APPS</div>
|
|
||||||
<div class="f5-row f5-r0" style="top:190px"><div class="f5-ic f5-d" style="background:#A8E6CF">L</div><div class="f5-nm f5-d">Learn</div><div class="f5-tg f5-t0"><div class="f5-kn f5-k0"></div></div></div>
|
|
||||||
<div class="f5-row f5-r1" style="top:400px"><div class="f5-ic f5-d" style="background:#A8D8F0">D</div><div class="f5-nm f5-d">Draw</div><div class="f5-tg f5-t1"><div class="f5-kn f5-k1"></div></div></div>
|
|
||||||
<div class="f5-row f5-r2" style="top:610px"><div class="f5-ic f5-d" style="background:#FFCBA4">G</div><div class="f5-nm f5-d">Games</div><div class="f5-tg f5-t2"><div class="f5-kn f5-k2"></div></div><div class="f5-blk f5-d f5-bl2">BLOCKED</div></div>
|
|
||||||
<div class="f5-row f5-r3" style="top:820px"><div class="f5-ic f5-d" style="background:#D4A5E8">V</div><div class="f5-nm f5-d">Videos</div><div class="f5-tg f5-t3"><div class="f5-kn f5-k3"></div></div><div class="f5-blk f5-d f5-bl3">BLOCKED</div></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 5.703;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f5-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f5-phone", { y: 160, opacity: 0, rotation: -4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, 0.38);
|
|
||||||
tl.fromTo(".f5-r0", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 0.88);
|
|
||||||
tl.fromTo(".f5-r1", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.16);
|
|
||||||
tl.fromTo(".f5-r2", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.44);
|
|
||||||
tl.fromTo(".f5-r3", { x: 90, opacity: 0 }, { x: 0, opacity: 1, duration: 0.45, ease: "power3.out" }, 1.72);
|
|
||||||
tl.to(".f5-k0", { x: 54, duration: 0.3, ease: "back.out(1.7)" }, 2.12).to(".f5-t0", { backgroundColor: "#A8E6CF", duration: 0.3 }, 2.12);
|
|
||||||
tl.to(".f5-k1", { x: 54, duration: 0.3, ease: "back.out(1.7)" }, 2.57).to(".f5-t1", { backgroundColor: "#A8E6CF", duration: 0.3 }, 2.57);
|
|
||||||
tl.to(".f5-r2", { opacity: 0.55, duration: 0.3 }, 3.49).fromTo(".f5-bl2", { opacity: 0, scale: 0.3, rotation: -10 }, { opacity: 1, scale: 1, rotation: -4, duration: 0.4, ease: "back.out(2)" }, 3.49);
|
|
||||||
tl.to(".f5-r3", { opacity: 0.55, duration: 0.3 }, 3.79).fromTo(".f5-bl3", { opacity: 0, scale: 0.3, rotation: -10 }, { opacity: 1, scale: 1, rotation: -4, duration: 0.4, ease: "back.out(2)" }, 3.79);
|
|
||||||
window.__timelines["05-apps"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f6-bg { position: absolute; inset: 0; }
|
|
||||||
.f6-orn { position: absolute; z-index: 1; }
|
|
||||||
.f6-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f6-z { position: absolute; z-index: 2; }
|
|
||||||
.f6-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f6-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f6-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f6-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f6-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f6-btn { position: absolute; left: 70px; top: 100px; width: 420px; justify-content: center; padding: 18px 0; background: #7ECDC0; box-shadow: 6px 6px 0 #2D2D2D; }
|
|
||||||
.f6-btn span { font-size: 50px; }
|
|
||||||
.f6-map { position: absolute; left: 36px; top: 260px; width: 488px; height: 780px; border: 5px solid #2D2D2D; border-radius: 40px; background: #A8E6CF; overflow: hidden; }
|
|
||||||
.f6-map svg { position: absolute; inset: 0; width: 100%; height: 100%; }
|
|
||||||
.f6-pin { position: absolute; left: 244px; top: 360px; width: 120px; margin-left: -60px; height: 150px; margin-top: -150px; }
|
|
||||||
.f6-pin svg { width: 100%; height: 100%; }
|
|
||||||
.f6-pulse { position: absolute; left: 244px; top: 360px; width: 200px; height: 200px; margin: -100px 0 0 -100px; border: 6px solid #F8635F; border-radius: 50%; opacity: 0; }
|
|
||||||
.f6-note { left: 0; right: 0; top: 1430px; text-align: center; }
|
|
||||||
.f6-note .f6-pill { background: #FDE68A; } .f6-note span { font-size: 62px; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="06-location" data-width="1080" data-height="1920" data-duration="7.013">
|
|
||||||
<div id="scene6-bg" class="clip f6-bg" data-start="0" data-duration="7.013" data-track-index="0" style="background:#FFCBA4"></div>
|
|
||||||
<div class="f6-orn" style="left:-80px;top:90px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f6-orn" style="left:890px;top:120px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f6-orn" style="left:880px;top:1230px;width:220px;height:220px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f6-orn" style="left:-90px;top:1180px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(4deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f6-orn" style="left:40px;top:700px;width:100px;height:100px"><svg viewBox="0 0 100 100" style="transform:rotate(-12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f6-phone">
|
|
||||||
<div class="f6-notch"></div>
|
|
||||||
<div class="f6-pill f6-d f6-btn"><span class="f6-d">CHECK NOW</span></div>
|
|
||||||
<div class="f6-map">
|
|
||||||
<svg viewBox="0 0 488 780">
|
|
||||||
<rect x="300" y="0" width="188" height="780" fill="#A8D8F0"/>
|
|
||||||
<rect x="30" y="40" width="190" height="150" rx="26" fill="#8ADBB6" stroke="#2D2D2D" stroke-width="3"/>
|
|
||||||
<rect x="30" y="520" width="230" height="200" rx="26" fill="#8ADBB6" stroke="#2D2D2D" stroke-width="3"/>
|
|
||||||
<path d="M0 250 H488 M150 0 V780 M0 470 H300 M360 60 L120 780" fill="none" stroke="#FFFFFF" stroke-width="22" stroke-linecap="round"/>
|
|
||||||
<path d="M0 250 H488 M150 0 V780 M0 470 H300 M360 60 L120 780" fill="none" stroke="#E8E0D4" stroke-width="3" stroke-dasharray="14 18"/>
|
|
||||||
</svg>
|
|
||||||
<div class="f6-pulse f6-pu1"></div><div class="f6-pulse f6-pu2"></div>
|
|
||||||
<div class="f6-pin"><svg viewBox="0 0 100 125"><path d="M50 118 C14 74 12 54 12 42 a38 38 0 0 1 76 0 C88 54 86 74 50 118 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="5" stroke-linejoin="round"/><circle cx="50" cy="42" r="14" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/></svg></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="f6-z f6-note"><span class="f6-pill"><span class="f6-d">ONLY WHEN NEEDED</span></span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 6.513;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f6-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f6-phone", { y: 160, opacity: 0, rotation: 4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.7, ease: "power3.out" }, 0.38);
|
|
||||||
tl.fromTo(".f6-map", { opacity: 0, scale: 0.9 }, { opacity: 1, scale: 1, duration: 0.5, ease: "power3.out" }, 0.61);
|
|
||||||
tl.set(".f6-pin", { opacity: 0 }, 0);
|
|
||||||
tl.to(".f6-btn", { scale: 0.9, duration: 0.12, ease: "power2.in" }, 1.8).to(".f6-btn", { scale: 1, duration: 0.25, ease: "back.out(2.5)" }, ">");
|
|
||||||
tl.fromTo(".f6-pin", { y: -520, opacity: 1, scaleY: 1.1 }, { y: 0, opacity: 1, scaleY: 1, duration: 0.7, ease: "bounce.out" }, 2.2);
|
|
||||||
tl.fromTo(".f6-pu1", { scale: 0.3, opacity: 0.9 }, { scale: 2.6, opacity: 0, duration: 1.1, ease: "power2.out" }, 2.95);
|
|
||||||
tl.fromTo(".f6-pu2", { scale: 0.3, opacity: 0.9 }, { scale: 2.6, opacity: 0, duration: 1.1, ease: "power2.out" }, 3.45);
|
|
||||||
tl.fromTo(".f6-note", { y: 80, opacity: 0, scale: 0.8 }, { y: 0, opacity: 1, scale: 1, duration: 0.55, ease: "back.out(1.8)" }, 3.89);
|
|
||||||
window.__timelines["06-location"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f7-bg { position: absolute; inset: 0; }
|
|
||||||
.f7-orn { position: absolute; z-index: 1; }
|
|
||||||
.f7-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f7-z { position: absolute; z-index: 2; }
|
|
||||||
.f7-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f7-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f7-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f7-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f7-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f7-big { position: absolute; z-index: 2; left: 110px; width: 860px; height: 400px; display: flex; align-items: center; justify-content: center; gap: 30px; }
|
|
||||||
.f7-big span { font-size: 138px; }
|
|
||||||
.f7-big svg { width: 120px; height: 120px; flex: none; }
|
|
||||||
.f7-heart { position: absolute; z-index: 3; left: 50%; top: 720px; margin-left: -110px; width: 220px; height: 220px; }
|
|
||||||
.f7-heart svg { width: 100%; height: 100%; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="07-balance" data-width="1080" data-height="1920" data-duration="5.968">
|
|
||||||
<div id="scene7-bg" class="clip f7-bg" data-start="0" data-duration="5.968" data-track-index="0" style="background:#F7C8D4"></div>
|
|
||||||
<div class="f7-orn" style="left:-60px;top:100px;width:240px;height:240px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f7-orn" style="left:880px;top:120px;width:160px;height:160px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f7-orn" style="left:800px;top:1230px;width:280px;height:280px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f7-orn" style="left:-80px;top:1210px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f7-orn" style="left:60px;top:760px;width:110px;height:110px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f7-big f7-card f7-c1" style="top:240px;background:#A8D8F0"><svg viewBox="0 0 100 100" style="transform:rotate(8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg><span class="f7-d">FREEDOM</span></div>
|
|
||||||
<div class="f7-heart f7-h"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M50 86 C8 56 10 18 32 18 C42 18 48 25 50 32 C52 25 58 18 68 18 C90 18 92 56 50 86 Z" fill="#F8635F" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f7-big f7-card f7-c2" style="top:1000px;background:#A8E6CF"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg><span class="f7-d">CALM</span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 5.468;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f7-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f7-c1", { x: -300, opacity: 0, rotation: -10 }, { x: 0, opacity: 1, rotation: -2, duration: 0.65, ease: "back.out(1.5)" }, 0.81);
|
|
||||||
tl.fromTo(".f7-c2", { x: 300, opacity: 0, rotation: 10 }, { x: 0, opacity: 1, rotation: 2, duration: 0.65, ease: "back.out(1.5)" }, 2.63);
|
|
||||||
tl.fromTo(".f7-h", { scale: 0, opacity: 0, rotation: -30 }, { scale: 1, opacity: 1, rotation: 0, duration: 0.7, ease: "back.out(2.2)" }, 3.49);
|
|
||||||
tl.to(".f7-h", { scale: 1.18, duration: 0.3, ease: "sine.inOut" }, 4.29).to(".f7-h", { scale: 1, duration: 0.3, ease: "sine.inOut" }, ">");
|
|
||||||
window.__timelines["07-balance"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f8-bg { position: absolute; inset: 0; }
|
|
||||||
.f8-orn { position: absolute; z-index: 1; }
|
|
||||||
.f8-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f8-z { position: absolute; z-index: 2; }
|
|
||||||
.f8-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f8-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f8-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f8-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f8-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f8-big { position: absolute; z-index: 2; left: 110px; width: 860px; height: 400px; display: flex; align-items: center; justify-content: center; gap: 36px; }
|
|
||||||
.f8-big span { font-size: 124px; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="08-platforms" data-width="1080" data-height="1920" data-duration="4.183">
|
|
||||||
<div id="scene8-bg" class="clip f8-bg" data-start="0" data-duration="4.183" data-track-index="0" style="background:#FDE68A"></div>
|
|
||||||
<div class="f8-orn" style="left:800px;top:90px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f8-orn" style="left:-60px;top:130px;width:230px;height:230px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f8-orn" style="left:-70px;top:1230px;width:290px;height:290px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f8-orn" style="left:880px;top:1300px;width:150px;height:150px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f8-orn" style="left:520px;top:1440px;width:100px;height:100px"><svg viewBox="0 0 100 100" style="transform:rotate(-8deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f8-big f8-card f8-c1" style="top:290px;background:#A8E6CF"><svg viewBox="0 0 100 100" style="width:150px;height:150px;flex:none"><rect x="26" y="6" width="48" height="88" rx="12" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/><rect x="34" y="18" width="32" height="56" rx="4" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3"/><circle cx="50" cy="83" r="4" fill="#2D2D2D"/></svg><span class="f8-d">ANDROID</span></div>
|
|
||||||
<div class="f8-big f8-card f8-c2" style="top:790px;background:#A8D8F0"><svg viewBox="0 0 100 100" style="width:150px;height:150px;flex:none"><rect x="26" y="6" width="48" height="88" rx="12" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="5"/><rect x="34" y="18" width="32" height="56" rx="4" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3"/><circle cx="50" cy="83" r="4" fill="#2D2D2D"/></svg><span class="f8-d">iOS</span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 3.783;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f8-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f8-c1", { y: 160, opacity: 0, rotation: -6, scale: 0.8 }, { y: 0, opacity: 1, rotation: -2, scale: 1, duration: 0.6, ease: "back.out(1.6)" }, 0.96);
|
|
||||||
tl.fromTo(".f8-c2", { y: 160, opacity: 0, rotation: 6, scale: 0.8 }, { y: 0, opacity: 1, rotation: 2, scale: 1, duration: 0.6, ease: "back.out(1.6)" }, 1.92);
|
|
||||||
window.__timelines["08-platforms"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
<template>
|
|
||||||
<style>
|
|
||||||
@font-face { font-family: "Fredoka"; src: url("assets/fonts/fredoka-600.woff2") format("woff2"); font-weight: 600; font-style: normal; }
|
|
||||||
@font-face { font-family: "Quicksand"; src: url("assets/fonts/quicksand-var.woff2") format("woff2"); font-weight: 500 700; font-style: normal; }
|
|
||||||
#root { position: absolute; inset: 0; overflow: hidden; color: #2D2D2D; font-family: "Quicksand", sans-serif; font-weight: 600; }
|
|
||||||
.f9-bg { position: absolute; inset: 0; }
|
|
||||||
.f9-orn { position: absolute; z-index: 1; }
|
|
||||||
.f9-orn svg { width: 100%; height: 100%; display: block; overflow: visible; }
|
|
||||||
.f9-z { position: absolute; z-index: 2; }
|
|
||||||
.f9-d { font-family: "Fredoka", sans-serif; font-weight: 600; letter-spacing: 0.02em; line-height: 1.05; }
|
|
||||||
.f9-card { background: #FFFFFF; border: 5px solid #2D2D2D; border-radius: 40px; box-shadow: 12px 12px 0 #2D2D2D; }
|
|
||||||
.f9-pill { display: inline-flex; align-items: center; gap: 18px; border: 5px solid #2D2D2D; border-radius: 80px; box-shadow: 8px 8px 0 #2D2D2D; padding: 20px 46px; white-space: nowrap; }
|
|
||||||
.f9-phone { position: absolute; z-index: 2; left: 260px; top: 230px; width: 560px; height: 1120px; background: #FFFFFF; border: 6px solid #2D2D2D; border-radius: 80px; box-shadow: 16px 16px 0 #2D2D2D; overflow: hidden; }
|
|
||||||
.f9-notch { position: absolute; left: 50%; top: 22px; margin-left: -70px; width: 140px; height: 28px; border-radius: 20px; background: #2D2D2D; }
|
|
||||||
|
|
||||||
.f9-eye { display: block; position: relative; width: 140px; height: 140px; border-radius: 50%; background: #FFFFFF; border: 7.5px solid #2D2D2D; box-sizing: border-box; text-shadow: none; }
|
|
||||||
.f9-pup { position: absolute; left: 50%; top: 50%; width: 55px; height: 55px; margin: -27.5px 0 0 -27.5px; border-radius: 50%; background: #2D2D2D; }
|
|
||||||
.f9-wmwrap { left: 0; right: 0; top: 400px; }
|
|
||||||
.f9-tag { left: 50%; top: 840px; margin-left: -400px; width: 800px; height: 190px; display: flex; align-items: center; justify-content: center; background: #FFFFFF; }
|
|
||||||
.f9-tag span { font-size: 64px; }
|
|
||||||
.f9-chips { left: 0; right: 0; top: 1130px; display: flex; justify-content: center; gap: 40px; }
|
|
||||||
.f9-chips .f9-pill { padding: 16px 40px; } .f9-chips span { font-size: 56px; }
|
|
||||||
</style>
|
|
||||||
<div id="root" data-composition-id="09-cta" data-width="1080" data-height="1920">
|
|
||||||
<div id="scene9-bg" class="clip f9-bg" data-start="0" data-duration="5.772" data-track-index="0" style="background:#D4A5E8"></div>
|
|
||||||
<div class="f9-orn" style="left:780px;top:70px;width:300px;height:300px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><line x1="50" y1="9" x2="50" y2="22" transform="rotate(0 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(30 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(60 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(90 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(120 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(150 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(180 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(210 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(240 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(270 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(300 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><line x1="50" y1="9" x2="50" y2="22" transform="rotate(330 50 50)" stroke="#2D2D2D" stroke-width="3.2" stroke-linecap="round"/><circle cx="50" cy="50" r="24" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f9-orn" style="left:-80px;top:160px;width:260px;height:260px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f9-orn" style="left:850px;top:1250px;width:250px;height:250px"><svg viewBox="0 0 100 100" style="transform:rotate(0deg)" xmlns="http://www.w3.org/2000/svg"><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(0 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(45 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(90 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(135 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(180 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(225 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(270 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><ellipse cx="50" cy="21" rx="11" ry="19" transform="rotate(315 50 50)" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2"/><circle cx="50" cy="50" r="14" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2"/></svg></div>
|
|
||||||
<div class="f9-orn" style="left:-90px;top:1220px;width:330px;height:330px"><svg viewBox="0 0 100 100" style="transform:rotate(3deg)" xmlns="http://www.w3.org/2000/svg"><path d="M24 72 a17 17 0 0 1 3-33 a22 22 0 0 1 43-3 a17 17 0 0 1 6 36 z" fill="#FFFFFF" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f9-orn" style="left:140px;top:1010px;width:130px;height:130px"><svg viewBox="0 0 100 100" style="transform:rotate(-10deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
<div class="f9-orn" style="left:810px;top:1000px;width:120px;height:120px"><svg viewBox="0 0 100 100" style="transform:rotate(12deg)" xmlns="http://www.w3.org/2000/svg"><polygon points="50,6 61,38 95,38 67,58 78,92 50,71 22,92 33,58 5,38 39,38" fill="#FDE68A" stroke="#2D2D2D" stroke-width="3.2" stroke-linejoin="round"/></svg></div>
|
|
||||||
|
|
||||||
<div class="f9-z f9-wmwrap">
|
|
||||||
<div class="f9-wm f9-d" style="display:flex;align-items:flex-end;justify-content:center;font-size:250px;color:#2D2D2D;text-shadow:9px 9px 0 #FFFFFF;line-height:1">
|
|
||||||
<span>l</span>
|
|
||||||
<span class="f9-eye" style="margin:0 7.5px 15px 12.5px"><i class="f9-pup"></i></span>
|
|
||||||
<span class="f9-eye" style="margin:0 12.5px 15px 7.5px"><i class="f9-pup"></i></span>
|
|
||||||
<span>katu</span>
|
|
||||||
</div></div>
|
|
||||||
<div class="f9-z f9-card f9-tag"><span class="f9-d">LOOK OUT FOR THEM</span></div>
|
|
||||||
<div class="f9-z f9-chips"><span class="f9-pill f9-ch1" style="background:#A8E6CF"><span class="f9-d">ANDROID</span></span><span class="f9-pill f9-ch2" style="background:#A8D8F0"><span class="f9-d">iOS</span></span></div>
|
|
||||||
</div>
|
|
||||||
<script>
|
|
||||||
(function () {
|
|
||||||
const DUR = 5.772;
|
|
||||||
const tl = gsap.timeline({ paused: true });
|
|
||||||
|
|
||||||
const orns = document.querySelectorAll(".f9-orn");
|
|
||||||
tl.fromTo(orns, { scale: 0, opacity: 0 }, { scale: 1, opacity: 1, duration: 0.55, ease: "back.out(1.8)", stagger: 0.07 }, 0.05);
|
|
||||||
orns.forEach((el, i) => {
|
|
||||||
const t0 = 0.9 + i * 0.12, half = Math.max(1.2, (DUR - 1.6) / 2), amp = i % 2 ? -14 : 14;
|
|
||||||
tl.to(el, { y: amp, duration: half, ease: "sine.inOut" }, t0).to(el, { y: 0, duration: half, ease: "sine.inOut" }, t0 + half);
|
|
||||||
});
|
|
||||||
|
|
||||||
tl.fromTo(".f9-wm", { scale: 0.4, opacity: 0, y: 70 }, { scale: 1, opacity: 1, y: 0, duration: 0.75, ease: "back.out(1.6)" }, 0.12);
|
|
||||||
|
|
||||||
const pups = document.querySelectorAll(".f9-pup"), r = 25;
|
|
||||||
tl.fromTo(pups, { x: 0, y: 0 }, { x: -r, y: -r * 0.4, duration: 0.35, ease: "power2.out" }, 0.7)
|
|
||||||
.to(pups, { x: r, y: r * 0.3, duration: 0.5, ease: "power2.inOut" }, 1.2999999999999998)
|
|
||||||
.to(pups, { x: 0, y: 0, duration: 0.35, ease: "power2.out" }, 2.05);
|
|
||||||
tl.fromTo(".f9-tag", { y: 90, opacity: 0, rotation: -4 }, { y: 0, opacity: 1, rotation: 0, duration: 0.6, ease: "back.out(1.6)" }, 1.04);
|
|
||||||
tl.fromTo(".f9-ch1", { y: 70, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.8)" }, 2.27);
|
|
||||||
tl.fromTo(".f9-ch2", { y: 70, opacity: 0, scale: 0.7 }, { y: 0, opacity: 1, scale: 1, duration: 0.5, ease: "back.out(1.8)" }, 2.45);
|
|
||||||
tl.to(".f9-wm", { scale: 1.04, duration: 0.5, ease: "sine.inOut" }, 3.28).to(".f9-wm", { scale: 1, duration: 0.5, ease: "sine.inOut" }, ">");
|
|
||||||
window.__timelines["09-cta"] = tl;
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</template>
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
---
|
|
||||||
version: alpha
|
|
||||||
name: Daisy Days — Frame (video / frame layer)
|
|
||||||
description: >
|
|
||||||
Video-first companion to Daisy Days' design.md. The unit is the frame (1920×1080). Atoms are
|
|
||||||
identical and sacred — the sunny-garden pastel palette (cream + turquoise/pink/butter/mint/
|
|
||||||
lavender/peach/sky + coral accent), charcoal 3px outlines, hard offset shadows (6/4px, no blur),
|
|
||||||
the Fredoka + Quicksand pairing, generous radii, headline text-shadow on color, dot bullets, and
|
|
||||||
the hand-drawn SVG ornament layer. Composition + frame scale rewritten. Motion out of scope.
|
|
||||||
unit: the frame — 1920×1080 primary; 9:16 and 1:1 documented
|
|
||||||
principle: atoms are sacred · composition is free · numbers come from the script
|
|
||||||
|
|
||||||
colors:
|
|
||||||
cream: "#F5F0E6"
|
|
||||||
turquoise: "#7ECDC0"
|
|
||||||
soft-pink: "#F7C8D4"
|
|
||||||
butter: "#FDE68A"
|
|
||||||
mint: "#A8E6CF"
|
|
||||||
lavender: "#D4A5E8"
|
|
||||||
peach: "#FFCBA4"
|
|
||||||
sky: "#A8D8F0"
|
|
||||||
coral: "#F8635F"
|
|
||||||
text-dark: "#2D2D2D"
|
|
||||||
text-muted: "#6B6B6B"
|
|
||||||
white: "#FFFFFF"
|
|
||||||
|
|
||||||
borders: { primary: "3px solid text-dark", thin: "2px solid text-dark" }
|
|
||||||
shadows: { default: "6px 6px 0 text-dark", small: "4px 4px 0 text-dark", text-headline: "3px 3px 0 text-dark", text-headline-soft: "3px 3px 0 rgba(0,0,0,0.2)" }
|
|
||||||
|
|
||||||
typography:
|
|
||||||
# — reading ramp (Quicksand) —
|
|
||||||
body: { fontFamily: "Quicksand", cqw: 0.95, weight: 500, lineHeight: 1.6 }
|
|
||||||
body-strong:{ fontFamily: "Quicksand", cqw: 0.95, weight: 600, lineHeight: 1.5 }
|
|
||||||
meta: { fontFamily: "Quicksand", cqw: 0.78, weight: 600, lineHeight: 1.45 }
|
|
||||||
# — display ramp (Fredoka One / Fredoka 600 — single weight, never italic) —
|
|
||||||
badge: { fontFamily: "Fredoka One", cqw: 0.9, tracking: "0.02em" }
|
|
||||||
label-display:{ fontFamily: "Fredoka One", cqw: 1.3, lineHeight: 1.3, tracking: "0.02em" }
|
|
||||||
subtitle:{ fontFamily: "Fredoka One", cqw: 1.8, lineHeight: 1.2, tracking: "0.02em" }
|
|
||||||
quote: { fontFamily: "Fredoka One", cqw: 2.6, lineHeight: 1.35 }
|
|
||||||
title: { fontFamily: "Fredoka One", cqw: 3.0, lineHeight: 1.15, tracking: "0.02em" }
|
|
||||||
headline:{ fontFamily: "Fredoka One", cqw: 4.5, lineHeight: 1.1, tracking: "0.02em" }
|
|
||||||
display: { fontFamily: "Fredoka One", cqw: 6.5, lineHeight: 1.1, tracking: "0.02em" }
|
|
||||||
|
|
||||||
spacing:
|
|
||||||
pad-slide: "3cqw"
|
|
||||||
radius: "20px"
|
|
||||||
radius-lg: "28px"
|
|
||||||
radius-pill: "50px"
|
|
||||||
radius-round: "50%"
|
|
||||||
|
|
||||||
components:
|
|
||||||
card:
|
|
||||||
backgroundColor: "{colors.white}"
|
|
||||||
border: "3px solid {colors.text-dark}"
|
|
||||||
rounded: "{spacing.radius} (28px featured)"
|
|
||||||
shadow: "6px 6px 0 {colors.text-dark}"
|
|
||||||
description: "The universal container; white-on-pastel is standard."
|
|
||||||
framed-header:
|
|
||||||
backgroundColor: "pastel cap + {colors.white} body"
|
|
||||||
border: "3px solid {colors.text-dark} (one continuous)"
|
|
||||||
rounded: "{spacing.radius-lg}"
|
|
||||||
shadow: "6px 6px 0 {colors.text-dark}"
|
|
||||||
description: "Pastel header strip flush above a white body — one unit, one shadow."
|
|
||||||
badge-pill:
|
|
||||||
backgroundColor: "{colors.butter}"
|
|
||||||
border: "3px solid {colors.text-dark}"
|
|
||||||
rounded: "{spacing.radius-pill}"
|
|
||||||
typography: "{typography.badge}"
|
|
||||||
shadow: "4px 4px 0 {colors.text-dark}"
|
|
||||||
description: "Section tag. white-space:nowrap."
|
|
||||||
circle-marker:
|
|
||||||
backgroundColor: "any pastel"
|
|
||||||
textColor: "{colors.white} (dark on butter)"
|
|
||||||
border: "3px solid {colors.text-dark}"
|
|
||||||
rounded: "50%"
|
|
||||||
size: "bullet 20 / icon 44 / dot 48 / step 90"
|
|
||||||
typography: "Fredoka numeral"
|
|
||||||
description: "Steps carry a small shadow."
|
|
||||||
bullet-dot:
|
|
||||||
backgroundColor: "{colors.butter}"
|
|
||||||
border: "2px solid {colors.text-dark}"
|
|
||||||
rounded: "50%"
|
|
||||||
size: "20px (::before, 4px from line top)"
|
|
||||||
description: "Lists never use glyph bullets."
|
|
||||||
ornament:
|
|
||||||
type: "hand-drawn SVG (daisy, star, sun, cloud, rainbow)"
|
|
||||||
stroke: "2.1px {colors.text-dark}"
|
|
||||||
placement: "z-index:1 behind content (z-index:2), cropping past the frame edge"
|
|
||||||
description: "3–7 per frame, clustered at corners/edges."
|
|
||||||
quote-mark:
|
|
||||||
typography: "oversized Fredoka quote glyph"
|
|
||||||
color: "{colors.soft-pink} (charcoal stroke)"
|
|
||||||
description: "Above a quote body."
|
|
||||||
text-headline-shadow:
|
|
||||||
shadow: "3px 3px 0 {colors.text-dark} (soft 20% variant on pink/mint)"
|
|
||||||
appliesTo: "Fredoka headlines on saturated surfaces (cream headlines sit flat)"
|
|
||||||
description: "Makes the headline read 'outlined' like the shapes."
|
|
||||||
---
|
|
||||||
|
|
||||||
# Daisy Days — Frame (video / frame layer)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
Daisy Days at frame scale is a **cheerful, childlike system** — picture-book illustration meets
|
|
||||||
sticker-sheet kawaii. Every shape carries a **3px charcoal outline**, every elevated element a
|
|
||||||
**solid hard offset shadow** (no blur), every surface a sunny-garden pastel. The voice is one
|
|
||||||
pairing: **Fredoka One** (chunky rounded, single weight) for every headline, **Quicksand** for
|
|
||||||
every body and meta line. The signature is the **hand-drawn SVG ornament layer** — daisies, stars,
|
|
||||||
suns, clouds, rainbows clustering at corners and cropping past the edge.
|
|
||||||
|
|
||||||
The palette is **multi-pastel with one warm pop**: cream canvas, seven pastel surfaces, and
|
|
||||||
`{colors.coral}` reserved for small high-attention markers only. Headlines on a saturated surface
|
|
||||||
get a 3px charcoal text-shadow (so they read "outlined" like the shapes) and switch to white;
|
|
||||||
headlines on cream sit flat in charcoal. Depth is 2D and graphic — thick outline + hard offset =
|
|
||||||
sticker-on-paper.
|
|
||||||
|
|
||||||
**Key characteristics at frame scale:**
|
|
||||||
|
|
||||||
- **Cream default + rotating pastel surfaces**; `{colors.coral}` is a marker accent, never a surface.
|
|
||||||
- **Fredoka One** headlines + **Quicksand 500/600** body — strict by role, never crossed.
|
|
||||||
- **3px charcoal outline + hard offset shadow** (6/4px, zero blur) on every elevated shape.
|
|
||||||
- **Generous radii** — 20px cards, 28px featured, pill badges, full-circle markers; no square corners.
|
|
||||||
- **Headline text-shadow on saturated surfaces** (white text); flat charcoal on cream.
|
|
||||||
- **Dot bullets** (outlined butter discs, never glyphs) + a **3–7 ornament wreath** per frame.
|
|
||||||
|
|
||||||
## The Frame
|
|
||||||
|
|
||||||
### Frame Craft Bar
|
|
||||||
|
|
||||||
Three eyeball tests gate every frame before any structural check:
|
|
||||||
|
|
||||||
- **Squint** — one Fredoka headline or content card dominates at 3–6× its neighbor.
|
|
||||||
- **Silence** — **one content container per frame** surrounded by an ornament wreath; the **info-card grid is the one dense exception**. Empty corners read as broken.
|
|
||||||
- **Restraint** — cream or **one** pastel surface; coral is a small-marker accent (never a surface); charcoal borders + hard offset shadows only; no ninth color.
|
|
||||||
- **Reference** — aim at a **children's picture-book / sticker-sheet kawaii zine**; failure looks like a **flat, square-cornered, blurred-shadow corporate slide**.
|
|
||||||
|
|
||||||
- **Primary:** 1920×1080 (16:9). Display authored in **`cqw`** (`px ÷ 1920 × 100 = cqw`).
|
|
||||||
- **Vertical:** 1080×1920 (9:16). **Square:** 1080×1080 (1:1).
|
|
||||||
- **Safe area:** `pad-slide` ~3cqw; ornaments deliberately bleed past the edge.
|
|
||||||
|
|
||||||
**The container law (load-bearing).** Every frame ground sets `container-type: size`; ALL
|
|
||||||
frame-relative units are `cqw`/`cqh` against it — never `vw`. Borders stay 3px/2px; radii stay
|
|
||||||
20/28/50px; shadow offsets scale in `cqw` so the sticker offset holds proportionally.
|
|
||||||
|
|
||||||
## Colors
|
|
||||||
|
|
||||||
Tokens identical to the source. Default ground `{colors.cream}`; rotate saturated pastels
|
|
||||||
(turquoise / soft-pink / butter / mint / lavender / peach / sky) for tonal mood. **Cards are white**
|
|
||||||
on any surface. **Borders + shadows are always `{colors.text-dark}` charcoal** — never colored,
|
|
||||||
never blurred, never `rgba` (save the soft text-shadow variant). `{colors.coral}` is the lone
|
|
||||||
high-saturation accent — small markers (step circles, dots, headers) **only**, never a surface.
|
|
||||||
Body is charcoal/muted; pastels carry **no semantic meaning**. No ninth color.
|
|
||||||
|
|
||||||
## Typography
|
|
||||||
|
|
||||||
Two ramps. The **reading ramp** (Quicksand 500 body 0.95cqw, 600 emphasis, meta) carries copy; the
|
|
||||||
**display ramp** (Fredoka One `label-display` 1.3cqw → `display` 6.5cqw, single weight) carries
|
|
||||||
every headline, title, quote, and marker numeral.
|
|
||||||
|
|
||||||
- **Legibility floor:** any load-bearing line ≥ **1.4cqw**; meta is chrome only.
|
|
||||||
- **Fit-to-measure:** size the headline to its length. Cap the block at **≤ 78cqw**; ≤3 words → `display`; 4–6 → `headline`; 7+ → `title`.
|
|
||||||
- **Fredoka One for all display, Quicksand for all body — never crossed.** Fredoka is single-weight (no italic, no underline, no alt weights); Quicksand stays 500/600/700. Fredoka tracking 0.02em; Quicksand body never uppercase.
|
|
||||||
|
|
||||||
## Depth & Surface
|
|
||||||
|
|
||||||
2D graphic depth — hard offset shadow, solid charcoal, zero blur, bottom-right:
|
|
||||||
|
|
||||||
- **`shadows.default` 6px** cards, frames, badges, chart containers.
|
|
||||||
- **`shadows.small` 4px** small cards, step circles, avatars.
|
|
||||||
- **Text-headline shadow** — 3px charcoal on Fredoka headlines over saturated surfaces (20% soft variant on pink/mint); cream headlines flat.
|
|
||||||
- **Outline + offset** together are the sticker-on-paper signature.
|
|
||||||
|
|
||||||
**Ceiling:** no blurred shadow, no `rgba` (except the soft text-shadow), no gradient, no glow; an element either casts a hard charcoal offset or none.
|
|
||||||
|
|
||||||
## Shapes
|
|
||||||
|
|
||||||
- **20px** cards, **28px** featured, **50px** pill (badges, counter), **50%** all circles, **4px** legend swatch. Zero square corners — every region is rounded.
|
|
||||||
|
|
||||||
## Components
|
|
||||||
|
|
||||||
- **card / framed-header** — the white-fill bordered containers (one shadow). **badge-pill** — butter section tag.
|
|
||||||
- **circle-marker** family (bullet 20 / icon 44 / dot 48 / step 90) + **bullet-dot** — outlined pastel discs, Fredoka numerals.
|
|
||||||
- **ornament** — the hand-drawn SVG sticker layer (3–7 per frame). **quote-mark** — soft-pink Fredoka anchor. **text-headline-shadow** — the on-color headline treatment.
|
|
||||||
|
|
||||||
## Frame Treatments
|
|
||||||
|
|
||||||
> Recipe: ground · container · composes · focal · chrome · accent · silence · Fixed/Free · density.
|
|
||||||
> One content container per frame + a 3–7 ornament wreath; empty corners read as broken.
|
|
||||||
|
|
||||||
### 1 · Cover (identity · move: ornament wreath · saturated · centered)
|
|
||||||
|
|
||||||
**Ground** a saturated pastel (e.g. `{colors.turquoise}`). **Composes** badge-pill, display, body sub, 3–7 ornaments, counter. **Focal** a 1–2 line Fredoka `display` in **white with the 3px charcoal text-shadow**, centered, under a butter badge-pill. **Chrome** counter pill. **Accent** the ornament wreath (daisies + stars at corners, cropping past edges). **Silence** content centered; ornaments fill the edges. **Fixed** text-shadow on color, 3px outlines, charcoal shadows. **Free** surface color, ornament mix/positions, copy. **Density** full-but-not-crowded.
|
|
||||||
|
|
||||||
### 2 · Info Cards (catalog · move: 3-up white cards · cream · the dense frame)
|
|
||||||
|
|
||||||
**Ground** `{colors.cream}`, `pad-slide`. **Composes** headline (flat charcoal), 3× card (circle-icon + Fredoka title + Quicksand body), a couple ornaments. **Focal** three white 3px-bordered cards with 6px shadows. **Chrome** flat headline. **Accent** the pastel circle-icons (rotate turquoise/coral/lavender). **Silence** tight — the density exception (fewer ornaments here). **Fixed** white cards, 3px + 6px, 20px radius. **Free** card content, icon hues. **Density** dense-exception.
|
|
||||||
|
|
||||||
### 3 · Process Steps (sequence · move: rotating circle markers · peach · centered)
|
|
||||||
|
|
||||||
**Ground** `{colors.peach}` (or another pastel). **Composes** headline (white + text-shadow), 3–4 step circles + `→` arrows, ornaments. **Focal** a row of 90px outlined step circles, fills **rotating coral → mint → sky → lavender**, Fredoka white numerals, linked by Fredoka arrows. **Chrome** white headline with text-shadow. **Accent** the rotating circle fills. **Silence** moderate. **Fixed** 3px circles + small shadow, rotating fills, arrow glyphs. **Free** step count, labels. **Density** standard.
|
|
||||||
|
|
||||||
### 4 · Quote (quote · move: quote-mark anchor · soft-pink · centered)
|
|
||||||
|
|
||||||
**Ground** `{colors.soft-pink}`. **Composes** a white quote card (28px radius, 6px shadow), quote-mark, Fredoka quote, Quicksand attribution, ornaments. **Focal** a Fredoka `quote` in charcoal inside the white card, under an oversized soft-pink quote-mark. **Chrome** Quicksand 700 attribution. **Accent** the quote-mark + a star or two. **Silence** card centered, ornaments at corners. **Fixed** quote-mark anchor, white card. **Free** quote, attribution. **Density** moderate.
|
|
||||||
|
|
||||||
### 5 · Framed Section (feature · move: cap+body card · cream)
|
|
||||||
|
|
||||||
**Ground** `{colors.cream}`. **Composes** framed-header (pastel cap + white body), bullet-dot list, ornaments. **Focal** a framed-header — a pastel cap strip (Fredoka title, optional text-shadow) above a white body with a butter-dot bullet list. **Accent** the cap color + bullet dots. **Silence** moderate. **Fixed** one continuous 3px border + one shadow, butter dot bullets. **Free** cap color, list. **Density** standard.
|
|
||||||
|
|
||||||
### 6 · Closing (closer · move: ornament wreath · saturated · centered)
|
|
||||||
|
|
||||||
**Ground** a saturated pastel (e.g. `{colors.lavender}`). **Composes** badge-pill, display (white + text-shadow), 3–7 ornaments. **Focal** a Fredoka `display` sign-off in white with the charcoal text-shadow, centered. **Accent** the ornament wreath. **Silence** content centered. **Fixed** text-shadow on color, ornaments fill corners. **Free** sign-off, surface, ornaments. **Density** full.
|
|
||||||
|
|
||||||
## Composition Rules
|
|
||||||
|
|
||||||
### Do
|
|
||||||
|
|
||||||
- Pair **Fredoka One headlines + Quicksand body** strictly by role.
|
|
||||||
- Outline every shape **3px charcoal + a hard offset shadow** (6/4px, no blur); white card fills on any surface.
|
|
||||||
- Give Fredoka headlines on **saturated surfaces a 3px charcoal text-shadow + white text**; flat charcoal on cream.
|
|
||||||
- Use **outlined butter-disc bullets** (never glyphs); cluster **3–7 hand-drawn ornaments** per frame (corners, cropping past edges).
|
|
||||||
- Keep **one content container per frame**; rotate marker colors (coral → mint → sky → lavender → butter); reserve coral for small markers.
|
|
||||||
- Center most frames; lean cream for content-dense frames, saturated for cover/closer/quote.
|
|
||||||
|
|
||||||
### Don't
|
|
||||||
|
|
||||||
- No square corners; no blurred or `rgba` shadows (save the soft text-shadow).
|
|
||||||
- No colored borders (charcoal only); no coral surface; no ninth color.
|
|
||||||
- No third font; no Quicksand headline or Fredoka body; no italic/underline Fredoka; no uppercase Quicksand body.
|
|
||||||
- No glyph bullets; no empty corners (ornaments fill them); no two competing content panels.
|
|
||||||
- Don't blow a headline edge-to-edge — fit to measure.
|
|
||||||
|
|
||||||
## Aspect-Ratio Behavior
|
|
||||||
|
|
||||||
| Treatment | 16:9 | 9:16 | 1:1 |
|
|
||||||
| -------------- | -------------------------- | -------------------------------- | ---------- |
|
|
||||||
| Cover | centered, corner ornaments | centered, taller, more ornaments | centered |
|
|
||||||
| Info Cards | 3 across | stacked | 2+1 |
|
|
||||||
| Process Steps | horizontal + arrows | vertical, arrows rotate down | 2×2 |
|
|
||||||
| Quote | centered card | centered, taller | centered |
|
|
||||||
| Framed Section | cap + body | cap + body taller | cap + body |
|
|
||||||
| Closing | centered, wreath | centered, wreath | centered |
|
|
||||||
|
|
||||||
`pad-slide` holds on the short edge; re-step display above the 1.4cqw floor. On tighter ratios keep
|
|
||||||
the ornament count toward the upper end (5–7) so corners never read empty.
|
|
||||||
|
|
||||||
## Approved Real Entities
|
|
||||||
|
|
||||||
No real customers, logos, or vendors are defined in the source — render any such mark as a
|
|
||||||
placeholder. Ornaments and pastels are content-agnostic; counters/badges carry per-deck text.
|
|
||||||
|
|
||||||
## Numerals & Claims (hard rule)
|
|
||||||
|
|
||||||
Never invent figures, dates, or counts at frame scale. Render slots as `— figure —`, `{metric}`,
|
|
||||||
`N`. Step numbers and any chart values carry placeholders until the script supplies them; the slide
|
|
||||||
counter is decorative chrome.
|
|
||||||
|
|
||||||
## Pre-Render Self-Audit
|
|
||||||
|
|
||||||
- **Squint** — one Fredoka headline or content card dominates per frame.
|
|
||||||
- **Silence** — one container per frame surrounded by an ornament wreath; only the info-card grid runs dense.
|
|
||||||
- **Color** — cream or one pastel surface; coral markers only; charcoal borders/shadows; no ninth hue.
|
|
||||||
- **Type** — Fredoka headlines (text-shadow + white on saturated, flat charcoal on cream), Quicksand body; ≥1.4cqw floor.
|
|
||||||
- **Depth** — 3px outline + hard offset (no blur, no rgba save soft text-shadow); rounded corners only.
|
|
||||||
- **Ornaments** — 3–7 per frame, cropping past edges; no empty corners. **Bullets** — outlined discs, never glyphs.
|
|
||||||
- **Fabrication** — every numeral traces to the script, else placeholder.
|
|
||||||
|
|
||||||
## Known Gaps
|
|
||||||
|
|
||||||
- **Motion intentionally out of scope.** frame.md specifies composition only; the source uses scroll-snap nav, no transition spec.
|
|
||||||
- **Fonts:** the source names _Fredoka One_; Google now serves the **Fredoka** variable family — request `Fredoka:wght@500;600;700` and set display weight 600 (visually equal to Fredoka One), with `Fredoka One` kept first in the stack for environments that still serve it. Quicksand loads normally. CJK: ZCOOL XiaoWei (display) / Yozai (body).
|
|
||||||
- **9:16 / 1:1 are guidance**; keep ornament count high so corners stay filled per ratio.
|
|
||||||
- Ornaments (daisy/star/sun/cloud/rainbow), markers, and framed headers are CSS/SVG-only; recoloring SVG ornaments requires editing their stroke values.
|
|
||||||
- **Contrast:** keep `{colors.text-muted}` off pastel surfaces (cream/white cards only); small text on saturated grounds should be charcoal or white.
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
{
|
|
||||||
"$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
|
|
||||||
"registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
|
|
||||||
"paths": {
|
|
||||||
"blocks": "compositions",
|
|
||||||
"components": "compositions/components",
|
|
||||||
"assets": "assets"
|
|
||||||
},
|
|
||||||
"media": {
|
|
||||||
"autoProxy": true
|
|
||||||
},
|
|
||||||
"authoringSkill": "product-launch-video"
|
|
||||||
}
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=1080, height=1920" />
|
|
||||||
<script src="assets/vendor/gsap.min.js"></script>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
html,
|
|
||||||
body {
|
|
||||||
width: 1080px;
|
|
||||||
height: 1920px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #000;
|
|
||||||
}
|
|
||||||
#root {
|
|
||||||
position: relative;
|
|
||||||
width: 1080px;
|
|
||||||
height: 1920px;
|
|
||||||
overflow: hidden;
|
|
||||||
background: #F5F0E6;
|
|
||||||
}
|
|
||||||
.scene {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div
|
|
||||||
id="root"
|
|
||||||
data-composition-id="main"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="54.733"
|
|
||||||
data-width="1080"
|
|
||||||
data-height="1920"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
id="el-01-hook"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="01-hook"
|
|
||||||
data-composition-src="compositions/frames/01-hook.html"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="6.14"
|
|
||||||
data-track-index="0"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-01-hook-voice"
|
|
||||||
src="assets/voice/01.wav"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="5.64"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-02-worry"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="02-worry"
|
|
||||||
data-composition-src="compositions/frames/02-worry.html"
|
|
||||||
data-start="5.64"
|
|
||||||
data-duration="7.511"
|
|
||||||
data-track-index="1"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-02-worry-voice"
|
|
||||||
src="assets/voice/02.wav"
|
|
||||||
data-start="5.64"
|
|
||||||
data-duration="7.111"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-03-meet"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="03-meet"
|
|
||||||
data-composition-src="compositions/frames/03-meet.html"
|
|
||||||
data-start="12.751"
|
|
||||||
data-duration="8.644"
|
|
||||||
data-track-index="0"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-03-meet-voice"
|
|
||||||
src="assets/voice/03.wav"
|
|
||||||
data-start="12.751"
|
|
||||||
data-duration="8.144"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-04-limits"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="04-limits"
|
|
||||||
data-composition-src="compositions/frames/04-limits.html"
|
|
||||||
data-start="20.895"
|
|
||||||
data-duration="7.099"
|
|
||||||
data-track-index="1"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-04-limits-voice"
|
|
||||||
src="assets/voice/04.wav"
|
|
||||||
data-start="20.895"
|
|
||||||
data-duration="6.599"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-05-apps"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="05-apps"
|
|
||||||
data-composition-src="compositions/frames/05-apps.html"
|
|
||||||
data-start="27.494"
|
|
||||||
data-duration="6.203"
|
|
||||||
data-track-index="0"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-05-apps-voice"
|
|
||||||
src="assets/voice/05.wav"
|
|
||||||
data-start="27.494"
|
|
||||||
data-duration="5.703"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-06-location"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="06-location"
|
|
||||||
data-composition-src="compositions/frames/06-location.html"
|
|
||||||
data-start="33.197"
|
|
||||||
data-duration="7.013"
|
|
||||||
data-track-index="1"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-06-location-voice"
|
|
||||||
src="assets/voice/06.wav"
|
|
||||||
data-start="33.197"
|
|
||||||
data-duration="6.513"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-07-balance"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="07-balance"
|
|
||||||
data-composition-src="compositions/frames/07-balance.html"
|
|
||||||
data-start="39.71"
|
|
||||||
data-duration="5.968"
|
|
||||||
data-track-index="0"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-07-balance-voice"
|
|
||||||
src="assets/voice/07.wav"
|
|
||||||
data-start="39.71"
|
|
||||||
data-duration="5.468"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-08-platforms"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="08-platforms"
|
|
||||||
data-composition-src="compositions/frames/08-platforms.html"
|
|
||||||
data-start="45.178"
|
|
||||||
data-duration="4.183"
|
|
||||||
data-track-index="1"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-08-platforms-voice"
|
|
||||||
src="assets/voice/08.wav"
|
|
||||||
data-start="45.178"
|
|
||||||
data-duration="3.783"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<div
|
|
||||||
id="el-09-cta"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="09-cta"
|
|
||||||
data-composition-src="compositions/frames/09-cta.html"
|
|
||||||
data-start="48.961"
|
|
||||||
data-duration="5.772"
|
|
||||||
data-track-index="0"
|
|
||||||
></div>
|
|
||||||
<audio
|
|
||||||
id="el-09-cta-voice"
|
|
||||||
src="assets/voice/09.wav"
|
|
||||||
data-start="48.961"
|
|
||||||
data-duration="5.772"
|
|
||||||
data-track-index="10"
|
|
||||||
data-volume="1"
|
|
||||||
></audio>
|
|
||||||
|
|
||||||
<!-- captions -->
|
|
||||||
<div
|
|
||||||
id="el-captions"
|
|
||||||
data-track-kind="captions"
|
|
||||||
class="scene"
|
|
||||||
data-composition-id="captions"
|
|
||||||
data-composition-src="compositions/captions.html"
|
|
||||||
data-start="0"
|
|
||||||
data-duration="54.733"
|
|
||||||
data-track-index="2"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.__timelines = window.__timelines || {};
|
|
||||||
window.__timelines["main"] = gsap.timeline({ paused: true });
|
|
||||||
// ── frame transitions (injected by transitions.mjs) ──
|
|
||||||
(function () { var tl = window.__timelines["main"];
|
|
||||||
tl.to("#el-01-hook", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 5.64);
|
|
||||||
tl.fromTo("#el-02-worry", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 5.64);
|
|
||||||
tl.to("#el-02-worry", { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, 12.751);
|
|
||||||
tl.fromTo("#el-03-meet", { scale: 0.5, opacity: 0, filter: "blur(8px)" }, { scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, 12.751);
|
|
||||||
tl.to("#el-03-meet", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 20.895);
|
|
||||||
tl.fromTo("#el-04-limits", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 20.895);
|
|
||||||
tl.to("#el-04-limits", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 27.494);
|
|
||||||
tl.fromTo("#el-05-apps", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 27.494);
|
|
||||||
tl.to("#el-05-apps", { x: -1080, duration: 0.5, ease: "power3.inOut" }, 33.197);
|
|
||||||
tl.fromTo("#el-06-location", { x: 1080, opacity: 1 }, { x: 0, opacity: 1, duration: 0.5, ease: "power3.inOut" }, 33.197);
|
|
||||||
tl.to("#el-06-location", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 39.71);
|
|
||||||
tl.fromTo("#el-07-balance", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 39.71);
|
|
||||||
tl.to("#el-07-balance", { opacity: 0, duration: 0.5, ease: "power2.inOut" }, 45.178);
|
|
||||||
tl.fromTo("#el-08-platforms", { opacity: 0 }, { opacity: 1, duration: 0.5, ease: "power2.inOut" }, 45.178);
|
|
||||||
tl.to("#el-08-platforms", { scale: 2.5, opacity: 0, filter: "blur(8px)", duration: 0.4, ease: "power3.in" }, 48.961);
|
|
||||||
tl.fromTo("#el-09-cta", { scale: 0.5, opacity: 0, filter: "blur(8px)" }, { scale: 1, opacity: 1, filter: "blur(0px)", duration: 0.4, ease: "power3.out" }, 48.961);
|
|
||||||
tl.to({}, { duration: 54.733 }, 0); // full-span anchor — main.duration() == composition total (Studio master duration)
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"id": "lookatu-promo",
|
|
||||||
"name": "lookatu-promo",
|
|
||||||
"createdAt": "2026-10-14T22:04:45.006Z"
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "lookatu-promo",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "npx --yes hyperframes@0.8.158 preview",
|
|
||||||
"check": "npx --yes hyperframes@0.8.158 check",
|
|
||||||
"render": "npx --yes hyperframes@0.8.158 render",
|
|
||||||
"publish": "npx --yes hyperframes@0.8.158 publish"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
Reference in new issue
Block a user