Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions brand/generated-assets.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
"brand/source/video2ctx-logo-192.png": "44472cbe3e7ef39bc557278efc808ebc038effe90b96c8089387f5f5a6bf35ac",
"brand/source/video2ctx-favicon-32.png": "77ae28a2e39a6020442a3ba136145f52bb1155b195d453595cb8d597c083f38a",
"brand/source/video2ctx-favicon.ico": "2ce82549c6b86fa66fa8c1510b0c85a404c14aa9aadead05b02d02a74b6201e5",
"brand/source/video2ctx-og-background-hybrid-icons.png": "089fb9fe62cf1dac7c243b46462d547def9c89da9f3c747284bb8520ed19b783",
"web/public/brand/logo-120.png": "d44f423ec5b91a584ed87ef41ffcb0a73a6ec69b13bc5d852f9d589723e03829",
"web/public/android-chrome-192x192.png": "44472cbe3e7ef39bc557278efc808ebc038effe90b96c8089387f5f5a6bf35ac",
"web/public/android-chrome-512x512.png": "4b3126c170ddeaded8d18d63e65be8a80ee2f58763cfd78a9d04f35afb2991af",
"web/app/icon.png": "4b3126c170ddeaded8d18d63e65be8a80ee2f58763cfd78a9d04f35afb2991af",
"web/app/apple-icon.png": "0fe814ed4ab3d78fea8bdb723b67218d89ccb2fa0e0f9a5d3d386d04cff99133",
"web/app/favicon.ico": "2ce82549c6b86fa66fa8c1510b0c85a404c14aa9aadead05b02d02a74b6201e5",
"web/app/opengraph-image.jpg": "12b0a4c3335a37c1ad3154b2db4753e9afb9e68b35cb8a6cb911ebe33e72311a",
"web/app/twitter-image.jpg": "12b0a4c3335a37c1ad3154b2db4753e9afb9e68b35cb8a6cb911ebe33e72311a",
"docs/favicon.png": "77ae28a2e39a6020442a3ba136145f52bb1155b195d453595cb8d597c083f38a",
"docs/logo/light.svg": "5744539b3df809e3ba7a20d4a1faae4e0f59aad5b9e31f9b5b2973c3f1c438d4",
"docs/logo/dark.svg": "9e72ae6153580322d7a42c2690cb341aa14fcdc7953bc69e5573a37c2f5bf0e5",
Expand Down
File renamed without changes.
File renamed without changes.
76 changes: 76 additions & 0 deletions brand/prompts/voxel-horizon-fold-loop-seedance.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
Fold loop: animate the voxel horizon still.
2026-08-12. Higgsfield, seedance_2_0.

SUPERSEDED. Nothing this produced still ships. Kept as the record of how the
scene was originally animated, and because its four failures still apply to any
future render of this landscape.

Its output is brand/generated/fold-scene.{webm,mp4,-poster.webp}, replaced on
desktop by fold-scene-symbols.* and, since 2026-08-22, in portrait too — the
portrait loop is a crop of whichever landscape loop is current:

crop=416:720:432:0,scale=440:762

(a native 9:16 render was tried and rejected — the same prompt invented
fires across the terraces and drifted the camera, loop gap 6.20 vs 1.63)

Input
start image brand/source/voxel-horizon.webp, decoded to PNG for upload:
dwebp voxel-horizon.webp -o voxel-horizon-source.png
end image deliberately NOT set — see below

Command
higgsfield generate create seedance_2_0 \
--prompt "$(cat prompt.txt)" \
--start-image voxel-horizon-source.png \
--duration 8 --resolution 720p --mode std --aspect_ratio 16:9 \
--generate_audio false --wait --wait-timeout 25m --json

---- prompt ----

Thick pale mist lies over the valley floor and between the stone terraces of this dark isometric voxel landscape. The mist rises upward, swells, and sinks back down again in a slow continuous cycle, clearly visible throughout the whole shot — always moving, never still. It is already present and thick in the very first frame.

Embers rise from many separate places at once: from the ruined terraces at the far left edge, from the foreground ruins along the bottom centre, from the valley floor in the middle, and from the dark slopes at the far right edge. They lift slowly, glowing warm orange, and fade out into the black air. The right side of the frame has just as many embers as the left side. They are spread evenly across the entire width of the frame and never pour out of a single spot.

New mist and new embers appear continuously, so the whole landscape is alive with slow movement at every moment, from the very first frame to the very last.

The camera is completely locked on a tripod. It does not move, pan, tilt, drift or zoom at any point. The voxel terrain is solid rock and stays perfectly still.

Night, deep charcoal shadows, volumetric light, cinematic.

No text, no logos, no people.

---- end prompt ----

Four failures this prompt is shaped around. Full write-up in
reference/design/HIGGSFIELD_ASSET_GUIDE.md §4.

