// While² — landing page marketing sections // Sans-serif (Geist) marketing chrome a la openclaw.ai. In-app screen // internals stay DotGothic16 monospace. const _C = '#5cd7ff'; const _INK = '#e8e6df'; const _DIM = 'rgba(232,230,223,0.6)'; const _FAINT = 'rgba(232,230,223,0.32)'; const _HAIR = 'rgba(232,230,223,0.12)'; const _SANS = '"Geist", "Inter", -apple-system, BlinkMacSystemFont, system-ui, sans-serif'; const _MONO = '"Geist Mono", "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace'; // Inject one-time dolphin float keyframes if (typeof document !== 'undefined' && !document.getElementById('w2-dolphin-anim')) { const s = document.createElement('style'); s.id = 'w2-dolphin-anim'; s.textContent = ` @keyframes w2-dolphin-float { 0% { transform: translateY(0) rotate(0deg); } 25% { transform: translateY(-8px) rotate(-2deg); } 50% { transform: translateY(-14px) rotate(0deg); } 75% { transform: translateY(-8px) rotate(2deg); } 100% { transform: translateY(0) rotate(0deg); } } @keyframes w2-cmd-fade { 0% { opacity: 0; transform: translateY(6px); } 100% { opacity: 1; transform: translateY(0); } } @keyframes w2-tab-progress { 0% { transform: scaleX(0); } 100% { transform: scaleX(1); } } @keyframes w2-shout-shake { 0%, 100% { transform: translateX(0); } 25% { transform: translateX(-1.5px); } 75% { transform: translateX(1.5px); } } .w2-dolphin-float { animation: w2-dolphin-float 4.2s ease-in-out infinite; } /* multi-part dolphin — independent loops per limb, kept subtle */ @keyframes w2-fin-flap { 0%, 100% { transform: rotate(-0.8deg); } 50% { transform: rotate(2.5deg); } } @keyframes w2-tail-sway { 0%, 100% { transform: rotate(0.8deg); } 50% { transform: rotate(-1.6deg); } } @keyframes w2-right-wave { 0%, 100% { transform: translateY(0) rotate(0deg); } 50% { transform: translateY(-1px) rotate(0.8deg); } } .w2-dol-part { position: absolute; inset: 0; width: 100%; height: 100%; } /* group node = body float (everyone follows) */ .w2-dol-group { position: relative; width: 100%; height: 100%; animation: w2-dolphin-float 4.2s ease-in-out infinite; } /* per-part overlays animate ON TOP of the group transform */ .w2-dol-tail { animation: w2-tail-sway 4.6s ease-in-out infinite; transform-origin: 70% 60%; } .w2-dol-fin { animation: w2-fin-flap 3.6s ease-in-out infinite; transform-origin: 30% 80%; } .w2-dol-right{ animation: w2-right-wave 5s ease-in-out infinite; transform-origin: 30% 50%; } @keyframes w2-phrase-fade { 0% { opacity: 0; transform: translateY(4px); } 12% { opacity: 1; transform: translateY(0); } 88% { opacity: 1; transform: translateY(0); } 100% { opacity: 0; transform: translateY(-4px); } } /* ── Responsive (≤ 720px) ───────────────────────────── */ @media (max-width: 720px) { .w2-hero { padding: 64px 20px 28px !important; } .w2-section-pad { padding: 16px 32px 56px !important; } .w2-section-pad-y { padding: 56px 0 !important; } .w2-section-pad-y > div { padding: 0 32px !important; } .w2-download { padding: 56px 32px 96px !important; } .w2-cmd-body { grid-template-columns: 1fr !important; } .w2-cmd-screen { border-left: none !important; border-top: 1px solid rgba(232,230,223,0.12) !important; min-height: 320px !important; } .w2-cmd-block .w2-cmd-body > div:first-child { padding: 22px 20px !important; } .w2-footer-grid { grid-template-columns: 1fr !important; } .w2-footer-socials { justify-content: flex-start !important; } } @media (max-width: 480px) { .w2-hero { padding: 48px 16px 24px !important; } .w2-section-pad { padding: 12px 28px 48px !important; } .w2-section-pad-y > div { padding: 0 28px !important; } .w2-download { padding: 48px 28px 80px !important; } } `; document.head.appendChild(s); } // Patched-line pairs now live in copy.heroPatchedPairs (per-language). // Layered, animated dolphin. 4 PNG parts stacked by suffix order. The whole // group floats together (so fins/tail follow the body), and each part adds its // own subtle micro-motion on top of the group transform. const Dolphin = ({ size = 64, animate = false, style }) =>
; function TopBar() {return null;} // ── Hero ────────────────────────────────────────────────────── function Hero({ copy }) { const [shout, setShout] = React.useState(false); return (
setShout(true)} onMouseLeave={() => setShout(false)} onTouchStart={() => setShout(s => !s)} style={{ cursor: 'pointer', marginBottom: 22, lineHeight: 0 }}>
{/* OpenClaw-style wordmark — heavy sans, horizontal cyan gradient */}

