{
  "slug": "outro-group-photo-launch",
  "title": "Outro group photo launch",
  "tagline": "Elements from across the whole film fly in from every direction to pose around the wordmark, staged like a launch-event finale.",
  "description": "Elements from across the whole film fly in from every direction to pose around the wordmark, staged like a launch-event finale.",
  "category": "outro",
  "tags": [
    "shotcraft",
    "remotion",
    "outro"
  ],
  "tier": "free",
  "license": "Apache-2.0",
  "spec": {
    "width": 960,
    "height": 540,
    "fps": 30,
    "duration": 4.83,
    "aspect": "16:9"
  },
  "kind": "remotion",
  "params_schema": {
    "type": "object",
    "properties": {}
  },
  "default_params": {},
  "preview": {
    "poster": "https://gimgs.net/motion/media/outro-group-photo-launch/poster-06bc73ea4bfe.jpg",
    "video": "https://gimgs.net/motion/media/outro-group-photo-launch/video-ee0228f97f55.mp4"
  },
  "variants": [
    {
      "key": "outro-group-photo-launch",
      "label": "outro-group-photo-launch",
      "description": "Every element from the film flies in from all directions to surround the wordmark for a group photo — a crane camera move landing on the final position, stage lighting, and gold dust make it read as a launch-event finale.",
      "use": "",
      "video": "https://gimgs.net/motion/media/outro-group-photo-launch/video-ee0228f97f55.mp4",
      "poster": "https://gimgs.net/motion/media/outro-group-photo-launch/poster-06bc73ea4bfe.jpg"
    }
  ],
  "credits": {
    "author": "video-shotcraft (Vincent Wei)",
    "url": "https://github.com/Vincentwei1021/video-shotcraft"
  },
  "page_url": "https://gimgs.net/motion/outro-group-photo-launch.html",
  "json_url": "https://gimgs.net/motion/outro-group-photo-launch.json",
  "template_url": "https://gimgs.net/motion/outro-group-photo-launch/template.html",
  "play_url": null,
  "markdown_url": "https://gimgs.net/motion/outro-group-photo-launch.md",
  "updated_at": "2026-09-24T07:40:10.275Z",
  "prompt": "## When to use\n\nOutro / brand sign-off; a \"family photo\" closing shot for multi-feature products\n\n## Intention\n\nPull one representative element from every feature shown so far and call them back for a group photo, with the wordmark landing last as the headliner — the last thing the viewer remembers before leaving is \"these all belong to the same product.\" The spec has to be launch-event grade: energy pushed to the highest point in the whole film.\n\n## Duration & energy\n\n- Duration: About 4.8s (145f)\n- Energy: Peak (the highest point of the whole film)\n\n## Shot recipe (original Chinese — timing, easing, parameter table, known pitfalls)\n\n## 意图\n把看过的每个功能各抽一个代表元素叫回来合影，字标压轴登场——观众离场前最后记住的是\"这些东西属于同一个产品\"。规格必须是发布会级：能量推到全片最高。\n\n## 动效核心\n- 9 个页面元素（nav/卡片/内容条/搜索框/统计条/文档头）从四方带旋转飞入，落地强调色（模板片为琥珀）glow\n- 字标 letterpress 逐字登场，全员\"退后排\"让主角；rule 长出、延长线射出\n- crane 运镜：合影层整体 rotateX 4°→0 + scale 落下，之后缓推\n- 发布会氛围三件套：开场光带扫过、字标背后舞台光、金尘上飘\n- 背景页面 blur 化作景深\n\n## 参数表\n| 参数 | 典型值 | 调节手感 |\n|------|--------|----------|\n| 元素飞入 | 9 元素 cue 从 4 起每 3f 一个，各飞 12f；起点偏移 ±360–500px；渲染顺序=cue 顺序，后到者叠上 | 每个已展示过的功能都要有代表元素（Q8 自检项），缺谁观众都会察觉 |\n| 飞入缓动 | bezier(0.34,1.4,0.44,1) | 代码注释点名教训：旧 bezier(0.25,0.9,0.3,1) \"never crossed 1\" 没有真过冲——落地要弹 y1 必须 >1 |\n| 飞行姿态 | 飞行中 rot×2→settled rot（±5° 内）、scale ×1.12→1；ghost 残影滞后 8% blur 8px；落地强调色 glow 6f（0.35→0）multiply | 落定角度收进 ±5° 内——合影要整齐但不刻板 |\n| 退后排 | 字标登场时 42–50f 全员 opacity −12%、saturate −8% | 主角登场配角必须让位，不让位字标压不住 9 个元素 |\n| crane 运镜 | `perspective(1400px) rotateX(4°→0) scale(1.06→1)` 前 40f 落下，之后缓推 +0.035 | crane 幅度就这么小（4°）已经够\"落机位\"读感，大了会晕（推测） |\n| 氛围三件套 | 光带 2–14f（600px 宽 overlay 峰值 opacity 0.12）；舞台光 42→50→58f 0→0.5→0.25；金尘 20 颗 2–3px、opacity 0.15–0.35、全参数 index 派生确定性 | 粒子/光效参数必须确定性派生（硬规则：渲染必须可复现）；金尘超过 30 颗开始像雪（推测） |\n| 字标 | 字母 delay=42+i·1.8、8f；rule 58→70f 长出、两端 190px 延长线 8f 射出 6f 淡去；字距 62–66f 呼吸；背景页 24f 内 blur 0→14；落定后 sign-off hold 30f | 字标落定 hold 满 1 秒（R1 定案）；outro 不加解说 caption 保持干净（C1 例外项） |\n\n## 声音\n固定三拍句式：riser-cine 铺进组装（模板片钉在 f945）→ impact-deep-whoosh 钉字标 stamp（f980，vol 0.55 全片峰值）→ sparkle 点 rule（f1005）。这是模板片定稿后唯一从未改动的段落句式（S2；详见 sound-design 4.3）。\n\n## 已知坑\n- 初版收尾几乎总是偏保守（Q8：安静签名式→合影→发布会三级加码）——起稿就按\"像一个产品发布\"的规格给足 crane/舞台光/粒子\n- 结构先于特效：先有\"四方飞入合影\"结构再加码氛围；直接堆特效没有骨架\n- outro tagline 与字标下方文案查重（P4 dedupe 判例），同一句话全片只出现一次\n\n## 参考实现\ndemos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx（原 template/src/aifl/live/SceneOutroLive.tsx；30f sign-off hold 时长见 template/src/aifl/Main.tsx 时间线）\n\n## Reference implementation (Remotion)\n\n- demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx — https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx\n\nThe component imports shared fixtures from `demos/_fixtures` and helpers from `assets/lib` in the repo; SKILL.md there documents the render workflow.\n",
  "flow": [
    {
      "step": "Read the recipe",
      "note": "Prompt = when to use, intention, parameter table with typical values and how each one feels, known pitfalls. Treat values as calibrated starting points."
    },
    {
      "step": "Get the code",
      "tool": "git",
      "cmd": "git clone https://github.com/Vincentwei1021/video-shotcraft && cd video-shotcraft && npm install"
    },
    {
      "step": "Register + render",
      "tool": "remotion",
      "cmd": "npx remotion render <CompositionId> out.mp4   # component: demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx"
    },
    {
      "step": "Or re-implement",
      "note": "Port the timing/easing from the recipe to HTML + WAAPI (gimgs motion contract) or any engine; keep the same segment windows and easing."
    }
  ],
  "files": [
    {
      "path": "demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx",
      "url": "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx"
    }
  ],
  "contract": "React/Remotion composition (TSX) from the open-source video-shotcraft library (Apache-2.0). It is not a browser-playable template: clone the repo, register the component in a Remotion project and render with `npx remotion render`. The recipe (prompt) describes timing, easing and parameters so you can also re-implement the shot in any engine.",
  "render": {
    "engine": "remotion",
    "frames": 145,
    "repo": "https://github.com/Vincentwei1021/video-shotcraft",
    "files": [
      "https://raw.githubusercontent.com/Vincentwei1021/video-shotcraft/main/demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx"
    ],
    "note": "Clone https://github.com/Vincentwei1021/video-shotcraft (npm install). The component lives under demos/{category}/{name}/ and imports shared fixtures from demos/_fixtures and helpers from assets/lib. Copy it (with those imports) into a Remotion project, register a <Composition> with the template's width/height/fps/duration in Root.tsx, then `npx remotion render <CompositionId> out.mp4`. SKILL.md in the repo documents the full Claude Code / Codex workflow. The prompt below is the shot recipe (timing, easing, parameter table, pitfalls) — enough to re-implement the shot in HTML/WAAPI or another engine if you prefer."
  },
  "source": "// ===== demos/outro/outro-group-photo-launch/OutroGroupPhotoLaunch.tsx =====\n// outro-group-photo-launch —— 全片元素四方飞来围住字标合影，crane 落机位 + 舞台光 +\n// 金尘做成发布会收场。outro/品牌收尾，多功能产品\"全家福\"式终镜，能量推到全片最高。\n// 参考实现从 template SceneOutroLive 剥离（self-contained）：\n// 9 个页面元素从四方带旋转飞入（cue 4 起每 3f 一个，飞 12f，bezier y1>1 真过冲，\n// 渲染顺序=cue 顺序后到者叠上），落地强调色 glow（0.35→0）；字标 letterpress 逐字\n// 登场（delay=42+i·1.8）全员退后排（opacity −12% / saturate −8%）；rule 长出 +\n// 190px 延长线射出；crane 运镜 perspective rotateX(4°→0) + scale 落下后缓推；\n// 发布会三件套：开场光带扫过、字标背后舞台光、20 颗确定性金尘上飘；背景页 blur 化\n// 景深；落定后 sign-off hold 30f。outro 不加解说 caption 保持干净。\nimport { AbsoluteFill, Img, interpolate, staticFile, useCurrentFrame, Easing } from 'remotion';\nimport { PageCam2D } from '../../_fixtures/PageCam2D';\nimport layout from '../../_textures/live-layout.json';\n\nexport const OUTRO_GROUP_PHOTO_LAUNCH_DURATION = 145;\n\nconst SERIF = 'ui-serif, Georgia, \"Times New Roman\", serif';\nconst MONO = 'ui-monospace, SFMono-Regular, Menlo, monospace';\nconst LETTERS = 'AI Foundation Lab'.split('');\nconst PAGE_H = layout.projects.pageH;\nconst WBR_PAGE_H = layout.wbr.pageH;\n\nconst FLY_EASE = Easing.bezier(0.34, 1.4, 0.44, 1);\nconst CRANE_EASE = Easing.bezier(0.3, 0, 0.2, 1);\n\ntype FlyEl = {\n  key: string;\n  file: string;\n  w: number;\n  h: number;\n  cx: number;\n  cy: number;\n  scale: number;\n  rot: number;\n  dx: number;\n  dy: number;\n  radius: number;\n  cue: number;\n  wbrCrop?: boolean;\n};\n\n// render order = cue order, so later arrivals stack on top\nconst ELS: FlyEl[] = [\n  { key: 'nav', file: 'nav.png', w: 1920, h: 61, cx: 960, cy: 84, scale: 0.62, rot: 0, dx: 0, dy: -120, radius: 10, cue: 4 },\n  { key: 'card4', file: 'card4-hires.png', w: 358, h: 312, cx: 300, cy: 400, scale: 0.72, rot: -5, dx: -500, dy: 0, radius: 16, cue: 7 },\n  { key: 'card1', file: 'card1.png', w: 358, h: 288, cx: 1630, cy: 380, scale: 0.68, rot: 4, dx: 500, dy: 0, radius: 16, cue: 10 },\n  { key: 'paper1', file: 'paper1.png', w: 1104, h: 225, cx: 1460, cy: 730, scale: 0.5, rot: -3, dx: 450, dy: 260, radius: 12, cue: 13 },\n  { key: 'card7', file: 'card7.png', w: 358, h: 312, cx: 260, cy: 800, scale: 0.6, rot: 3, dx: -400, dy: 300, radius: 16, cue: 16 },\n  { key: 'paper3', file: 'paper3.png', w: 1104, h: 225, cx: 620, cy: 930, scale: 0.48, rot: 2, dx: 0, dy: 320, radius: 12, cue: 19 },\n  { key: 'search', file: 'float-search.png', w: 1016, h: 44, cx: 760, cy: 180, scale: 0.55, rot: -1.5, dx: 0, dy: -240, radius: 10, cue: 22 },\n  { key: 'stats', file: 'float-stats.png', w: 315, h: 56, cx: 1560, cy: 950, scale: 0.9, rot: -2, dx: 380, dy: 0, radius: 10, cue: 25 },\n  { key: 'wbr', file: 'wbr-full.png', w: 688, h: 54, cx: 1620, cy: 170, scale: 0.8, rot: 2.5, dx: 360, dy: -200, radius: 8, cue: 28, wbrCrop: true },\n];\n\n// 20 gold dust motes, all parameters index-derived (deterministic)\nconst DUST = Array.from({ length: 20 }, (_, i) => ({\n  x: (i * 439 + 137) % 1920,\n  y0: (i * 613 + 271) % 1080,\n  rise: 0.3 + (i % 5) * 0.11,\n  swayAmp: 9 + (i % 4) * 5,\n  swayFreq: 0.022 + (i % 3) * 0.008,\n  phase: (i * 0.83) % (Math.PI * 2),\n  size: 2 + (i % 3) * 0.5,\n  opacity: 0.15 + ((i * 7) % 5) * 0.05,\n}));\n\nexport const OutroGroupPhotoLaunch: React.FC = () => {\n  const frame = useCurrentFrame();\n  const duration = OUTRO_GROUP_PHOTO_LAUNCH_DURATION;\n\n  const blur = interpolate(frame, [0, 24], [0, 14], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.4, 1),\n  });\n  const rule = interpolate(frame, [58, 70], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1),\n  });\n  const tag = interpolate(frame, [68, 80], [0, 1], { extrapolateLeft: 'clamp', extrapolateRight: 'clamp' });\n  const fadeOut = interpolate(frame, [duration - 12, duration], [1, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const recede = interpolate(frame, [42, 50], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // crane on the whole group-photo layer\n  const craneT = interpolate(frame, [0, 40], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: CRANE_EASE,\n  });\n  const pushT = interpolate(frame, [40, duration], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const camScale = 1.06 - 0.06 * craneT + 0.035 * pushT;\n  const camTilt = 4 * (1 - craneT);\n\n  // opening light sweep\n  const sweepX = interpolate(frame, [2, 14], [-700, 2020], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.4, 0, 0.6, 1),\n  });\n  const sweepOpacity = interpolate(frame, [2, 5, 11, 14], [0, 0.12, 0.12, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // stage light behind the wordmark\n  const stageLight = interpolate(frame, [42, 50, 58], [0, 0.5, 0.25], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n  const vignette = interpolate(frame, [42, 54], [0, 0.1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // rule extension lines\n  const ruleExt = interpolate(frame, [58, 66], [0, 1], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1),\n  });\n  const ruleExtFade = interpolate(frame, [66, 72], [1, 0], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n  });\n\n  // one breath of letter-spacing once the wordmark is fully set\n  const wordSpacing = interpolate(frame, [62, 66], [-0.01, 0.005], {\n    extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.3, 0, 0.2, 1),\n  });\n\n  return (\n    <AbsoluteFill style={{ opacity: fadeOut }}>\n      {/* group-photo layer under a slow crane-in camera */}\n      <AbsoluteFill\n        style={{\n          transform: `perspective(1400px) rotateX(${camTilt}deg) scale(${camScale})`,\n          transformOrigin: '50% 45%',\n        }}\n      >\n        {/* blur 只作用背景页：PageCam2D 自闭合，飞入元素放在未过滤的兄弟层\n            （屏幕空间坐标，与 template SceneOutroLive 同构）——放进 children\n            会连\"合影元素\"一起糊掉，只剩字标清晰 */}\n        <PageCam2D src=\"textures/live/projects-full.png\" pageH={PAGE_H} keys={[{ frame: 0, cx: 960, cy: 700, zoom: 0.75 }]} blur={blur} />\n        {/* warm scrim under the flying elements */}\n        <AbsoluteFill style={{ background: 'radial-gradient(1200px 800px at 50% 48%, rgba(250,247,242,0.82), rgba(250,247,242,0.55) 60%, rgba(250,247,242,0.35))', pointerEvents: 'none' }} />\n\n        {/* group photo: elements fly in from all sides and settle around the wordmark */}\n        <AbsoluteFill style={{ pointerEvents: 'none' }}>\n          {ELS.map((el) => {\n            if (frame < el.cue) return null;\n            const t = interpolate(frame, [el.cue, el.cue + 12], [0, 1], {\n              extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: FLY_EASE,\n            });\n            const opacity = interpolate(frame, [el.cue, el.cue + 3], [0, 1], {\n              extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n            });\n            const x = el.dx * (1 - t);\n            const y = el.dy * (1 - t);\n            const rot = el.rot * (2 - t);\n            const scale = el.scale * (1.12 - 0.12 * t);\n            const air = Math.max(0, 1 - t);\n            const shadow =\n              air > 0.01\n                ? `0 ${10 + 26 * air}px ${24 + 46 * air}px rgba(30,25,18,${0.16 + 0.1 * air}), 0 2px 6px rgba(30,25,18,.08)`\n                : '0 10px 24px rgba(30,25,18,.16), 0 2px 6px rgba(30,25,18,.08)';\n            const settledOpacity = opacity * (1 - 0.12 * recede);\n            const saturate = 1 - 0.08 * recede;\n            const texture = el.wbrCrop\n              ? {\n                  background: `#fff url(${staticFile(`textures/live/${el.file}`)}) -576px -173px / 1920px ${WBR_PAGE_H}px no-repeat`,\n                  border: '1px solid oklch(90% .008 82)',\n                }\n              : null;\n            const linT = interpolate(frame, [el.cue, el.cue + 12], [0, 1], {\n              extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n            });\n            const showGhost = linT > 0.05 && linT < 0.95;\n            const glow = interpolate(frame, [el.cue + 12, el.cue + 18], [0.35, 0], {\n              extrapolateLeft: 'clamp', extrapolateRight: 'clamp',\n            });\n            const showGlow = frame >= el.cue + 12 && frame < el.cue + 18;\n            const glowR = el.w * el.scale * 0.5;\n\n            return (\n              <div key={el.key}>\n                {showGhost ? (\n                  <div\n                    style={{\n                      position: 'absolute', left: el.cx - el.w / 2, top: el.cy - el.h / 2,\n                      width: el.w, height: el.h,\n                      transform: `translate(${x + el.dx * 0.08}px, ${y + el.dy * 0.08}px) rotate(${rot}deg) scale(${scale})`,\n                      transformOrigin: 'center center', borderRadius: el.radius, overflow: 'hidden',\n                      opacity: 0.2 * Math.max(0, 1 - linT), filter: 'blur(8px)', ...texture,\n                    }}\n                  >\n                    {el.wbrCrop ? null : (\n                      <Img src={staticFile(`textures/live/${el.file}`)} style={{ position: 'absolute', inset: 0, width: el.w, height: el.h, display: 'block' }} />\n                    )}\n                  </div>\n                ) : null}\n                <div\n                  style={{\n                    position: 'absolute', left: el.cx - el.w / 2, top: el.cy - el.h / 2,\n                    width: el.w, height: el.h,\n                    transform: `translate(${x}px, ${y}px) rotate(${rot}deg) scale(${scale})`,\n                    transformOrigin: 'center center', borderRadius: el.radius, overflow: 'hidden',\n                    boxShadow: shadow, opacity: settledOpacity, filter: `saturate(${saturate})`, ...texture,\n                  }}\n                >\n                  {el.wbrCrop ? null : (\n                    <Img src={staticFile(`textures/live/${el.file}`)} style={{ position: 'absolute', inset: 0, width: el.w, height: el.h, display: 'block' }} />\n                  )}\n                </div>\n                {showGlow ? (\n                  <div\n                    style={{\n                      position: 'absolute', left: el.cx - glowR, top: el.cy - glowR,\n                      width: glowR * 2, height: glowR * 2, borderRadius: '50%',\n                      background: 'radial-gradient(circle, oklch(78% 0.13 70 / 0.9), oklch(78% 0.13 70 / 0) 70%)',\n                      opacity: glow, mixBlendMode: 'multiply',\n                    }}\n                  />\n                ) : null}\n              </div>\n            );\n          })}\n        </AbsoluteFill>\n      </AbsoluteFill>\n\n      {/* gold dust drifting up in front of the group photo */}\n      <AbsoluteFill style={{ pointerEvents: 'none' }}>\n        {DUST.map((d, i) => {\n          const y = (((d.y0 - frame * d.rise) % 1080) + 1080) % 1080;\n          const x = d.x + Math.sin(frame * d.swayFreq + d.phase) * d.swayAmp;\n          return (\n            <div\n              key={i}\n              style={{\n                position: 'absolute', left: x, top: y, width: d.size, height: d.size,\n                borderRadius: '50%', background: 'oklch(75% 0.08 85)', opacity: d.opacity,\n              }}\n            />\n          );\n        })}\n      </AbsoluteFill>\n\n      {/* opening light sweep */}\n      {sweepOpacity > 0 ? (\n        <AbsoluteFill style={{ pointerEvents: 'none', mixBlendMode: 'overlay' }}>\n          <div\n            style={{\n              position: 'absolute', top: 0, bottom: 0, left: sweepX - 300, width: 600,\n              background: 'linear-gradient(90deg, rgba(255,244,224,0), rgba(255,244,224,1) 50%, rgba(255,244,224,0))',\n              opacity: sweepOpacity,\n            }}\n          />\n        </AbsoluteFill>\n      ) : null}\n\n      {/* stage light behind the wordmark */}\n      {stageLight > 0 ? (\n        <AbsoluteFill\n          style={{\n            pointerEvents: 'none',\n            background: 'radial-gradient(700px 360px at 960px 470px, rgba(255,246,228,0.95), rgba(255,246,228,0.35) 55%, rgba(255,246,228,0) 75%)',\n            opacity: stageLight,\n          }}\n        />\n      ) : null}\n\n      {/* closing vignette */}\n      {vignette > 0 ? (\n        <AbsoluteFill\n          style={{\n            pointerEvents: 'none',\n            background: 'radial-gradient(1400px 900px at 50% 50%, rgba(62,48,32,0) 55%, rgba(62,48,32,0.7) 100%)',\n            opacity: vignette,\n          }}\n        />\n      ) : null}\n\n      {/* wordmark sign-off */}\n      <AbsoluteFill style={{ justifyContent: 'center', alignItems: 'center', pointerEvents: 'none' }}>\n        <div style={{ textAlign: 'center' }}>\n          <div style={{ fontFamily: SERIF, fontSize: 148, fontWeight: 600, color: 'oklch(18% 0.006 82)', letterSpacing: `${wordSpacing}em`, display: 'flex' }}>\n            {LETTERS.map((ch, i) => {\n              const delay = Math.round(42 + i * 1.8);\n              const t = interpolate(frame, [delay, delay + 8], [0, 1], {\n                extrapolateLeft: 'clamp', extrapolateRight: 'clamp', easing: Easing.bezier(0.2, 0.75, 0.3, 1),\n              });\n              return (\n                <span\n                  key={i}\n                  style={{\n                    opacity: t, transform: `translateY(${(1 - t) * 28}px) scale(${1.35 - 0.35 * t})`,\n                    filter: `blur(${(1 - t) * 8}px)`, display: 'inline-block', whiteSpace: 'pre',\n                  }}\n                >\n                  {ch}\n                </span>\n              );\n            })}\n          </div>\n          <div style={{ position: 'relative', height: 6, width: 260, margin: '34px auto 0' }}>\n            <div style={{ position: 'absolute', inset: 0, borderRadius: 3, background: 'oklch(52% 0.115 65)', transform: `scaleX(${rule})` }} />\n            {ruleExt > 0 && ruleExtFade > 0 ? (\n              <>\n                <div style={{ position: 'absolute', top: 2.5, height: 1, right: '100%', width: 190 * ruleExt, background: 'oklch(52% 0.115 65)', opacity: ruleExtFade }} />\n                <div style={{ position: 'absolute', top: 2.5, height: 1, left: '100%', width: 190 * ruleExt, background: 'oklch(52% 0.115 65)', opacity: ruleExtFade }} />\n              </>\n            ) : null}\n          </div>\n          <div style={{ fontFamily: MONO, fontSize: 25, letterSpacing: '0.14em', color: 'oklch(50% 0.006 82)', marginTop: 30, opacity: tag, textTransform: 'uppercase' }}>\n            Team Research Console\n          </div>\n        </div>\n      </AbsoluteFill>\n    </AbsoluteFill>\n  );\n};\n\n\n"
}