# Maintaining the landing page

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](AGENTS.md).

## 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()` in `demos.js` sets `dwell = 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 = 2` is 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 in `hero()`.

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.l` and `.hero .corner.r`;
  putting them on the shared `.corner` classes 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 animate` in the HTML.
  Other section waves keep their own motion.
- **Melting edges:** `meltShape()` in `landing.js` draws tapered necks and round
  droplets. The outer 12% on each side retains its reach; the drips shorten
  gradually toward the center. CSS `--melt` controls stretching with scroll.
- **Emoji depth:** `.emoji-depth::before` draws 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-depth` adjusts the shadow
  opacity for an individual wrapper.
- **Footer:** `.endmark` supplies the white glow behind the logo. Its
  `pointer-events:none` lets 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()` and `resume()`. Leaving
  view, hiding the tab or opening an in-page overlay pauses their ongoing work.
  Visibility, `online` and `pageshow` events 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 `requestAnimationFrame` throttling 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:

```bash
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:

```bash
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:

1. 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.
2. 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.
3. Try both AI prompts, switch prompts mid-reveal and check that the section
   height stays stable and only the latest result finishes.
4. 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.
5. 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:

```bash
node --check public/lib/landing/landing.js
node --check public/lib/landing/demos.js
node --check public/lib/landing/sound.js
git diff --check
```