while2

{/* FACT line — cyan gradient, OpenClaw-tagline style */} {shout ? (
ALLOW! ALLOW!
) : (
{copy.heroFact}
)} {/* TARGET line — who it's for */}
{copy.heroTarget}
); } // ScrambledText: per-char wave-jitter reveal a la ScrambledText.swift const W2_GLYPH_POOL = '!<>-_\\/[]{}—=+*^?#·░▒▓█'; function useScramble(target, duration = 520) { const [out, setOut] = React.useState(target); const startRef = React.useRef(0); const rafRef = React.useRef(0); const targetRef = React.useRef(target); const revealsRef = React.useRef([]); React.useEffect(() => { targetRef.current = target; startRef.current = performance.now(); // per-char reveal time: (i/len*0.55 + rand*0.45) * duration const len = target.length; revealsRef.current = Array.from({ length: len }, (_, i) => (i / Math.max(1, len) * 0.55 + Math.random() * 0.45) * duration ); const tick = (now) => { const t = now - startRef.current; let next = ''; let allDone = true; for (let i = 0; i < target.length; i++) { const reveal = revealsRef.current[i]; const ch = target[i]; if (t >= reveal || ch === ' ') { next += ch; } else { allDone = false; // pick a random glyph; refresh ~every frame next += W2_GLYPH_POOL[Math.floor(Math.random() * W2_GLYPH_POOL.length)]; } } setOut(next); if (!allDone) { rafRef.current = setTimeout(() => tick(performance.now()), 16); } else { setOut(target); } }; rafRef.current = setTimeout(() => tick(performance.now()), 16); return () => clearTimeout(rafRef.current); }, [target, duration]); return out; } function ScrambledSpan({ value, style }) { const out = useScramble(value); return ( {out} ); } function ScrambledPatchedLine({ copy }) { const pairs = copy.heroPatchedPairs; const tmpl = copy.heroPatchedTemplate; const label = copy.heroPatchedLabel; const [i, setI] = React.useState(0); const measureRef = React.useRef(null); const [shift, setShift] = React.useState(0); const [isNarrow, setIsNarrow] = React.useState( typeof window !== 'undefined' ? window.innerWidth <= 720 : false ); React.useEffect(() => { const onResize = () => setIsNarrow(window.innerWidth <= 720); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); // reset to a fresh random index whenever language (pairs) changes React.useEffect(() => {setI(0);}, [pairs]); React.useEffect(() => { const id = setInterval(() => { setI((prev) => { // never repeat the same pair let next = prev; while (next === prev && pairs.length > 1) { next = Math.floor(Math.random() * pairs.length); } return next; }); }, 5000); return () => clearInterval(id); }, [pairs]); React.useLayoutEffect(() => { if (!measureRef.current) return; if (isNarrow) { setShift(0); return; } const ro = new ResizeObserver(() => { const w = measureRef.current.offsetWidth; setShift(-w / 2); }); ro.observe(measureRef.current); const w = measureRef.current.offsetWidth; setShift(-w / 2); return () => ro.disconnect(); }, [i, tmpl, isNarrow]); const [v1, v2] = pairs[i % pairs.length]; // Split the template at __A__ and __B__ so we render scrambled spans inline // in the right position regardless of language word order. // tmpl examples: // "Remembering to __A__ when __B__." // "__B__ときに __A__ ことを覚えていること。" // We tokenize into a list of { kind: 'text'|'A'|'B', val? }. const tokens = React.useMemo(() => { const out = []; const re = /__A__|__B__/g; let last = 0,m; while ((m = re.exec(tmpl)) !== null) { if (m.index > last) out.push({ kind: 'text', val: tmpl.slice(last, m.index) }); out.push({ kind: m[0] === '__A__' ? 'A' : 'B' }); last = m.index + m[0].length; } if (last < tmpl.length) out.push({ kind: 'text', val: tmpl.slice(last) }); return out; }, [tmpl]); return (
{label} {tokens.map((t, ti) => { if (t.kind === 'text') { // On narrow screens, encourage a break before " when " so the line // doesn't overflow horizontally. if (isNarrow) { const parts = t.val.split(/(\s+when\s+)/); return ( {parts.map((p, pi) => { if (/^\s+when\s+$/.test(p)) { return {' '}when{' '}; } return {p}; })} ); } return {t.val}; } const val = t.kind === 'A' ? v1 : v2; return ( ); })}
); } function RotatingPatchedLine() { const [i, setI] = React.useState(0); const measureRef = React.useRef(null); const [shift, setShift] = React.useState(0); React.useEffect(() => { const id = setInterval(() => { setI((p) => (p + 1) % W2_PHRASES.length); }, 3000); return () => clearInterval(id); }, []); // Center the whole line by measuring its width and shifting half-of-difference. React.useLayoutEffect(() => { if (!measureRef.current) return; const ro = new ResizeObserver(() => { const w = measureRef.current.offsetWidth; setShift(-w / 2); }); ro.observe(measureRef.current); const w = measureRef.current.offsetWidth; setShift(-w / 2); return () => ro.disconnect(); }, [i]); return (
Patched: “{W2_PHRASES[i]}”
); } function RotatingPhrase() { const [i, setI] = React.useState(0); React.useEffect(() => { const id = setInterval(() => { setI((p) => (p + 1) % W2_PHRASES.length); }, 3000); return () => clearInterval(id); }, []); return ( “{W2_PHRASES[i]}” ); } function AppStoreBtn({ store, copy, primary }) { // Coming-soon state — not yet released. Render as a non-interactive div. return (
{/* Apple logo */}
{copy ? copy.comingSoonTo : 'Coming soon to'}
{store === 'ios' ? copy ? copy.downloadIos : 'App Store' : copy ? copy.downloadMac : 'Mac App Store'}
); } function SectionHeader({ marker, title, sub }) { return (
{marker}

{title}

{sub ?

{sub}

: null}
); } // ── Use Cases (instant relatability) ────────────────────────── function UseCasesSection({ copy }) { return (
{copy.useCases.map((item, i) =>
{item}
)}
); } // ── Hidden Mode (terminal easter egg via shake) ────────────── function HiddenModeSection({ copy }) { return (
{/* visual hint — phone with shake motion arrows */}
); } // ── Dev Note (personal letter) ─────────────────────────────── function DevNoteSection({ copy }) { return (

