diff --git a/site/assets/home.css b/site/assets/home.css index 4d76f0ce..bac64b66 100644 --- a/site/assets/home.css +++ b/site/assets/home.css @@ -1,10 +1,11 @@ /* ========================================================================== PocketJS — "Build modern apps for impossible devices" landing page Direction: Docs navy with brushed metal accents. All classes are .lp-* scoped. - The hero plays the baked demo wall (site/bake-demo-wall.ts) as a full-bleed - background and mounts the demand-rendered Pocket Stage below its CTA. The - page then walks the platform story: proof strip → target selector → - .pocket → device taxonomy → showcase → closing CTA. + The hero is a machine collage: eight devices, every screen a real capture + playing at once, cropped live out of the baked demo wall + (site/bake-demo-wall.ts). The page then walks the story: target selector → + use-case cards (/for/ pages) → showcase → closing CTA. home.css also + styles the /for/ pages. ========================================================================== */ @font-face { @@ -261,124 +262,54 @@ a.lp-btn--primary:hover { .lp-nav__link { padding: 0.5rem 0.32rem; font-size: 12px; } } -/* ---- Hero ----------------------------------------------------------------- */ +/* ---- Hero: the machine collage --------------------------------------------- + Eight devices on a fixed 1440x820 canvas, cover-scaled by home.js to fill + the first viewport; the copy overlays it unscaled, and narrow screens crop + the collage's sides instead of stacking devices. Every screen is a real + capture cropped live out of the baked demo wall's 4x4 tile grid; the Figma + screen is a still of the 8% zoomed-out spread. */ .lp-hero { position: relative; + height: max(620px, 100vh); + height: max(620px, 100svh); overflow: hidden; display: flex; align-items: center; - min-height: clamp(560px, 88vh, 920px); - padding: clamp(4.5rem, 11vw, 8rem) 0 clamp(4rem, 9vw, 7rem); -} -/* The demo wall: 16 real recordings baked into one muted loop. The scrim digs - a legibility well under the copy and dissolves the wall into the page - background at the bottom edge. */ -.lp-hero__wall { - position: absolute; - inset: 0; - pointer-events: none; -} -.lp-hero__wall-video { - position: absolute; - inset: 0; - width: 100%; - height: 100%; - object-fit: cover; - filter: saturate(0.72) contrast(1.03) brightness(0.8); -} -.lp-hero__wall-scrim { - position: absolute; - inset: 0; background: - radial-gradient(88% 76% at 50% 40%, rgba(5, 7, 13, 0.9), rgba(5, 7, 13, 0.66) 52%, rgba(5, 7, 13, 0.32) 76%, rgba(5, 7, 13, 0.14) 100%), - linear-gradient(180deg, rgba(5, 7, 13, 0.92) 0%, rgba(5, 7, 13, 0.4) 24%, rgba(5, 7, 13, 0.32) 60%, var(--bg) 97%); + radial-gradient(50% 60% at 50% 45%, rgba(96, 165, 250, 0.07), transparent 70%), + radial-gradient(40% 45% at 12% 10%, rgba(157, 229, 199, 0.05), transparent 70%), + linear-gradient(180deg, #070a11, #05070d 55%, #070a11); } -.lp-hero__motion-credit { +.lp-hero__canvas { position: absolute; - right: clamp(1rem, 2vw, 2rem); - bottom: 1rem; - z-index: 2; - padding: 0.28rem 0.5rem; - border: 1px solid rgba(148, 163, 184, 0.22); - border-radius: 0.4rem; - background: rgba(5, 7, 13, 0.68); - color: rgba(226, 234, 246, 0.76); - font-family: var(--mono); - font-size: 10px; - line-height: 1.25; - text-decoration: none; - backdrop-filter: blur(4px); -} -.lp-hero__motion-credit:hover { color: #fff; border-color: rgba(148, 163, 184, 0.42); } -@media (prefers-reduced-motion: reduce) { - /* home.js also pauses the video; keeping the poster frame visible is fine. */ - .lp-hero__wall-video { display: none; } -} -@media (max-width: 640px) { - .lp-hero__motion-credit { - right: 50%; - transform: translateX(50%); - width: max-content; - max-width: calc(100% - 2rem); - text-align: center; - } + left: 50%; + top: 50%; + width: 1440px; + height: 820px; + transform: translate(-50%, -50%); } .lp-hero__in { position: relative; - z-index: 1; + z-index: 6; display: flex; flex-direction: column; align-items: center; text-align: center; } -.lp-eyebrow { - display: inline-flex; - align-items: center; - gap: 0.55rem; - margin-bottom: 0.75rem; - font-size: 13px; - font-weight: 500; - color: var(--ink-2); - text-decoration: none; - padding: 0.38rem 0.9rem 0.38rem 0.45rem; - border-radius: 999px; - border: 1px solid var(--line-2); - background: - linear-gradient(180deg, rgba(20, 29, 49, 0.84), rgba(11, 15, 26, 0.74)), - rgba(15, 22, 38, 0.76); - backdrop-filter: blur(6px); - transition: border-color 0.25s ease, color 0.25s ease; -} -.lp-eyebrow:hover { border-color: rgba(96, 165, 250, 0.5); color: var(--ink); } -.lp-eyebrow__tag { - font-family: var(--mono); - font-size: 11px; - font-weight: 700; - letter-spacing: 0.03em; - color: #04060b; - padding: 0.22rem 0.55rem; - border-radius: 999px; - background: var(--metal-lite); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.56), inset 0 -6px 10px rgba(20, 37, 64, 0.13); -} -.lp-eyebrow__arw { opacity: 0.55; } .lp-h1 { - margin-top: 1.5rem; + max-width: 700px; font-weight: 800; - letter-spacing: -0.035em; - line-height: 1.02; - font-size: clamp(2.45rem, 7.4vw, 5.3rem); + letter-spacing: -0.03em; + line-height: 1.06; + font-size: clamp(2rem, 4.6vw, 3.6rem); color: var(--ink); -} -.lp-h1 .lp-grad { - display: block; - filter: drop-shadow(0 8px 30px rgba(96, 165, 250, 0.14)); + text-shadow: 0 12px 40px rgba(3, 9, 20, 0.8); } .lp-sub { - margin: 1.6rem auto 0; - max-width: 680px; - font-size: clamp(1.05rem, 2.3vw, 1.3rem); - line-height: 1.55; + margin: 1rem auto 0; + max-width: 580px; + font-size: clamp(0.95rem, 1.7vw, 1.06rem); + line-height: 1.6; color: var(--ink-2); } .lp-hero__cta { @@ -386,152 +317,73 @@ a.lp-btn--primary:hover { flex-wrap: wrap; justify-content: center; gap: 0.85rem; - margin-top: 2.1rem; + margin-top: 1.5rem; } -/* ---- Live Pocket Stage ---------------------------------------------------- */ -.lp-stage { - width: min(100%, 980px); - margin: clamp(1.5rem, 3.2vw, 2.35rem) auto 0; -} -.lp-stage__viewport { - position: relative; - width: 100%; - height: clamp(285px, 39vw, 455px); - overflow: hidden; - background: transparent; -} -.lp-stage__canvas { - display: block; - width: 100%; - height: 100%; - outline: none; - cursor: grab; - touch-action: none; -} -.lp-stage__canvas:active { cursor: grabbing; } -.lp-stage__canvas:focus-visible { - box-shadow: inset 0 0 0 2px rgba(157, 229, 199, 0.88); -} -.lp-stage__screen { display: none; } -.lp-stage__status { +/* devices */ +.lp-dev { position: absolute; z-index: 3; transition: transform 0.3s ease; } +.lp-dev:hover { transform: scale(1.045); z-index: 8; } +.lp-dev__cap { position: absolute; left: 50%; - bottom: 1.15rem; - display: inline-flex; - align-items: center; - gap: 0.55rem; - padding: 0.48rem 0.8rem; - border: 1px solid rgba(85, 103, 133, 0.56); - border-radius: 999px; - color: var(--ink-2); - background: rgba(6, 10, 18, 0.78); - font-family: var(--mono); - font-size: 11px; - letter-spacing: 0.025em; + bottom: -1.8rem; transform: translateX(-50%); - backdrop-filter: blur(8px); - transition: opacity 0.25s ease, transform 0.25s ease; -} -.lp-stage__status-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: var(--mint); - box-shadow: 0 0 10px rgba(157, 229, 199, 0.82); - animation: lp-stage-pulse 1.1s ease-in-out infinite alternate; -} -.lp-stage.is-ready .lp-stage__status { - opacity: 0; - transform: translate(-50%, 5px); - pointer-events: none; -} -.lp-stage.has-error .lp-stage__status { - color: #f0b8b8; - border-color: rgba(239, 143, 143, 0.4); -} -.lp-stage.has-error .lp-stage__status-dot { - background: #ef8f8f; - box-shadow: none; - animation: none; -} -.lp-stage__caption { - display: flex; - flex-wrap: wrap; - justify-content: space-between; - gap: 0.45rem 1.25rem; - padding: 0.75rem 0.35rem 0; - color: rgba(169, 181, 201, 0.72); - font-size: 11px; - line-height: 1.5; - text-align: left; -} -.lp-stage__caption strong { color: rgba(226, 234, 246, 0.88); font-weight: 600; } -.lp-stage__caption a { color: rgba(190, 206, 228, 0.86); text-underline-offset: 2px; } -.lp-stage__caption a:hover { color: #fff; } -.lp-stage__credit { margin-left: auto; } -@keyframes lp-stage-pulse { - from { opacity: 0.35; transform: scale(0.78); } - to { opacity: 1; transform: scale(1); } -} -@media (max-width: 640px) { - .lp-stage__viewport { height: clamp(245px, 65vw, 330px); } - .lp-stage__credit { margin-left: 0; } -} - -/* ---- Proof strip ---------------------------------------------------------- - Five seconds of "this actually happened": six clickable field results in a - band that reads like a wire ticker, each linking to its story. */ -.lp-proofstrip { - border-top: 1px solid var(--line); - border-bottom: 1px solid var(--line); - background: linear-gradient(180deg, rgba(15, 22, 38, 0.45), rgba(9, 12, 20, 0.3)); -} -.lp-proofstrip__row { - list-style: none; - margin: 0; - padding: 0; - display: grid; - grid-template-columns: repeat(6, 1fr); -} -.lp-proofstrip__row li { min-width: 0; } -.lp-proofstrip__row li + li { border-left: 1px solid var(--line); } -.lp-fact { - display: flex; - flex-direction: column; - gap: 0.3rem; - height: 100%; - padding: 1.15rem 1.1rem 1.2rem; - text-decoration: none; - transition: background 0.22s ease; -} -.lp-fact:hover { background: rgba(20, 29, 49, 0.55); } -.lp-fact b { - font-size: 0.94rem; - font-weight: 700; - letter-spacing: -0.01em; - line-height: 1.3; - color: var(--ink); -} -.lp-fact span { - font-family: var(--mono); - font-size: 10.5px; - line-height: 1.45; - letter-spacing: 0.01em; + white-space: nowrap; + font: 500 10.5px/1 var(--mono); color: var(--muted); + background: rgba(5, 7, 13, 0.7); + border: 1px solid rgba(43, 58, 85, 0.5); + border-radius: 999px; + padding: 0.32rem 0.55rem; } -.lp-fact:hover span { color: var(--ink-2); } -@media (max-width: 1080px) { - .lp-proofstrip__row { grid-template-columns: repeat(3, 1fr); } - .lp-proofstrip__row li:nth-child(4) { border-left: 0; } - .lp-proofstrip__row li:nth-child(n + 4) { border-top: 1px solid var(--line); } -} -@media (max-width: 640px) { - .lp-proofstrip__row { grid-template-columns: repeat(2, 1fr); } - .lp-proofstrip__row li { border-left: 0; } - .lp-proofstrip__row li:nth-child(even) { border-left: 1px solid var(--line); } - .lp-proofstrip__row li:nth-child(n + 3) { border-top: 1px solid var(--line); } -} +.lp-dev__cap b { color: var(--ink-2); font-weight: 600; } +.lp-dev:hover .lp-dev__cap { color: var(--ink-2); border-color: rgba(96, 165, 250, 0.4); } + +/* live captures, cropped out of the 4x4 wall (1920x1088, 480x272 tiles) */ +.lp-shot { position: relative; overflow: hidden; aspect-ratio: 480/272; background: #000; } +.lp-shot > video { position: absolute; width: 400%; height: 400%; left: calc(var(--c) * -100%); top: calc(var(--r) * -100%); } +.lp-shot > img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; } +.lp-shot--eink > video { filter: grayscale(1) contrast(1.15) brightness(1.5); } + +/* bezels: abstract but recognizable */ +.lp-psp { width: 350px; padding: 16px 52px; background: linear-gradient(180deg, #191d24, #0d1015); border: 1px solid #2a303b; border-radius: 26px; box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.9); } +.lp-psp--silver { background: linear-gradient(180deg, #3a4048, #23272e); } +.lp-psp::before { content: ""; position: absolute; left: 17px; top: 40%; width: 20px; height: 20px; border-radius: 50%; background: #23272f; box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.8), 0 1px 0 rgba(255, 255, 255, 0.06); } +.lp-psp::after { content: ""; position: absolute; right: 19px; top: 42%; width: 16px; height: 16px; border-radius: 50%; background: #23272f; box-shadow: -22px 12px 0 -4px #23272f, -22px -12px 0 -4px #23272f, -44px 0 0 -4px #23272f; } +.lp-vita { width: 370px; padding: 15px 56px; background: linear-gradient(180deg, #14171c, #0a0c10); border: 1px solid #262c36; border-radius: 40px; box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.9); } +.lp-vita::before { content: ""; position: absolute; left: 20px; top: 38%; width: 18px; height: 18px; border-radius: 50%; background: #202631; box-shadow: 0 26px 0 -3px #202631; } +.lp-vita::after { content: ""; position: absolute; right: 20px; top: 38%; width: 18px; height: 18px; border-radius: 50%; background: #202631; box-shadow: 0 26px 0 -3px #202631; } +.lp-gb { width: 158px; padding: 12px 12px 20px; background: linear-gradient(180deg, #c9c5bd, #b4b0a6); border-radius: 10px 10px 34px 10px; box-shadow: 0 24px 50px -24px rgba(0, 0, 0, 0.9); } +.lp-gb__screen { background: #9bbc0f; border: 8px solid #4a4f57; border-radius: 6px; aspect-ratio: 160/144; padding: 8px 7px; font: 700 9px/1.7 var(--mono); color: #0f380f; } +.lp-gb__t { text-align: center; border-bottom: 1.5px solid #306230; margin-bottom: 4px; } +.lp-gb__done { color: #306230; text-decoration: line-through; } +.lp-gb__btns { display: flex; justify-content: flex-end; gap: 7px; margin-top: 10px; } +.lp-gb__btns i { width: 15px; height: 15px; border-radius: 50%; background: #8d2255; box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.4); } +.lp-gb__dpad { position: absolute; left: 18px; bottom: 22px; width: 30px; height: 30px; background: #3a3d43; clip-path: polygon(33% 0, 66% 0, 66% 33%, 100% 33%, 100% 66%, 66% 66%, 66% 100%, 33% 100%, 33% 66%, 0 66%, 0 33%, 33% 33%); } +.lp-e7 { width: 300px; padding: 14px 20px; background: linear-gradient(180deg, #1a1e25, #0c0e13); border: 1px solid #2a303b; border-radius: 18px; box-shadow: 0 26px 55px -26px rgba(0, 0, 0, 0.9); } +.lp-e7::before { content: ""; position: absolute; top: 6px; left: 50%; transform: translateX(-50%); width: 40px; height: 3px; border-radius: 3px; background: #262c36; } +.lp-pb { width: 180px; padding: 12px 12px 34px; background: linear-gradient(180deg, #d9dbdd, #c2c5c9); border-radius: 14px; box-shadow: 0 24px 50px -24px rgba(0, 0, 0, 0.9); } +.lp-pb .lp-shot { aspect-ratio: 758/900; background: #eceded; } +.lp-pb .lp-shot > video { width: 400%; height: auto; aspect-ratio: 1920/1088; top: -117%; } +.lp-pb::after { content: ""; position: absolute; bottom: 12px; left: 50%; transform: translateX(-50%); width: 44px; height: 5px; border-radius: 5px; background: #aeb2b7; } +.lp-macw { width: 270px; border-radius: 16px; overflow: hidden; box-shadow: 0 34px 70px -28px rgba(0, 0, 0, 0.95), 0 0 0 1px rgba(255, 255, 255, 0.07); } +.lp-p4 { width: 270px; padding: 12px 14px 16px; background: linear-gradient(180deg, #0e2318, #081a11); border: 1px solid #1d4030; border-radius: 8px; box-shadow: 0 24px 50px -24px rgba(0, 0, 0, 0.9); } +.lp-p4::after { content: ""; position: absolute; inset: auto 10px 5px; height: 4px; background: repeating-linear-gradient(90deg, #c9a648 0 5px, transparent 5px 10px); opacity: 0.7; } +.lp-p4__term { aspect-ratio: 480/272; background: #071018; border: 1px solid #16394f; border-radius: 4px; padding: 8px 9px; font: 500 9.5px/1.65 var(--mono); color: #9edcff; overflow: hidden; } +.lp-t-p { color: var(--mint); } +.lp-t-d { color: #4e7a95; } + +/* collage placement, in % of the fixed canvas */ +.lp-dev--gb { left: 8%; top: 10%; transform: rotate(-6deg); } +.lp-dev--figma { left: 4%; top: 42%; transform: rotate(-3deg); } +.lp-dev--pi { left: 38%; bottom: 4.5%; transform: rotate(1deg); } +.lp-dev--yt { left: 30.5%; top: 4.5%; transform: rotate(2deg); width: 270px; padding: 12px 40px; } +.lp-dev--mac { right: 25.5%; top: 5%; transform: rotate(-2deg); } +.lp-dev--e7 { right: 4%; top: 13%; transform: rotate(4deg); } +.lp-dev--voxel { right: 3%; top: 45%; transform: rotate(2.5deg); } +.lp-dev--pb { right: 15%; bottom: 5%; transform: rotate(-4deg); } +.lp-dev:hover { transform: scale(1.05) rotate(0deg); } +@media (prefers-reduced-motion: reduce) { .lp-dev { transition: none; } } /* ---- Section scaffolding -------------------------------------------------- */ .lp-section { padding: clamp(4.5rem, 10vw, 8rem) 0; } @@ -720,196 +572,82 @@ a.lp-btn--primary:hover { .lp-proof a { color: var(--ink-2); text-underline-offset: 3px; } .lp-proof a:hover { color: var(--ink); } -/* ---- .pocket package ------------------------------------------------------- - Ship the application, not the port. */ -.lp-pkg { border-top: 1px solid var(--line); } -.lp-pkg__split { +/* ---- Use-case cards --------------------------------------------------------- + Four kinds of software; each card is a real capture with one short claim, + and each opens its own /for/ page. */ +.lp-uc-grid { display: grid; - gap: clamp(2rem, 5vw, 3.5rem); - align-items: center; -} -@media (min-width: 940px) { .lp-pkg__split { grid-template-columns: 0.95fr 1.05fr; } } -.lp-pkg__copy .lp-kicker { display: block; margin-bottom: 0.9rem; } -.lp-pkg__copy .lp-h2 { font-size: clamp(1.75rem, 4vw, 2.6rem); } -.lp-pkg__copy p { - margin-top: 1.1rem; - font-size: 1.02rem; - line-height: 1.65; - color: var(--ink-2); + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1rem; + margin-top: clamp(2rem, 4.5vw, 3rem); } -.lp-pkg__copy em { font-style: normal; font-weight: 700; color: var(--ink); } -.lp-pkg__verbs { +.lp-uc { + position: relative; display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin-top: 1.6rem; -} -.lp-pkg__verbs .lp-mono { font-size: 12px; padding: 0.4em 0.7em; } -.lp-pkg__tree { + align-items: flex-end; + min-height: 300px; border-radius: 16px; border: 1px solid var(--line-2); - background: - linear-gradient(180deg, rgba(20, 29, 49, 0.62), rgba(9, 12, 20, 0.94)), - #090c14; overflow: hidden; - box-shadow: 0 46px 90px -46px rgba(3, 9, 20, 0.94); -} -.lp-pkg__tree-bar { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 0.78rem 1.05rem; - border-bottom: 1px solid var(--line); - background: rgba(20, 29, 49, 0.72); + text-decoration: none; + box-shadow: 0 30px 60px -34px rgba(3, 9, 20, 0.9); + transition: transform 0.25s ease, border-color 0.25s ease; } -.lp-pkg__pre { - padding: 1.3rem 1.4rem; - overflow-x: auto; - font-family: var(--mono); - font-size: 13px; - line-height: 1.9; - color: #8b99ad; -} -.lp-pkg__pre code { font: inherit; white-space: pre; } -.lp-pkg__pre b { color: var(--ink); font-weight: 700; } -.lp-pkg__pre i { font-style: normal; color: var(--sky); } -.lp-pkg__cap { - padding: 0 1.4rem 1.2rem; - font-size: 0.85rem; - line-height: 1.55; - color: var(--muted); +.lp-uc:hover { transform: translateY(-4px); border-color: rgba(96, 165, 250, 0.45); } +.lp-uc__bg { + position: absolute; + inset: 0; + background-size: cover; + background-position: center; + filter: saturate(0.9); + transform: scale(1.01); + transition: transform 0.3s ease; +} +.lp-uc:hover .lp-uc__bg { transform: scale(1.05); } +.lp-uc__bg--term { + background: #071018; + padding: 1rem 1.15rem; + font: 500 12px/1.9 var(--mono); + color: #9edcff; +} +.lp-uc__body { + position: relative; + z-index: 2; + width: 100%; + padding: 5.5rem 1.15rem 1.15rem; + background: linear-gradient(180deg, rgba(5, 7, 13, 0) 0%, rgba(5, 7, 13, 0.55) 36%, rgba(5, 7, 13, 0.93) 72%); } - -/* system-software grid under the package split */ -.lp-sys { margin-top: clamp(3.4rem, 7vw, 5rem); } -.lp-sys__head { max-width: 640px; } -.lp-sys__head h3 { - font-size: clamp(1.3rem, 2.6vw, 1.7rem); - font-weight: 800; - letter-spacing: -0.02em; +.lp-uc__body h3 { color: var(--ink); font-size: 1.05rem; font-weight: 700; } +.lp-uc__body p { margin-top: 0.4rem; color: var(--ink-2); font-size: 0.86rem; line-height: 1.55; } +.lp-uc__more { + display: inline-block; + margin-top: 0.65rem; + font: 600 12px/1 var(--font); color: var(--ink); + border-bottom: 1px solid var(--line-2); + padding-bottom: 2px; } -.lp-sys__head p { margin-top: 0.55rem; color: var(--ink-2); font-size: 0.98rem; line-height: 1.6; } -.lp-sys__grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 1rem; - margin-top: 1.6rem; -} +.lp-uc:hover .lp-uc__more { border-color: var(--muted); } -/* ---- Device taxonomy ------------------------------------------------------- - "Impossible" classified by constraint, not by nostalgia. */ -.lp-imposs { - border-top: 1px solid var(--line); - background: - radial-gradient(40% 55% at 100% 0%, rgba(96, 165, 250, 0.05), transparent 70%), - radial-gradient(34% 46% at 0% 20%, rgba(34, 211, 238, 0.035), transparent 72%); -} -.lp-imposs__grid { - display: grid; - grid-template-columns: repeat(6, 1fr); - gap: 1rem; - margin-top: clamp(2.4rem, 5vw, 3.6rem); -} -.lp-imp { - grid-column: span 2; - display: flex; - flex-direction: column; - padding: 1.5rem 1.5rem 1.6rem; - border-radius: 18px; - border: 1px solid transparent; - background: - linear-gradient(180deg, rgba(15, 22, 38, 0.92), rgba(9, 12, 20, 0.88)) padding-box, - linear-gradient(165deg, rgba(96, 165, 250, 0.3), rgba(43, 58, 85, 0.5) 42%, rgba(34, 211, 238, 0.14)) border-box; - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04), 0 22px 50px -32px rgba(0, 0, 0, 0.9); - transition: transform 0.28s ease, box-shadow 0.28s ease; -} -.lp-imp:hover { - transform: translateY(-3px); - box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.06), 0 30px 60px -30px rgba(0, 0, 0, 0.95); -} -.lp-imp:nth-child(4), .lp-imp:nth-child(5), .lp-imp:nth-child(6) { grid-column: span 3; } -.lp-imp h3 { - font-size: 1.12rem; +/* ---- /for/ pages ------------------------------------------------------------- + Landing-styled use-case pages: shared nav and footer, no hero, straight + into how PocketJS serves the scenario. */ +.lp-formain { padding: clamp(3.2rem, 7vw, 5.2rem) 0 clamp(3rem, 6vw, 4.6rem); } +.lp-for__head { max-width: 780px; } +.lp-for__head h1 { + margin-top: 0.9rem; + font-size: clamp(1.9rem, 4.4vw, 3rem); font-weight: 800; - letter-spacing: -0.015em; + letter-spacing: -0.03em; + line-height: 1.08; color: var(--ink); } -.lp-imp > p { - margin-top: 0.5rem; - font-size: 0.92rem; - line-height: 1.6; - color: var(--muted); -} -.lp-imp.lp-imp--legend { - grid-column: 1 / -1; - flex-direction: row; - align-items: center; - flex-wrap: wrap; - justify-content: space-between; - gap: 0.9rem 1.6rem; - background: - linear-gradient(180deg, rgba(9, 12, 20, 0.6), rgba(9, 12, 20, 0.4)) padding-box, - linear-gradient(165deg, rgba(43, 58, 85, 0.5), rgba(43, 58, 85, 0.3)) border-box; - box-shadow: none; -} -.lp-imp--legend:hover { transform: none; } -.lp-imp__note { - margin-top: 0.95rem; - font-size: 0.82rem; - line-height: 1.55; - color: var(--muted); -} -.lp-imp__note a { color: var(--ink-2); text-underline-offset: 3px; } -.lp-imp__note a:hover { color: var(--ink); } -.lp-imp--legend p { margin-top: 0; font-size: 0.88rem; color: var(--muted); line-height: 1.6; flex: 1 1 320px; } -.lp-imp--legend a { color: var(--ink-2); text-underline-offset: 3px; } -.lp-imp--legend a:hover { color: var(--ink); } -.lp-chips { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; - margin: 1.05rem 0 0; - padding: 0; - list-style: none; -} -.lp-chips li { - display: inline-flex; - align-items: center; - gap: 0.5rem; - font-size: 0.85rem; - font-weight: 600; - color: var(--ink-2); - padding: 0.48rem 0.8rem; - border-radius: 999px; - border: 1px solid var(--line-2); - background: rgba(5, 7, 13, 0.55); -} -.lp-chips b, .vb-legend b { - width: 7px; - height: 7px; - border-radius: 999px; - background: var(--steel-4); -} -.lp-chips li.is-live b, .vb-legend b.is-live { background: var(--mint); box-shadow: 0 0 8px rgba(157, 229, 199, 0.55); } -.lp-chips li.is-wip b, .vb-legend b.is-wip { background: var(--sky); box-shadow: 0 0 8px rgba(96, 165, 250, 0.5); } -.lp-chips li.is-live { border-color: rgba(157, 229, 199, 0.32); } -.vb-legend { - display: flex; - flex-wrap: wrap; - gap: 1.2rem; - font: 500 11px/1 var(--mono); - color: var(--muted); -} -.vb-legend span { display: inline-flex; align-items: center; gap: 0.45rem; } -@media (max-width: 920px) { - .lp-imposs__grid { grid-template-columns: 1fr 1fr; } - .lp-imp, .lp-imp:nth-child(4), .lp-imp:nth-child(5), .lp-imp:nth-child(6) { grid-column: span 1; } - .lp-imp.lp-imp--legend, .lp-imposs__grid .lp-imp--legend:nth-child(6) { grid-column: 1 / -1; } -} -@media (max-width: 620px) { - .lp-imposs__grid { grid-template-columns: 1fr; } -} +.lp-for__lead { margin-top: 1rem; color: var(--ink-2); font-size: clamp(1rem, 1.9vw, 1.15rem); line-height: 1.6; } +.lp-for__shot { margin-top: clamp(2rem, 4.5vw, 2.8rem); border-radius: 16px; border: 1px solid var(--line-2); overflow: hidden; max-width: 780px; } +.lp-for__shot img { width: 100%; display: block; } +.lp-for__shotcap { max-width: 780px; padding: 0.6rem 0.2rem 0; font: 500 11px/1.6 var(--mono); color: var(--muted); } +.lp-for__links { display: flex; flex-wrap: wrap; gap: 0.85rem; margin-top: clamp(2.2rem, 4.5vw, 3rem); } +.lp-for__term { padding: 1.4rem 1.5rem; background: #071018; font: 500 13px/1.9 var(--mono); color: #9edcff; } /* ---- Showcase — six proof apps -------------------------------------------- */ .lp-showcase { border-top: 1px solid var(--line); } diff --git a/site/assets/home.js b/site/assets/home.js index bc605f62..04a7f430 100644 --- a/site/assets/home.js +++ b/site/assets/home.js @@ -1,7 +1,7 @@ -// site/assets/home.js — homepage behaviors. The background remains a cheap -// baked demo wall; the live Pocket Stage below the CTA is code-split and boots -// only when it approaches the viewport. Tab groups (framework tabs on the code -// card, target chips on the selector) are static HTML — JS only toggles state. +// site/assets/home.js — homepage behaviors. The hero is a machine collage on +// a fixed canvas that cover-scales to the viewport, its screens cropped live +// out of the baked demo wall. Tab groups (framework tabs on the code card, +// target chips on the selector) are static HTML — JS only toggles state. function setupTabs(tabAttr, panelAttr) { const tabs = [...document.querySelectorAll(`[data-${tabAttr}]`)]; @@ -24,8 +24,6 @@ function setupTabs(tabAttr, panelAttr) { } } -// Pause the wall when it can't be seen (scrolled away) or shouldn't move -// (prefers-reduced-motion — the CSS also hides it there). function setupCodeCardName() { const nameEl = document.getElementById("lp-codecard-name"); if (!nameEl) return; @@ -43,14 +41,28 @@ function setupCodeCardName() { } } -function setupDemoWall() { - const video = document.querySelector(".lp-hero__wall-video"); - if (!video) return; +// The hero collage: a fixed 1440x820 canvas cover-scaled to fill the hero, so +// narrow screens crop the collage's sides instead of stacking the devices. +// The screen videos pause off screen and under prefers-reduced-motion. +function setupHeroCollage() { + const hero = document.querySelector(".lp-hero"); + const canvas = document.querySelector("[data-collage]"); + if (!hero || !canvas) return; + const fit = () => { + const s = Math.max(hero.clientWidth / 1440, hero.clientHeight / 820); + canvas.style.transform = `translate(-50%, -50%) scale(${s})`; + }; + addEventListener("resize", fit); + fit(); + + const videos = [...canvas.querySelectorAll("video")]; const reduced = matchMedia("(prefers-reduced-motion: reduce)"); let visible = true; const apply = () => { - if (reduced.matches || !visible) video.pause(); - else video.play().catch(() => {}); + for (const video of videos) { + if (reduced.matches || !visible) video.pause(); + else video.play().catch(() => {}); + } }; reduced.addEventListener?.("change", apply); const io = new IntersectionObserver( @@ -60,39 +72,11 @@ function setupDemoWall() { }, { threshold: 0.05 }, ); - io.observe(video); -} - -function setupPocketStage() { - const root = document.querySelector("[data-pocket-stage]"); - if (!root) return; - let booted = false; - const boot = async () => { - if (booted) return; - booted = true; - try { - const { mountPocketStage } = await import("/assets/pocket-stage-web.js"); - await mountPocketStage(root); - } catch (error) { - root.classList.add("has-error"); - const status = root.querySelector("[data-stage-status]"); - if (status) status.textContent = "Pocket Stage could not be loaded."; - console.error("Pocket Stage module failed", error); - } - }; - const io = new IntersectionObserver( - (entries) => { - if (!entries.some((entry) => entry.isIntersecting)) return; - io.disconnect(); - void boot(); - }, - { rootMargin: "240px 0px", threshold: 0.01 }, - ); - io.observe(root); + io.observe(hero); + apply(); } setupTabs("code-tab", "code-panel"); setupCodeCardName(); setupTabs("tgt-tab", "tgt-panel"); -setupDemoWall(); -setupPocketStage(); +setupHeroCollage(); diff --git a/site/assets/wall/card-im.jpg b/site/assets/wall/card-im.jpg new file mode 100644 index 00000000..8bee7a4c Binary files /dev/null and b/site/assets/wall/card-im.jpg differ diff --git a/site/assets/wall/card-openstrike.jpg b/site/assets/wall/card-openstrike.jpg new file mode 100644 index 00000000..ff6a87a5 Binary files /dev/null and b/site/assets/wall/card-openstrike.jpg differ diff --git a/site/assets/wall/card-voxel.jpg b/site/assets/wall/card-voxel.jpg new file mode 100644 index 00000000..0bc0efd4 Binary files /dev/null and b/site/assets/wall/card-voxel.jpg differ diff --git a/site/assets/wall/figma-8pct.jpg b/site/assets/wall/figma-8pct.jpg new file mode 100644 index 00000000..b47f5c50 Binary files /dev/null and b/site/assets/wall/figma-8pct.jpg differ diff --git a/site/build.ts b/site/build.ts index ca47a2ac..45ae7a4b 100644 --- a/site/build.ts +++ b/site/build.ts @@ -567,6 +567,11 @@ async function main() { if (existsSync(SITE + "assets/blog/")) { for (const f of readdirSync(SITE + "assets/blog/")) copy(SITE + "assets/blog/" + f, "assets/blog/" + f); } + // Wall crops: hero-collage stills + use-case card backgrounds, extracted + // from the baked demo wall's known tile grid. + if (existsSync(SITE + "assets/wall/")) { + for (const f of readdirSync(SITE + "assets/wall/")) copy(SITE + "assets/wall/" + f, "assets/wall/" + f); + } // 5. playground page write("playground/index.html", renderPage({ @@ -585,6 +590,13 @@ async function main() { copy(SITE + "assets/home.css", "assets/home.css"); await bundle("assets/home.js", "assets/home.js"); + // 6b. /for/ use-case pages — landing-styled (home.css chrome, no hero). + // site/for/shell.html carries the shared nav + footer; each fragment + // carries the page's main content. + for (const page of FOR_PAGES) { + write(`for/${page.slug}/index.html`, renderForPage(page)); + } + // 7. docs + blog (setupMarkdown installs the shared marked/shiki renderer) const highlight = await setupMarkdown(); await buildDocs(highlight); @@ -677,6 +689,70 @@ ${body} `; } +// The /for/ pages share the homepage's bespoke chrome (home.css, no shared +// Tailwind shell): site/for/shell.html holds the nav + footer, and +// site/for/.html the main content. Linked from the use-case cards. +const FOR_PAGES = [ + { + slug: "interfaces", + title: "PocketJS for user interfaces", + desc: "Components, signals and Tailwind classes in Solid, Vue Vapor or Octane become one native tree that runs on every machine in the registry.", + }, + { + slug: "games", + title: "PocketJS for games", + desc: "Rust engine cores under JavaScript gameplay: OpenStrike holds a locked 60 fps on a 333 MHz PSP while the HUD stays a Solid app.", + }, + { + slug: "worlds", + title: "PocketJS for 3D worlds", + desc: "Pocket3D cooks a scene once and renders it through wgpu, sceGu, GXM and OpenGL ES, up to VRM humans with spring-bone physics.", + }, + { + slug: "agents", + title: "PocketJS for AI-native apps", + desc: "A virtual clock makes every frame a pure function of an input tape: sessions agents can drive, replay and diff, on a runtime small enough to host the agent itself.", + }, +]; + +function renderForPage(page: { slug: string; title: string; desc: string }): string { + const shell = injectSiteFooterDescription(readFileSync(SITE + "for/shell.html", "utf8")); + const main = readFileSync(SITE + `for/${page.slug}.html`, "utf8"); + if (!shell.includes("{{FOR_MAIN}}")) throw new Error("for/shell.html must contain {{FOR_MAIN}}"); + const body = shell.replace("{{FOR_MAIN}}", main); + const url = `${SITE_URL}/for/${page.slug}/`; + return ` + + + + +${page.title} · PocketJS + + + + + + + + + + + + + + + + + + + + + +${body} + +`; +} + async function compileCss() { const proc = Bun.spawnSync( ["bunx", "@tailwindcss/cli", "-i", SITE + "assets/tailwind.css", "-o", OUT + "assets/site.css", "--minify"], diff --git a/site/for/agents.html b/site/for/agents.html new file mode 100644 index 00000000..5ac0f31e --- /dev/null +++ b/site/for/agents.html @@ -0,0 +1,32 @@ +
+
+
+ PocketJS for AI-native apps +

A runtime agents can drive, replay and diff.

+

A virtual clock makes every frame a pure function of an input tape. Sessions are data: an agent can run one, rewind it, and compare the pixels byte for byte.

+
+
+
+

Sessions are data

+

Input tapes record every session. Time-travel DevTools step to any frame and inspect the native tree, over USB to real hardware. Time-travel debugging →

+
+
+

Tests that cannot flake

+

Goldens compare framebuffers byte for byte, and the same tape produces the same pixels on every run, on every machine. The UI runtime that can’t flake →

+
+
+

Small enough to host the agent

+

Pocket Pi runs the unmodified pi coding agent in QuickJS with no Node underneath: the full harness on a desktop, a 304 KB core profile on an ESP32‑P4 board with a PocketJS touch UI. Just enough Node →

+
+
+
+
pi > fix the failing build
plan  read Makefile, patch CFLAGS
tool  fs.write src/config.h ok
tool  proc.run make ... exit 0
pi > done. 2 files changed_
+
+

Pocket Pi at work: the pi agent planning, editing and rebuilding, with no Node underneath.

+ +
+
diff --git a/site/for/games.html b/site/for/games.html new file mode 100644 index 00000000..e318096f --- /dev/null +++ b/site/for/games.html @@ -0,0 +1,32 @@ +
+
+
+ PocketJS for games +

Native where it must be fast. JavaScript where you iterate.

+

OpenStrike runs the classic maps with bots, lightmaps and recoil at a locked 60 fps on a 333 MHz PSP. The engine parts are Rust; the gameplay, HUD and menus stay JavaScript you can edit and rerun.

+
+
+
+

The split that ships

+

Pocket3D carries BSP worlds, visibility and a painter-sorted pipeline in the core. The HUD is a Solid app on the same native tree as every other PocketJS interface. OpenStrike, shipped →

+
+
+

A whole game in the guest

+

Pocket Voxel keeps world state, battles, scripts and saves in TypeScript inside QuickJS, while per-machine Rust renderers draw the diorama at a locked 30 fps. A creature-RPG from first principles →

+
+
+

Matches replay themselves

+

Input tapes make every run reproducible: the same tape gives the same frames, so a whole match replays frame for frame while you debug it.

+
+
+
+ OpenStrike gameplay: sunlit BSP geometry with the HUD showing 100 HP and 30/90 ammo. +
+

OpenStrike on de_dust2: a locked 60 fps with bots on real PSP hardware.

+ +
+
diff --git a/site/for/interfaces.html b/site/for/interfaces.html new file mode 100644 index 00000000..9812cd14 --- /dev/null +++ b/site/for/interfaces.html @@ -0,0 +1,36 @@ +
+
+
+ PocketJS for user interfaces +

Write the interface once. Render it native everywhere.

+

Components, signals and Tailwind classes in Solid, Vue Vapor or Octane become one native tree that runs on every machine in the registry, from a 2004 PSP to a transparent macOS widget.

+
+
+
+

Three frameworks, one tree

+

Solid signals, Vue refs and Octane hooks drive the same host components. Pick per project, or mix apps on the same deck; the runtime renders one tree either way. How the three coexist →

+
+
+

Layout and style, compiled

+

taffy runs flexbox in the Rust core, so a familiar flex-col gap-4 lands on identical pixels on every target. Class literals compile to compact numeric records; no CSS parser ships to the device.

+
+
+

Input that feels right

+

D-pad focus, real touch with gesture physics, kinetic scroll and tap-to-press, a virtual cursor for pointerless machines, and a system keyboard any app summons with one call.

+
+
+

Motion, baked

+

animate-* classes compile to keyframe timelines the core plays back on the device’s own budget, 3D transforms and arcs included.

+
+
+
+ Pocket Talk, the IM demo, showing a message thread on a PSP-shaped screen. +
+

Pocket Talk, the IM demo: a VirtualList thread, the system keyboard and kinetic scroll on a PSP.

+ +
+
diff --git a/site/for/shell.html b/site/for/shell.html new file mode 100644 index 00000000..9f01e6ee --- /dev/null +++ b/site/for/shell.html @@ -0,0 +1,88 @@ +
+
+ +
+ +{{FOR_MAIN}} + + +
diff --git a/site/for/worlds.html b/site/for/worlds.html new file mode 100644 index 00000000..e7f7e5d9 --- /dev/null +++ b/site/for/worlds.html @@ -0,0 +1,32 @@ +
+
+
+ PocketJS for 3D worlds +

One 3D core, from desktop GPUs to 2004 silicon.

+

Pocket3D cooks a scene once and renders it through wgpu on desktop, sceGu on the PSP, GXM on the Vita and OpenGL ES on a 2011 Nokia. Same bundle, same frames.

+
+
+
+

Cooked, then streamed

+

BSP worlds ship as cooked .p3d paks with visibility baked in. Big documents stream: a real Figma file becomes CLUT8 tile pyramids the device pans and zooms at 60 fps without ever parsing the source. Figma at 333 MHz →

+
+
+

Motion that fits the budget

+

A painter-sorted pipeline keeps overdraw predictable on fixed-function hardware and renders identically on modern GPUs, so a desktop preview is the console frame.

+
+
+

Characters included

+

A VRM digital human with morph targets and spring-bone physics runs as one native process at 118 MB, where the Electron equivalent needs 2.2 GB. One process, whole character →

+
+
+
+ Pocket Voxel's town rendered as a voxel diorama with gabled roofs and a player character on the path. +
+

Pocket Voxel's town: a creature-RPG as a walking voxel diorama on a PSP.

+ +
+
diff --git a/site/home.html b/site/home.html index 2dda8b30..b9e20080 100644 --- a/site/home.html +++ b/site/home.html @@ -34,99 +34,62 @@
- +
-
- - -
- -
- - -
-
-
- Rich interactive software -

Interfaces, games, worlds. And agents.

-

This is not a UI kit with ambitions. The runtime already carries four kinds of software, all of it shipped and open source.

-
-
-
-

User interfaces

-

Solid, Vue Vapor and Octane components share one native tree, with flexbox layout, compile-time Tailwind, baked motion, a system keyboard, and a touch stack with real gesture physics. Three frameworks, one tree →

-
-
-

Games

-

A locked 60 FPS on 2004 silicon, with receipts: BSP worlds, bots and tracers under a JSX HUD, and input tapes that replay whole matches frame for frame. OpenStrike, shipped →

-
-
-

3D experiences

-

A portable BSP engine, a painter-sorted motion pipeline, and VRM digital humans with spring-bone physics. The same scene runs on desktop wgpu or the PSP’s GE. The launcher above is one. One process, whole character →

-
-
-

AI-native apps

-

Every frame is a pure function of an input tape, so sessions are data an agent can drive, replay and diff. Small enough to host one, too: a real coding agent runs inside the QuickJS guest, no Node underneath. The runtime agents want →

-
-
@@ -347,134 +310,48 @@

Reactivity becomes what fits

- -
-
-
-
- The .pocket format -

Ship the application,
not the port.

-

A .pocket file describes what the app is and what it needs: identity, capabilities, build plan, payloads. Each device decides how it should run: which viewport, which presentation, which execution class. If a target can’t honor the contract, it says so before launch, not after.

-
- build --all-targets - inspect - verify - thin -
-
-
-
- - my-app.pocket -
-
my-app.pocket
-├─ identity       app id, version, entry
-├─ capabilities   input.touch · display.viewport.live · …
-├─ build plan     how every target gets produced
-├─ payloads       guest bundle · AOT program
-└─ assets         style packs · fonts · tiles
-

One deterministic file. Every target re-verifies the plan before launch.

-
-
- -
-
-

System software, built into the runtime

-

Not just rectangles on unusual screens, but the pieces apps need to become an ecosystem.

-
-
-
-

App launcher

-

A Cover Flow deck that cold-boots, freezes and switches whole apps. It runs live in the PSP at the top of this page.

-
-
-

Capability contracts

-

A registry of real, tested hosts. Apps declare needs; targets admit or refuse them at build time.

-
-
-

Input & OSK

-

D-pad focus, analog, a virtual cursor, real touch with gestures, kinetic scroll and tap-to-press, and a system keyboard any app summons with one call.

-
-
-

Host services

-

USB companions bring what the device lacks: yt-dlp on a Mac becomes YouTube on a PSP.

-
-
-

Time-travel DevTools

-

Every session records itself. Step to any frame and inspect the native tree, over USB to real hardware.

-
- -
-
-
-
- - -
+ +
- Radically different devices -

Built for every kind of impossible.

-

Radically different doesn’t mean old. It means no browser, no modern OS, no JS engine, no continuous refresh, or no vendor left to care.

+ Rich interactive software +

Interfaces, games, worlds. And agents.

-
-
-

Consoles & handhelds

-

No OS to lean on, and 333 MHz is the fast one.

-
    -
  • Sony PSP
  • -
  • PS Vita
  • -
  • Game Boy Advance
  • -
  • Game Boy
  • -
  • NES
  • -
  • Playdate
  • -
  • Nintendo 3DS
  • -
-
-
-

Abandoned smartphones

-

The vendor moved on. The hardware didn’t.

-
    -
  • Symbian Belle
  • -
  • MeeGo
  • -
  • BlackBerry
  • -
  • Windows Phone
  • -
-
-
-

E-ink surfaces

-

A screen that fights every frame, so the runtime picks partial, dynamic or full refresh per update.

-
    -
  • PocketBook
  • -
  • Kindle
  • -
-
-
-

Microcontrollers

-

Kilobytes, not gigabytes. Sometimes no allocator at all.

-
    -
  • ESP32 MeowBit
  • -
  • ESP32‑P4
  • -
  • M5Stack Tab5
  • -
-

Built on Pocket Vapor, the experimental compile-away pipeline.

-
-
-

Native desktop surfaces

-

Modern machines that deserve better than a bundled browser per app.

-
    -
  • macOS widgets
  • -
  • Transparent windows
  • -
  • iOS
  • -
-
-
-
shippingin progressplanned
-

Every “shipping” chip is a real, tested host in the registry, not a roadmap slide. See the target profiles →

-
+
@@ -489,15 +366,15 @@

The proof is what it can carry.

- + The Pocket Launcher Cover Flow deck on a Nokia E7's landscape AMOLED screen.

Pocket Launcher

Proves: multi-app lifecycle & system UI

-

A Cover Flow deck that boots, freezes and switches whole apps on the PSP and on a 2011 Nokia. It’s running live in the 3D PSP at the top of this page.

+

A Cover Flow deck that boots, freezes and switches whole apps. SELECT summons the switcher; a frozen shot stands in while the next app cold-boots. Verified on the PSP, the Vita and a 2011 Nokia.

diff --git a/tests/site-stage.test.ts b/tests/site-stage.test.ts index e4444ff5..772fc8e7 100644 --- a/tests/site-stage.test.ts +++ b/tests/site-stage.test.ts @@ -54,29 +54,69 @@ test("homepage Stage package has one semantic screen and its declared suppressio } }); -test("homepage declares the live launcher and visible attributions", () => { +test("homepage plays the machine collage", () => { const home = readFileSync(ROOT + "site/home.html", "utf8"); - expect(home).toContain("data-pocket-stage"); - expect(home).toContain("The live Pocket Launcher"); - expect(home).toContain("Dibad"); - expect(home).toContain("creativecommons.org/licenses/by/4.0"); - expect(home).toContain("Motion studies by (yui540) · credited per author request"); - expect(home).toMatch( - /