StealThis .dev

Remotion — Radio / Podcast Audio Card

A polished 4-second Remotion animation showcasing a broadcast-style Radio/Podcast 'Now On Air' social card — featuring an animated SVG radio tower with expanding signal rings, a pulsing ON AIR badge, bold station branding, show title and host credit, a 30-bar live waveform driven by layered sine waves, and a show progress bar that fills from 0% to 60% over the clip.

Open Remotion
remotion react typescript
Targets: TS React

Preview

Code

import React from "react";
import {
  AbsoluteFill,
  interpolate,
  spring,
  useCurrentFrame,
  useVideoConfig,
  Easing,
} from "remotion";

// ─── Station metadata ─────────────────────────────────────────────────────────
const STATION_NAME = "NOVA FM 94.7";
const STATION_ABBR = "NF";
const SHOW_TITLE = "Late Night Frequencies";
const HOST_NAME = "with DJ Solara";
const NEXT_SHOW = "Morning Pulse";
const NEXT_TIME = "6:00 AM";
const SOCIAL_HANDLE = "@novafm947";

// ─── Design tokens ────────────────────────────────────────────────────────────
const BG = "#0a0a0f";
const SURFACE_CARD = "#12121a";
const ACCENT = "#a855f7";
const ACCENT_2 = "#06b6d4";
const ACCENT_3 = "#ec4899";
const TEXT = "#f1f5f9";
const MUTED = "#94a3b8";

const BAR_COUNT = 30;

// ─── Audio simulation helpers ─────────────────────────────────────────────────
function simWaveBar(frame: number, barIndex: number): number {
  const f = frame;
  const i = barIndex;
  // Layer four sine waves per bar for rich organic feel
  const w1 = Math.sin(f * 0.14 + i * 0.52) * 0.38;
  const w2 = Math.sin(f * 0.08 + i * 1.05 + 1.1) * 0.25;
  const w3 = Math.sin(f * 0.27 + i * 0.33 + 2.4) * 0.18;
  const w4 = Math.sin(f * 0.41 + i * 0.71 + 0.6) * 0.10;
  return Math.max(0.06, 0.45 + w1 + w2 + w3 + w4);
}

// ─── On-Air Badge ─────────────────────────────────────────────────────────────
const OnAirBadge: React.FC<{ frame: number; entranceOpacity: number }> = ({
  frame,
  entranceOpacity,
}) => {
  // Pulse: scale oscillates 1 → 1.05 every ~60 frames (2 seconds)
  const pulseCycle = Math.sin(frame * (Math.PI / 30));
  const scale = 1 + pulseCycle * 0.04;
  // Glow intensity pulsing in sync
  const glowAlpha = 0.55 + pulseCycle * 0.25;

  return (
    <div
      style={{
        opacity: entranceOpacity,
        transform: `scale(${scale})`,
        display: "inline-flex",
        alignItems: "center",
        gap: 8,
        padding: "6px 18px",
        borderRadius: 100,
        background: `linear-gradient(135deg, ${ACCENT_3}, ${ACCENT})`,
        boxShadow: `0 0 20px rgba(236,72,153,${glowAlpha}), 0 0 40px rgba(168,85,247,${glowAlpha * 0.5})`,
      }}
    >
      {/* Live dot */}
      <div
        style={{
          width: 8,
          height: 8,
          borderRadius: "50%",
          background: "#fff",
          boxShadow: "0 0 8px rgba(255,255,255,0.9)",
          opacity: 0.85 + Math.sin(frame * 0.25) * 0.15,
        }}
      />
      <span
        style={{
          fontFamily: "Inter, sans-serif",
          fontWeight: 800,
          fontSize: 14,
          color: "#fff",
          letterSpacing: "0.18em",
          textTransform: "uppercase" as const,
        }}
      >
        ON AIR
      </span>
    </div>
  );
};

