Remocn
componentrelease-teasersource: ok · refreshed
Release Teaser
A 16-second cinematic release teaser with center-out typography, a procedural dimensional mark, moving edge light, and an independent fictional identity.
npx shadcn add "https://www.remocn.dev/r/release-teaser.json"free
Where it goes
changelog
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightremotion@fontsource/inter
Files8 · source cached 10/7/2026, 7:37:08 PM
registry/remocn-templates/release-teaser/index.tsx
"use client";
import "@fontsource/inter/500.css";
import "@fontsource/inter/600.css";
import { useMemo } from "react";
import {
AbsoluteFill,
Audio,
Sequence,
useCurrentFrame,
useVideoConfig,
} from "remotion";
import { TeaserBackground } from "./background";
import { type ReleaseTeaserProps, resolveReleaseTeaserProps } from "./content";
import {
atFps,
RELEASE_TEASER_FPS,
RELEASE_TEASER_FRAMES,
RELEASE_TEASER_HEIGHT,
RELEASE_TEASER_WIDTH,
} from "./motion";
import { ReleaseLockup } from "./scenes/closing";
import { Statement } from "./scenes/statement";
export type { ReleaseTeaserProps, ReleaseTeaserTheme } from "./content";
export { releaseTeaserStatements, releaseTeaserTheme } from "./content";
export {
RELEASE_TEASER_FPS,
RELEASE_TEASER_FRAMES,
RELEASE_TEASER_HEIGHT,
RELEASE_TEASER_WIDTH,
releaseTeaserTimeline,
} from "./motion";
export function ReleaseTeaser(props: ReleaseTeaserProps) {
const currentFrame = useCurrentFrame();
const { fps, width, height } = useVideoConfig();
const frame = (currentFrame / fps) * RELEASE_TEASER_FPS;
const {
brandName,
release,
tagline,
statements,
accentColor,
theme,
logoSrc,
lightIntensity,
reducedMotion,
} = props;
const scene = useMemo(
() =>
resolveReleaseTeaserProps({
brandName,
release,
tagline,
statements,
accentColor,
theme,
logoSrc,
lightIntensity,
reducedMotion,
}),
[
brandName,
release,
tagline,
statements,
accentColor,
theme,
logoSrc,
lightIntensity,
reducedMotion,
],
);
const scale = Math.min(width / 960, height / 540);
return (
<AbsoluteFill
style={{
background: scene.theme.background,
overflow: "hidden",
fontFamily: '"Inter", sans-serif',
WebkitFontSmoothing: "antialiased",
}}
>
<div
style={{
position: "absolute",
left: (width - 960 * scale) / 2,
top: (height - 540 * scale) / 2,
width: 960,
height: 540,
scale,
transformOrigin: "0 0",
overflow: "hidden",
}}
>
<TeaserBackground scene={scene} frame={frame} />
<Sequence
name="An idea"
from={0}
durationInFrames={atFps(104, fps)}
layout="none"
>
<Statement
scene={scene}
text={scene.statements[0]}
frame={frame}
duration={104}
/>
</Sequence>
<Sequence
name="Better work"
from={atFps(104, fps)}
durationInFrames={atFps(208, fps) - atFps(104, fps)}
layout="none"
>
<Statement
scene={scene}
text={scene.statements[1]}
frame={frame - 104}
duration={104}
/>
</Sequence>
<Sequence
name="Every detail"
from={atFps(208, fps)}
durationInFrames={atFps(326, fps) - atFps(208, fps)}
layout="none"
>
<Statement
scene={scene}
text={scene.statements[2]}
frame={frame - 208}
duration={118}
/>
</Sequence>
<Sequence
name="Together"
from={atFps(326, fps)}
durationInFrames={atFps(446, fps) - atFps(326, fps)}
layout="none"
>
<Statement
scene={scene}
text={scene.statements[3]}
frame={frame - 326}
duration={120}
/>
</Sequence>
<Sequence
name="A fresh chapter"
from={atFps(446, fps)}
durationInFrames={atFps(610, fps) - atFps(446, fps)}
layout="none"
>
<Statement
scene={scene}
text={scene.statements[4]}
frame={frame - 446}
duration={164}
/>
</Sequence>
<Sequence
name="Release reveal"
from={atFps(610, fps)}
durationInFrames={atFps(960, fps) - atFps(610, fps)}
layout="none"
>
<ReleaseLockup scene={scene} frame={frame - 610} />
</Sequence>
</div>
{props.audioSrc?.trim() ? (
<Audio
src={props.audioSrc.trim()}
volume={
Number.isFinite(props.volume)
? Math.max(0, Math.min(1, props.volume ?? 1))
: 1
}
/>
) : null}
</AbsoluteFill>
);
}
export const releaseTeaserConfig = {
componentName: "ReleaseTeaser",
importPath: "@/components/remocn/templates/release-teaser",
controls: {
brandName: {
type: "text-content" as const,
default: "Orvio",
description: "Name in the final release lockup",
},
release: {
type: "text-content" as const,
default: "2",
description: "Version suffix; leave empty to hide",
},
tagline: {
type: "text-content" as const,
default: "Your next chapter starts here.",
description: "Final supporting line",
},
accentColor: {
type: "color" as const,
default: "#9BC8DD",
description: "Mark and dimensional edge lighting",
},
reducedMotion: {
type: "boolean" as const,
default: false,
description:
"Static mark and complete statements, without camera motion or blur",
},
},
durationInFrames: RELEASE_TEASER_FRAMES,
fps: RELEASE_TEASER_FPS,
compositionWidth: RELEASE_TEASER_WIDTH,
compositionHeight: RELEASE_TEASER_HEIGHT,
};
registry/remocn-templates/release-teaser/content.ts
export interface ReleaseTeaserTheme {
background: string;
surface: string;
foreground: string;
muted: string;
accent: string;
}
export interface ReleaseTeaserProps {
brandName?: string;
release?: string;
tagline?: string;
statements?: string[];
accentColor?: string;
theme?: Partial<ReleaseTeaserTheme>;
logoSrc?: string;
lightIntensity?: number;
reducedMotion?: boolean;
audioSrc?: string;
volume?: number;
}
export const releaseTeaserTheme: ReleaseTeaserTheme = {
background: "#050709",
surface: "#151C22",
foreground: "#F2F5F7",
muted: "#A4ADB5",
accent: "#9BC8DD",
};
export const releaseTeaserStatements = [
"Start with an idea",
"Make room\nfor better work",
"Bring every detail to life",
"Move together.\nGo further.",
"A fresh chapter.\nNo starting over.",
] as const;
const nonempty = (value: string | undefined, fallback: string) =>
value?.trim() || fallback;
export function resolveReleaseTeaserProps(props: ReleaseTeaserProps = {}) {
return {
brandName: nonempty(props.brandName, "Orvio"),
// An explicit empty release intentionally hides the version suffix.
release: props.release === undefined ? "2" : props.release.trim(),
tagline: nonempty(props.tagline, "Your next chapter starts here."),
statements: releaseTeaserStatements.map((fallback, i) =>
nonempty(props.statements?.[i], fallback),
),
theme: {
background: nonempty(
props.theme?.background,
releaseTeaserTheme.background,
),
surface: nonempty(props.theme?.surface, releaseTeaserTheme.surface),
foreground: nonempty(
props.theme?.foreground,
releaseTeaserTheme.foreground,
),
muted: nonempty(props.theme?.muted, releaseTeaserTheme.muted),
accent: nonempty(
props.accentColor,
nonempty(props.theme?.accent, releaseTeaserTheme.accent),
),
},
logoSrc: props.logoSrc?.trim() || undefined,
lightIntensity: Number.isFinite(props.lightIntensity)
? Math.max(0, Math.min(2, props.lightIntensity ?? 1))
: 1,
reducedMotion: props.reducedMotion ?? false,
};
}
export type ReleaseTeaserScene = ReturnType<typeof resolveReleaseTeaserProps>;
registry/remocn-templates/release-teaser/motion.ts
import { Easing, interpolate } from "remotion";
export const RELEASE_TEASER_FPS = 60000 / 1001;
export const RELEASE_TEASER_FRAMES = 960;
export const RELEASE_TEASER_WIDTH = 1920;
export const RELEASE_TEASER_HEIGHT = 1080;
// Half-open intervals on a doubled NTSC picture clock (16.016 seconds).
export const releaseTeaserTimeline = [
{ name: "An idea", from: 0, to: 104 },
{ name: "Better work", from: 104, to: 208 },
{ name: "Every detail", from: 208, to: 326 },
{ name: "Together", from: 326, to: 446 },
{ name: "A fresh chapter", from: 446, to: 610 },
{ name: "Release reveal", from: 610, to: 960 },
] as const;
export const atFps = (frame: number, fps: number) =>
Math.round((frame / RELEASE_TEASER_FPS) * fps);
export const clamp = (value: number) => Math.max(0, Math.min(1, value));
export const progress = (frame: number, from: number, to: number) =>
clamp((frame - from) / (to - from));
export const ease = (frame: number, from: number, to: number) =>
interpolate(frame, [from, to], [0, 1], {
easing: Easing.bezier(0.5, 0, 0.2, 1),
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
});
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
export const graphemes = (text: string) =>
Array.from(segmenter.segment(text), (part) => part.segment);
export function textUnits(text: string) {
return graphemes(text).reduce((sum, char) => {
if (/\s/.test(char)) return sum + 0.3;
if (/[ilI.,'!|]/.test(char)) return sum + 0.29;
if (/[MW@%]/.test(char)) return sum + 0.98;
return sum + ((char.codePointAt(0) ?? 0) > 0x024f ? 1.02 : 0.63);
}, 0);
}
/** Honor explicit lines; keep arbitrary overrides within the safe area. */
export function fitText(text: string, maxSize = 48, width = 808) {
const lines = text.split("\n");
return Math.min(
maxSize,
width / Math.max(1, ...lines.map(textUnits)),
220 / (Math.max(1, lines.length) * 1.15),
);
}
/** Center-out focus, with a bounded stagger independent of copy length. */
export function characterState(
frame: number,
end: number,
index: number,
count: number,
) {
const distance =
count <= 1 ? 0 : Math.abs(index - (count - 1) / 2) / ((count - 1) / 2);
const delay = distance * 7;
const enter = ease(frame, delay, delay + 18);
const exit = ease(frame, end - 16 - distance * 5, end - 1 - distance * 5);
return {
opacity: enter * (1 - exit),
blur: (1 - enter) * 5 + exit * 3,
y: (1 - enter) * 2 - exit * 1.5,
};
}
export function ringPose(frame: number, reducedMotion = false) {
const f = reducedMotion ? 760 : Math.max(0, Math.min(959, frame));
const travel = progress(f, 0, 610);
const reveal = ease(f, 602, 702);
return {
x: 480 + (1 - reveal) * (18 - travel * 26),
y: (-200 + travel * 100) * (1 - reveal) + 270 * reveal,
scale: (2.78 - travel * 0.32) * (1 - reveal) + 1.08 * reveal,
rx: (0.4 - travel * 0.1) * (1 - reveal) + 0.14 * reveal,
ry: (-0.55 + travel * 0.12) * (1 - reveal) - 0.16 * reveal,
rz: (-0.42 + travel * 0.22) * (1 - reveal) - 0.13 * reveal,
lightAngle: -0.8 + (f / 960) * 1.1,
opacity: reducedMotion ? 0.72 : ease(f, 0, 48) * (1 - ease(f, 858, 946)),
};
}
registry/remocn-templates/release-teaser/geometry.ts
export type Vec3 = [number, number, number];
export interface RingFace {
vertices: Vec3[];
normal: Vec3;
center: Vec3;
bevel: boolean;
edgeNormals?: [Vec3, Vec3];
}
const normalize = (v: Vec3): Vec3 => {
const length = Math.hypot(...v) || 1;
return [v[0] / length, v[1] / length, v[2] / length];
};
const subtract = (a: Vec3, b: Vec3): Vec3 => [
a[0] - b[0],
a[1] - b[1],
a[2] - b[2],
];
const cross = (a: Vec3, b: Vec3): Vec3 => [
a[1] * b[2] - a[2] * b[1],
a[2] * b[0] - a[0] * b[2],
a[0] * b[1] - a[1] * b[0],
];
export const dot = (a: Vec3, b: Vec3) =>
a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
function face(vertices: Vec3[], bevel: boolean): RingFace {
return {
vertices,
normal: normalize(
cross(
subtract(vertices[1], vertices[0]),
subtract(vertices[2], vertices[0]),
),
),
center: vertices.reduce<Vec3>(
(s, p) => [
s[0] + p[0] / vertices.length,
s[1] + p[1] / vertices.length,
s[2] + p[2] / vertices.length,
],
[0, 0, 0],
),
bevel,
};
}
/** A solid 320-degree annulus with a 40-degree opening and eight chamfered edges. */
export function createRing(segments = 96): RingFace[] {
const count = Math.max(12, Math.round(segments));
// Counter-clockwise cross-section: radius, depth. Front faces point toward +Z.
const profile = [
[144, 32],
[210, 32],
[220, 22],
[220, -22],
[210, -32],
[144, -32],
[134, -22],
[134, 22],
];
const section = (i: number): Vec3[] => {
const angle = ((-18 + (i / count) * 320) * Math.PI) / 180;
return profile.map(([r, z]) => [
r * Math.cos(angle),
r * Math.sin(angle),
z,
]);
};
const faces: RingFace[] = [];
for (let i = 0; i < count; i++) {
const a = section(i);
const b = section(i + 1);
for (let j = 0; j < profile.length; j++) {
const next = (j + 1) % profile.length;
const quad = face([a[j], a[next], b[next], b[j]], j % 2 === 1);
const halfStep = ((160 / count) * Math.PI) / 180;
// Continuous circumferential normals preserve the flat chamfers while
// allowing the renderer to shade each strip without visible tessellation.
quad.edgeNormals = [
rotate(quad.normal, 0, 0, -halfStep),
rotate(quad.normal, 0, 0, halfStep),
];
faces.push(quad);
}
}
faces.push(face(section(0).reverse(), false), face(section(count), false));
return faces;
}
export function rotate(v: Vec3, rx: number, ry: number, rz: number): Vec3 {
const y = v[1] * Math.cos(rx) - v[2] * Math.sin(rx);
const z = v[1] * Math.sin(rx) + v[2] * Math.cos(rx);
const x = v[0] * Math.cos(ry) + z * Math.sin(ry);
const depth = -v[0] * Math.sin(ry) + z * Math.cos(ry);
return [
x * Math.cos(rz) - y * Math.sin(rz),
x * Math.sin(rz) + y * Math.cos(rz),
depth,
];
}
/** Compute trigonometry once for a whole frame, not for every mesh vertex. */
export function createRotator(rx: number, ry: number, rz: number) {
const x = rotate([1, 0, 0], rx, ry, rz);
const y = rotate([0, 1, 0], rx, ry, rz);
const z = rotate([0, 0, 1], rx, ry, rz);
return (v: Vec3): Vec3 => [
x[0] * v[0] + y[0] * v[1] + z[0] * v[2],
x[1] * v[0] + y[1] * v[1] + z[1] * v[2],
x[2] * v[0] + y[2] * v[1] + z[2] * v[2],
];
}
export function project(
v: Vec3,
scale: number,
x: number,
y: number,
): [number, number] {
const perspective = 1100 / Math.max(300, 1100 - v[2] * scale);
return [x + v[0] * scale * perspective, y + v[1] * scale * perspective];
}
export { normalize };
registry/remocn-templates/release-teaser/background.tsx
import { useLayoutEffect, useRef } from "react";
import type { ReleaseTeaserScene } from "./content";
import {
createRing,
createRotator,
dot,
normalize,
project,
type Vec3,
} from "./geometry";
import { ringPose } from "./motion";
import { BrandImage } from "./ui";
const ring = createRing();
/** Resolve any browser CSS color to channels without bundling a color library. */
function channels(ctx: CanvasRenderingContext2D, color: string): number[] {
ctx.clearRect(0, 0, 1, 1);
ctx.fillStyle = "#000000";
ctx.fillStyle = color;
ctx.fillRect(0, 0, 1, 1);
return Array.from(ctx.getImageData(0, 0, 1, 1).data).slice(0, 3);
}
function DimensionalRing({
scene,
frame,
}: {
scene: ReleaseTeaserScene;
frame: number;
}) {
const canvas = useRef<HTMLCanvasElement>(null);
const colorCache = useRef(new Map<string, number[]>());
// Synchronous layout paint: a seek paints only its requested frame, with no RAF or history.
useLayoutEffect(() => {
const ctx = canvas.current?.getContext("2d", { willReadFrequently: true });
if (!ctx) return;
ctx.setTransform(1, 0, 0, 1, 0, 0);
const colorChannels = (color: string) => {
const cached = colorCache.current.get(color);
if (cached) return cached;
const value = channels(ctx, color);
colorCache.current.set(color, value);
return value;
};
const accent = colorChannels(scene.theme.accent);
const surface = colorChannels(scene.theme.surface);
ctx.clearRect(0, 0, 1920, 1080);
ctx.scale(2, 2);
const pose = ringPose(frame, scene.reducedMotion);
const rotatePoint = createRotator(pose.rx, pose.ry, pose.rz);
const key = normalize([Math.cos(pose.lightAngle) * 0.95, 0.6, 0.3]);
const rim = normalize([-0.8, -0.5, 0.18]);
const half = normalize([key[0], key[1], key[2] + 1]);
const faces = ring
.map((face) => ({
...face,
normal: rotatePoint(face.normal),
edgeNormals: face.edgeNormals?.map(rotatePoint),
center: rotatePoint(face.center),
vertices: face.vertices.map(rotatePoint),
}))
.sort((a, b) => a.center[2] - b.center[2]);
for (const face of faces) {
if (
dot(
face.normal,
normalize([
-face.center[0],
-face.center[1],
1100 / pose.scale - face.center[2],
]),
) <= 0
)
continue;
const shade = (normal: Vec3) => {
const diffuse = Math.max(0, dot(normal, key));
const rimLight = Math.max(0, dot(normal, rim));
const specular =
Math.max(0, dot(normal, half)) ** (face.bevel ? 26 : 48);
const intensity = scene.lightIntensity * pose.opacity;
const rgb = surface.map((c, i) =>
Math.min(
255,
c * 0.19 +
intensity *
(accent[i] * (diffuse ** 3 * 0.36 + rimLight ** 6 * 0.16) +
180 * specular * 0.68),
),
);
return `rgb(${rgb.join(",")})`;
};
const points = face.vertices.map((v) =>
project(v, pose.scale, pose.x, pose.y),
);
ctx.beginPath();
ctx.moveTo(...points[0]);
for (const p of points.slice(1)) ctx.lineTo(...p);
ctx.closePath();
if (face.edgeNormals) {
const gradient = ctx.createLinearGradient(
(points[0][0] + points[1][0]) / 2,
(points[0][1] + points[1][1]) / 2,
(points[2][0] + points[3][0]) / 2,
(points[2][1] + points[3][1]) / 2,
);
gradient.addColorStop(0, shade(face.edgeNormals[0]));
gradient.addColorStop(1, shade(face.edgeNormals[1]));
ctx.fillStyle = gradient;
} else {
ctx.fillStyle = shade(face.normal);
}
ctx.fill();
// A sub-pixel seam overlap prevents hairlines between adjoining mesh quads.
ctx.strokeStyle = ctx.fillStyle;
ctx.lineWidth = 0.45;
ctx.stroke();
}
}, [
frame,
scene.reducedMotion,
scene.lightIntensity,
scene.theme.accent,
scene.theme.surface,
]);
return (
<canvas
ref={canvas}
width={1920}
height={1080}
tabIndex={-1}
aria-hidden="true"
style={{ width: 960, height: 540, position: "absolute", inset: 0 }}
/>
);
}
export function TeaserBackground({
scene,
frame,
}: {
scene: ReleaseTeaserScene;
frame: number;
}) {
const pose = ringPose(frame, scene.reducedMotion);
return (
<div
style={{
position: "absolute",
inset: 0,
overflow: "hidden",
opacity: pose.opacity,
}}
>
{scene.logoSrc ? (
<div
style={{
position: "absolute",
left: pose.x - 220,
top: pose.y - 220,
width: 440,
height: 440,
scale: pose.scale,
rotate: `${(pose.rz * 180) / Math.PI}deg`,
opacity: 0.18 * scene.lightIntensity,
}}
>
<BrandImage
src={scene.logoSrc}
style={{ width: "100%", height: "100%", objectFit: "contain" }}
/>
</div>
) : (
<DimensionalRing scene={scene} frame={frame} />
)}
</div>
);
}
registry/remocn-templates/release-teaser/ui.tsx
import type { CSSProperties } from "react";
import { Img, useRemotionEnvironment } from "remotion";
export function BrandImage({
src,
style,
}: {
src: string;
style: CSSProperties;
}) {
const { isStudio, isRendering } = useRemotionEnvironment();
if (isStudio && !isRendering) {
// biome-ignore lint/performance/noImgElement: Studio-only media registration workaround; exports and embedded players retain Img's loading gate.
return <img src={src} alt="" style={style} />;
}
return <Img src={src} style={style} />;
}
/** Original open-orbit mark; the opening matches the dimensional background. */
export function OrbitMark({
color,
size = 30,
}: {
color: string;
size?: number;
}) {
return (
<svg
width={size}
height={size}
viewBox="-240 -240 480 480"
fill="none"
aria-hidden="true"
>
<path
d="M209.23 -67.98 A220 220 0 1 1 116.58 -186.57 L71.01 -113.64 A134 134 0 1 0 127.44 -41.41 Z"
fill={color}
/>
</svg>
);
}
registry/remocn-templates/release-teaser/scenes/statement.tsx
import { Interactive, interpolate } from "remotion";
import type { ReleaseTeaserScene } from "../content";
import { characterState, fitText, graphemes } from "../motion";
export function Statement({
scene,
text,
frame,
duration,
}: {
scene: ReleaseTeaserScene;
text: string;
frame: number;
duration: number;
}) {
return (
<Interactive.Div
name="Statement"
role="img"
aria-label={text}
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
fontSize: fitText(text),
fontWeight: 500,
lineHeight: 1.13,
color: scene.theme.foreground,
scale: scene.reducedMotion
? 1
: interpolate(frame, [0, duration], [0.99, 1.025], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
}}
>
{text.split("\n").map((line, lineIndex) => {
const chars = graphemes(line);
return (
<div
key={`${lineIndex}-${line}`}
style={{
whiteSpace: "pre",
textAlign: "center",
minHeight: "1.13em",
}}
>
{scene.reducedMotion
? line
: chars.map((char, index) => {
const state = characterState(
frame,
duration,
index,
chars.length,
);
return (
<span
key={`${index}-${char}`}
style={{
display: "inline-block",
opacity: state.opacity,
filter:
state.blur > 0.08
? `blur(${state.blur}px)`
: undefined,
translate: `0 ${state.y}px`,
}}
>
{char}
</span>
);
})}
</div>
);
})}
</Interactive.Div>
);
}
registry/remocn-templates/release-teaser/scenes/closing.tsx
import { Interactive, interpolate } from "remotion";
import type { ReleaseTeaserScene } from "../content";
import { ease, fitText } from "../motion";
import { BrandImage, OrbitMark } from "../ui";
export function ReleaseLockup({
scene,
frame,
}: {
scene: ReleaseTeaserScene;
frame: number;
}) {
const title = `${scene.brandName}${scene.release ? ` ${scene.release}` : ""}`;
const reveal = scene.reducedMotion ? 1 : ease(frame, 0, 32);
return (
<Interactive.Div
name="Release lockup"
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
opacity: reveal,
filter:
!scene.reducedMotion && reveal < 0.99
? `blur(${(1 - reveal) * 6}px)`
: undefined,
scale: scene.reducedMotion
? 1
: interpolate(frame, [0, 350], [0.97, 1.03], {
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}),
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 16,
marginBottom: 16,
}}
>
<div style={{ width: 34, height: 34, flexShrink: 0 }}>
{scene.logoSrc ? (
<BrandImage
src={scene.logoSrc}
style={{ width: 34, height: 34, objectFit: "contain" }}
/>
) : (
<OrbitMark size={34} color={scene.theme.accent} />
)}
</div>
<Interactive.Div
name="Brand and release"
style={{
fontSize: fitText(title, 70, 750),
fontWeight: 600,
lineHeight: 1,
color: scene.theme.foreground,
whiteSpace: "pre",
textAlign: "center",
}}
>
{title}
</Interactive.Div>
</div>
<Interactive.Div
name="Closing line"
style={{
fontSize: fitText(scene.tagline, 20, 740),
lineHeight: 1.3,
color: scene.theme.muted,
whiteSpace: "pre",
textAlign: "center",
opacity: scene.reducedMotion ? 1 : ease(frame, 8, 40),
}}
>
{scene.tagline}
</Interactive.Div>
</Interactive.Div>
);
}