[
 {
  "id": "T01",
  "name": "Match Cut: Shape",
  "family": "cut",
  "energy": 1,
  "tones": ["premium", "calm", "trustworthy"],
  "what": "A hard cut between two circles with the same centre and diameter, so the eye reads one shape turning into another.",
  "when": "Reframing a problem as its answer: the coral alert dot becomes the teal status ring. Best on the hook-to-reveal cut, where the viewer should feel 'same thing, new meaning'.",
  "avoid": "A near-miss in position or size reads as a mistake, so lock both shapes to one centre. Use it once per film; a second shape match turns a clever beat into a gimmick.",
  "recipe": "Lock both circles to one centre and diameter in the 1920×1080 frame, then let the push-in run through the cut.\ntl.fromTo('#a',{scale:1},{scale:1.12,duration:1.4,ease:'power2.in',transformOrigin:'50% 45%'},T-1.4); tl.set('#a',{opacity:0},T); tl.set('#b',{opacity:1},T);\ntl.fromTo('#b',{scale:1.12},{scale:1.2,duration:0.7,ease:'power3.out',transformOrigin:'50% 45%'},T);",
  "hyperframes": "custom (hard cut); css-other morph circle is the nearest built-in"
 },
 {
  "id": "T02",
  "name": "Match Cut: Motion",
  "family": "cut",
  "energy": 2,
  "tones": ["bold", "playful", "premium"],
  "what": "An object moving across scene A is cut mid-movement, and a different object in scene B carries on along the same path at the same speed.",
  "when": "Handing momentum from cause to effect: a 'Deploy' pill slides right and a 'Shipped' badge lands where it was heading. Good for workflow steps that should feel like one gesture.",
  "avoid": "Direction, screen position and speed must match on the cut frame or the eye jumps. Don't cut on a still object, and don't follow it with a whip pan: two directional handoffs in a row blur together.",
  "recipe": "Cut on the fastest frame; both objects share one motion path and velocity at T.\ntl.fromTo('#a .pill',{x:-520},{x:120,duration:1,ease:'power2.in'},T-1); tl.set('#a',{opacity:0},T); tl.set('#b',{opacity:1},T);\ntl.fromTo('#b .badge',{x:120},{x:560,duration:0.6,ease:'power2.out'},T);",
  "hyperframes": "custom (hard cut); css-blur directional blur can soften the seam"
 },
 {
  "id": "T03",
  "name": "Graphic Match: Color",
  "family": "cut",
  "energy": 2,
  "tones": ["bold", "premium", "warm"],
  "what": "The camera pushes into a flat field of colour in scene A until it fills the frame, then cuts to scene B, which uses that colour as its background.",
  "when": "Going from a detail to a chapter card or brand moment: the teal bar in a chart becomes the teal of the next scene, so the colour carries the idea ('growth') across.",
  "avoid": "Needs a flat, clean colour area; gradients, photos and textures break the match. Pointless between two scenes that already share a background.",
  "recipe": "Scale A from the centre of the colour area until it covers the frame, then cut.\ntl.fromTo('#a',{scale:1},{scale:16,duration:0.8,ease:'power3.in',transformOrigin:'63% 53%'},T-0.8); tl.set('#a',{opacity:0},T); tl.set('#b',{opacity:1},T);\ntl.fromTo('#b .title',{y:40,opacity:0},{y:0,opacity:1,duration:0.5,ease:'power3.out'},T+0.1);",
  "hyperframes": "css-other (morph circle), closest; here the colour comes from a push-in, not a circle"
 },
 {
  "id": "T04",
  "name": "Beat Cut with Hold-frame",
  "family": "cut",
  "energy": 3,
  "tones": ["urgent", "bold", "celebratory"],
  "what": "Scene A freezes for one beat, drained of colour, then a hard cut lands exactly on the downbeat into a punched-in scene B.",
  "when": "The 'wait for it' before a reveal or a drop: the freeze tells the viewer to look, the downbeat cut delivers the answer. Built for music-led promos and launch reels.",
  "avoid": "Only on a real downbeat; off the grid it reads as a glitch. One hold per section at most, and never on a frame the viewer still needs to read.",
  "recipe": "Snap T to the beat grid (hyperframes beats). The freeze is simply no tweens on #a between T-0.5 and T.\ntl.fromTo('#a',{filter:'saturate(1)'},{filter:'saturate(0.2) contrast(1.1)',duration:0.08,ease:'none'},T-0.5); tl.set('#a',{opacity:0},T); tl.set('#b',{opacity:1},T);\ntl.fromTo('#b',{scale:1.1},{scale:1,duration:0.35,ease:'expo.out'},T);",
  "hyperframes": "custom (hard cut on the beat grid)"
 },
 {
  "id": "T05",
  "name": "Object Carry",
  "family": "carry",
  "energy": 2,
  "tones": ["playful", "trustworthy", "warm"],
  "what": "One element lifts out of scene A, flies across the change and docks into its slot in scene B, becoming part of the new layout.",
  "when": "Showing one item moving through a system: an alert becomes a ticket, a file becomes a row, a card lands on a board. The viewer follows the object, so the scene change feels earned.",
  "avoid": "The object needs an obvious destination in B. One carried object per transition, and not on the next cut too: carry after carry starts to feel like a screensaver.",
  "recipe": "Render the carried element once, above both scenes, and tween it between two measured rects.\ntl.fromTo('#chip',{x:0,y:0,scale:1},{x:-880,y:120,scale:0.92,duration:0.9,ease:'power3.inOut'},T);\ntl.fromTo('#a',{x:0,opacity:1},{x:-240,opacity:0,duration:0.6,ease:'power2.in'},T); tl.fromTo('#b',{x:320,opacity:0},{x:0,opacity:1,duration:0.7,ease:'power3.out'},T+0.2);",
  "hyperframes": "shared-element (Tier-A, worker-authored)"
 },
 {
  "id": "T06",
  "name": "Type Carry",
  "family": "carry",
  "energy": 1,
  "tones": ["calm", "premium", "trustworthy"],
  "what": "One word of the headline stays on screen while the rest leaves, then scene B builds its layout around that word.",
  "when": "When the next scene proves one word of the claim: 'watched' becomes the title of the monitoring panel, 'owned' becomes the owner column. A calm bridge from headline to proof.",
  "avoid": "The word must mean the same thing in both scenes. Don't carry filler words, don't swap the font mid-flight, and don't carry type twice in a row.",
  "recipe": "Split the headline into word spans; the kept word is its own layer above both scenes.\ntl.fromTo('#a .rest',{y:0,opacity:1},{y:-30,opacity:0,duration:0.4,ease:'power2.in',stagger:0.05},T);\ntl.fromTo('#word',{x:0,y:0,scale:1},{x:0,y:-300,scale:0.55,duration:0.8,ease:'power3.inOut',transformOrigin:'0% 0%'},T+0.1); tl.fromTo('#b .item',{y:24,opacity:0},{y:0,opacity:1,duration:0.5,ease:'power3.out',stagger:0.06},T+0.5);",
  "hyperframes": "shared-element (Tier-A, worker-authored)"
 },
 {
  "id": "T07",
  "name": "Cursor Carry",
  "family": "carry",
  "energy": 2,
  "tones": ["technical", "trustworthy", "calm"],
  "what": "A cursor clicks a card and the clicked card grows into the next full scene, like a container transform.",
  "when": "Product demos and walkthroughs: 'click here and this opens'. Cause and effect are on screen, so it suits navigation and drilling into detail.",
  "avoid": "The clicked element must be what the next scene is about. Keep the cursor slow and deliberate; don't click into a screen with no clear target, or three times in one demo.",
  "recipe": "Clip scene B to the clicked card's rect, then open the clip to the full frame.\ntl.fromTo('#cursor',{x:-800,y:420},{x:0,y:0,duration:0.7,ease:'power2.inOut'},T-0.9); tl.fromTo('#cursor',{scale:1},{scale:0.85,duration:0.08,ease:'power1.inOut',yoyo:true,repeat:1},T-0.16);\ntl.fromTo('#b',{clipPath:'inset(25% 10% 40% 58% round 24px)'},{clipPath:'inset(0% 0% 0% 0% round 0px)',duration:0.8,ease:'power3.inOut'},T);",
  "hyperframes": "shared-element (Tier-A); rules card-morph-anchor + cursor-click-ripple"
 },
 {
  "id": "T08",
  "name": "Line-to-Horizon",
  "family": "carry",
  "energy": 1,
  "tones": ["calm", "warm", "premium"],
  "what": "A chart line flattens and stretches across the full width, and becomes the horizon or divider of the next scene.",
  "when": "Going from data to meaning: 'the numbers settled' becomes a calm landscape, a roadmap or a section divider. Good for the exhale after a proof moment.",
  "avoid": "Needs one clean line, not a busy multi-series chart, and a scene B with a strong horizontal. Don't reuse it for the outro.",
  "recipe": "Keep the stroke constant (vector-effect: non-scaling-stroke) while you squash the line.\ntl.fromTo('#line path',{scaleY:1},{scaleY:0.01,duration:0.7,ease:'power3.inOut',transformOrigin:'50% 50%'},T); tl.fromTo('#line',{scaleX:1,y:0},{scaleX:1.4,y:150,duration:0.6,ease:'power3.inOut'},T+0.4);\ntl.fromTo('#b .sky',{opacity:0},{opacity:1,duration:0.6,ease:'sine.out'},T+0.7); tl.fromTo('#b .sun',{y:120},{y:0,duration:0.9,ease:'power2.out'},T+0.8);",
  "hyperframes": "custom"
 },
 {
  "id": "T09",
  "name": "Push-Through",
  "family": "camera",
  "energy": 3,
  "tones": ["bold", "technical", "mysterious"],
  "what": "The camera flies into a window inside scene A (a card preview, a screen) and comes out the other side in scene B.",
  "when": "Going deeper: from the dashboard into one report, from the overview into the product. Works as a hero reveal because it feels like entering the product.",
  "avoid": "The window must hold a true miniature of B, or the landing jumps. It is high energy: once per film, and not straight after a zoom or dolly.",
  "recipe": "Nest a mini copy of B in the window, scale A until the window exactly fills the frame, then swap.\ntl.fromTo('#a',{scale:1},{scale:5,duration:0.9,ease:'power4.inOut',transformOrigin:'1440px 528px'},T); // origin = s·(window top-left)/(s−1)\ntl.set('#a',{opacity:0},T+0.9); tl.set('#b',{opacity:1},T+0.9);",
  "hyperframes": "css-scale (zoom through), aimed at a UI window"
 },
 {
  "id": "T10",
  "name": "Whip Pan with Motion Blur",
  "family": "camera",
  "energy": 3,
  "tones": ["urgent", "playful", "bold"],
  "what": "The camera whips sideways so fast that scene A smears into a horizontal blur and scene B snaps sharp beside it.",
  "when": "Two things happening at once ('meanwhile, in support…') or a quick before/after. Adds energy without changing the subject.",
  "avoid": "Twice per film at most, and never back and forth in a row. Only a horizontal blur sells it; a plain gaussian blur reads as a focus problem.",
  "recipe": "Put A and B on one wide strip and fade in a directionally blurred copy at peak speed.\ntl.fromTo('#strip',{x:0},{x:-1920,duration:0.45,ease:'expo.inOut'},T);\ntl.fromTo('#strip-blur',{opacity:0},{opacity:1,duration:0.18,ease:'power2.in',yoyo:true,repeat:1},T+0.05); // copy uses an SVG feGaussianBlur stdDeviation='40 0'",
  "hyperframes": "shader: whip pan · css-blur (directional blur)"
 },
 {
  "id": "T11",
  "name": "Dolly-Out Reveal",
  "family": "camera",
  "energy": 2,
  "tones": ["premium", "trustworthy", "celebratory"],
  "what": "The camera pulls back from scene A until it becomes one tile in a grid of other scenes.",
  "when": "Revealing scale or context: 'this is one of 40 workflows', 'every team gets this view'. A strong move from one feature to the whole platform.",
  "avoid": "The other tiles must be real, readable product surfaces, not filler. Don't follow it with a push into a different tile; it reads as indecision.",
  "recipe": "Build the grid at its final size with A inside the target tile, and start it scaled so that tile fills the frame.\ntl.fromTo('#grid',{scale:3.333},{scale:1,duration:1.2,ease:'power3.inOut',transformOrigin:'50% 50%'},T);\ntl.fromTo('#grid .hero',{borderRadius:0},{borderRadius:16,duration:1.2,ease:'power3.inOut'},T);",
  "hyperframes": "css-scale (zoom out), inverted into a grid"
 },
 {
  "id": "T12",
  "name": "Parallax Slide",
  "family": "camera",
  "energy": 2,
  "tones": ["warm", "playful", "calm"],
  "what": "A slide from A to B on three layers at three speeds: the backdrop drifts, the main card slides, the foreground details rush past.",
  "when": "Stepping through a sequence (plan, build, ship) when a plain push feels flat. The depth turns a sideways move into travel through a space.",
  "avoid": "Three layers with clear ratios (about 0.3×, 1×, 1.4×) and no more. Not for dense data scenes: text moving at different speeds is hard to read.",
  "recipe": "One ease, three distances.\ntl.fromTo('#bg',{x:0},{x:-580,duration:1,ease:'power3.inOut'},T); tl.fromTo('#mid',{x:0},{x:-1920,duration:1,ease:'power3.inOut'},T);\ntl.fromTo('#fg',{x:0},{x:-2690,duration:1,ease:'power3.inOut'},T);",
  "hyperframes": "css-push (push slide), split into three depth layers"
 },
 {
  "id": "T13",
  "name": "Product-Shape Mask",
  "family": "mask",
  "energy": 2,
  "tones": ["premium", "bold", "celebratory"],
  "what": "Scene B shows through the brand mark (or a key UI shape), and the shape grows until B fills the frame.",
  "when": "Brand-to-product moments: the logo opens onto the product, the app icon opens onto the app. Also a strong first cut after a cold open.",
  "avoid": "The shape must be recognisable when small, so use simple marks and UI shapes. Once per film, and not in the same film as a text mask.",
  "recipe": "Animate clip-path inset() with the mark's corner radius from its rect to the full frame.\ntl.fromTo('#mark .fill',{opacity:1},{opacity:0,duration:0.25,ease:'power1.out'},T);\ntl.fromTo('#b',{clipPath:'inset(28% 43.5% 49% 43.5% round 62px)'},{clipPath:'inset(0% 0% 0% 0% round 0px)',duration:0.9,ease:'power3.inOut'},T+0.25); tl.fromTo('#mark .line',{scale:1,opacity:1},{scale:6,opacity:0,duration:0.7,ease:'power2.in'},T+0.25);",
  "hyperframes": "css-radial (circle / diamond iris) with a custom clip shape; shader: sdf-iris"
 },
 {
  "id": "T14",
  "name": "Text Mask",
  "family": "mask",
  "energy": 2,
  "tones": ["bold", "premium", "mysterious"],
  "what": "A huge word fills the frame with scene B showing through its letters; then the letters open up and B takes over.",
  "when": "Title moments and chapter openers where one word is the message ('LIVE', 'NOW', the product name). The classic name reveal for a launch film.",
  "avoid": "Needs heavy type and a visually rich scene B, or the letters look empty. Short words only (3–5 letters); never twice per film.",
  "recipe": "Knockout layer: ink background, white type, mix-blend-mode: multiply over B (isolate the parent).\ntl.fromTo('#knock',{scale:1.2,opacity:0},{scale:1,opacity:1,duration:0.3,ease:'power3.out'},T);\ntl.fromTo('#knock .ch',{scale:1},{scale:5,duration:0.7,ease:'power4.in',stagger:0.04},T+0.7); tl.fromTo('#knock',{backgroundColor:'#0E1726'},{backgroundColor:'#FFFFFF',duration:0.5,ease:'power2.in'},T+0.85);",
  "hyperframes": "custom (mix-blend knockout)"
 },
 {
  "id": "T15",
  "name": "Focal Iris",
  "family": "mask",
  "energy": 2,
  "tones": ["technical", "urgent", "mysterious"],
  "what": "An iris opens from the subject (the anomaly, the button, the face) rather than the frame centre, and scene B grows out of that point.",
  "when": "Drilling into a detail: 'look here, and this is what it means'. Where the iris starts tells the viewer what caused the next scene.",
  "avoid": "Make the subject obvious first (pulse or ring it). Don't open from the centre (that's the stock iris) or on a busy frame with two possible subjects.",
  "recipe": "Use the subject's centre as the origin of the ring, the clip and the lean-in.\ntl.fromTo('#a .ring',{scale:0.5,opacity:0},{scale:1,opacity:1,duration:0.4,ease:'back.out(2)'},T-0.5);\ntl.fromTo('#b',{clipPath:'circle(0% at 72% 34%)'},{clipPath:'circle(105% at 72% 34%)',duration:0.8,ease:'power3.in'},T); tl.fromTo('#a',{scale:1},{scale:1.2,duration:0.8,ease:'power3.in',transformOrigin:'72% 34%'},T);",
  "hyperframes": "css-radial (circle iris) with the origin moved to the subject"
 },
 {
  "id": "T16",
  "name": "Split Stack",
  "family": "mask",
  "energy": 3,
  "tones": ["bold", "technical", "urgent"],
  "what": "The frame splits into three horizontal strips and scene B slides in strip by strip, each at its own speed.",
  "when": "Topic changes and list beats ('three things changed'): the strips feel editorial and structured. Also good in a fast feature montage.",
  "avoid": "Line the strips up with B's layout so no headline is cut in half at rest. Too busy for calm films; don't pair it with other stripe or blind effects.",
  "recipe": "Three copies of B clipped to thirds, same ease, rising durations.\n['#b1','#b2','#b3'].forEach((s,i)=>tl.fromTo(s,{xPercent:100},{xPercent:0,duration:0.5+i*0.2,ease:'power4.out'},T+i*0.06));\ntl.fromTo('#a',{xPercent:0,filter:'brightness(1)'},{xPercent:-12,filter:'brightness(0.85)',duration:0.9,ease:'power2.in'},T);",
  "hyperframes": "css-cover (staggered blocks / blinds), reduced to three strips"
 },
 {
  "id": "T17",
  "name": "Paper Fold",
  "family": "material",
  "energy": 1,
  "tones": ["warm", "playful", "nostalgic"],
  "what": "Scene A folds in half like a sheet of paper, and the folded sheet drops away to show scene B underneath.",
  "when": "Closing one chapter and opening the next: a report folded into a proposal, a draft put away. Friendly and analogue, good for onboarding and small-team stories.",
  "avoid": "Needs a perspective parent, and the front and back faces must swap at exactly 90° or the fold flickers. Too slow for launch energy; never fold twice in a film.",
  "recipe": "Split A into halves; hinge the top half on its bottom edge (parent: perspective 1600px) and swap its front/back faces at 90°.\ntl.fromTo('#a-top',{rotationX:0},{rotationX:-180,duration:0.9,ease:'power2.inOut',transformOrigin:'50% 100%'},T); tl.set('#a-top .front',{opacity:0},T+0.45); tl.set('#a-top .back',{opacity:1},T+0.45);\ntl.fromTo('#a-sheet',{y:0,rotation:0},{y:1300,rotation:-6,duration:0.5,ease:'power3.in'},T+0.9);",
  "hyperframes": "css-3d (3D card flip), closest"
 },
 {
  "id": "T18",
  "name": "Ink Bleed",
  "family": "material",
  "energy": 1,
  "tones": ["mysterious", "premium", "warm"],
  "what": "A drop of ink lands on scene A and spreads with a rough, organic edge until the frame is ink, and scene B surfaces from it.",
  "when": "Moments of commitment and craft: signing, approving, publishing. Feels handmade, a welcome contrast in a very digital film.",
  "avoid": "Needs a slow, calm track; fast ink looks like a paint splash. One ink colour only, and not in the same film as glitch or dither.",
  "recipe": "Grow blobs inside a layer with a goo filter (feDisplacementMap + feGaussianBlur + alpha threshold).\ntl.fromTo('#ink .blob',{scale:0},{scale:1,duration:1.2,ease:'power2.in',stagger:0.08},T);\ntl.fromTo('#b .title',{opacity:0,y:20},{opacity:1,y:0,duration:0.6,ease:'power3.out'},T+1.1);",
  "hyperframes": "shader: domain warp / cross-warp morph, closest"
 },
 {
  "id": "T19",
  "name": "Dither / Pixel Sort",
  "family": "material",
  "energy": 3,
  "tones": ["technical", "nostalgic", "bold"],
  "what": "Scene B replaces A through an ordered dither: a wave of chunky pixels sweeps across, led by a band of sorted colour streaks.",
  "when": "Tech moments: data being processed, an AI 'thinking', a system rebuilding. A retro-digital accent that suits developer tools.",
  "avoid": "Chunky pixels only; fine dither vanishes in video compression. Once per film, and never over small text that must stay readable.",
  "recipe": "Four copies of B, each masked to one cell of a 2×2 Bayer pattern, wiped in with a lag.\n[0,1,2,3].forEach(i=>tl.fromTo('#b'+i,{clipPath:'inset(0% 100% 0% 0%)'},{clipPath:'inset(0% 0% 0% 0%)',duration:0.9,ease:'none'},T+i*0.12));\ntl.fromTo('#sort-band',{x:-200},{x:1920,duration:0.9,ease:'none'},T);",
  "hyperframes": "css-grid (grid dissolve), closest; shader: glitch"
 },
 {
  "id": "T20",
  "name": "Glass Refraction Wipe",
  "family": "material",
  "energy": 1,
  "tones": ["premium", "calm", "technical"],
  "what": "A pane of frosted glass slides across the frame; behind it scene A turns into scene B, softened and bent as it passes.",
  "when": "Premium, calm reveals: a new plan, a redesigned screen, a before/after told with restraint.",
  "avoid": "backdrop-filter is heavy to render, so keep the pane narrow and the move short. It doesn't read on flat, empty scenes; don't combine it with other blur transitions.",
  "recipe": "Drive the pane and the reveal edge with the same ease so the edge always sits inside the glass.\ntl.fromTo('#pane',{x:-640},{x:2120,duration:1.4,ease:'power2.inOut'},T); // pane: backdrop-filter blur(6px) saturate(1.5)\ntl.fromTo('#b',{clipPath:'inset(0% 100% 0% 0%)'},{clipPath:'inset(0% 0% 0% 0%)',duration:1.4,ease:'power2.inOut'},T);",
  "hyperframes": "custom; shader: chromatic split for the fringe"
 },
 {
  "id": "T21",
  "name": "Freeze & Rewind Scrub",
  "family": "time",
  "energy": 2,
  "tones": ["nostalgic", "playful", "technical"],
  "what": "Scene A freezes, then scrubs backwards at speed, the clock rolling back, until it lands on the earlier moment that becomes scene B.",
  "when": "Root-cause stories ('let's go back to 09:42'), flashbacks and 'how did we get here' openers.",
  "avoid": "The rewind has to reverse motion the viewer just saw, or it looks like a random glitch. Once per film; skip the VHS noise for a premium brand.",
  "recipe": "Reverse A's own tween with a second fromTo, step the clock back, then land on B.\ntl.fromTo('#line',{clipPath:'inset(0% 0% 0% 0%)'},{clipPath:'inset(0% 62% 0% 0%)',duration:0.7,ease:'power2.in'},T);\ntl.fromTo('#clock',{yPercent:-80},{yPercent:0,duration:0.7,ease:'steps(4)'},T); tl.fromTo('#b .marker',{scale:0},{scale:1,duration:0.4,ease:'back.out(2)'},T+0.7);",
  "hyperframes": "css-distortion (VHS tape) for the rewind texture"
 },
 {
  "id": "T22",
  "name": "Speed Ramp",
  "family": "time",
  "energy": 3,
  "tones": ["urgent", "bold", "celebratory"],
  "what": "Motion slows almost to a stop, rockets into a streaking blur through the change, then eases back to normal speed in scene B.",
  "when": "Covering distance or time in one breath: 'from plan to launch', flying along a roadmap, the drop in the music.",
  "avoid": "Needs continuous motion on both sides to ramp. One or two per film, on the biggest musical moments, and not next to a whip pan.",
  "recipe": "One long track, three chained fromTo tweens: slow, expo in, expo out.\ntl.fromTo('#track',{x:0},{x:-240,duration:1.2,ease:'none'},T-1.2); tl.fromTo('#track',{x:-240},{x:-10800,duration:0.65,ease:'expo.in',immediateRender:false},T);\ntl.fromTo('#track',{x:-10800},{x:-11500,duration:0.55,ease:'expo.out',immediateRender:false},T+0.65); // plus a blurred copy at the peak",
  "hyperframes": "css-blur (blur through / directional blur) at the peak"
 },
 {
  "id": "T23",
  "name": "Time-lapse Tick",
  "family": "time",
  "energy": 2,
  "tones": ["trustworthy", "warm", "nostalgic"],
  "what": "A clock or calendar ticks through hours or days at speed while the content grows with it, and the ticking stops on scene B.",
  "when": "Results over time: 'a week later', 'by Friday', a sprint in five seconds. Time itself becomes the proof.",
  "avoid": "The content must change as time passes, not just the clock. It implies waiting, so not for instant results; don't pair it with a speed ramp.",
  "recipe": "Step the calendar and the bars with steps(), spin the hand smoothly over the same window.\ntl.fromTo('#hand',{rotation:0},{rotation:1440,duration:1.2,ease:'power2.inOut'},T);\ntl.fromTo('#days',{yPercent:0},{yPercent:-80,duration:1.2,ease:'steps(4)'},T); tl.fromTo('#bars .bar',{scaleY:0},{scaleY:1,duration:0.01,ease:'none',stagger:0.3},T+0.3);",
  "hyperframes": "css-mechanical (clock wipe), closest"
 },
 {
  "id": "T24",
  "name": "Loop Seam",
  "family": "time",
  "energy": 1,
  "tones": ["calm", "premium", "playful"],
  "what": "The film's last move lands exactly on its first frame, so the loop plays forever without a visible seam.",
  "when": "Booth screens, social loops, website hero videos and GIFs: anything that plays on repeat, often without sound.",
  "avoid": "Every property must match at the seam: position, colour, grain, even a particle's phase. Don't hide a bad seam with a fade; it shows on every loop.",
  "recipe": "Author the end as the start: the last tween's 'to' is the first frame's 'from'.\ntl.fromTo('#strip',{yPercent:0},{yPercent:-33.333,duration:0.8,ease:'power3.inOut'},1); tl.fromTo('#strip',{yPercent:-33.333},{yPercent:-66.667,duration:0.8,ease:'power3.inOut',immediateRender:false},2.6);\ntl.fromTo('#orbit',{rotation:0},{rotation:360,duration:DUR,ease:'none'},0); // then: python tools/loop_check.py render.mp4",
  "hyperframes": "custom; verify with tools/loop_check.py"
 }
]