// ─── Radio Tower Icon with signal rings ──────────────────────────────────────
const RadioTower: React.FC<{ frame: number; opacity: number }> = ({
  frame,
  opacity,
}) => {
  // Three rings animate outward in sequence, each offset by 20 frames
  const ringConfigs = [
    { delay: 0, maxR: 36, strokeW: 2 },
    { delay: 20, maxR: 56, strokeW: 1.5 },
    { delay: 40, maxR: 76, strokeW: 1 },
  ];

  return (
    <div
      style={{
        opacity,
        display: "flex",
        flexDirection: "column" as const,
        alignItems: "center",
        gap: 16,
      }}
    >
      <svg
        width="120"
        height="120"
        viewBox="0 0 120 120"
        style={{ overflow: "visible" }}
      >
        {/* Tower base structure */}
        <g transform="translate(60, 60)">
          {/* Main mast */}
          <line
            x1="0"
            y1="10"
            x2="0"
            y2="42"
            stroke={ACCENT}
            strokeWidth="3"
            strokeLinecap="round"
          />
          {/* Left strut */}
          <line
            x1="0"
            y1="42"
            x2="-18"
            y2="58"
            stroke={ACCENT}
            strokeWidth="2.5"
            strokeLinecap="round"
          />
          {/* Right strut */}
          <line
            x1="0"
            y1="42"
            x2="18"
            y2="58"
            stroke={ACCENT}
            strokeWidth="2.5"
            strokeLinecap="round"
          />
          {/* Left base */}
          <line
            x1="-18"
            y1="58"
            x2="-26"
            y2="62"
            stroke={ACCENT}
            strokeWidth="2.5"
            strokeLinecap="round"
          />
          {/* Right base */}
          <line
            x1="18"
            y1="58"
            x2="26"
            y2="62"
            stroke={ACCENT}
            strokeWidth="2.5"
            strokeLinecap="round"
          />
          {/* Cross brace left */}
          <line
            x1="-8"
            y1="48"
            x2="8"
            y2="54"
            stroke={ACCENT}
            strokeWidth="1.5"
            strokeLinecap="round"
            opacity={0.6}
          />
          {/* Cross brace right */}
          <line
            x1="8"
            y1="48"
            x2="-8"
            y2="54"
            stroke={ACCENT}
            strokeWidth="1.5"
            strokeLinecap="round"
            opacity={0.6}
          />
          {/* Antenna tip dot */}
          <circle
            cx="0"
            cy="7"
            r="4"
            fill={ACCENT_3}
            style={{
              filter: `drop-shadow(0 0 6px ${ACCENT_3})`,
            }}
          />
          {/* Tip glow pulse */}
          <circle
            cx="0"
            cy="7"
            r={5 + Math.sin(frame * 0.22) * 2}
            fill="none"
            stroke={ACCENT_3}
            strokeWidth="1.5"
            opacity={0.4 + Math.sin(frame * 0.22) * 0.3}
          />

          {/* Animated signal rings - staggered outward */}
          {ringConfigs.map((cfg, idx) => {
            // Each ring loops on a 60-frame period, offset by delay
            const ringFrame = (frame - cfg.delay + 180) % 60;
            const ringProgress = ringFrame / 60;
            const r = 14 + ringProgress * (cfg.maxR - 14);
            const ringOpacity = interpolate(
              ringProgress,
              [0, 0.3, 0.8, 1.0],
              [0, 0.8, 0.4, 0],
              { extrapolateLeft: "clamp", extrapolateRight: "clamp" }
            );

            return (
              <circle
                key={idx}
                cx="0"
                cy="7"
                r={r}
                fill="none"
                stroke={ACCENT}
                strokeWidth={cfg.strokeW}
                opacity={ringOpacity}
                style={{
                  filter: `drop-shadow(0 0 4px ${ACCENT})`,
                }}
              />
            );
          })}
        </g>
      </svg>
    </div>
  );
};

// ─── Station Logo Circle ──────────────────────────────────────────────────────
const StationLogo: React.FC<{ opacity: number; frame: number }> = ({
  opacity,
  frame,
}) => {
  const glowPulse = 0.5 + Math.sin(frame * 0.15) * 0.25;
  return (
    <div
      style={{
        opacity,
        width: 80,
        height: 80,
        borderRadius: "50%",
        background: `linear-gradient(135deg, ${ACCENT}, ${ACCENT_3})`,
        boxShadow: `0 0 24px rgba(168,85,247,${glowPulse}), 0 0 48px rgba(168,85,247,${glowPulse * 0.4})`,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        position: "relative" as const,
        flexShrink: 0,
      }}
    >
      {/* Inner sheen */}
      <div
        style={{
          position: "absolute",
          inset: 0,
          borderRadius: "50%",
          background:
            "radial-gradient(circle at 35% 30%, rgba(255,255,255,0.22) 0%, transparent 65%)",
        }}
      />
      <span
        style={{
          fontFamily: "Inter, sans-serif",
          fontWeight: 900,
          fontSize: 24,
          color: "#fff",
          letterSpacing: "-0.02em",
          position: "relative" as const,
          zIndex: 1,
        }}
      >
        {STATION_ABBR}
      </span>
    </div>
  );
};

