Stage width policy for everything under the poster: the content cap
is now a root token, --stage-max: clamp(1280px, 72vw, 1600px) —
sections genuinely widen on big monitors instead of donating
everything past 1280px to margin. All four cap sites (section shell,
hero bands, footer) consume the token; live-tunable via the new Stage
group (stage-width.tsx writes the root var on knob changes).
The announce strip becomes a release slate: "Open-source AI studio"
on the left; on the right a mono release stamp (New · 2026.09) and
model chips — Seedance 2.5, Nano Banana 2 — that deep-link into the
webapp surface where each model runs (+ more → the webapp). Data
lives in RELEASE_SLATE (landing-data.ts): one place to update per
release.
Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
Scope correction: "under the wordmark" meant the announce strip
("Open-source AI studio") and everything below, not the hero poster's
own sales stack. The viewfinder slate, specimen headline, and proof
strip are removed and the hero returns to its settled form.
Kept from the detour: the pre-flight WebGL probe (real robustness
fix) and the 35-clip showcase pool.
Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
The flank stat stacks are gone — live galaxy readouts expressed
nothing about ArtCraft to a visitor; the card spiral itself is the
expressive element, and this is a product site with a creative blend,
not a creative showcase. The telemetry channel shrinks to the scene
clock (still driving the slate timecode and doubling as the chrome's
liveness gate); the flank tunables and the galaxy's extra per-frame
publishes go with it.
Showcase pool: 17 -> 35 clips (001-035 all verified 200 on the CDN;
036 404s). At 51 cards that's ~1.5 cards per clip — most cards now
unique on screen.
Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
When WebGL context creation fails (exhausted drivers, blocklisted
GPU, webgl.disabled), THREE.WebGLRenderer throws inside r3f's async
init — an unhandled REJECTION that the render-phase CanvasBoundary
never catches, surfacing as a page error instead of the designed
static fallback.
Both r3f canvas gates (galaxy, comparator blocking scene) now consult
a shared cached probe (src/lib/webgl-support.ts) before mounting the
<Canvas>: no context available → the canvas never mounts, the page's
static layers stand, and the slate chrome already hides itself via
the dead-instrument rule. The probe releases its test context through
WEBGL_lose_context so it never eats a context slot.
Verified with Playwright Firefox: webgl.disabled=true → zero
canvases, zero page errors, copy/ruler fully functional; normal run →
both canvases mount, zero errors.
Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
The sales layer under the wordmark, redesigned against the
center-blob-with-scalable-margins failure mode: on wide stages the
composition now *uses* the width instead of donating it to margin.
- Fluid poster type: the headline/subline scale with the viewport
(cqw-style clamp; knobs Headline max rem / vw slope) so their ratio
to the ARTCRAFT wordmark is a designed constant at every width. The
serif phrase breaks as a unit, giving a clean two-line lockup when
the stage outgrows one line. Sizes ride CSS variables on a `contents`
wrapper — RevealManager ends reveals with clearProps:"all", which
wipes inline styles on the [data-reveal] elements themselves.
- Specimen life on the display letters (variable Archivo): an intro
settle (letters arrive at the wordmark's poster extreme, wdth 125 /
wght 700, and cascade down to the display setting on the copy beat;
clock-driven so the tuner replay rewinds it) and a cursor field
afterwards (gaussian width-bulge under the pointer — the galaxy's
mouse-weight idea spoken in typography).
- Viewfinder slate chrome, the product's own metaphor (a viewport
where shots are composed): corner framing brackets + a REC/SC/TC
timecode line running on the galaxy's scene clock (lg+), and live
telemetry chips at the far flanks (xl+) — cards riding, clips
playing, FPS, spin θ, lens (boost) — real values published by the
galaxy frame loop through src/lib/hero-telemetry.ts and written
imperatively at a throttled rate. Chrome hides entirely while the
galaxy isn't running (reduced motion, WebGL failure): a dead
instrument gets no slate. Right edge clears the scroll ruler's rail.
- Proof line promoted to a full-width ruled strip at the poster's
bottom edge (justify-between at md+) — the composition's horizontal
anchor; extra right padding under an active ruler so it clears the
section queue.
Everything is tunable under the new "Hero slate" group (documented in
TUNER.md). Verified via Playwright Firefox screenshots at 3360×1380
and 1440×860.
Claude-Session: https://claude.ai/code/session_013vwLpYJbWEqTJg9PGV6B6H
Reproduced in a real Firefox (Playwright): returning to the page via
browser Back was NOT a bfcache restore (pageshow persisted=false,
navType back_forward) — it was a fresh load on which Next's client
router threw "Internal Next.js error: Router action dispatched before
initialization" three times, aborting hydration. No effects ran, so
data-ruler was never stamped, the galaxy canvas never mounted, and
data-intro never lifted (the wordmark stayed CSS-hidden at opacity 0)
— the whole page looked unmounted.
The 16.2.12 patch fixes the router-initialization race; the same repro
now comes back fully mounted. The Firefox-gated unload bfcache-buster
from the previous attempt is removed (wrong theory, and it cost
Firefox users instant-back site-wide); the pageshow persisted reload
guard stays as insurance for genuinely broken bfcache restores.
Production build verified on 16.2.12.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
Returning to the page via browser Back (e.g. from the /download
redirect's external target) restored it from the bfcache in Firefox:
the JS heap thaws but the WebGL context is discarded, so the galaxy
canvas came back blank and the rAF-driven instruments looked
unmounted. Chrome restores GPU state, which is why it never showed.
The pre-paint head script now attaches a pageshow listener that
reloads on e.persisted — the listener lives inside the frozen page,
so it fires on the restore itself. The intro is seen-flagged by
then and the page is statically prerendered, so the reload is cheap.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The per-arm conveyor offset was in absolute cycle units, so it
resonated whenever it landed near a multiple of the slot spacing:
at 51 cards (9 slots/arm, spacing .111) the .13 offset was one slot
+ 17%, radially aligning every arm's ladder into six-fold rings —
the starfish. 64 cards (spacing .0909) just happened to dodge it.
The stagger is now measured in slot gaps (divided by slotsPerArm), so
it can never ring-align at any card count or viewport scale, and the
default moves to 0.38 — the golden-ratio conjugate — which lands the
six arms at low-discrepancy phases within the gap.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
With 17 clips on the CDN, 51 cards lands the round-robin deal at
exactly three cards per clip (down from ~4.6 repeats at 64). The
neighbor-gap collision sizing grows the cards to keep the same screen
fill, and smaller viewports still scale the count down from the same
Tuned @ Mpx reference.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The CDN holds 17 clips but the wall only dealt the first 14. Verified
015-017 return 200 (018+ 404). At the tuned 64-card count this drops
simultaneous repeats per clip from ~4.6 to ~3.8; the least-recently-
shown rebirth reassignment spreads what remains.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The max-width content container carried the site's border-x rails, so
two vertical hairlines cut through the immersive spiral/wordmark stage.
The poster now runs edge to edge; the structural rail-and-tick language
begins at the announce strip (the fold line), which keeps its border-x.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The #1916 restyling wrote `<LoaderCircleIcon spin>` in five spots, but
raw lucide-react icons have no `spin` prop — the attribute fell through
to the SVG (React DOM warning) and the spinner never animated. The two
spots inside ImagePromptRow were corrected during the services/main
merge resolution; this covers the remaining three files with the same
animate-spin class main uses. Also drops the unused DynamicIcon import
in create-video (pre-existing on both lineages).
`tsc --noEmit -p apps/artcraft-webapp/tsconfig.app.json` is now clean.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
The design branch forked from the old monorepo main at f3442f3146;
main then advanced through #1910 (last-viewed badge), #1911 (iOS
upload fix — uploading queue stores previewUrl, not File), #1912,
#1699 (keybinds), the auth-page video update (976f2ae757, Vimeo
iframe → muted <video>), and the modal freeze fix (ed57c6de47,
auto-merged) before the artcraft/artcraft-services split.
Seven files conflicted where main's functional work crossed the
branch's #1913/#1916 webapp restyling. Resolution rule: main's
functionality, the design branch's styling —
- auth-showcase: main's <video> element, squared corners kept
- ImagePromptRow (webapp + libs): main's previewUrl refactor; kept
square corners/hairline borders. The branch's `spin` prop was a
latent bug (raw lucide icons have no such prop — no animation and
a React DOM warning), so main's animate-spin class wins.
- GalleryCard/GalleryRow: main's lastViewed ring + badge woven into
the branch's squared-corner classes (badge pill squared to match)
- old-site app.tsx: union of both new routes (landingp0 + creator)
- TimelineClipRow: main's enableClipTransition undo-step flow
superseded the branch's setClipTransitionEasing(DEFAULT_EASING)
artcraft-website-next is untouched by the incoming side. Webapp
typechecks clean after the follow-up spinner fixes.
Claude-Session: https://claude.ai/code/session_01KpbGFYxEXuaYU9J75vTK9j
Proofing-pass deployment plumbing. The app referenced seven marketing
routes it doesn't serve yet (/download, /pricing, /tutorials, /news,
/faq, /press-kit, /support) — every one now 307s to the shipping Vite
site, which this app already depends on staying live (MEDIA_BASE
feature footage). Drop a route from LEGACY_ROUTES as its page migrates.
netlify.toml follows the sibling apps' discovery pattern with base
frontend/apps/artcraft-website-next (standalone lockfile, not the Nx
workspace) and the Next runtime plugin, which also carries the
/cdn-media and /ext-media proxy rewrites into production.
turbopack.root and outputFileTracingRoot are pinned to the app dir —
Next was guessing the Nx workspace's lockfile as the project root,
which breaks output file tracing on serverless deploys.
Claude-Session: https://claude.ai/code/session_01YBoaBEt3gynzTwogBFtn4B
Opening a generated image/video and quickly clicking the backdrop to
close left the whole page unclickable, with no visible overlay to
explain why.
**Cause:** `Dialog.Overlay`/`Dialog.Content` are `forceMount`ed so
react-spring owns the leave animation. On close `Dialog.Root`'s `open`
flips false while the layer is still mounted, and Radix derives
`disableOutsidePointerEvents` from that flag, so its cleanup never
restores the `pointer-events: none` it set on `<body>`. Closing slowly
masked it; closing fast interrupts the leave spring so the clearing
render never comes.
**Fix:** `unregisterModal` now schedules `releaseStrandedBodyLock`,
clearing a lock nothing still owns. Bails if a modal is still registered
or a real open Radix dialog exists.
Adds a new creator spotlight landing page to the artcraft-website under
`/creators/`.
### Changes
- New spotlight page: hero with fanned reel triptych, credits marquee,
work grid, craft section, and closing CTA, all in the existing
landing-page design language
- Reel cards use locally hosted thumbnails (`public/images/creators/`)
and link out to the posts on Instagram, no embeds
- Primary "Create Now" CTA routes subscribers to the webapp and everyone
else to `/pricing`
- SEO wired up: `PAGE_META` entry for the prerender step, `_redirects`
rules plus short 301 aliases for the route
Recreate restored reference videos/audios from the prompt record with duration: 0. The cost quote forwards that as a zero input-duration hint, and the backend deliberately prices a zero/unknown input at the worst-case 30-second billing maximum (Seedance 2.5 bills reference-video input seconds), so a recreated generation quoted far above the original
charge, e.g. a 1080p 10s output with ~5s of input quotes 548 credits, but 1096 with the failsafe.
Measure each restored reference's duration from its CDN copy while building the recreate payload, the same way the library "Send to prompt" path probes durations. Unmeasurable files keep 0 (worst-case quote as before; generation always bills from its own server-side measurement).
Add Billing tab to webapp settings
- New "Billing" tab in the settings modal, between Account and API Keys
- Shows your current plan (Free/Basic/Pro/Max)
- Subscribers get a "Manage Plan" button that opens the Stripe portal,
same as the pricing page, plus a "View Plans" link for switching
- Free users get a "View Plans" button to the pricing page
- Handles signed-out and loading states like the other tabs