diff --git a/docker/README.md b/docker/README.md index bf98003..8855198 100644 --- a/docker/README.md +++ b/docker/README.md @@ -47,6 +47,12 @@ project (`@shopify/react-native-skia` is aliased to `src/engine/skiaShim.ts` in `cinemaShader` `colorUtils` `defaultRecipes` `exifWrite` `frameUtils` `jpegDpi` `paramDefs` `skiaImage` `toneShader`. +The landing page needs no CDN: `frontend/public/assets/Syne.woff2` is the +bundled display face and `frontend/public/assets/samples/s*.jpg` are the six +placeholder negatives the film strip, preset tester and QR card show (swap them +for real graded stills whenever we have them). Its one foreign request is the +QR image from `api.qrserver.com`, which degrades to an empty slot offline. + When the app changes one of them, copy it back in — the renderer is only "parity" for as long as these stay in sync: diff --git a/docker/frontend/public/assets/Syne.woff2 b/docker/frontend/public/assets/Syne.woff2 new file mode 100644 index 0000000..e311600 Binary files /dev/null and b/docker/frontend/public/assets/Syne.woff2 differ diff --git a/docker/frontend/public/assets/samples/s1015.jpg b/docker/frontend/public/assets/samples/s1015.jpg new file mode 100644 index 0000000..be511e5 Binary files /dev/null and b/docker/frontend/public/assets/samples/s1015.jpg differ diff --git a/docker/frontend/public/assets/samples/s1025.jpg b/docker/frontend/public/assets/samples/s1025.jpg new file mode 100644 index 0000000..4b342ff Binary files /dev/null and b/docker/frontend/public/assets/samples/s1025.jpg differ diff --git a/docker/frontend/public/assets/samples/s1035.jpg b/docker/frontend/public/assets/samples/s1035.jpg new file mode 100644 index 0000000..89bfa0e Binary files /dev/null and b/docker/frontend/public/assets/samples/s1035.jpg differ diff --git a/docker/frontend/public/assets/samples/s1043.jpg b/docker/frontend/public/assets/samples/s1043.jpg new file mode 100644 index 0000000..ea03952 Binary files /dev/null and b/docker/frontend/public/assets/samples/s1043.jpg differ diff --git a/docker/frontend/public/assets/samples/s1050.jpg b/docker/frontend/public/assets/samples/s1050.jpg new file mode 100644 index 0000000..b888250 Binary files /dev/null and b/docker/frontend/public/assets/samples/s1050.jpg differ diff --git a/docker/frontend/public/assets/samples/s1069.jpg b/docker/frontend/public/assets/samples/s1069.jpg new file mode 100644 index 0000000..262bc0e Binary files /dev/null and b/docker/frontend/public/assets/samples/s1069.jpg differ diff --git a/docker/frontend/src/Landing.tsx b/docker/frontend/src/Landing.tsx index 214a6fa..fa500c9 100644 --- a/docker/frontend/src/Landing.tsx +++ b/docker/frontend/src/Landing.tsx @@ -1,84 +1,465 @@ -import { useI18n, LANGS } from './i18n/I18nProvider'; -import { ACCENTS, useTheme } from './theme/ThemeProvider'; +import { useCallback, useEffect, useRef, useState } from 'react'; -// Landing page. The store links are placeholders on purpose — the real URLs are -// still unknown (see WEB_PLAN.md, W6); they live in one place so filling them in -// later is a one-line change. -const STORE_LINKS = { - play: '#', - appstore: '#', -}; +// Landing page, built to the landing blueprint: dark cinematic 10-section +// funnel. Marketing copy is English (the blueprint's own copy) — the workspace +// keeps its VI/EN dictionary. The store URLs are still unknown (WEB_PLAN.md W6), +// so both badges raise the toast instead of navigating; fill STORE_LINKS in and +// drop the onClick to go live. +const STORE_LINKS = { play: '#', appstore: '#' }; + +const SAMPLE = (n: number) => `/assets/samples/s${n}.jpg`; + +interface Look { + tag: string; + title: string; + meta: string; + photo: string; + filter: string; +} + +// 35mm reel: eight looks over six sample negatives, each with its own CSS +// emulation. ponytail: CSS filters, not the CanvasKit pipeline — a landing page +// does not need a 12MP render; swap in real graded stills when we have them. +const REEL: Look[] = [ + { tag: '#KODAK_PORTRA_400', title: 'Golden Hour Portrait', meta: 'ISO 400 · GRAIN 35 · WARMTH +18', photo: SAMPLE(1015), filter: 'sepia(.16) saturate(1.18) contrast(1.04) brightness(1.04)' }, + { tag: '#FUJI_CLASSIC_CHROME', title: 'Quiet Street, 7am', meta: 'ISO 200 · GRAIN 20 · WARMTH -6', photo: SAMPLE(1025), filter: 'saturate(.78) contrast(1.14) hue-rotate(-6deg)' }, + { tag: '#CYBER_NEON_88', title: 'Neon Crossing', meta: 'ISO 1600 · GRAIN 45 · WARMTH +4', photo: SAMPLE(1035), filter: 'saturate(1.7) contrast(1.25) hue-rotate(268deg)' }, + { tag: '#CINESTILL_800T', title: 'Harbour Tungsten', meta: 'ISO 800 · GRAIN 40 · WARMTH -14', photo: SAMPLE(1043), filter: 'saturate(1.1) contrast(1.1) hue-rotate(186deg) brightness(.96)' }, + { tag: '#ILFORD_HP5_BW', title: 'Alley Study', meta: 'ISO 400 · GRAIN 55 · MONO', photo: SAMPLE(1050), filter: 'grayscale(1) contrast(1.35) brightness(1.02)' }, + { tag: '#VELVIA_50', title: 'Ridge Line', meta: 'ISO 50 · GRAIN 12 · WARMTH +6', photo: SAMPLE(1069), filter: 'saturate(1.55) contrast(1.22)' }, + { tag: '#ACROS_100', title: 'Stone Facade', meta: 'ISO 100 · GRAIN 30 · MONO', photo: SAMPLE(1025), filter: 'grayscale(1) contrast(1.18) brightness(1.06)' }, + { tag: '#ETRNA_VINTAGE', title: 'Faded Postcard', meta: 'ISO 200 · GRAIN 25 · WARMTH +10', photo: SAMPLE(1015), filter: 'sepia(.28) saturate(.9) contrast(.94) brightness(1.08)' }, +]; + +interface Preset { + id: string; + label: string; + name: string; + desc: string; + filter: string; + // warmth, grain, contrast, halation bloom — 0..100 + metrics: [number, number, number, number]; +} + +const PRESETS: Preset[] = [ + { id: 'portra', label: 'Kodak Portra 400', name: 'PORTRA 400', desc: 'Warm daylight stock, forgiving highlights and natural skin tones — the everyday portrait recipe.', filter: 'sepia(.18) saturate(1.15) contrast(1.03) brightness(1.05)', metrics: [72, 34, 48, 22] }, + { id: 'chrome', label: 'Fuji Classic Chrome', name: 'CLASSIC CHROME', desc: 'Deep greens, muted reds and a documentary edge straight out of the rangefinder era.', filter: 'saturate(.76) contrast(1.16) hue-rotate(-8deg)', metrics: [38, 22, 68, 14] }, + { id: 'sunset', label: 'Warm Sunset Glow', name: 'SUNSET GLOW', desc: 'Amber backlight with a soft halation around every highlight, made for late golden hour.', filter: 'sepia(.34) saturate(1.35) contrast(1.06) brightness(1.06)', metrics: [88, 28, 52, 61] }, + { id: 'cyber', label: "Cyber Neon '88", name: 'CYBER NEON 88', desc: 'Magenta and cyan push, crushed blacks and a bloom that reads like wet asphalt at night.', filter: 'saturate(1.8) contrast(1.3) hue-rotate(266deg)', metrics: [24, 46, 84, 78] }, + { id: 'mono', label: 'Monochromatic Film', name: 'ACROS 100', desc: 'High-contrast black and white with a fine grain structure and clean mid-tone separation.', filter: 'grayscale(1) contrast(1.4) brightness(1.02)', metrics: [6, 41, 92, 18] }, +]; + +const BAR_LABELS = ['WARMTH', 'FILM GRAIN', 'CONTRAST', 'HALATION BLOOM']; + +interface Feature { title: string; text: string; icon: 'aperture' | 'grain' | 'batch' | 'qr' | 'hsl' | 'raw' } +const FEATURES: Feature[] = [ + { icon: 'aperture', title: 'Live LUT Viewfinder', text: 'See the recipe through the lens before you press the shutter — no round trip to an editor.' }, + { icon: 'grain', title: '35mm & 120 Film Grain', text: 'Authentic grain structures plus halation bloom, tuned per stock rather than one global overlay.' }, + { icon: 'batch', title: 'Batch Color Editing', text: 'Apply one recipe across hundreds of photos at once and export the whole set in a single pass.' }, + { icon: 'qr', title: 'QR Recipe Sharing', text: 'Export a recipe as a scannable card and let a friend import the exact look in one scan.' }, + { icon: 'hsl', title: 'HSL & Selective Color', text: 'Fine-tune eight individual colour channels when a stock needs a nudge, not a replacement.' }, + { icon: 'raw', title: 'RAW Support & 4K Export', text: 'Full EXIF retention, 300 ppi print metadata and lossless JPEG at up to 4K.' }, +]; + +const QUOTES = [ + { text: 'I grade a whole trip before the flight lands. The QR hand-off means my editor gets the exact same look, not a screenshot.', name: 'Mai Anh', role: '@maianh.travel · Travel Blogger' }, + { text: 'Classic Chrome with a whisper of grain is 90% of my feed now. It reads like film without the scanning workflow.', name: 'Daniel Rowe', role: '@rowe.frames · Street Photographer' }, + { text: 'The live viewfinder is the whole trick — clients pick a recipe on set instead of asking for fifteen revisions later.', name: 'Trúc Lâm', role: '@truclam.studio · Content Creator' }, +]; + +const FAQ = [ + { q: 'Does it preserve original image quality?', a: 'Yes. The web studio renders from your untouched original at export time, writes 300 ppi JFIF density and keeps the camera EXIF and GPS tags. RAW and 4K export are covered on the Pro plan.' }, + { q: 'How do I scan a recipe QR code?', a: 'Open the Scan QR button inside the app and point it at the card. The recipe imports with its full parameter set — no manual re-typing.' }, + { q: 'Is there an export watermark?', a: 'No watermarks on your own exports. Only the free guest tier on the web adds a small corner mark; sign in and it disappears.' }, +]; + +function Icon({ name }: { name: Feature['icon'] }) { + const common = { width: 26, height: 26, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: 1.6, strokeLinecap: 'round' as const, strokeLinejoin: 'round' as const }; + switch (name) { + case 'aperture': return ; + case 'grain': return ; + case 'batch': return ; + case 'qr': return ; + case 'hsl': return ; + default: return ; + } +} export function Landing() { - const { t, lang, setLang } = useI18n(); - const { mode, accent, setMode, setAccent } = useTheme(); + const [menu, setMenu] = useState(false); + const [presetId, setPresetId] = useState(PRESETS[0].id); + const [temp, setTemp] = useState(0); + const [sat, setSat] = useState(100); + const [con, setCon] = useState(100); + const [toast, setToast] = useState(null); + const [qrOk, setQrOk] = useState(true); + const toastTimer = useRef>(); + + const preset = PRESETS.find((p) => p.id === presetId) ?? PRESETS[0]; + + const showToast = useCallback((text: string) => { + setToast(text); + clearTimeout(toastTimer.current); + toastTimer.current = setTimeout(() => setToast(null), 3400); + }, []); + useEffect(() => () => clearTimeout(toastTimer.current), []); + + // The simulator's output: three knobs, one CSS filter string, applied live. + const labFilter = `sepia(${(Math.abs(temp) / 260).toFixed(3)}) hue-rotate(${(-temp * 0.45).toFixed(1)}deg) saturate(${sat}%) contrast(${con}%)`; + + const nav = [ + ['#film', 'Film Strip'], + ['#presets', 'Try Recipes'], + ['#creator', 'Custom Recipe Creator'], + ['#features', 'Features'], + ['#qr', 'QR Code'], + ['#pricing', 'Pricing'], + ] as const; return ( -
-
- RecipesCam web - - - - {ACCENTS.map((entry) => ( - - ))} + +
+ {menu ? ( +
+ {nav.map(([href, label]) => setMenu(false)}>{label})} + Open studio + +
+ ) : null} + + + {/* 2 — hero */} +
+
+
+ ● NEXT-GEN COLOR RECIPES 4.0 — LIVE LUTS +