// ─── Waveform bars ────────────────────────────────────────────────────────────
const WaveformBars: React.FC<{ frame: number; opacity: number }> = ({
  frame,
  opacity,
}) => {
  const BAR_W = 7;
  const BAR_GAP = 3;
  const MAX_H = 80;

  return (
    <div
      style={{
        opacity,
        display: "flex",
        alignItems: "flex-end",
        gap: BAR_GAP,
        height: MAX_H,
      }}
    >
      {Array.from({ length: BAR_COUNT }, (_, i) => {
        const amp = simWaveBar(frame, i);
        const barH = Math.max(4, Math.round(amp * MAX_H));
        // Gradient from purple (left) to pink (center) to cyan (right)
        const t = i / (BAR_COUNT - 1);
        const hue = 270 - t * 80; // 270 (purple) → 190 (cyan-ish)
        const barColor =
          i < BAR_COUNT / 2
            ? `hsl(${270 - t * 60}, 90%, 65%)`
            : `hsl(${210 + (t - 0.5) * 60}, 90%, 65%)`;

        return (
          <div
            key={i}
            style={{
              width: BAR_W,
              height: barH,
              borderRadius: 4,
              background: barColor,
              boxShadow: `0 0 6px ${barColor}`,
              flexShrink: 0,
            }}
          />
        );
      })}
    </div>
  );
};

// ─── Show Progress Bar ────────────────────────────────────────────────────────
const ShowProgressBar: React.FC<{ progress: number }> = ({ progress }) => {
  const elapsed = Math.round(progress * 60); // 0→60 minutes
  const elapsedStr = `${elapsed}:00`;

  return (
    <div style={{ width: "100%" }}>
      <div
        style={{
          display: "flex",
          justifyContent: "space-between",
          marginBottom: 6,
          fontFamily: "Inter, sans-serif",
          fontSize: 11,
          color: MUTED,
          letterSpacing: "0.04em",
        }}
      >
        <span>{elapsedStr}</span>
        <span>60:00</span>
      </div>
      <div
        style={{
          width: "100%",
          height: 5,
          borderRadius: 3,
          background: "rgba(255,255,255,0.10)",
          position: "relative" as const,
          overflow: "hidden",
        }}
      >
        <div
          style={{
            position: "absolute" as const,
            left: 0,
            top: 0,
            height: "100%",
            width: `${progress * 100}%`,
            background: `linear-gradient(90deg, ${ACCENT}, ${ACCENT_3})`,
            borderRadius: 3,
            boxShadow: `0 0 12px rgba(168,85,247,0.7)`,
          }}
        />
        {/* Scrubber dot */}
        <div
          style={{
            position: "absolute" as const,
            top: "50%",
            left: `${progress * 100}%`,
            transform: "translate(-50%, -50%)",
            width: 10,
            height: 10,
            borderRadius: "50%",
            background: "#fff",
            boxShadow: "0 0 10px rgba(255,255,255,0.85)",
          }}
        />
      </div>
    </div>
  );
};

