{"id":"kite-day","title":"Kite Day","category":"Drawn film","prompt":"A 24-second illustrated film: a red kite climbs over rolling green hills on a bright windy day. Gusts ripple through the long grass in waves, cloud shadows slide across the hills, and the kite dips, recovers and soars as its tail streams behind. Closing title: KITE DAY. Paint it, do not assemble it: graded skies, soft radial glows built from gradients (never a flat translucent disc), layered depth with atmospheric fade, paper-cut shapes with subtle shadows, gentle continuous motion, and one clear moment of change. No text except a small closing title in clear space. Every frame should stand as a composed picture.","designedBy":{"provider":"google","model":"gemini-3.7-flash · 1 revision","inference":{"inputTokens":7124,"outputTokens":6397,"costUsd":0.1573}},"render":{"credits":10,"priceUsd":0.15,"format":"mp4","renderMs":116013,"frameCount":576,"bytes":5089067,"seed":88,"width":1280,"height":720,"fps":24,"durationSeconds":24},"howToReproduce":{"renderApi":"POST /v1/renders with `renderRequest` below as the body (Authorization: Bearer <your pp_sa_… key>).","chat":"Paste the prompt into /chat: an illustrated film brief is answered with a program like this one, and plays at once.","note":"This is a draw contract — a Canvas 2D program. It has no editor ops; it opens in the Cloud Studio as a film (/studio?gallery-film=kite-day), and it exports as MP4 or GIF, not as a still."},"renderRequest":{"drawContract":"const meta = { duration: 24, fps: 24, size: [1280, 720], seed: 88 };\n\nfunction draw(ctx, t, params, rng) {\n  const W = 1280;\n  const H = 720;\n\n  // ---------------------------------------------------------\n  // Timing & Story Acts\n  // ---------------------------------------------------------\n  // Act 1 (0-7s):   Low flutter & struggle near the grassy slopes.\n  // Act 2 (7-13s):  The Gust: sudden lift, dramatic swoop & soaring climb.\n  // Act 3 (13-19s): High altitude grace against open azure sky.\n  // Act 4 (19-24s): Settled flight in upper right; bold title in clear sky.\n\n  // ---------------------------------------------------------\n  // Sky Gradient & Atmospheric Sun Glow\n  // ---------------------------------------------------------\n  const skyGrad = ctx.createLinearGradient(0, 0, 0, H * 0.78);\n  skyGrad.addColorStop(0.0, \"#1d5c9c\"); // Deep vibrant azure at zenith\n  skyGrad.addColorStop(0.35, \"#4b95d0\"); // Radiant cerulean\n  skyGrad.addColorStop(0.72, \"#a0d2f0\"); // Soft atmospheric blue\n  skyGrad.addColorStop(1.0, \"#edf6fc\"); // Warm horizon glow\n  ctx.fillStyle = skyGrad;\n  ctx.fillRect(0, 0, W, H);\n\n  // Soft sun glow (multi-stop radial gradient, placed upper right)\n  const sunX = W * 0.82;\n  const sunY = H * 0.18;\n  const sunGlow = ctx.createRadialGradient(sunX, sunY, 20, sunX, sunY, 360);\n  sunGlow.addColorStop(0.0, \"rgba(255, 254, 238, 0.95)\");\n  sunGlow.addColorStop(0.18, \"rgba(255, 246, 200, 0.5)\");\n  sunGlow.addColorStop(0.48, \"rgba(255, 240, 180, 0.16)\");\n  sunGlow.addColorStop(1.0, \"rgba(255, 255, 255, 0.0)\");\n  ctx.fillStyle = sunGlow;\n  ctx.beginPath();\n  ctx.arc(sunX, sunY, 360, 0, Math.PI * 2);\n  ctx.fill();\n\n  // ---------------------------------------------------------\n  // Paper-cut Clouds (Staged to keep top-left sky crystal clear for title)\n  // ---------------------------------------------------------\n  function drawCumulus(cx, cy, scale, opacity) {\n    ctx.save();\n    ctx.translate(cx, cy);\n    ctx.scale(scale, scale);\n    ctx.fillStyle = `rgba(255, 255, 255, ${opacity})`;\n\n    ctx.shadowColor = \"rgba(45, 95, 140, 0.15)\";\n    ctx.shadowBlur = 16;\n    ctx.shadowOffsetY = 8;\n\n    ctx.beginPath();\n    ctx.arc(-80, 20, 42, 0, Math.PI * 2);\n    ctx.arc(-35, -12, 58, 0, Math.PI * 2);\n    ctx.arc(35, -28, 72, 0, Math.PI * 2);\n    ctx.arc(105, -2, 52, 0, Math.PI * 2);\n    ctx.arc(150, 22, 36, 0, Math.PI * 2);\n    ctx.rect(-80, 20, 230, 38);\n    ctx.closePath();\n    ctx.fill();\n    ctx.restore();\n  }\n\n  // Drifting clouds stay at mid-lower sky and right side so top-left stays open\n  const cloudSpeed = 26;\n  const cloud1X = ((t * cloudSpeed + 500) % (W + 650)) - 250;\n  const cloud2X = ((t * (cloudSpeed * 1.25) + 950) % (W + 650)) - 250;\n  const cloud3X = ((t * (cloudSpeed * 0.75) + 150) % (W + 650)) - 250;\n\n  drawCumulus(cloud3X, 260, 0.7, 0.75);\n  drawCumulus(cloud1X, 220, 0.9, 0.88);\n  drawCumulus(cloud2X, 280, 1.1, 0.82);\n\n  // ---------------------------------------------------------\n  // Rolling Hills (Layered depth with smooth full-width atmospheric fade)\n  // ---------------------------------------------------------\n  const hills = [\n    {\n      baseY: 410,\n      amp: 28,\n      freq: 0.002,\n      phase: 1.2,\n      fill: \"#7da4ae\",\n      shadow: false,\n    },\n    {\n      baseY: 455,\n      amp: 36,\n      freq: 0.0035,\n      phase: 3.5,\n      fill: \"#689972\",\n      shadow: true,\n    },\n    {\n      baseY: 510,\n      amp: 46,\n      freq: 0.0042,\n      phase: 0.7,\n      fill: \"#4f8748\",\n      shadow: true,\n    },\n    {\n      baseY: 580,\n      amp: 55,\n      freq: 0.0048,\n      phase: 4.8,\n      fill: \"#3b7837\",\n      shadow: true,\n    },\n    {\n      baseY: 652,\n      amp: 65,\n      freq: 0.0055,\n      phase: 2.1,\n      fill: \"#286629\",\n      shadow: true,\n    },\n  ];\n\n  function getHillY(hill, x) {\n    return (\n      hill.baseY +\n      Math.sin(x * hill.freq + hill.phase) * hill.amp +\n      Math.cos(x * hill.freq * 0.6 + hill.phase * 1.5) * (hill.amp * 0.4)\n    );\n  }\n\n  hills.forEach((hill, idx) => {\n    ctx.save();\n    if (hill.shadow) {\n      ctx.shadowColor = \"rgba(12, 38, 16, 0.2)\";\n      ctx.shadowBlur = 18;\n      ctx.shadowOffsetY = -6;\n    }\n\n    ctx.beginPath();\n    ctx.moveTo(-20, H + 20);\n    ctx.lineTo(-20, getHillY(hill, -20));\n\n    const step = 8;\n    for (let x = 0; x <= W + 20; x += step) {\n      ctx.lineTo(x, getHillY(hill, x));\n    }\n\n    ctx.lineTo(W + 20, H + 20);\n    ctx.closePath();\n    ctx.fillStyle = hill.fill;\n    ctx.fill();\n    ctx.restore();\n\n    // Full-width subtle atmospheric haze gradient on top of each ridge\n    ctx.save();\n    ctx.beginPath();\n    ctx.moveTo(-20, H + 20);\n    ctx.lineTo(-20, getHillY(hill, -20));\n    for (let x = 0; x <= W + 20; x += step) {\n      ctx.lineTo(x, getHillY(hill, x));\n    }\n    ctx.lineTo(W + 20, H + 20);\n    ctx.closePath();\n    ctx.clip();\n\n    const ridgeHaze = ctx.createLinearGradient(0, hill.baseY - 40, 0, hill.baseY + 120);\n    ridgeHaze.addColorStop(0.0, \"rgba(225, 242, 252, 0.22)\");\n    ridgeHaze.addColorStop(0.5, \"rgba(200, 235, 245, 0.06)\");\n    ridgeHaze.addColorStop(1.0, \"rgba(0, 0, 0, 0.0)\");\n    ctx.fillStyle = ridgeHaze;\n    ctx.fillRect(0, hill.baseY - 50, W, 200);\n\n    // Smooth cloud shadows sliding across the slopes\n    if (idx >= 1) {\n      const shadowSpeed = 50;\n      const shadowX1 = ((t * shadowSpeed + idx * 260) % (W + 700)) - 350;\n      const shadowX2 = ((t * shadowSpeed + idx * 260 + 580) % (W + 700)) - 350;\n\n      // Radial soft shadow patches with zero hard edges\n      const sGrad1 = ctx.createRadialGradient(shadowX1, hill.baseY + 35, 20, shadowX1, hill.baseY + 35, 200);\n      sGrad1.addColorStop(0.0, \"rgba(10, 32, 14, 0.22)\");\n      sGrad1.addColorStop(0.6, \"rgba(10, 32, 14, 0.12)\");\n      sGrad1.addColorStop(1.0, \"rgba(10, 32, 14, 0.0)\");\n\n      ctx.fillStyle = sGrad1;\n      ctx.beginPath();\n      ctx.ellipse(shadowX1, hill.baseY + 35, 240, 75, -0.06, 0, Math.PI * 2);\n      ctx.fill();\n\n      const sGrad2 = ctx.createRadialGradient(shadowX2, hill.baseY + 25, 15, shadowX2, hill.baseY + 25, 160);\n      sGrad2.addColorStop(0.0, \"rgba(10, 32, 14, 0.2)\");\n      sGrad2.addColorStop(0.7, \"rgba(10, 32, 14, 0.08)\");\n      sGrad2.addColorStop(1.0, \"rgba(10, 32, 14, 0.0)\");\n\n      ctx.fillStyle = sGrad2;\n      ctx.beginPath();\n      ctx.ellipse(shadowX2, hill.baseY + 25, 180, 60, 0.04, 0, Math.PI * 2);\n      ctx.fill();\n    }\n    ctx.restore();\n  });\n\n  // ---------------------------------------------------------\n  // Wind Ripples & Grass Waves across Slopes\n  // (Travelling gusts of bright chartreuse / sunlit blades)\n  // ---------------------------------------------------------\n  ctx.save();\n  const windT = t * 5.2;\n\n  // Midground grass ripple wave along ridge 3\n  const midBladeCount = 80;\n  for (let i = 0; i < midBladeCount; i++) {\n    const mx = (i / midBladeCount) * (W + 60) - 30;\n    const my = getHillY(hills[3], mx);\n    \n    // Travelling gust wave equation\n    const wavePhase = mx * 0.018 - windT * 1.1;\n    const gustStrength = Math.max(0, Math.sin(wavePhase)) * Math.max(0, Math.cos(wavePhase * 0.5 + 0.3));\n    const bend = 12 + gustStrength * 30;\n    const bladeH = 18 + (i % 4) * 6;\n\n    // Lighter, sunlit chartreuse when swept by the gust\n    const lightAlpha = 0.3 + gustStrength * 0.65;\n    ctx.strokeStyle = `rgba(215, 250, 140, ${lightAlpha})`;\n    ctx.lineWidth = 1.6;\n    ctx.lineCap = \"round\";\n\n    ctx.beginPath();\n    ctx.moveTo(mx, my);\n    ctx.quadraticCurveTo(mx + bend * 0.5, my - bladeH * 0.6, mx + bend, my - bladeH);\n    ctx.stroke();\n  }\n\n  // Foreground dense grass with distinct traveling ripple waves\n  const fgBladeCount = 120;\n  for (let i = 0; i < fgBladeCount; i++) {\n    const fx = (i / fgBladeCount) * (W + 80) - 40;\n    const fy = getHillY(hills[4], fx) + ((i % 5) * 3);\n\n    const wavePhase1 = fx * 0.014 - windT;\n    const wavePhase2 = fx * 0.032 - windT * 1.35;\n    const gustPulse = Math.pow((Math.sin(wavePhase1) * 0.5 + 0.5), 2) * 0.7 + (Math.sin(wavePhase2) * 0.3);\n    const bend = 16 + gustPulse * 38;\n    const bladeH = 26 + (i % 6) * 10;\n\n    // Deep grass body\n    ctx.strokeStyle = (i % 3 === 0) ? \"#5da638\" : \"#3c7e26\";\n    ctx.lineWidth = 2.4;\n    ctx.lineCap = \"round\";\n    ctx.beginPath();\n    ctx.moveTo(fx, fy);\n    ctx.quadraticCurveTo(fx + bend * 0.45, fy - bladeH * 0.55, fx + bend, fy - bladeH);\n    ctx.stroke();\n\n    // Bright illuminated tip catching the gust\n    if (gustPulse > 0.25) {\n      const tipAlpha = Math.min(1.0, (gustPulse - 0.25) * 1.8);\n      ctx.strokeStyle = `rgba(235, 255, 160, ${tipAlpha})`;\n      ctx.lineWidth = 1.8;\n      ctx.beginPath();\n      ctx.moveTo(fx + bend * 0.45, fy - bladeH * 0.55);\n      ctx.lineTo(fx + bend, fy - bladeH);\n      ctx.stroke();\n    }\n  }\n  ctx.restore();\n\n  // ---------------------------------------------------------\n  // Floating Wind Seeds / Drifting Spores\n  // ---------------------------------------------------------\n  ctx.save();\n  ctx.fillStyle = \"rgba(255, 255, 235, 0.85)\";\n  for (let p = 0; p < 32; p++) {\n    const pSeed = p * 137.5;\n    const pSpeed = 95 + (p % 7) * 28;\n    const px = ((t * pSpeed + pSeed * 8) % (W + 200)) - 100;\n    const pyBase = 180 + (p * 21) % 480;\n    const py = pyBase + Math.sin(t * 3.2 + p) * 16;\n    const pSize = 1.1 + (p % 3) * 0.9;\n\n    ctx.beginPath();\n    ctx.arc(px, py, pSize, 0, Math.PI * 2);\n    ctx.fill();\n  }\n  ctx.restore();\n\n  // ---------------------------------------------------------\n  // Kite Kinematics & Choreography\n  // ---------------------------------------------------------\n  let kiteX, kiteY, kiteAngle, tension;\n\n  if (t < 7.0) {\n    // Act 1: Low altitude flutter & turbulence near grass\n    const u = t / 7.0;\n    const sway = Math.sin(t * 3.8) * 45;\n    const dip = Math.cos(t * 4.2) * 28;\n    kiteX = 280 + u * 120 + sway;\n    kiteY = 480 - u * 80 + dip;\n    kiteAngle = Math.sin(t * 4.0) * 0.28 - 0.15;\n    tension = 0.5 + Math.sin(t * 4.2) * 0.3;\n  } else if (t < 13.0) {\n    // Act 2: The Gust — Big swooping climb (Clear moment of change)\n    const u = (t - 7.0) / 6.0;\n    const ease = u < 0.5 ? 4 * u * u * u : 1 - Math.pow(-2 * u + 2, 3) / 2;\n\n    const startX = 400;\n    const endX = 760;\n    const startY = 400;\n    const endY = 175;\n\n    const swoop = Math.sin(u * Math.PI) * -55;\n    kiteX = startX + (endX - startX) * ease + Math.sin(t * 4.5) * (1 - u) * 25;\n    kiteY = startY + (endY - startY) * ease + swoop + Math.sin(t * 3.0) * 12;\n    kiteAngle = -0.45 + (1 - u) * Math.sin(t * 5.0) * 0.3 + u * 0.16;\n    tension = 0.85 + Math.sin(t * 6.0) * 0.15;\n  } else if (t < 19.0) {\n    // Act 3: High altitude triumphant soaring in open azure\n    const u = (t - 13.0) / 6.0;\n    const startX = 760;\n    const endX = 890;\n    kiteX = startX + (endX - startX) * u + Math.sin(t * 1.8) * 35;\n    kiteY = 175 + Math.cos(t * 1.6) * 22;\n    kiteAngle = -0.12 + Math.sin(t * 2.2) * 0.14;\n    tension = 0.95;\n  } else {\n    // Act 4: Settled, serene flight held high in upper right\n    kiteX = 890 + Math.sin(t * 1.2) * 18;\n    kiteY = 175 + Math.cos(t * 1.1) * 14;\n    kiteAngle = -0.08 + Math.sin(t * 1.5) * 0.08;\n    tension = 0.92;\n  }\n\n  // Anchor point where the tether originates\n  const tetherOriginX = 140;\n  const tetherOriginY = 680;\n\n  // ---------------------------------------------------------\n  // Tether Line (Curved catenary bowed by wind)\n  // ---------------------------------------------------------\n  ctx.save();\n  ctx.strokeStyle = \"rgba(245, 250, 255, 0.72)\";\n  ctx.lineWidth = 1.4;\n\n  const midX = (tetherOriginX + kiteX) * 0.5;\n  const midY = (tetherOriginY + kiteY) * 0.5;\n  const sag = (1.0 - tension) * 85;\n  const windBowX = midX + 35 + (1.0 - tension) * 40;\n  const windBowY = midY + sag + Math.sin(t * 5.0) * 8;\n\n  ctx.beginPath();\n  ctx.moveTo(tetherOriginX, tetherOriginY);\n  ctx.quadraticCurveTo(windBowX, windBowY, kiteX, kiteY);\n  ctx.stroke();\n  ctx.restore();\n\n  // ---------------------------------------------------------\n  // Kite Drawing (Faceted Ruby Diamond with Paper Highlights)\n  // ---------------------------------------------------------\n  ctx.save();\n  ctx.translate(kiteX, kiteY);\n  ctx.rotate(kiteAngle);\n\n  // Soft drop shadow\n  ctx.shadowColor = \"rgba(12, 35, 60, 0.28)\";\n  ctx.shadowBlur = 18;\n  ctx.shadowOffsetY = 12;\n\n  const kw = 34;\n  const khTop = 32;\n  const khBot = 62;\n\n  // Left upper facet (Vivid scarlet)\n  ctx.fillStyle = \"#e62828\";\n  ctx.beginPath();\n  ctx.moveTo(0, -khTop);\n  ctx.lineTo(-kw, 0);\n  ctx.lineTo(0, 0);\n  ctx.closePath();\n  ctx.fill();\n\n  // Right upper facet (Sunlit vermilion coral)\n  ctx.fillStyle = \"#ff4f4f\";\n  ctx.beginPath();\n  ctx.moveTo(0, -khTop);\n  ctx.lineTo(kw, 0);\n  ctx.lineTo(0, 0);\n  ctx.closePath();\n  ctx.fill();\n\n  // Left lower facet (Deep crimson shadow)\n  ctx.fillStyle = \"#be1414\";\n  ctx.beginPath();\n  ctx.moveTo(0, 0);\n  ctx.lineTo(-kw, 0);\n  ctx.lineTo(0, khBot);\n  ctx.closePath();\n  ctx.fill();\n\n  // Right lower facet (Rich carmine)\n  ctx.fillStyle = \"#df2323\";\n  ctx.beginPath();\n  ctx.moveTo(0, 0);\n  ctx.lineTo(kw, 0);\n  ctx.lineTo(0, khBot);\n  ctx.closePath();\n  ctx.fill();\n\n  // Bamboo spars / cross struts\n  ctx.strokeStyle = \"#ffe6a8\";\n  ctx.lineWidth = 2.0;\n  ctx.shadowBlur = 0;\n  ctx.beginPath();\n  ctx.moveTo(0, -khTop);\n  ctx.lineTo(0, khBot);\n  ctx.moveTo(-kw, 0);\n  ctx.lineTo(kw, 0);\n  ctx.stroke();\n\n  // Crisp rim highlight\n  ctx.strokeStyle = \"rgba(255, 235, 235, 0.88)\";\n  ctx.lineWidth = 1.1;\n  ctx.beginPath();\n  ctx.moveTo(0, -khTop);\n  ctx.lineTo(kw, 0);\n  ctx.lineTo(0, khBot);\n  ctx.lineTo(-kw, 0);\n  ctx.closePath();\n  ctx.stroke();\n\n  ctx.restore(); // Exit kite transform\n\n  // ---------------------------------------------------------\n  // Long Ribbon Tail with Colourful Fluttering Bows\n  // ---------------------------------------------------------\n  ctx.save();\n  const tailBaseX = kiteX + Math.sin(kiteAngle) * khBot;\n  const tailBaseY = kiteY + Math.cos(kiteAngle) * khBot;\n\n  const tailPoints = [];\n  const tailLength = 230;\n  const segments = 24;\n\n  ctx.strokeStyle = \"rgba(255, 222, 85, 0.92)\";\n  ctx.lineWidth = 2.2;\n  ctx.lineCap = \"round\";\n  ctx.beginPath();\n  ctx.moveTo(tailBaseX, tailBaseY);\n\n  const tailWaveSpeed = t * 6.8;\n\n  for (let s = 1; s <= segments; s++) {\n    const dist = (s / segments) * tailLength;\n    const wave =\n      Math.sin(tailWaveSpeed - s * 0.42) * (10 + s * 1.9) +\n      Math.cos(tailWaveSpeed * 0.65 - s * 0.28) * (6 + s * 0.85);\n\n    const tx = tailBaseX - dist * 0.9 + wave * 0.38;\n    const ty = tailBaseY + dist * 0.62 + wave;\n\n    tailPoints.push({ x: tx, y: ty, angle: wave * 0.05 });\n    ctx.lineTo(tx, ty);\n  }\n  ctx.stroke();\n\n  const bowColors = [\"#ffd000\", \"#3a86ff\", \"#ffffff\", \"#ff006e\", \"#00f5d4\"];\n\n  tailPoints.forEach((pt, idx) => {\n    if (idx % 3 === 2) {\n      const color = bowColors[(idx / 3) % bowColors.length];\n      const bowSize = 8 + (idx % 2) * 2;\n\n      ctx.save();\n      ctx.translate(pt.x, pt.y);\n      ctx.rotate(pt.angle + Math.sin(t * 5.2 + idx) * 0.4);\n\n      ctx.fillStyle = color;\n      ctx.beginPath();\n      ctx.moveTo(0, 0);\n      ctx.lineTo(-bowSize, -bowSize * 0.6);\n      ctx.lineTo(-bowSize, bowSize * 0.6);\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.beginPath();\n      ctx.moveTo(0, 0);\n      ctx.lineTo(bowSize, -bowSize * 0.6);\n      ctx.lineTo(bowSize, bowSize * 0.6);\n      ctx.closePath();\n      ctx.fill();\n\n      ctx.fillStyle = \"#fff\";\n      ctx.beginPath();\n      ctx.arc(0, 0, 2.0, 0, Math.PI * 2);\n      ctx.fill();\n\n      ctx.restore();\n    }\n  });\n  ctx.restore();\n\n  // ---------------------------------------------------------\n  // Closing Title: \"K I T E   D A Y\"\n  // (Large, high-contrast, set in clear, open cerulean sky)\n  // ---------------------------------------------------------\n  if (t >= 17.5) {\n    const titleProgress = Math.min(1.0, (t - 17.5) / 2.5);\n    const alpha = titleProgress * titleProgress * (3 - 2 * titleProgress);\n\n    ctx.save();\n    const titleX = W * 0.12;\n    const titleY = H * 0.22;\n\n    // Strong, crisp legibility against the deep blue sky\n    ctx.shadowColor = `rgba(10, 35, 65, ${alpha * 0.65})`;\n    ctx.shadowBlur = 18;\n    ctx.shadowOffsetY = 4;\n\n    ctx.fillStyle = `rgba(255, 255, 255, ${alpha * 0.98})`;\n    ctx.font = \"700 46px 'Avenir Next', 'Futura', 'Helvetica Neue', sans-serif\";\n    ctx.textAlign = \"left\";\n    ctx.textBaseline = \"middle\";\n\n    const titleText = \"K I T E   D A Y\";\n    ctx.fillText(titleText, titleX, titleY);\n\n    // Elegant accent underline\n    ctx.strokeStyle = `rgba(255, 255, 255, ${alpha * 0.75})`;\n    ctx.lineWidth = 2.2;\n    ctx.beginPath();\n    ctx.moveTo(titleX, titleY + 34);\n    ctx.lineTo(titleX + 310, titleY + 34);\n    ctx.stroke();\n\n    ctx.restore();\n  }\n}\n","exportProfile":{"format":"mp4","resolution":"720p","fps":24,"durationSeconds":24},"source":{"prompt":"A 24-second illustrated film: a red kite climbs over rolling green hills on a bright windy day. Gusts ripple through the long grass in waves, cloud shadows slide across the hills, and the kite dips, recovers and soars as its tail streams behind. Closing title: KITE DAY. Paint it, do not assemble it: graded skies, soft radial glows built from gradients (never a flat translucent disc), layered depth with atmospheric fade, paper-cut shapes with subtle shadows, gentle continuous motion, and one clear moment of change. No text except a small closing title in clear space. Every frame should stand as a composed picture.","provider":"google","model":"gemini-3.7-flash · 1 revision","app":"gallery-reproduction"}},"license":"CC0-1.0"}