{copy.devNoteTitle}

daisandenki
{copy.devNote.map((p, i) =>

{p}

)}
); } // ── Reference (compact command docs) ───────────────────────── function ReferenceSection({ copy }) { const [active, setActive] = React.useState(0); const cmds = copy.refCmds; // Below this width we keep the tab+single-phone UX; above it we lay out all // three phones in a row so the section doesn't feel sparse on wide screens. const [isNarrow, setIsNarrow] = React.useState( typeof window !== 'undefined' ? window.innerWidth <= 900 : false ); React.useEffect(() => { const onResize = () => setIsNarrow(window.innerWidth <= 900); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); return (
{isNarrow ?
{/* prev / next arrows beside the phone */} {cmds.map((c, i) =>
)}
: // Desktop: all three side by side, no tabs needed.
{cmds.map((c, i) => )}
} {/* CTA below the showcase */}
); } function CommandBlock({ c, termPrefix, compact }) { // Map the legacy `screen` value to the screenshot name used in /assets/img/screens. const screenName = c.screen === 'home' ? 'list' : c.screen === 'new' ? 'add' : 'config'; return (
{/* phone frame with the matching screenshot */} {/* copy */}
{c.summary}

{c.detail}

); } // Real-device screenshot pair, alternating to mimic the cursor blink. // Renders directly into the phone-frame's viewport at full image height, // anchored to the top so the relevant top of the screen stays visible when the // frame is cropped. function ScreenshotPair({ name }) { const [frame, setFrame] = React.useState(0); React.useEffect(() => { const id = setInterval(() => setFrame((f) => 1 - f), 530); return () => clearInterval(id); }, []); return ( ); } function PhoneFrame({ label, children }) { // If children is a screenshot, skip the scale-down canvas; img fills the // frame directly anchored to the top. Otherwise use the iPhone-resolution // canvas (390×844 scaled) that the original JSX screen components expect. const isScreenshot = children?.type === ScreenshotPair; // Crop the bottom: phone is only ~2/3 visible. Bottom edge fades. const PHONE_W = 260; const VISIBLE_H = 380; const fadeMask = 'linear-gradient(180deg, #000 0, #000 80%, transparent 100%)'; return (
{/* inner screen — black, holds the image */}
{!isScreenshot &&
} {isScreenshot ? children :
{children}
}
{label ?
{label}
: null}
); } // ── Mobile (3 phone screens) ────────────────────────────── function MobileSection({ copy }) { // Track viewport — only true mobile width gets the carousel treatment. const [isNarrow, setIsNarrow] = React.useState( typeof window !== 'undefined' ? window.innerWidth <= 720 : false ); React.useEffect(() => { const onResize = () => setIsNarrow(window.innerWidth <= 720); window.addEventListener('resize', onResize); return () => window.removeEventListener('resize', onResize); }, []); const screens = [ , , , ]; const [idx, setIdx] = React.useState(0); // Swipe state — finger drag offset in px, null when not dragging. const PHONE_W = 260; const SWIPE_THRESHOLD = 50; const startX = React.useRef(null); const [drag, setDrag] = React.useState(0); const [dragging, setDragging] = React.useState(false); const onTouchStart = (e) => { startX.current = e.touches[0].clientX; setDragging(true); setDrag(0); }; const onTouchMove = (e) => { if (startX.current == null) return; setDrag(e.touches[0].clientX - startX.current); }; const onTouchEnd = () => { if (Math.abs(drag) > SWIPE_THRESHOLD) { const n = screens.length; if (drag < 0) setIdx((idx + 1) % n); else if (drag > 0) setIdx((idx - 1 + n) % n); } startX.current = null; setDragging(false); setDrag(0); }; return (
{isNarrow ? ( // Single phone, swipeable carousel
{screens.map((s, i) => { // Wrap-around: place each screen at its shortest signed offset // from the active one so the next/prev neighbour visually // appears even at the ends of the array. const n = screens.length; let offset = i - idx; if (offset > n / 2) offset -= n; else if (offset < -n / 2) offset += n; return (
{s}
); })}
{/* dots */}
{screens.map((_, i) => (
) : ( // Side-by-side (desktop)
)}
); } // ── Download ──────────────────────────────────────────────── function DownloadSection({ copy }) { return (
{/* radial halo behind the section */}
); } // ── Footer ──────────────────────────────────────────────────── function Footer({ copy }) { const linkStyle = { color: 'inherit', textDecoration: 'none', borderBottom: `1px solid ${_HAIR}`, paddingBottom: 1, transition: 'color 0.15s, border-color 0.15s' }; // copy.footerCopyright looks like "while² · 2026 · daisandenki" — turn the // trailing "daisandenki" segment into an X link. const parts = copy.footerCopyright.split('daisandenki'); return ( ); } // ── Mac (also-on-mac) ────────────────────────────────────────── function MacSection({ copy }) { return (
); } // ── AI Says (centered slideshow) ───────────────────────────── function AISaysSection({ copy }) { const cards = copy.aiSays; const N = cards.length; const [idx, setIdx] = React.useState(0); const GAP = 24; // Track viewport so we can keep the card narrow enough on phones for the // adjacent cards to peek in from both sides. const [cardW, setCardW] = React.useState(360); React.useEffect(() => { const update = () => setCardW(Math.min(360, Math.floor(window.innerWidth * 0.68))); update(); window.addEventListener('resize', update); return () => window.removeEventListener('resize', update); }, []); const STEP = cardW + GAP; // Auto-advance: every 6s, move to the next card. React.useEffect(() => { const id = setInterval(() => setIdx((i) => (i + 1) % N), 6000); return () => clearInterval(id); }, [N]); return (
{cards.map((r, i) => { // Shortest-path wrap so the active card stays in view and the // transition slides seamlessly when wrapping past the ends. let offset = i - idx; if (offset > N / 2) offset -= N; else if (offset < -N / 2) offset += N; const isActive = offset === 0; const dist = Math.abs(offset); return (
); })}
); } function AIReviewCard({ r }) { return (

“{r.review.split(/\*\*([^*]+)\*\*/).map((part, i) => i % 2 === 1 ? {part} : {part} )}”

{r.model}
{r.vendor}
); } const W2_LANGS = [ { code: 'en', label: 'EN', name: 'English' }, { code: 'ja', label: 'JA', name: '日本語' }, { code: 'zh-Hans', label: '中', name: '简体中文' }, { code: 'zh-Hant', label: '繁', name: '繁體中文' }, { code: 'ko', label: 'KO', name: '한국어' }, { code: 'de', label: 'DE', name: 'Deutsch' }, { code: 'fr', label: 'FR', name: 'Français' }, { code: 'es', label: 'ES', name: 'Español' }, { code: 'pt-BR', label: 'PT', name: 'Português' }, { code: 'it', label: 'IT', name: 'Italiano' }, { code: 'tr', label: 'TR', name: 'Türkçe' }, ]; function LangSwitch({ lang, setLang }) { const [open, setOpen] = React.useState(false); const wrapRef = React.useRef(null); // Close dropdown on outside click. React.useEffect(() => { if (!open) return; const onDoc = (e) => { if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); }, [open]); const current = W2_LANGS.find((l) => l.code === lang) || W2_LANGS[0]; return (
{open && (
{W2_LANGS.map((l) => )}
)}
); } Object.assign(window, { TopBar, Hero, UseCasesSection, HiddenModeSection, DevNoteSection, ReferenceSection, AISaysSection, MobileSection, MacSection, DownloadSection, Footer, LangSwitch });