// ─── Main Component ───────────────────────────────────────────────────────────
export const RadioCard: React.FC = () => {
  const frame = useCurrentFrame();
  const { fps, width, height, durationInFrames } = useVideoConfig();

  // ── Card entrance: scale 0.8 → 1 with spring ─────────────────────────────
  const cardScale = spring({
    frame,
    fps,
    config: { damping: 16, stiffness: 110, mass: 1.0 },
    durationInFrames: 28,
    from: 0.8,
    to: 1,
  });
  const cardOpacity = interpolate(frame, [0, 12], [0, 1], {
    extrapolateRight: "clamp",
  });

  // ── Staggered section fade-ins (tower @ f15, text @ f30, waveform @ f45) ─
  const towerOpacity = interpolate(frame, [15, 30], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const textOpacity = interpolate(frame, [30, 48], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const waveformOpacity = interpolate(frame, [45, 62], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });
  const bottomOpacity = interpolate(frame, [55, 70], [0, 1], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
  });

  // Text slide-in from left (staggered)
  const textSlideX = interpolate(frame, [30, 52], [24, 0], {
    extrapolateLeft: "clamp",
    extrapolateRight: "clamp",
    easing: Easing.out(Easing.cubic),
  });

  // Show progress: 0% → 60% over full clip
  const showProgress = interpolate(
    frame,
    [0, durationInFrames - 1],
    [0, 0.6],
    { extrapolateRight: "clamp" }
  );

  const CARD_W = 920;
  const CARD_H = 500;

  // ── Ambient background particles ─────────────────────────────────────────
  const particleData = [
    { x: 0.12, y: 0.18, size: 3, phase: 0.0, color: ACCENT },
    { x: 0.88, y: 0.22, size: 4, phase: 1.2, color: ACCENT_2 },
    { x: 0.08, y: 0.78, size: 2.5, phase: 2.5, color: ACCENT_3 },
    { x: 0.92, y: 0.72, size: 3.5, phase: 0.8, color: "#f59e0b" },
    { x: 0.5, y: 0.1, size: 2, phase: 1.8, color: ACCENT },
    { x: 0.22, y: 0.88, size: 3, phase: 3.1, color: ACCENT_2 },
    { x: 0.72, y: 0.85, size: 2.5, phase: 0.4, color: ACCENT_3 },
  ];

  return (
    <AbsoluteFill
      style={{
        background: `
          radial-gradient(ellipse at 20% 50%, rgba(168,85,247,0.14) 0%, transparent 50%),
          radial-gradient(ellipse at 80% 40%, rgba(6,182,212,0.10) 0%, transparent 50%),
          radial-gradient(ellipse at 50% 90%, rgba(236,72,153,0.07) 0%, transparent 45%),
          ${BG}
        `,
        display: "flex",
        alignItems: "center",
        justifyContent: "center",
        fontFamily: "Inter, sans-serif",
      }}
    >
      {/* Ambient particles in background */}
      {particleData.map((p, i) => {
        const px = p.x * width + Math.sin(frame * 0.04 + p.phase) * 20;
        const py = p.y * height + Math.cos(frame * 0.05 + p.phase) * 15;
        const popIn = interpolate(frame, [i * 8, i * 8 + 16], [0, 1], {
          extrapolateLeft: "clamp",
          extrapolateRight: "clamp",
        });
        return (
          <div
            key={i}
            style={{
              position: "absolute",
              left: px,
              top: py,
              width: p.size,
              height: p.size,
              borderRadius: "50%",
              background: p.color,
              opacity: popIn * (0.25 + Math.sin(frame * 0.07 + p.phase) * 0.12),
              boxShadow: `0 0 10px ${p.color}`,
            }}
          />
        );
      })}

      {/* Ambient glow halo behind the card */}
      <div
        style={{
          position: "absolute",
          width: CARD_W + 160,
          height: CARD_H + 160,
          borderRadius: 50,
          background: `radial-gradient(ellipse, rgba(168,85,247,0.16) 0%, transparent 70%)`,
          filter: "blur(36px)",
          opacity: cardOpacity,
          transform: `scale(${cardScale})`,
        }}
      />

      {/* Card */}
      <div
        style={{
          width: CARD_W,
          height: CARD_H,
          borderRadius: 28,
          background: `linear-gradient(145deg, #18182a 0%, ${SURFACE_CARD} 55%, #0d0d1a 100%)`,
          border: "1px solid rgba(168,85,247,0.22)",
          boxShadow: `
            0 40px 100px rgba(0,0,0,0.75),
            0 0 0 1px rgba(168,85,247,0.12),
            inset 0 1px 0 rgba(255,255,255,0.05),
            inset 0 -1px 0 rgba(0,0,0,0.3)
          `,
          transform: `scale(${cardScale})`,
          opacity: cardOpacity,
          position: "relative" as const,
          overflow: "hidden",
          display: "flex",
          flexDirection: "column" as const,
          padding: "32px 36px 28px 36px",
          gap: 0,
        }}
      >
        {/* Card inner top-left gradient sheen */}
        <div
          style={{
            position: "absolute",
            top: 0,
            left: 0,
            width: 280,
            height: 150,
            background:
              "radial-gradient(ellipse at 0% 0%, rgba(168,85,247,0.14), transparent 70%)",
            pointerEvents: "none",
          }}
        />
        {/* Card bottom-right accent */}
        <div
          style={{
            position: "absolute",
            bottom: 0,
            right: 0,
            width: 200,
            height: 120,
            background:
              "radial-gradient(ellipse at 100% 100%, rgba(6,182,212,0.10), transparent 70%)",
            pointerEvents: "none",
          }}
        />

        {/* ── TOP ROW: Left (tower + logo) | Center (text) | Right (waveform) ── */}
        <div
          style={{
            display: "flex",
            alignItems: "flex-start",
            gap: 32,
            flex: 1,
          }}
        >
          {/* LEFT COLUMN: Tower + Logo */}
          <div
            style={{
              display: "flex",
              flexDirection: "column" as const,
              alignItems: "center",
              gap: 20,
              flexShrink: 0,
              width: 120,
            }}
          >
            <RadioTower frame={frame} opacity={towerOpacity} />
            <StationLogo opacity={towerOpacity} frame={frame} />
          </div>

          {/* CENTER COLUMN: Station info + progress */}
          <div
            style={{
              flex: 1,
              display: "flex",
              flexDirection: "column" as const,
              justifyContent: "space-between",
              gap: 12,
              paddingTop: 8,
              opacity: textOpacity,
              transform: `translateX(${textSlideX}px)`,
            }}
          >
            {/* ON AIR badge */}
            <div>
              <OnAirBadge frame={frame} entranceOpacity={textOpacity} />
            </div>

            {/* Station name */}
            <div
              style={{
                fontFamily: "Inter, sans-serif",
                fontWeight: 900,
                fontSize: 42,
                color: TEXT,
                letterSpacing: "-0.02em",
                lineHeight: 1.0,
                marginTop: 12,
              }}
            >
              {STATION_NAME}
            </div>

            {/* Show title */}
            <div
              style={{
                fontFamily: "Inter, sans-serif",
                fontWeight: 700,
                fontSize: 22,
                color: ACCENT_2,
                letterSpacing: "-0.01em",
                marginTop: 4,
              }}
            >
              {SHOW_TITLE}
            </div>

            {/* Host */}
            <div
              style={{
                fontFamily: "Inter, sans-serif",
                fontWeight: 400,
                fontSize: 15,
                color: MUTED,
                letterSpacing: "0.01em",
              }}
            >
              {HOST_NAME}
            </div>

            {/* Show progress bar */}
            <div style={{ marginTop: 18 }}>
              <ShowProgressBar progress={showProgress} />
            </div>
          </div>

          {/* RIGHT COLUMN: Waveform */}
          <div
            style={{
              flexShrink: 0,
              display: "flex",
              flexDirection: "column" as const,
              alignItems: "flex-end",
              justifyContent: "flex-start",
              paddingTop: 24,
              gap: 12,
            }}
          >
            <div
              style={{
                fontFamily: "Inter, sans-serif",
                fontWeight: 600,
                fontSize: 11,
                color: MUTED,
                letterSpacing: "0.12em",
                textTransform: "uppercase" as const,
                opacity: waveformOpacity,
              }}
            >
              Live Audio
            </div>
            <WaveformBars frame={frame} opacity={waveformOpacity} />
          </div>
        </div>

        {/* ── BOTTOM ROW: Schedule + social ──────────────────────────────────── */}
        <div
          style={{
            display: "flex",
            alignItems: "center",
            justifyContent: "space-between",
            marginTop: 20,
            paddingTop: 16,
            borderTop: "1px solid rgba(255,255,255,0.07)",
            opacity: bottomOpacity,
          }}
        >
          {/* Next show */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 10,
            }}
          >
            {/* Small clock icon */}
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none">
              <circle
                cx="12"
                cy="12"
                r="10"
                stroke={MUTED}
                strokeWidth="1.8"
              />
              <polyline
                points="12,6 12,12 16,14"
                stroke={MUTED}
                strokeWidth="1.8"
                strokeLinecap="round"
                strokeLinejoin="round"
              />
            </svg>
            <span
              style={{
                fontFamily: "Inter, sans-serif",
                fontSize: 14,
                color: MUTED,
                letterSpacing: "0.01em",
              }}
            >
              Next:{" "}
              <span
                style={{
                  color: TEXT,
                  fontWeight: 600,
                }}
              >
                {NEXT_SHOW}
              </span>{" "}
              at{" "}
              <span
                style={{
                  color: ACCENT_2,
                  fontWeight: 600,
                }}
              >
                {NEXT_TIME}
              </span>
            </span>
          </div>

          {/* Frequency badge + social handle */}
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: 16,
            }}
          >
            {/* Frequency badge */}
            <div
              style={{
                padding: "4px 12px",
                borderRadius: 100,
                background: "rgba(168,85,247,0.14)",
                border: "1px solid rgba(168,85,247,0.28)",
              }}
            >
              <span
                style={{
                  fontFamily: "Inter, sans-serif",
                  fontSize: 12,
                  fontWeight: 700,
                  color: ACCENT,
                  letterSpacing: "0.06em",
                }}
              >
                94.7 FM
              </span>
            </div>

            {/* Social handle */}
            <div
              style={{
                display: "flex",
                alignItems: "center",
                gap: 6,
              }}
            >
              {/* Stylized @ icon */}
              <svg width="14" height="14" viewBox="0 0 24 24" fill="none">
                <circle
                  cx="12"
                  cy="12"
                  r="4"
                  stroke={ACCENT_3}
                  strokeWidth="2"
                />
                <path
                  d="M16 8v5a3 3 0 006 0v-1a10 10 0 10-3.92 7.94"
                  stroke={ACCENT_3}
                  strokeWidth="2"
                  strokeLinecap="round"
                />
              </svg>
              <span
                style={{
                  fontFamily: "Inter, sans-serif",
                  fontSize: 13,
                  fontWeight: 500,
                  color: ACCENT_3,
                  letterSpacing: "0.01em",
                }}
              >
                {SOCIAL_HANDLE}
              </span>
            </div>
          </div>
        </div>
      </div>
    </AbsoluteFill>
  );
};