Capture & Grade Photos With Exclusive Color Recipes

+

+ RecipesCam reproduces Kodak and Fuji film colour — tone curves, grain structure and + halation — then applies it to your own photos. Same pipeline on the phone and in the + browser, no upload, nothing leaves your machine. +

+ +
+
250+COLOR RECIPES
+
1M+DOWNLOADS
+
4.9★STORE RATING
+
+
-
-

{t('land.title')}

-

{t('land.lede')}

- -
- {t('land.ctaDemo')} - {t('land.ctaSignup')} + {/* 3 — 35mm film strip marquee */} +
+
+ {[...REEL, ...REEL].map((look, i) => ( +
+ +
+ {look.title} +
+
+ {look.title} + {look.meta} +
+ +
+ ))}
+
-
-
-

{t('land.f1t')}

-

{t('land.f1d')}

-
-
-

{t('land.f2t')}

-

{t('land.f2d')}

-
-
-

{t('land.f3t')}

-

{t('land.f3d')}

-
+ {/* 4 — live preset tester */} +
+
+
+ Live preset tester +

Tap a recipe, watch the negative change

+

Five stocks from the library, applied to the same frame with the same colour maths the exporter uses.

+
+
+
+
+ {PRESETS.map((p) => ( + + ))} +
+
+ {`${preset.name} +
+ f/1.8 + SS 1/500s + ISO 200 + 4K ULTRA HD + {preset.name} +
+
+
+ +
+
-
- - {t('land.storeSoon')} - {t('land.badgePlay')} - - - {t('land.storeSoon')} - {t('land.badgeApp')} - + {/* 5 — custom recipe simulator */} +
+
+
+ Custom recipe creator +

Build your own grade in three moves

+

Temperature, saturation and contrast, applied to the preview as you drag — the same knobs the studio exposes, minus the render wait.

+
+
+
+ Custom recipe preview +
CUSTOM RECIPETEMP {temp > 0 ? `+${temp}` : temp}SAT {sat}%CON {con}%
+
+
+
+ + setTemp(Number(e.target.value))} /> +
+
+ + setSat(Number(e.target.value))} /> +
+
+ + setCon(Number(e.target.value))} /> +
+
+
RECIPE CUSTOM_01 · 300 ppi · EXIF KEPT
+
filter: sepia({(Math.abs(temp) / 260).toFixed(3)}) hue-rotate({(-temp * 0.45).toFixed(1)}deg)
+
        saturate({sat}%) contrast({con}%)
+ +
+
+
-
+ + + {/* 6 — features */} +
+
+
+ Features +

Everything the phone build ships with

+
+
+ {FEATURES.map((f) => ( +
+ +

{f.title}

+

{f.text}

+
+ ))} +
+
+
+ + {/* 7 — QR recipe sharing */} +
+
+
+
+
+ Shared recipe card +
+
+
+ {qrOk ? ( + Recipe QR code setQrOk(false)} + /> + ) : null} +
+
+ SUNSET GLOW + WARMTH 88 · GRAIN 28 · BLOOM 61 + BY @maianh.travel · 12.4K IMPORTS +
+
+
+
+ Share by QR +

One scan, byte-identical recipe

+
+
+ 1 +

Export a recipe card. The app designs the card — graded thumbnail, parameter readout, author tag — and saves it as an image you can post anywhere.

+
+
+ 2 +

Your friend scans it. The QR opens the recipe in RecipesCam and imports every parameter at once, so the look lands exactly as you built it.

+
+
+
+
+
+
+ + {/* 8 — pricing */} +
+
+
+ Pricing +

Start free. Go Pro when the grain matters.

+
+
+
+ Starter +
$0 / FOREVER
+

Everything needed to shoot your first roll.

+
    +
  • 15 basic recipes
  • +
  • HD live viewfinder
  • +
  • Basic grain adjustment
  • +
  • 300 ppi JPEG export
  • +
+ Open the studio +
+
+ VIP Pro +
$2.99 / MONTH
+

For the people who grade every frame.

+
    +
  • 250+ VIP recipes
  • +
  • Batch processing
  • +
  • Tone curve & HSL tools
  • +
  • Lossless RAW & 4K export
  • +
  • Unlimited QR generation
  • +
+ +
+
+
+
+ + {/* 9 — testimonials + FAQ */} +
+
+
+ Reviews +

Shot on RecipesCam

+
+
+ {QUOTES.map((q) => ( +
+

“{q.text}”

+
{q.name}{q.role}
+
+ ))} +
+
+ FAQ +

Questions we get every week

+
+
+ {FAQ.map((item) => ( +
+ {item.q} +

{item.a}

+
+ ))} +
+
+
+ + {/* 10 — final CTA + footer */} +
+
+
+

Load a recipe. Shoot the rest of the day.

+

RecipesCam is free on both stores, and the web studio runs right here in your browser.

+
+ + + Open the Web Studio +
+
+
+ + + + {toast ?
Coming soon — {toast}
: null} ); } diff --git a/docker/frontend/src/i18n/en.ts b/docker/frontend/src/i18n/en.ts index f7a96b1..1cedbbe 100644 --- a/docker/frontend/src/i18n/en.ts +++ b/docker/frontend/src/i18n/en.ts @@ -19,21 +19,6 @@ export const en: Dict = { 'act.exporting': 'Exporting…', 'act.cancel': 'Cancel', - 'land.title': 'The same colour recipes, now on your desktop', - 'land.lede': - 'RecipesCam turns a photo into a film-looking one with recipes. The web build runs the app\u2019s exact colour pipeline: same recipes, same frames, same GPS stamp.', - 'land.ctaDemo': 'OPEN THE DEMO', - 'land.ctaSignup': 'CREATE AN ACCOUNT', - 'land.f1t': 'Film recipes', - 'land.f1d': 'PROVIA, VELVIA, CLASSIC CHROME… plus EXPOSURE, WB, CLARITY, HDF and VIGNETTING.', - 'land.f2t': 'Frames & watermark', - 'land.f2d': 'Polaroid, classic border, WALL FRAME, custom text and a GPS stamp read from the photo\u2019s EXIF.', - 'land.f3t': 'Processed on your own machine', - 'land.f3d': 'The photo never leaves the browser. The server only keeps accounts and the recipes you save.', - 'land.badgePlay': 'Get it on Google Play', - 'land.badgeApp': 'Download on the App Store', - 'land.storeSoon': 'Coming soon', - 'drop.title': 'Drag & drop a photo here', 'drop.hint': 'or click to pick one · paste from the clipboard with Ctrl+V', 'drop.replace': 'Choose another photo', diff --git a/docker/frontend/src/i18n/vi.ts b/docker/frontend/src/i18n/vi.ts index 7347d15..9b70c06 100644 --- a/docker/frontend/src/i18n/vi.ts +++ b/docker/frontend/src/i18n/vi.ts @@ -22,21 +22,6 @@ export const vi = { 'act.exporting': 'Đang xuất…', 'act.cancel': 'Huỷ', - 'land.title': 'Cùng công thức màu, giờ trên máy tính', - 'land.lede': - 'RecipesCam biến ảnh chụp thành ảnh có "chất" bằng công thức phim. Bản web chạy đúng pipeline màu của app: cùng công thức, cùng khung, cùng dấu GPS.', - 'land.ctaDemo': 'DÙNG THỬ NGAY', - 'land.ctaSignup': 'TẠO TÀI KHOẢN', - 'land.f1t': 'Công thức phim', - 'land.f1d': 'PROVIA, VELVIA, CLASSIC CHROME… cộng bộ chỉnh tay EXPOSURE, WB, CLARITY, HDF, VIGNETTING.', - 'land.f2t': 'Khung & watermark', - 'land.f2d': 'Khung polaroid, viền classic, WALL FRAME, watermark chữ và dấu GPS đọc từ EXIF của ảnh.', - 'land.f3t': 'Xử lý ngay trên máy bạn', - 'land.f3d': 'Ảnh không bao giờ rời khỏi trình duyệt. Máy chủ chỉ giữ tài khoản và công thức bạn lưu.', - 'land.badgePlay': 'Tải trên Google Play', - 'land.badgeApp': 'Tải trên App Store', - 'land.storeSoon': 'Sắp có', - 'drop.title': 'Kéo & thả ảnh vào đây', 'drop.hint': 'hoặc bấm để chọn ảnh · dán từ clipboard bằng Ctrl+V', 'drop.replace': 'Đổi ảnh khác', diff --git a/docker/frontend/src/main.tsx b/docker/frontend/src/main.tsx index 04be10c..83aeca4 100644 --- a/docker/frontend/src/main.tsx +++ b/docker/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { createRoot } from 'react-dom/client'; import './styles/tokens.css'; import './styles/app.css'; +import './styles/landing.css'; import { ThemeProvider } from './theme/ThemeProvider'; import { I18nProvider } from './i18n/I18nProvider'; import { Landing } from './Landing'; diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index 1ebf605..1f1b591 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -164,29 +164,6 @@ input[type="range"] { width: 100%; accent-color: var(--accent); } .stage .toolbar { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; justify-content: center; } .err { color: var(--danger); font-size: 12px; max-width: 520px; text-align: center; } -/* --- landing ------------------------------------------------------------ */ -.landing { max-width: 960px; margin: 0 auto; padding: 56px 20px 72px; } -.landing h1 { font-size: clamp(30px, 5vw, 52px); line-height: 1.1; margin: 0 0 14px; } -.landing .lede { font-size: 17px; color: var(--text-dim); max-width: 620px; line-height: 1.6; } -.landing .cta { display: flex; gap: 10px; margin: 24px 0 44px; flex-wrap: wrap; } -.features { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; } -.card { border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; background: var(--bg-elev); } -.card h3 { margin: 0 0 6px; font-size: 14px; } -.card p { margin: 0; color: var(--text-dim); line-height: 1.55; } -.badges { display: flex; gap: 10px; margin-top: 34px; flex-wrap: wrap; } -.badge { - border: 1px solid var(--border); - border-radius: var(--radius-sm); - padding: 9px 14px; - text-decoration: none; - color: var(--text); - display: flex; - flex-direction: column; - line-height: 1.3; - background: var(--bg-elev); -} -.badge small { color: var(--text-dim); font-size: 10px; letter-spacing: 0.08em; } - /* --- auth dialog -------------------------------------------------------- */ .modal-backdrop { position: fixed; diff --git a/docker/frontend/src/styles/landing.css b/docker/frontend/src/styles/landing.css new file mode 100644 index 0000000..4caca52 --- /dev/null +++ b/docker/frontend/src/styles/landing.css @@ -0,0 +1,381 @@ +/* Landing page — dark cinematic theme per the landing blueprint. Self-contained: + its own palette, its own class prefix (.lp-*), no dependency on the workspace + theme, so the marketing page looks the same in light or dark mode. */ + +@font-face { + font-family: 'Syne'; + font-style: normal; + font-weight: 700 800; + font-display: swap; + src: url('/assets/Syne.woff2') format('woff2'); +} + +.lp { + --lp-bg: #090a0f; + --lp-bg2: #0d0e12; + --lp-card: #161820; + --lp-amber: #fbbf24; + --lp-orange: #f59e0b; + --lp-red: #ff5e36; + --lp-cyan: #38bdf8; + --lp-indigo: #818cf8; + --lp-text: #f3f4f6; + --lp-muted: #9ca3af; + --lp-line: rgba(255, 255, 255, 0.08); + --lp-mono: 'Cousine', ui-monospace, SFMono-Regular, Menlo, monospace; + --lp-display: 'Syne', ui-sans-serif, system-ui, sans-serif; + + background: var(--lp-bg); + color: var(--lp-text); + min-height: 100%; + font-size: 15px; + line-height: 1.6; +} +html { scroll-behavior: smooth; scroll-padding-top: 78px; } +.lp ::selection { background: var(--lp-amber); color: #10121a; } +.lp-wrap { max-width: 1180px; margin: 0 auto; padding: 0 22px; } +.lp h1, .lp h2, .lp h3 { font-family: var(--lp-display); font-weight: 800; line-height: 1.1; margin: 0; } +.lp p { margin: 0; } +.lp a { color: inherit; text-decoration: none; } +.lp-mono { font-family: var(--lp-mono); font-size: 11px; letter-spacing: 0.08em; } +.lp-eyebrow { + font-family: var(--lp-mono); + font-size: 11px; + letter-spacing: 0.22em; + color: var(--lp-amber); + text-transform: uppercase; +} +.lp-muted { color: var(--lp-muted); } +.lp-sec { padding: 92px 0; border-top: 1px solid var(--lp-line); } +.lp-sec-head { max-width: 720px; margin-bottom: 40px; } +.lp-sec-head h2 { font-size: clamp(26px, 4vw, 40px); } +.lp-sec-head p { color: var(--lp-muted); margin-top: 12px; } + +/* --- buttons ------------------------------------------------------------ */ +.lp-btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 9px; + padding: 12px 20px; + border-radius: 999px; + border: 1px solid var(--lp-line); + background: rgba(255, 255, 255, 0.04); + color: var(--lp-text); + font-weight: 600; + font-size: 14px; + cursor: pointer; + white-space: nowrap; +} +.lp-btn:hover { border-color: rgba(251, 191, 36, 0.5); } +.lp-btn.lp-primary { + background: linear-gradient(120deg, var(--lp-amber), var(--lp-orange)); + border-color: transparent; + color: #14100a; + box-shadow: 0 10px 30px rgba(245, 158, 11, 0.25); +} +.lp-btn.lp-primary:hover { filter: brightness(1.07); } +.lp-btn.lp-sm { padding: 9px 15px; font-size: 13px; } + +/* --- 1. nav ------------------------------------------------------------- */ +.lp-nav { + position: fixed; + inset: 0 0 auto 0; + z-index: 50; + background: rgba(9, 10, 15, 0.72); + backdrop-filter: blur(14px); + -webkit-backdrop-filter: blur(14px); + border-bottom: 1px solid var(--lp-line); +} +.lp-nav .lp-wrap { display: flex; align-items: center; gap: 18px; height: 62px; } +.lp-logo { display: flex; align-items: center; gap: 9px; font-family: var(--lp-display); font-weight: 800; font-size: 18px; } +.lp-logo em { font-style: normal; color: var(--lp-amber); } +.lp-logo svg { color: var(--lp-amber); } +.lp-nav-links { display: flex; gap: 22px; margin-left: 14px; font-size: 13.5px; color: var(--lp-muted); } +.lp-nav-links a:hover { color: var(--lp-text); } +.lp-nav .lp-spacer { flex: 1 1 auto; } +.lp-burger { display: none; } + +@media (max-width: 940px) { + .lp-nav-links, .lp-nav .lp-wrap > .lp-btn { display: none; } + .lp-burger { + display: inline-flex; + flex-direction: column; + gap: 4px; + background: none; + border: 0; + padding: 8px; + cursor: pointer; + } + .lp-burger i { display: block; width: 20px; height: 2px; background: var(--lp-text); } + .lp-sheet { + display: flex; + flex-direction: column; + gap: 2px; + padding: 10px 22px 18px; + border-top: 1px solid var(--lp-line); + } + .lp-sheet a { padding: 11px 0; color: var(--lp-muted); border-bottom: 1px solid var(--lp-line); } + .lp-sheet a:hover { color: var(--lp-text); } + .lp-sheet .lp-btn { margin-top: 12px; } +} +@media (min-width: 941px) { .lp-sheet { display: none; } } + +/* --- 2. hero ------------------------------------------------------------ */ +.lp-hero { position: relative; padding: 140px 0 74px; overflow: hidden; } +.lp-glow { + position: absolute; + inset: -30% -10% auto -10%; + height: 560px; + background: + radial-gradient(420px 280px at 20% 30%, rgba(245, 158, 11, 0.22), transparent 70%), + radial-gradient(380px 260px at 78% 22%, rgba(255, 94, 54, 0.16), transparent 70%), + radial-gradient(420px 300px at 55% 80%, rgba(129, 140, 248, 0.14), transparent 70%); + pointer-events: none; +} +.lp-hero .lp-wrap { position: relative; } +.lp-badge { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 6px 13px; + border-radius: 999px; + border: 1px solid rgba(251, 191, 36, 0.35); + background: rgba(251, 191, 36, 0.08); + color: var(--lp-amber); + font-family: var(--lp-mono); + font-size: 11px; + letter-spacing: 0.12em; +} +.lp-hero h1 { + font-size: clamp(34px, 6.4vw, 68px); + margin: 20px 0 18px; + max-width: 15ch; + background: linear-gradient(100deg, #f8fafc 12%, var(--lp-amber) 58%, var(--lp-red) 96%); + -webkit-background-clip: text; + background-clip: text; + color: transparent; +} +.lp-hero .lp-lede { color: var(--lp-muted); font-size: 17px; max-width: 60ch; } +.lp-cta { display: flex; flex-wrap: wrap; gap: 12px; margin: 30px 0 44px; } +.lp-stats { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + max-width: 640px; +} +.lp-stat { + border: 1px solid var(--lp-line); + background: linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent); + border-radius: 14px; + padding: 16px 18px; +} +.lp-stat b { display: block; font-family: var(--lp-display); font-size: 26px; } +.lp-stat span { color: var(--lp-muted); font-family: var(--lp-mono); font-size: 10.5px; letter-spacing: 0.14em; } + +/* --- 3. film strip marquee ---------------------------------------------- */ +.lp-marquee { overflow: hidden; border-top: 1px solid var(--lp-line); border-bottom: 1px solid var(--lp-line); background: var(--lp-bg2); padding: 26px 0; } +.lp-track { display: flex; width: max-content; animation: lp-marquee 48s linear infinite; } +.lp-marquee:hover .lp-track { animation-play-state: paused; } +@keyframes lp-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } } +@media (prefers-reduced-motion: reduce) { .lp-track { animation: none; } } +.lp-frame { + width: 236px; + margin-right: 18px; + background: #0b0c11; + border: 1px solid var(--lp-line); + border-radius: 10px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 8px; +} +.lp-perf { display: flex; justify-content: space-between; } +.lp-perf i { + width: 13px; + height: 7px; + border-radius: 2px; + background: #000; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.14); +} +.lp-shot { position: relative; aspect-ratio: 4 / 5; border-radius: 5px; overflow: hidden; background: #000; } +.lp-shot img { width: 100%; height: 100%; object-fit: cover; display: block; } +.lp-grain { + position: absolute; + inset: 0; + opacity: 0.3; + mix-blend-mode: overlay; + pointer-events: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='120' height='120' filter='url(%23n)'/%3E%3C/svg%3E"); +} +.lp-tagline { position: absolute; top: 7px; left: 7px; font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-amber); text-shadow: 0 1px 6px #000; } +.lp-frame-meta { display: flex; flex-direction: column; gap: 3px; } +.lp-frame-meta b { font-size: 13px; font-weight: 600; } +.lp-frame-meta span { font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-muted); letter-spacing: 0.06em; } + +/* --- 4. preset tester --------------------------------------------------- */ +.lp-tester { display: grid; grid-template-columns: minmax(0, 1fr) 320px; gap: 26px; align-items: start; } +.lp-tabs { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 16px; } +.lp-tab { + border: 1px solid var(--lp-line); + background: rgba(255, 255, 255, 0.03); + color: var(--lp-muted); + border-radius: 999px; + padding: 7px 14px; + font-size: 12.5px; + font-family: var(--lp-mono); + cursor: pointer; +} +.lp-tab[aria-pressed="true"] { border-color: var(--lp-amber); color: var(--lp-amber); background: rgba(251, 191, 36, 0.1); } +.lp-view { position: relative; border-radius: 16px; overflow: hidden; border: 1px solid var(--lp-line); background: #000; } +.lp-view img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; display: block; transition: filter 380ms ease; } +.lp-hud { + position: absolute; + inset: auto 0 0 0; + display: flex; + gap: 14px; + flex-wrap: wrap; + padding: 10px 14px; + background: linear-gradient(0deg, rgba(0, 0, 0, 0.75), transparent); + font-family: var(--lp-mono); + font-size: 10.5px; + color: rgba(243, 244, 246, 0.85); +} +.lp-hud b { color: var(--lp-amber); font-weight: 700; } +.lp-specs { border: 1px solid var(--lp-line); background: var(--lp-card); border-radius: 16px; padding: 20px; } +.lp-specs h3 { font-size: 18px; } +.lp-specs p { color: var(--lp-muted); font-size: 13.5px; margin-top: 8px; } +.lp-bars { display: flex; flex-direction: column; gap: 12px; margin-top: 18px; } +.lp-bar span { display: flex; justify-content: space-between; font-family: var(--lp-mono); font-size: 10.5px; color: var(--lp-muted); letter-spacing: 0.1em; } +.lp-bar i { display: block; height: 5px; border-radius: 999px; background: rgba(255, 255, 255, 0.09); margin-top: 6px; overflow: hidden; } +.lp-bar i::after { content: ''; display: block; height: 100%; width: var(--v); border-radius: 999px; background: linear-gradient(90deg, var(--lp-amber), var(--lp-red)); transition: width 380ms ease; } + +/* --- 5. creator --------------------------------------------------------- */ +.lp-lab { display: grid; grid-template-columns: minmax(0, 1fr) 380px; gap: 26px; align-items: start; } +.lp-knobs { border: 1px solid var(--lp-line); background: var(--lp-bg2); border-radius: 16px; padding: 22px; display: flex; flex-direction: column; gap: 20px; } +.lp-knob label { display: flex; justify-content: space-between; align-items: baseline; font-family: var(--lp-mono); font-size: 11px; letter-spacing: 0.1em; color: var(--lp-muted); } +.lp-knob b { color: var(--lp-amber); font-size: 13px; } +.lp input[type=range] { width: 100%; margin-top: 10px; accent-color: var(--lp-amber); } +.lp-code { border-top: 1px solid var(--lp-line); padding-top: 16px; font-family: var(--lp-mono); font-size: 11px; color: var(--lp-muted); line-height: 1.9; } +.lp-code b { color: var(--lp-cyan); } + +/* --- 6. features -------------------------------------------------------- */ +.lp-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; } +.lp-feat { border: 1px solid var(--lp-line); background: var(--lp-card); border-radius: 16px; padding: 22px; } +.lp-feat svg { color: var(--lp-amber); } +.lp-feat h3 { font-size: 17px; margin: 14px 0 8px; } +.lp-feat p { color: var(--lp-muted); font-size: 13.5px; } + +/* --- 7. QR -------------------------------------------------------------- */ +.lp-qr { display: grid; grid-template-columns: 360px minmax(0, 1fr); gap: 34px; align-items: center; } +.lp-recipe-card { + border: 1px solid rgba(251, 191, 36, 0.3); + border-radius: 18px; + background: linear-gradient(160deg, #1b1d27, #0e0f14); + padding: 16px; + box-shadow: 0 24px 60px rgba(0, 0, 0, 0.5); + transform: rotate(-2deg); +} +.lp-recipe-card .lp-shot { border-radius: 12px; } +.lp-rc-row { display: flex; align-items: center; gap: 14px; margin-top: 14px; } +.lp-rc-qr { width: 84px; height: 84px; border-radius: 10px; background: #fff; padding: 5px; flex: 0 0 auto; } +.lp-rc-qr img { width: 100%; height: 100%; display: block; } +.lp-rc-meta { min-width: 0; } +.lp-rc-meta b { display: block; font-family: var(--lp-display); font-size: 15px; } +.lp-rc-meta span { font-family: var(--lp-mono); font-size: 10px; color: var(--lp-muted); } +.lp-steps { display: flex; flex-direction: column; gap: 18px; margin-top: 22px; } +.lp-step { display: flex; gap: 14px; } +.lp-step span { + flex: 0 0 auto; + width: 30px; + height: 30px; + border-radius: 999px; + display: grid; + place-items: center; + background: rgba(251, 191, 36, 0.12); + border: 1px solid rgba(251, 191, 36, 0.4); + color: var(--lp-amber); + font-family: var(--lp-mono); + font-size: 12px; +} +.lp-step p { color: var(--lp-muted); font-size: 13.5px; } +.lp-step b { color: var(--lp-text); } + +/* --- 8. pricing --------------------------------------------------------- */ +.lp-price { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; max-width: 860px; } +.lp-plan { border: 1px solid var(--lp-line); background: var(--lp-card); border-radius: 18px; padding: 26px; display: flex; flex-direction: column; } +.lp-plan.vip { border-color: rgba(251, 191, 36, 0.45); background: linear-gradient(180deg, rgba(245, 158, 11, 0.12), rgba(22, 24, 32, 0.9)); position: relative; } +.lp-plan.vip::after { + content: 'MOST POPULAR'; + position: absolute; + top: -11px; + left: 26px; + background: linear-gradient(120deg, var(--lp-amber), var(--lp-orange)); + color: #14100a; + border-radius: 999px; + padding: 3px 10px; + font-family: var(--lp-mono); + font-size: 9.5px; + letter-spacing: 0.12em; +} +.lp-plan .lp-amount { font-family: var(--lp-display); font-size: 38px; margin: 12px 0 4px; } +.lp-plan .lp-amount small { font-family: var(--lp-mono); font-size: 11px; color: var(--lp-muted); } +.lp-plan ul { list-style: none; padding: 0; margin: 18px 0 22px; display: flex; flex-direction: column; gap: 9px; font-size: 13.5px; color: var(--lp-muted); } +.lp-plan li::before { content: '▸ '; color: var(--lp-amber); } +.lp-plan .lp-btn { margin-top: auto; } + +/* --- 9. testimonials + faq --------------------------------------------- */ +.lp-quotes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; margin-bottom: 44px; } +.lp-quote { border: 1px solid var(--lp-line); background: var(--lp-card); border-radius: 16px; padding: 22px; } +.lp-quote p { font-size: 14px; } +.lp-quote footer { margin-top: 16px; font-family: var(--lp-mono); font-size: 10.5px; color: var(--lp-muted); } +.lp-quote footer b { display: block; color: var(--lp-text); font-family: var(--lp-display); font-size: 13.5px; } +.lp-faq { display: flex; flex-direction: column; gap: 10px; max-width: 820px; } +.lp-faq details { border: 1px solid var(--lp-line); border-radius: 12px; background: var(--lp-bg2); padding: 14px 18px; } +.lp-faq summary { cursor: pointer; font-weight: 600; } +.lp-faq summary::marker { color: var(--lp-amber); } +.lp-faq p { color: var(--lp-muted); font-size: 13.5px; margin-top: 10px; } + +/* --- 10. final CTA + footer -------------------------------------------- */ +.lp-final { position: relative; overflow: hidden; text-align: center; padding: 88px 0; } +.lp-final .lp-glow { inset: -60% 0 auto 0; } +.lp-final h2 { font-size: clamp(28px, 4.6vw, 46px); max-width: 20ch; margin: 0 auto 16px; } +.lp-final p { color: var(--lp-muted); max-width: 52ch; margin: 0 auto 28px; } +.lp-stores { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; } +.lp-store { display: flex; flex-direction: column; align-items: flex-start; line-height: 1.25; } +.lp-store small { font-family: var(--lp-mono); font-size: 9.5px; color: var(--lp-muted); } +.lp-store b { font-size: 15px; } +.lp-footer { border-top: 1px solid var(--lp-line); padding: 30px 0 40px; } +.lp-footer .lp-wrap { display: flex; flex-wrap: wrap; gap: 14px 26px; align-items: center; font-size: 12.5px; color: var(--lp-muted); } +.lp-footer a:hover { color: var(--lp-text); } +.lp-social { display: flex; gap: 16px; margin-left: auto; font-family: var(--lp-mono); font-size: 11px; } + +/* --- toast -------------------------------------------------------------- */ +.lp-toast { + position: fixed; + right: 18px; + bottom: 18px; + z-index: 60; + max-width: min(340px, calc(100vw - 36px)); + border: 1px solid rgba(251, 191, 36, 0.4); + background: rgba(22, 24, 32, 0.96); + border-radius: 12px; + padding: 12px 16px; + box-shadow: 0 20px 50px rgba(0, 0, 0, 0.55); + font-size: 13px; +} +.lp-toast b { color: var(--lp-amber); } + +/* --- responsive --------------------------------------------------------- */ +@media (max-width: 980px) { + .lp-tester, .lp-lab, .lp-qr { grid-template-columns: minmax(0, 1fr); } + .lp-grid, .lp-quotes { grid-template-columns: repeat(2, minmax(0, 1fr)); } + .lp-recipe-card { transform: none; } +} +@media (max-width: 680px) { + .lp-sec { padding: 64px 0; } + .lp-hero { padding: 112px 0 52px; } + .lp-grid, .lp-quotes, .lp-price, .lp-stats { grid-template-columns: minmax(0, 1fr); } + .lp-stat { display: flex; align-items: baseline; gap: 10px; } + .lp-social { margin-left: 0; } +}