Never pin the end frame equal to the start frame. Two identical endpoints
make "change nothing" a valid and cheap solution; the first attempt came back
completely static.

Do not qualify the motion. An earlier version asked for movement that was
"extremely slow", "weightless", "almost imperceptible", and got exactly that.

Anchor motion to named places. "Embers rise from all across the landscape"
put them all in one spot left of centre, 6x more motion in one quarter than
another. Naming four anchors and stating that the right side has as many as
the left is what fixed it.

Give the mist a cycle, not a state. Trying to stop it flooding the valley by
saying it "stays the same thickness throughout" froze it completely, valley
motion 0.062. Rise / swell / sink moves without transforming, and a particle
system in equilibrium loops by itself — raw loop gap 1.5 against 16.6 for the
flooding version.

Post, all local. Trim, crossfade and encode in ONE ffmpeg pass straight from the
download; an accidental intermediate re-encode cost 60% of the bitrate before
the browser ever saw it.

TRIM=1.0 X=0.8 tail crossfaded back over the head
AV1 libsvtav1 crf 36 preset 4 -g 48
H264 libx264 crf 21 preset veryslow -g 48
poster from the shipped encode's own first frame

Cost: 5th of 6 renders, ~166 credits for the session. Two of six shipped
anything. Budget for that ratio.
59 changes: 59 additions & 0 deletions brand/prompts/voxel-horizon-symbols-videoedit.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
Video edit: add the carved symbols to the existing fold loop.
2026-08-22. Higgsfield job hf_20260822_143125_5e972ca2-931e-40c5-8f2f-27ff5469eaa5.

The raw render is deliberately not kept in the repo — only the graded loop that
ships, web/public/scene/fold-scene-symbols.{webm,mp4}. Re-run this prompt if the
grade, trim, crossfade or codec ever needs changing.

Inputs
video web/public/scene/fold-scene.mp4 (the previous fold loop, no symbols)
reference brand/source/voxel-horizon-video-icons-gpt.webp
(the scene still with five symbols carved in, made with GPT Image 2 —
prompt in voxel-horizon-video-icons-gpt-image.txt)

Four lines below are scar tissue, not decoration. Earlier renders in this scene
duplicated a symbol, rendered the glow bright pink, and scattered torch flames
across the terraces unprompted. Keep the prohibitions that name those.

---- prompt ----

The reference image is this exact scene with stone symbols already carved into it.
The video is the same landscape, same camera, same lighting. Add those carved
symbols to the video and change nothing else.

Carve five symbols into the terrain, exactly one of each, matching the reference
image's placement, scale, perspective and weathering:
- a play triangle inside a rectangular relief panel, on the upper-left terrace
- a horizontal timeline with a round scrubber, on the long wall below it
- a speaker with three sound arcs, on the lower-left terrace face
- four crop-frame corner brackets, on the upper-right slope
- a row of waveform bars, on the right slope below the crop marks

Every symbol is solid stone in the same voxel masonry as the terrain around it:
weathered, partially eroded, physically part of the wall. They are lit only by
the scene's existing warm light, exactly as in the reference image. They hold
perfectly still and keep identical shape and position in every frame.

Preserve the rest of the shot completely: the locked tripod camera, the terrain
geometry, the pale fog drifting through the central valley, the rising amber
embers, the deep charcoal palette, the exposure, and the dark empty upper centre.
The fog and embers must keep moving exactly as they already do.

The symbols never illuminate, pulse, glow, flicker, duplicate, morph, drift or
detach. No new light sources, no torches, no fire, no pink or magenta anywhere.
No text, letters, numbers, logos, screens, interface panels, people or watermarks.

---- end prompt ----

What came back, measured against the previous loop

all five symbols present, one each, correct placement, stable across the clip
camera locked — tested for both translation and zoom, 0px / 1.000 scale win
black point lifted 4.0 -> 9.1 graded back
saturation dropped 8.9 -> 7.5 graded back
an AAC audio track dropped with -an
fog motion 0.263 -> 0.177 NOT recovered; see below

The fog is the one real regression and it is not an encoding artefact. AV1 at
crf 30 only reaches 0.194 for an extra 231 KB, so it was not worth the bytes.
The delivered loop is calmer in the valley than its predecessor.
11 changes: 11 additions & 0 deletions brand/prompts/voxel-horizon-video-icons-gpt-image.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
Use case: precise-object-edit
Asset type: cinematic website hero background and source frame for later video generation
Input image: voxel-horizon.webp is the edit target.