// ─── Composition config ───────────────────────────────────────────────────────
export const compositionConfig = {
  id: "remotion-radio-card",
  component: RadioCard,
  durationInFrames: 120,
  fps: 30,
  width: 1920,
  height: 1080,
};

Radio / Podcast Audio Card

A 4-second Remotion animation that recreates the look of a broadcast studio’s social sharing card — the kind you see pinned to a station’s feed when a show goes live. The composition is built on a 1920 × 1080 dark canvas with layered radial gradients in purple, cyan, and pink that keep every frame rich and atmospheric. At the center sits a 920 × 500 card with a deep indigo-to-navy gradient body, a hairline purple glow border, and inner sheen overlays in both corners. On entrance the card springs in from scale 0.8 to 1.0 over 28 frames using a damped spring, giving it a satisfying physical pop without any bounce overshoot.

Three sections fade and slide in with staggered offsets: the left column (SVG radio tower and circular station logo) appears at frame 15, the center text column (ON AIR badge, station name, show title, host credit, and progress bar) slides in from the left at frame 30, and the 30-bar waveform column on the right fades up at frame 45. The radio tower is drawn in inline SVG with a mast, twin struts, cross braces, and a glowing tip dot; three concentric signal rings animate outward in a looping sequence, each delayed by 20 frames and fading from full to zero opacity as the radius expands — mimicking a real broadcast pulse. The ON AIR badge continuously pulses from scale 1.0 to 1.05 on a 2-second sine cycle with a synchronized pink-purple glow, so it commands attention throughout the clip.

The right-side waveform drives each of the 30 bars with four stacked sine waves at different frequencies and phases, creating a rich organic spectrum that looks convincingly live without touching any audio API. Bar colors sweep from purple on the left through pink in the center to cyan on the right, each bar casting a matching glow shadow. Below the center text, a show progress track fills from 0 to 60 minutes (0% → 60% of the progress bar) over the 4-second clip, complete with a glowing scrubber dot and live elapsed-time label. A bottom strip shows the next scheduled show name and time, a frequency badge, and the station’s social handle — all fading in last at frame 55 for a clean reveal sequence.

Simulated audio data — waveform values are generated mathematically. No real audio file is required.