Maintaining the landing page
Updated · Markdown
The public home page has its own orange-and-cream style, interactive previews and sound engine. This guide records the current behavior and tuning controls after the October 15, 2026 landing-page work.
Where to edit
| File | Holds |
|---|---|
public/index.html | Section order, page copy, intro, phone panels, AI preview markup, video source and dialogs. |
public/css/landing.css | Layout, typography, depth, transitions, responsive rules and reduced-motion styles. |
public/lib/landing/landing.js | Join and CTA behavior, intro, video, early access, AI preview, liquid edges, particles and visibility management. |
public/lib/landing/demos.js | The hero phone tour, three presenter cards, audience cheer jar, Everything scroller and footer ball pit. |
public/lib/landing/sound.js | Synthesized sounds, event-specific levels, mute and audio-context recovery. |
public/stickers/ | Existing animated WebP stickers and their .still.webp companions. |
The interactive previews use local sample data. Answering the hero poll or trying an AI prompt does not create a deck or join a live room. The code box, account-aware CTAs and early-access form retain their real app/API behavior. The actual AI connection is described in Your own AI.
Intro, hero and video
The intro uses a bouncing orange, glints and two large, readable greeting lines. It lasts about 2.2 seconds, can be skipped with a tap, and appears once per browser-tab session through the poured session-storage key. Reduced motion skips it. To review it again, clear that key in the browser's developer tools and reload.
The hero keeps the same phone shell, eyes, logo and video button while its screen cycles through live polls, a quiz and leaderboard, a word cloud, and anonymous questions with voting. Reactions can burst outside the phone. The small progress strips align with the pause/play control; each strip is also a button for jumping to that feature.
hero()indemos.jssetsdwell = 8500: 8.5 seconds per feature.holdInteraction()gives an answer or vote a 3-second reading pause, then resumes the remaining feature time. Explicitly pausing the tour stays paused.- Focusing the word input pauses the tour. Sending a valid word dismisses the keyboard and releases the typing pause; leaving the input also permits resume.
- Entering the quiz resets it to the question. The leaderboard appears only after an answer, and Try again restores the question.
quizCorrectAnswer = 2is the zero-based index of Participate, which earns 1,000 demo points. Keep that index aligned with any future option edits.- Some initial panel copy is in
index.html; generated options, reset text, feature names and feedback are inhero().
Set the hero video through data-video-url on #film in index.html. Both a YouTube watch URL and a youtu.be URL are accepted. Desktop opens the large dialog with an even frame, no title bar and a close button centered on the top-right corner. Closing clears the iframe and restores focus and sound.
Touch devices matching (hover: none) and (pointer: coarse) skip that dialog. The tap requests iframe fullscreen where available; if unavailable or refused, it navigates to the YouTube embed with playsinline=0. Native playback and autoplay still depend on the browser and YouTube. The fullscreen request must stay inside the original tap handler. Landing sounds and looping demos pause while the in-page player is active, and resume when it closes.
Sections, liquid transitions and depth
- Hear from everyone: three styled preview cards show a poll, voted questions and a word cloud. Their entire card subtree is excluded from hover sounds; supported controls retain click feedback.
- Every voice counts: keep its small captions centered. Left/right text alignment overrides belong under
.hero .corner.land.hero .corner.r; putting them on the shared.cornerclasses affects this section too. - Bring your own deck: the eyebrow has its own gap above the heading. At widths up to 1,100px, the five file badges sit in a two-column grid, with Video spanning both columns. Scroll parallax is disabled there so badges cannot collide with the text. Desktop retains the floating badge layout.
- Bring your own AI: immediately follows BYOD. Visitors choose Claude, ChatGPT, Gemini or Any AI, then a create/edit prompt. Selecting an AI alone does not reveal a deck. A prompt moves the controls upward, shows about 950ms of juicing, then squeezes out the result over 720ms. The output reserves its space while hidden, keeping the section height stable. A newer prompt cancels the previous reveal. Reduced motion reveals the result immediately.
- Orange water: the former splash below the hero is a gentle SVG path animation with a 20-second cycle, set on
.wave.water animatein the HTML. Other section waves keep their own motion. - Melting edges:
meltShape()inlanding.jsdraws tapered necks and round droplets. The outer 12% on each side retains its reach; the drips shorten gradually toward the center. CSS--meltcontrols stretching with scroll. - Emoji depth:
.emoji-depth::beforedraws a subtle radial ground shadow behind the sticker. Animate the wrapper rather than applying a live blur or drop-shadow filter to animated WebP frames. This keeps depth without the rectangular shadow boundary and flicker.--emoji-depthadjusts the shadow opacity for an individual wrapper. - Footer:
.endmarksupplies the white glow behind the logo. Itspointer-events:nonelets touches reach the interactive ball pit underneath.
Sound controls
Sound is generated with Web Audio. The speaker remembers mute in local storage under stoa.mute; if storage is blocked, mute still works for the current visit. Sound waits for a user gesture. Hidden documents do not start new sounds, and a subsequent gesture can resume a suspended or interrupted audio context. Video quieting is temporary and does not overwrite the saved mute preference.
| Control | Where and current behavior |
|---|---|
| Footer ball-pit level | PIT_VOLUME = 0.29 inside playground() in demos.js. Multiplies pour, grab, toss and collision sounds for this pit only. |
| Overall level | Master gain 0.55 in engine() in sound.js. The sound.quiet setter restores the same gain; update both if changing the overall level. |
| General click level | Local volume = 0.65 in sound.tap(). Other controls can use their own sound methods. |
| Section appearance | sound.slap() now plays two quiet, softly attacked chimes. |
| Mouse hover | sound.hover() uses bubbles, plucks or ticks, with a 150ms minimum gap. The three presenter cards are excluded in landing.js. |
| Scroller drag | sound.slide() makes a rubbery tick and small bubble when a block changes at the row's center during a drag; minimum gap 80ms. Grab and release accents have separate multipliers in ride(). |
0.29 is an amplitude multiplier applied before the master gain and each sound's own envelope, not a device-volume percentage. It is the latest chosen pit level; earlier 0.05 and 0.11 settings are superseded. Tune the pit here rather than changing the shared sound primitives or the app's results ball pit.
Mobile layout and performance
The current design is retained across widths. Responsive rules wrap the top bar and footer links, keep join inputs shrinkable, contain decorative horizontal overflow and respect safe-area insets. Short landscape screens allow the hero to grow vertically while keeping the phone intact. Header height feeds --top-height so section scrolling clears the fixed top bar. The early-access overlay locks background scrolling and can itself scroll when the keyboard reduces the available space.
Hover effects apply only to a fine pointer with hover support. Touch retains pressed feedback, and the phone's tiny tour controls have larger invisible hit areas. The word input uses a 16px font; touch word-send and question-vote controls are enlarged without resizing the phone.
The Everything row distinguishes horizontal dragging from vertical page scrolling after an 8px threshold. Vertical intent releases pointer capture without grab/toss sounds. Canceled drags and lost capture clear interaction state without throwing. General touch particles wait for a real tap; movement beyond 10px or a canceled pointer suppresses the tap burst.
Keep these performance measures when extending the page:
- Demos load near the viewport and expose
pause()andresume(). Leaving view, hiding the tab or opening an in-page overlay pauses their ongoing work. Visibility,onlineandpageshowevents resynchronize the active demos. - Offscreen decorative loops and SVG waves pause. Known animated stickers use their still companions while inactive or under reduced motion. When adding a sticker to
stillStickers, ensure the corresponding still asset exists. - The scroller caches widths and updates transforms only for nearby pills. It uses elapsed time rather than assuming a display refresh rate. Capsule widths follow their content so labels cannot spill out on narrow screens; only stickers near the viewport animate.
- Footer physics also uses elapsed time, including drag velocity and friction, so 120Hz displays do not run the simulation twice as fast. The solver stops once settled and wakes on interaction. Pausing clears held pointers and RAFs.
- Cheer flights are bounded and settle when paused. Visitor taps take priority, and flights stay below the mobile header when the jar is partly obscured. Hero bursts and global particles are bounded; the particle canvas caps its pixel ratio at 2 and clears on tab hiding. Sound methods have replay gaps and a voice budget.
- Visual scroll/resize work uses
requestAnimationFramethrottling and batches geometry reads before writes. Liquid-edge resizing is debounced by 200ms. Keep input feedback and fullscreen requests immediate; avoid adding a general debounce to taps, text entry or drag handling. - Reduced motion skips the intro and automatic room/tour activity, removes decorative motion and leaves previews available through direct interaction. The Everything row becomes a native horizontally scrollable list.
- On mobile, the code row collapses once the hero has left the viewport. It returns after a deliberate upward scroll or when the code field receives focus; downward scroll hides it again. The header's observed height keeps anchor offsets correct as it changes.
Asset URLs currently use v=mobile-20261015-4. When shipping another landing asset change, update the version consistently in the stylesheet and module references in index.html and the sound/demos imports in landing.js, so cached browsers receive a matching set. npm run deploy uploads the current files but does not change these query values. Backend-only changes do not need a landing asset version bump.
Standalone arrows, checks, the AI sparkle and the footer orange are inline SVG. The display fonts do not cover every symbol glyph, so keep new icon-only controls independent of font or emoji fallback too.
Previewing and deploying the main landing page
This repository may have several worktrees. npm run dev and npm run deploy use the files in the worktree where each command runs; neither command switches to main. If a backend branch is serving port 8787, its landing page can look older even when origin/main already has the mobile fixes. The same applies to a deployment started from that backend worktree.
From the main worktree, install dependencies once and start a separate preview without stopping the backend server:
npm ci
npm run dev -- --ip 127.0.0.1 --port 8788
Open http://127.0.0.1:8788/ in the iOS Simulator. The current main page's HTML should reference mobile-20261015-4; the older backend checkout may still reference mobile-20261015-2. Check both responses directly if the simulator appears stale:
curl -fsS http://127.0.0.1:8788/ | rg 'landing.css|landing.js'
curl -fsS https://joo.cy/ | rg 'landing.css|landing.js'
To deploy this main worktree, use node scripts/deploy-main.mjs. The script checks that its own worktree is clean, is on main, and matches origin/main before running the normal deployment. Use node scripts/deploy-main.mjs --check to verify the source without deploying. This check is relative to the script's location, so running it through an absolute path still deploys the intended main worktree. After deployment, the live HTML should reference the same asset version as the main preview.
Validation and future regression checks
The October 15 pass checked Safari layouts at 320, 360, 390, 430, 768 and 1,440px widths, plus 844 × 390 landscape. No page-wide horizontal overflow or clipped heading lines remained in those checks. The AI section kept the same height before and after its reveal; the hero word submission and answer-gated quiz leaderboard were exercised. An iPhone 16e simulator on iOS 26.3 was used for portrait/landscape inspection and early-access keyboard reachability.
Temporary interaction fixtures checked swipe direction, canceled drags, offscreen frame cleanup, nearby-pill updates, comparable falling speed at 60/120Hz, mute with blocked storage and audio-context recovery. These fixtures were outside the repository, not a permanent test suite. JavaScript syntax checks and git diff --check passed. This pass did not benchmark physical iPhones or Android devices, validate every YouTube fullscreen path, or test backend changes.
For another release, check these behaviors with the existing copy and shadows:
- Scroll the whole page at a narrow portrait width and in short landscape; check BYOD spacing, centered audience captions and absence of sideways page scrolling.
- Jump between every phone strip, pause/play, answer and retry the quiz, send a word, and verify the tour resumes after interactions and keyboard dismissal.
- Try both AI prompts, switch prompts mid-reveal and check that the section height stays stable and only the latest result finishes.
- Drag the Everything row horizontally, scroll vertically from it, cancel a drag, and grab/throw footer balls. Mute, switch tabs and return; check that audio and animation resume only where appropriate.
- Repeat with reduced motion. On physical iOS Safari and Android Chrome, also check native video fullscreen, keyboard behavior and sustained scrolling.
Local code checks, from the repository root:
node --check public/lib/landing/landing.js
node --check public/lib/landing/demos.js
node --check public/lib/landing/sound.js
git diff --check