Primary request: Make one surgical edit to the supplied image. Embed exactly five unique video-application motifs into existing stone terrace faces as ancient architectural carvings made from the same voxel masonry. Add exactly one of each: (1) a triangular play symbol inside a simple rectangular frame on an upper-left terrace face, (2) a horizontal video timeline with one circular scrubber on a middle-left terrace face, (3) a speaker with two sound-wave arcs on a lower-left terrace face, (4) four crop-corner brackets on an upper-right terrace face, and (5) a simple audio waveform on a lower-right terrace face.

Integration: Each motif must be physically carved or built into a different existing stone wall, following that wall's perspective, voxel grid, depth, erosion, scale, occlusion, amber rim lighting, and charcoal shadows. They should feel like subtle archaeological reliefs discovered in the terrain, not modern UI overlays. Use restrained dim amber illumination only inside a few recessed grooves. No bright neon.

Composition invariants: Preserve the original image's exact camera, centered composition, horizon, terrain silhouette, central valley, fog quality and density, black upper negative space, lighting direction, warm amber and charcoal palette, voxel scale, materials, texture, and all existing structures. Keep the central valley and upper-center copy area clear. Change only the local stone surfaces required to embed the five motifs.

Constraints: Exactly five motifs total, exactly one instance of each specified motif. Every motif must remain clearly distinguishable. No duplicates, no extra symbols, no floating icons, no panels, no screens, no labels, no text, no letters, no numbers, no logo, no people, no torches, no visible flames, no watermark. Do not redesign, crop, reframe, brighten, or restyle the original image.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/source/voxel-horizon-video-icons-gpt.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added brand/source/voxel-horizon-video-icons-gpt.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
63 changes: 63 additions & 0 deletions scripts/generate-brand-assets.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ const sourcePaths = [
'brand/source/video2ctx-logo-192.png',
'brand/source/video2ctx-favicon-32.png',
'brand/source/video2ctx-favicon.ico',
// Purpose-built for the card: mass on the right, near-black on the left, and
// the same carved symbols as the landing fold. Not the hero still — that one
// fills the frame, so text over it needs a scrim, and a scrim over a scene
// this dark lifts the blacks rather than deepening them.
'brand/source/video2ctx-og-background-hybrid-icons.png',
];
const outputPaths = [
'web/public/brand/logo-120.png',
Expand All @@ -20,6 +25,8 @@ const outputPaths = [
'web/app/icon.png',
'web/app/apple-icon.png',
'web/app/favicon.ico',
'web/app/opengraph-image.jpg',
'web/app/twitter-image.jpg',
'docs/favicon.png',
'docs/logo/light.svg',
'docs/logo/dark.svg',
Expand Down Expand Up @@ -57,6 +64,33 @@ function wordmark(logoDataUrl, color) {
`;
}

/* Type here tracks `.craft-nav-wordmark` in craft.css: Geist at weight 670 with
* -0.035em tracking. Geist ships static TTFs at 600 and 700, and 670 sits
* between them, so the card uses Bold as the nearest neighbour rather than a
* variable axis — librsvg does not reliably honour `font-variation-settings`.
*
* The navbar renders its `ctx` suffix in Geist Pixel Grid. That cannot be
* reproduced here: geist ships the pixel family as woff2 only, and librsvg
* silently ignores woff2 @font-face and falls back. So the card's wordmark is
* Geist throughout. Converting the woff2 to TTF would fix it. */
function openGraphOverlay(logoDataUrl, fontSemiBold, fontBold) {
return `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
<defs>
<style>
@font-face { font-family: 'Geist'; src: url('${fontSemiBold}') format('truetype'); font-weight: 600; }
@font-face { font-family: 'Geist'; src: url('${fontBold}') format('truetype'); font-weight: 700; }
.geist { font-family: 'Geist', Arial, Helvetica, sans-serif; }
</style>
</defs>
<image href="${logoDataUrl}" x="72" y="68" width="76" height="76"/>
<text class="geist" x="165" y="122" fill="#f5f1eb" font-size="50" font-weight="700" letter-spacing="-1.75">video2ctx</text>
<text class="geist" x="72" y="266" fill="#f5f1eb" font-size="64" font-weight="700" letter-spacing="-2.24">Video context</text>
<text class="geist" x="72" y="338" fill="#f5f1eb" font-size="64" font-weight="700" letter-spacing="-2.24">for agents</text>
<text class="geist" x="74" y="408" fill="#b7b1aa" font-size="23" font-weight="600" letter-spacing="-0.5">Structured, source-linked context from video.</text>
<text class="geist" x="74" y="552" fill="#ef5a4f" font-size="20" font-weight="600" letter-spacing="0.3">video2ctx.dev</text>
</svg>`;
}

async function generate() {
const sourceLogoPath = absolute(sourcePaths[0]);
await sharp(sourceLogoPath)
Expand All @@ -76,6 +110,35 @@ async function generate() {
.png({ palette: true, colours: 256, compressionLevel: 9 })
.toFile(absolute('web/app/apple-icon.png'));

const ogLogoDataUrl = `data:image/png;base64,${readFileSync(sourceLogoPath).toString('base64')}`;
const ogFontDataUrl = `data:font/ttf;base64,${readFileSync(
absolute('web/node_modules/geist/dist/fonts/geist-sans/Geist-SemiBold.ttf'),
).toString('base64')}`;
const ogFontBoldDataUrl = `data:font/ttf;base64,${readFileSync(
absolute('web/node_modules/geist/dist/fonts/geist-sans/Geist-Bold.ttf'),
).toString('base64')}`;
/* Centre-cropped and ungraded, both deliberate. This source is already
* composed and exposed for a card, unlike the hero still, which is graded to
* sit under a 64% dimmer and needs a brightness lift to survive a feed.
*
* There is also no scrim. The left of this frame is darker than #171513, so
* any scrim over it *raises* the black point instead of lowering it —
* measured, a 55% wipe took the text plate from 17.9 up to 23.8. */
await sharp(absolute(sourcePaths[4]))
.resize(1200, 630, { fit: 'cover', position: 'centre' })
.composite([{ input: Buffer.from(openGraphOverlay(ogLogoDataUrl, ogFontDataUrl, ogFontBoldDataUrl)) }])
/* JPEG, not PNG. The card is a dark photographic gradient with no flat
* colour and no transparency, which is PNG's worst case — it was 690 KB as
* a PNG against 152 KB here. Banding was the thing to check, since the
* frame is mostly near-black, and it does not appear: peak error against
* the lossless original measured 1/255 across the gradient.
*
* 4:4:4 is deliberate. The logo and the URL are saturated red, and chroma
* subsampling is exactly where those edges smear. */
.jpeg({ quality: 92, mozjpeg: true, chromaSubsampling: '4:4:4' })
.toFile(absolute('web/app/opengraph-image.jpg'));
copy('web/app/opengraph-image.jpg', 'web/app/twitter-image.jpg');

const navLogoDataUrl = `data:image/png;base64,${readFileSync(absolute('web/public/brand/logo-120.png')).toString('base64')}`;
write('docs/logo/light.svg', wordmark(navLogoDataUrl, '#191715'));
write('docs/logo/dark.svg', wordmark(navLogoDataUrl, '#f5f1eb'));
Expand Down
15 changes: 5 additions & 10 deletions web/DESIGN.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Product design system

Covers the live marketing surface (`/` and `/explore`) and the product workspace
Covers the live marketing surface (`/`) and the product workspace
(`/dashboard`). The dashboard keeps dedicated `--color-dashboard-*` roles in
`tokens.css` because dense application surfaces need their own contrast tuning,
but those roles now resolve to Craft's warm dark paper, off-white ink, and
Expand Down Expand Up @@ -216,8 +216,8 @@ reduced motion **0 bytes of video**.
- **No invented metrics.** No adoption counts, no speed multiples, no
testimonials that were not said. Every number on the page is one the platform
actually produces.
- **Name the cost, not just the benefit.** `/explore` lists each direction's bet
*and* what it gives up.
- **Name the cost, not just the benefit.** Each access path should say what it
gives up as well as what it provides.
- Plain language over jargon: "What was said" beats "Transcript"; header names
and response shapes belong in the API reference, not the landing page.

Expand Down Expand Up @@ -247,11 +247,8 @@ Recorded because it is real, not aspirational. Verified 2026-08-12.
8-state component discipline is not met.
3. **`--exit` is declared and never used.** The "exits faster than entrances"
rule is documented but enforced nowhere. Either apply it to the lens fade-out
and the beat crossfade, or drop the token.
4. **`--color-stage-*` in `tokens.css` is consumed only by `lens-home.css` and
`cinematic.css`** — both Decomposition-only. If that direction is ever
deleted, those tokens go with it.
5. **The fold scene is 720p** upscaled ~1.16x at common desktop widths. Sharpness
or drop the token.
4. **The fold scene is 720p** upscaled ~1.16x at common desktop widths. Sharpness
is currently fine because contrast, not resolution, was the real constraint —
but a wider display will expose it.

Expand All @@ -265,8 +262,6 @@ Resolved 2026-08-12: the stale `tokens.css` stamp, the missing
- Keep the resting-scene dimming and the `Flashlight` mount conditions **in
step**. The scene is only darkened where a light exists to recover it; dim it
anywhere the component does not mount and the fold is simply dark.
- Promoting a different landing direction is one line: `DEFAULT_DIRECTION` in
`_directions/registry.tsx`.
- Regenerating the fold scene: prompts, settings and encode commands are in
`reference/design/CINEMATIC_ASSETS.md` and `web/public/scene/README.md`. The reusable
method — model choice, prompting failures, the measurement harness — is in
Expand Down
Loading
Loading