{"id":14788,"date":"2026-08-01T14:19:23","date_gmt":"2026-08-01T14:19:23","guid":{"rendered":"https:\/\/www.agence-exoa.com\/?page_id=14788"},"modified":"2026-08-03T16:41:15","modified_gmt":"2026-08-03T16:41:15","slug":"pack-motion-ads","status":"publish","type":"page","link":"https:\/\/www.agence-exoa.com\/de\/pack-motion-ads\/","title":{"rendered":"Pack Motion &amp; Ads \u2014 Vid\u00e9o, carrousels et campagnes pr\u00eates \u00e0 diffuser | Exoa"},"content":{"rendered":"\n<style>:root{--navy:#060039;--navy2:#0C0A4A;--lime:#CDF980;--lav:#808CF9;--lav2:#5159A4;--violet:#7670E8;--ink:#060039;--body:#565967;--line:#E4E6FF;--bg:#F8FAFB}.exoa-pack-motion-ads-page *{margin:0;padding:0;box-sizing:border-box}html{scroll-behavior:smooth}.exoa-pack-motion-ads-page{font-family:'DM Sans',sans-serif;color:var(--ink);background:#fff;-webkit-font-smoothing:antialiased}.exoa-pack-motion-ads-page a{color:var(--lav2);text-decoration:none}.exoa-pack-motion-ads-page a:hover{color:var(--violet)}.exoa-pack-motion-ads-page .wrap{max-width:1200px;margin:0 auto;padding:0 32px}.exoa-pack-motion-ads-page .mono{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}.exoa-pack-motion-ads-page h1, .exoa-pack-motion-ads-page h2, .exoa-pack-motion-ads-page h3, .exoa-pack-motion-ads-page .disp{font-family:'Bricolage Grotesque',sans-serif;font-weight:800;letter-spacing:-.02em;line-height:1.02}.exoa-pack-motion-ads-page .nav{position:sticky;top:0;z-index:60;background:rgba(6,0,57,.88);backdrop-filter:blur(14px);border-bottom:1px solid rgba(255,255,255,.1)}.exoa-pack-motion-ads-page .nav-in{max-width:1200px;margin:0 auto;padding:14px 32px;display:flex;align-items:center;justify-content:space-between;gap:20px}.exoa-pack-motion-ads-page .logo{display:flex;align-items:center;gap:9px;color:#fff;font-weight:700;font-size:15px;letter-spacing:.02em}.exoa-pack-motion-ads-page .logo svg{width:17px;height:17px}.exoa-pack-motion-ads-page .nav-mid{display:flex;gap:26px}.exoa-pack-motion-ads-page .nav-mid a{color:#C6C9E0;font-size:14px;font-weight:500}.exoa-pack-motion-ads-page .nav-mid a:hover{color:#fff}.exoa-pack-motion-ads-page .btn{display:inline-flex;align-items:center;gap:8px;padding:13px 24px;border-radius:99px;font-size:14.5px;font-weight:600;font-family:inherit;border:0;cursor:pointer;transition:all .2s;white-space:nowrap}.exoa-pack-motion-ads-page .btn-lime{background:var(--lime);color:var(--navy)}.exoa-pack-motion-ads-page .btn-lime:hover{background:#dcff9c;color:var(--navy);transform:translateY(-1px)}.exoa-pack-motion-ads-page .btn-ghost{background:rgba(255,255,255,.08);color:#fff;border:1px solid rgba(255,255,255,.2)}.exoa-pack-motion-ads-page .btn-ghost:hover{background:rgba(255,255,255,.16);color:#fff}.exoa-pack-motion-ads-page .btn-navy{background:var(--navy);color:#fff}.exoa-pack-motion-ads-page .btn-navy:hover{background:var(--navy2);color:#fff;transform:translateY(-1px)}.exoa-pack-motion-ads-page .btn-out{background:#fff;color:var(--navy);border:1.5px solid var(--line)}.exoa-pack-motion-ads-page .btn-out:hover{border-color:var(--lav);color:var(--navy)}.exoa-pack-motion-ads-page .hero{background:var(--navy);color:#fff;padding:88px 0 0;position:relative;overflow:hidden}.exoa-pack-motion-ads-page .hero::before{content:'';position:absolute;top:-260px;right:-180px;width:640px;height:640px;border-radius:50%;background:radial-gradient(circle,rgba(118,112,232,.42),transparent 68%);pointer-events:none}.exoa-pack-motion-ads-page .hero::after{content:'';position:absolute;bottom:-200px;left:-160px;width:520px;height:520px;border-radius:50%;background:radial-gradient(circle,rgba(205,249,128,.14),transparent 70%);pointer-events:none}.exoa-pack-motion-ads-page .hero-in{position:relative;z-index:2}.exoa-pack-motion-ads-page .pill{display:inline-flex;align-items:center;gap:9px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.16);border-radius:99px;padding:8px 16px;margin-bottom:26px}.exoa-pack-motion-ads-page .pill .dot{width:7px;height:7px;border-radius:50%;background:var(--lime);flex-shrink:0;box-shadow:0 0 0 4px rgba(205,249,128,.18)}.exoa-pack-motion-ads-page .pill span{font-family:'JetBrains Mono',monospace;font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:#E8EAF6}.exoa-pack-motion-ads-page .hero h1{font-size:clamp(40px,5.6vw,76px);max-width:15ch}.exoa-pack-motion-ads-page .hero h1 em{font-style:normal;color:var(--lime)}.exoa-pack-motion-ads-page .hero h1 .v{font-style:normal;color:var(--violet)}.exoa-pack-motion-ads-page .hero-sub{font-size:clamp(16px,1.35vw,19px);color:#C6C9E0;line-height:1.6;max-width:60ch;margin:24px 0 32px;text-wrap:pretty}.exoa-pack-motion-ads-page .hero-cta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.exoa-pack-motion-ads-page .hero-note{font-size:13px;color:#9498b8;margin-top:18px;display:flex;gap:18px;flex-wrap:wrap}.exoa-pack-motion-ads-page .hero-note b{color:#E8EAF6;font-weight:600}.exoa-pack-motion-ads-page .reel{margin-top:56px;position:relative;z-index:2}.exoa-pack-motion-ads-page .reel-frame{position:relative;border-radius:20px 20px 0 0;overflow:hidden;border:1px solid rgba(255,255,255,.16);border-bottom:0;background:#0C0A4A;aspect-ratio:16\/7}.exoa-pack-motion-ads-page .reel-bar{position:absolute;top:0;left:0;right:0;height:38px;display:flex;align-items:center;gap:7px;padding:0 14px;background:rgba(6,0,57,.72);backdrop-filter:blur(8px);z-index:3;border-bottom:1px solid rgba(255,255,255,.1)}.exoa-pack-motion-ads-page .reel-bar i{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.22)}.exoa-pack-motion-ads-page .reel-bar em{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.1em;color:#9498b8;margin-left:8px}.exoa-pack-motion-ads-page .marq{background:var(--lime);color:var(--navy);padding:15px 0;overflow:hidden;white-space:nowrap;border-top:1px solid rgba(6,0,57,.1)}.exoa-pack-motion-ads-page .marq-t{display:inline-flex;gap:44px;animation:sc 26s linear infinite;padding-right:44px}.exoa-pack-motion-ads-page .marq-t span{font-family:'JetBrains Mono',monospace;font-size:12.5px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;display:inline-flex;align-items:center;gap:44px}.exoa-pack-motion-ads-page .marq-t span::after{content:'\u2726';opacity:.45}@keyframes sc{to{transform:translateX(-50%)}}.exoa-pack-motion-ads-page .sec{padding:104px 0}.exoa-pack-motion-ads-page .sec-lt{background:var(--bg)}.exoa-pack-motion-ads-page .sec-dk{background:var(--navy);color:#fff}.exoa-pack-motion-ads-page .tag{display:inline-flex;align-items:center;gap:8px;margin-bottom:16px}.exoa-pack-motion-ads-page .tag i{width:22px;height:2px;background:var(--lav);border-radius:2px}.exoa-pack-motion-ads-page .tag span{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-pack-motion-ads-page .sec-dk .tag span{color:var(--lime)}.exoa-pack-motion-ads-page .sec-dk .tag i{background:var(--lime)}.exoa-pack-motion-ads-page .h2{font-size:clamp(30px,3.6vw,50px);max-width:22ch}.exoa-pack-motion-ads-page .h2 em{font-style:normal;color:var(--lav2)}.exoa-pack-motion-ads-page .sec-dk .h2 em{color:var(--lime)}.exoa-pack-motion-ads-page .lead{font-size:17px;color:var(--body);line-height:1.65;max-width:64ch;margin-top:18px;text-wrap:pretty}.exoa-pack-motion-ads-page .sec-dk .lead{color:#C6C9E0}.exoa-pack-motion-ads-page .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:40px;margin-bottom:52px;flex-wrap:wrap}.exoa-pack-motion-ads-page .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:52px}.exoa-pack-motion-ads-page .stat{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:18px;padding:30px}.exoa-pack-motion-ads-page .stat-n{font-family:'Bricolage Grotesque',sans-serif;font-size:46px;font-weight:800;color:var(--lime);letter-spacing:-.03em;line-height:1}.exoa-pack-motion-ads-page .stat-l{font-size:14.5px;color:#C6C9E0;line-height:1.55;margin-top:12px}.exoa-pack-motion-ads-page .bricks{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.exoa-pack-motion-ads-page .brick{background:#fff;border:1px solid var(--line);border-radius:20px;overflow:hidden;display:flex;flex-direction:column;transition:all .22s}.exoa-pack-motion-ads-page .brick:hover{border-color:var(--lav);transform:translateY(-3px);box-shadow:0 16px 40px -20px rgba(6,0,57,.22)}.exoa-pack-motion-ads-page .brick-vis{width:100%;min-width:0;aspect-ratio:4\/3;background:var(--bg);border-bottom:1px solid var(--line);position:relative}.exoa-pack-motion-ads-page .brick>*{min-width:0}.exoa-pack-motion-ads-page .brick-b{padding:26px;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-pack-motion-ads-page .brick-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.12em;font-weight:700;color:var(--lav2)}.exoa-pack-motion-ads-page .brick h3{font-size:21px;letter-spacing:-.01em}.exoa-pack-motion-ads-page .brick p{font-size:14.5px;color:var(--body);line-height:1.62}.exoa-pack-motion-ads-page .brick ul{list-style:none;display:flex;flex-direction:column;gap:8px;margin-top:4px}.exoa-pack-motion-ads-page .brick li{font-size:13.5px;color:var(--body);padding-left:20px;position:relative;line-height:1.5}.exoa-pack-motion-ads-page .brick li::before{content:'';position:absolute;left:0;top:7px;width:8px;height:8px;border-radius:2px;background:var(--lime);border:1px solid rgba(6,0,57,.15)}.exoa-pack-motion-ads-page .packs{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;align-items:start}.exoa-pack-motion-ads-page .pack{background:#fff;border:1px solid var(--line);border-radius:22px;padding:32px;display:flex;flex-direction:column;gap:18px;position:relative}.exoa-pack-motion-ads-page .pack.hot{background:var(--navy);border-color:var(--navy);color:#fff;box-shadow:0 24px 60px -28px rgba(6,0,57,.5)}.exoa-pack-motion-ads-page .pack-bdg{position:absolute;top:-12px;left:32px;background:var(--lime);color:var(--navy);font-family:'JetBrains Mono',monospace;font-size:10px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;padding:6px 14px;border-radius:99px}.exoa-pack-motion-ads-page .pack-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-pack-motion-ads-page .pack.hot .pack-n{color:var(--lime)}.exoa-pack-motion-ads-page .pack h3{font-size:25px}.exoa-pack-motion-ads-page .pack-for{font-size:13.5px;color:var(--body);line-height:1.55}.exoa-pack-motion-ads-page .pack.hot .pack-for{color:#C6C9E0}.exoa-pack-motion-ads-page .pack-p{display:flex;align-items:baseline;gap:7px;padding-top:6px;border-top:1px solid var(--line)}.exoa-pack-motion-ads-page .pack.hot .pack-p{border-color:rgba(255,255,255,.15)}.exoa-pack-motion-ads-page .pack-p b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;letter-spacing:-.03em}.exoa-pack-motion-ads-page .pack.hot .pack-p b{color:var(--lime)}.exoa-pack-motion-ads-page .pack-p i{font-style:normal;font-size:13px;color:var(--body);font-weight:500}.exoa-pack-motion-ads-page .pack.hot .pack-p i{color:#9CA0C4}.exoa-pack-motion-ads-page .pack-l{list-style:none;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-pack-motion-ads-page .pack-l li{font-size:14px;line-height:1.5;padding-left:24px;position:relative;color:var(--body)}.exoa-pack-motion-ads-page .pack.hot .pack-l li{color:#DDE0F0}.exoa-pack-motion-ads-page .pack-l li::before{content:'';position:absolute;left:0;top:5px;width:13px;height:13px;border-radius:50%;background:var(--lime);box-shadow:inset 0 0 0 1px rgba(6,0,57,.2)}.exoa-pack-motion-ads-page .pack-l li b{color:var(--ink);font-weight:700}.exoa-pack-motion-ads-page .pack.hot .pack-l li b{color:#fff}.exoa-pack-motion-ads-page .pack-l li.plus::before{background:var(--violet);box-shadow:none}.exoa-pack-motion-ads-page .pack .btn{justify-content:center;width:100%}.exoa-pack-motion-ads-page .sub{margin-top:26px;background:linear-gradient(100deg,var(--violet),var(--lav2));border-radius:22px;padding:36px 40px;color:#fff;display:flex;gap:32px;align-items:center;flex-wrap:wrap}.exoa-pack-motion-ads-page .sub-l{flex:1;min-width:280px}.exoa-pack-motion-ads-page .sub .mono{color:rgba(255,255,255,.75);display:block;margin-bottom:9px}.exoa-pack-motion-ads-page .sub h3{font-size:26px;margin-bottom:10px}.exoa-pack-motion-ads-page .sub p{font-size:14.5px;color:rgba(255,255,255,.9);line-height:1.6;max-width:58ch}.exoa-pack-motion-ads-page .sub-r{text-align:right}.exoa-pack-motion-ads-page .sub-r b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;display:block;letter-spacing:-.03em}.exoa-pack-motion-ads-page .sub-r i{font-style:normal;font-size:13px;color:rgba(255,255,255,.8)}.exoa-pack-motion-ads-page .fmts{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:12px}.exoa-pack-motion-ads-page .fmt{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:16px;padding:22px;display:flex;flex-direction:column;gap:12px}.exoa-pack-motion-ads-page .fmt-r{border:1.5px dashed rgba(205,249,128,.5);border-radius:7px;background:rgba(205,249,128,.07)}.exoa-pack-motion-ads-page .fmt b{font-size:15px;color:#fff}.exoa-pack-motion-ads-page .fmt small{font-size:12.5px;color:#9CA0C4;line-height:1.5;display:block}.exoa-pack-motion-ads-page .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:12px;border-top:1px solid var(--line)}.exoa-pack-motion-ads-page .step{padding:30px 26px 30px 0;border-right:1px solid var(--line);position:relative}.exoa-pack-motion-ads-page .step:last-child{border-right:0;padding-right:0}.exoa-pack-motion-ads-page .step::before{content:'';position:absolute;top:-1px;left:0;width:38%;height:3px;background:var(--lime)}.exoa-pack-motion-ads-page .step-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.12em;font-weight:700;color:var(--lav2);margin-bottom:12px;display:block}.exoa-pack-motion-ads-page .step h3{font-size:18px;margin-bottom:9px;letter-spacing:-.01em}.exoa-pack-motion-ads-page .step p{font-size:14px;color:var(--body);line-height:1.6}.exoa-pack-motion-ads-page .step em{font-style:normal;display:inline-block;margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10.5px;font-weight:700;letter-spacing:.1em;color:var(--violet);background:rgba(118,112,232,.1);padding:4px 10px;border-radius:99px}.exoa-pack-motion-ads-page .adds{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}.exoa-pack-motion-ads-page .add{background:#fff;border:1px solid var(--line);border-radius:14px;padding:20px 22px;display:flex;justify-content:space-between;align-items:center;gap:14px;transition:all .2s}.exoa-pack-motion-ads-page .add:hover{border-color:var(--lav)}.exoa-pack-motion-ads-page .add b{font-size:14.5px;font-weight:600;display:block;margin-bottom:3px}.exoa-pack-motion-ads-page .add small{font-size:12.5px;color:var(--body)}.exoa-pack-motion-ads-page .add i{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:var(--lav2);white-space:nowrap}.exoa-pack-motion-ads-page .gal{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:12px}.exoa-pack-motion-ads-page .gal-i{aspect-ratio:9\/16;border-radius:16px;overflow:hidden;border:1px solid rgba(255,255,255,.14);background:rgba(255,255,255,.04)}.exoa-pack-motion-ads-page .gal-i.wide{grid-column:span 2;aspect-ratio:16\/10}.exoa-pack-motion-ads-page .faq{max-width:840px;margin:0 auto;display:flex;flex-direction:column;gap:10px}.exoa-pack-motion-ads-page details{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden;transition:border-color .2s}.exoa-pack-motion-ads-page details[open]{border-color:var(--lav)}.exoa-pack-motion-ads-page summary{padding:20px 24px;font-size:16px;font-weight:600;cursor:pointer;list-style:none;display:flex;justify-content:space-between;gap:18px;align-items:center}.exoa-pack-motion-ads-page summary::-webkit-details-marker{display:none}.exoa-pack-motion-ads-page summary::after{content:'+';font-family:'Bricolage Grotesque',sans-serif;font-size:22px;color:var(--lav2);flex-shrink:0;line-height:1}.exoa-pack-motion-ads-page details[open] summary::after{content:'\u2212'}.exoa-pack-motion-ads-page details p{padding:0 24px 22px;font-size:14.5px;color:var(--body);line-height:1.7}.exoa-pack-motion-ads-page details p b{color:var(--ink)}.exoa-pack-motion-ads-page .fin{background:var(--navy);color:#fff;padding:104px 0;position:relative;overflow:hidden}.exoa-pack-motion-ads-page .fin::before{content:'';position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);width:900px;height:900px;border-radius:50%;background:radial-gradient(circle,rgba(118,112,232,.3),transparent 65%)}.exoa-pack-motion-ads-page .fin-in{position:relative;z-index:2;display:grid;grid-template-columns:1fr .9fr;gap:56px;align-items:start}.exoa-pack-motion-ads-page .fin h2{font-size:clamp(32px,4vw,54px);max-width:16ch}.exoa-pack-motion-ads-page .fin h2 em{font-style:normal;color:var(--lime)}.exoa-pack-motion-ads-page .fin p{font-size:16.5px;color:#C6C9E0;line-height:1.65;margin-top:20px;max-width:50ch}.exoa-pack-motion-ads-page .fin-tr{display:flex;flex-direction:column;gap:12px;margin-top:30px}.exoa-pack-motion-ads-page .fin-tr div{display:flex;gap:11px;align-items:flex-start;font-size:14.5px;color:#DDE0F0;line-height:1.5}.exoa-pack-motion-ads-page .fin-tr b{color:var(--lime);flex-shrink:0}.exoa-pack-motion-ads-page .form{background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.14);border-radius:22px;padding:32px;backdrop-filter:blur(10px)}.exoa-pack-motion-ads-page .form h3{font-size:20px;margin-bottom:6px}.exoa-pack-motion-ads-page .form>p{font-size:13.5px;color:#9CA0C4;margin-bottom:22px}.exoa-pack-motion-ads-page .fg{display:grid;grid-template-columns:1fr 1fr;gap:13px}.exoa-pack-motion-ads-page .fld{display:flex;flex-direction:column;gap:6px}.exoa-pack-motion-ads-page .fld.full{grid-column:1\/-1}.exoa-pack-motion-ads-page .fld label{font-size:12.5px;font-weight:600;color:#DDE0F0}.exoa-pack-motion-ads-page .fld input, .exoa-pack-motion-ads-page .fld select, .exoa-pack-motion-ads-page .fld textarea{padding:11px 14px;border-radius:9px;border:1px solid rgba(255,255,255,.18);background:rgba(255,255,255,.07);color:#fff;font-size:14px;font-family:inherit}.exoa-pack-motion-ads-page .fld input::placeholder, .exoa-pack-motion-ads-page .fld textarea::placeholder{color:#7C81A6}.exoa-pack-motion-ads-page .fld select option{background:var(--navy);color:#fff}.exoa-pack-motion-ads-page .fld input:focus, .exoa-pack-motion-ads-page .fld select:focus, .exoa-pack-motion-ads-page .fld textarea:focus{outline:0;border-color:var(--lime)}.exoa-pack-motion-ads-page .chk{display:flex;gap:9px;align-items:flex-start;font-size:12px;color:#9CA0C4;line-height:1.5;margin-top:14px}.exoa-pack-motion-ads-page .chk input{margin-top:2px;accent-color:var(--lime)}.exoa-pack-motion-ads-page .ok{display:none;text-align:center;padding:36px 12px}.exoa-pack-motion-ads-page .ok.on{display:block}.exoa-pack-motion-ads-page .ok h3{font-size:23px;color:var(--lime);margin-bottom:10px}.exoa-pack-motion-ads-page .ok p{font-size:14.5px;color:#C6C9E0;line-height:1.6}.exoa-pack-motion-ads-page .foot{background:#04002B;color:#7C81A6;padding:34px 0;font-size:13px}.exoa-pack-motion-ads-page .foot-in{max-width:1200px;margin:0 auto;padding:0 32px;display:flex;justify-content:space-between;gap:20px;flex-wrap:wrap;align-items:center}.exoa-pack-motion-ads-page .foot a{color:#9CA0C4}.exoa-pack-motion-ads-page .foot a:hover{color:#fff}@media(max-width:1000px){.exoa-pack-motion-ads-page .bricks, .exoa-pack-motion-ads-page .packs, .exoa-pack-motion-ads-page .stats{grid-template-columns:1fr 1fr}.exoa-pack-motion-ads-page .fmts{grid-template-columns:1fr 1fr}.exoa-pack-motion-ads-page .steps{grid-template-columns:1fr 1fr}.exoa-pack-motion-ads-page .step{border-right:0;border-bottom:1px solid var(--line);padding-right:26px}.exoa-pack-motion-ads-page .fin-in{grid-template-columns:1fr}.exoa-pack-motion-ads-page .gal{grid-template-columns:1fr 1fr}.exoa-pack-motion-ads-page .gal-i.wide{grid-column:span 2}}@media(max-width:700px){.exoa-pack-motion-ads-page .wrap{padding:0 20px}.exoa-pack-motion-ads-page .nav-mid{display:none}.exoa-pack-motion-ads-page .sec{padding:64px 0}.exoa-pack-motion-ads-page .bricks, .exoa-pack-motion-ads-page .packs, .exoa-pack-motion-ads-page .stats, .exoa-pack-motion-ads-page .fmts, .exoa-pack-motion-ads-page .steps, .exoa-pack-motion-ads-page .gal, .exoa-pack-motion-ads-page .fg{grid-template-columns:1fr}.exoa-pack-motion-ads-page .gal-i.wide{grid-column:span 1}.exoa-pack-motion-ads-page .hero{padding-top:56px}.exoa-pack-motion-ads-page .hero-cta{flex-direction:column;align-items:stretch}.exoa-pack-motion-ads-page .hero-cta .btn{justify-content:center}.exoa-pack-motion-ads-page .sub{padding:28px 24px}.exoa-pack-motion-ads-page .sub-r{text-align:left}.exoa-pack-motion-ads-page .head-row{margin-bottom:36px}.exoa-pack-motion-ads-page .foot-in{flex-direction:column;align-items:flex-start}}.exoa-pack-motion-ads-page .pack-l li.neu::before{background:var(--lav);box-shadow:none}.exoa-pack-motion-ads-page .grid2{display:grid;grid-template-columns:repeat(2,1fr);gap:22px}.exoa-pack-motion-ads-page .lev{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:16px;padding:24px}.exoa-pack-motion-ads-page .lev b{font-size:16px;color:#fff;display:block;margin-bottom:7px}.exoa-pack-motion-ads-page .lev small{font-size:13.5px;color:#9CA0C4;line-height:1.6;display:block}.exoa-pack-motion-ads-page .lev-t{font-family:'JetBrains Mono',monospace;font-size:10.5px;letter-spacing:.12em;font-weight:700;color:var(--lime);display:block;margin-bottom:10px}.exoa-pack-motion-ads-page .cmp{width:100%;border-collapse:collapse;font-size:14.5px;background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden}.exoa-pack-motion-ads-page .cmp th{padding:16px 18px;text-align:left;font-weight:700;font-size:13.5px;color:var(--ink);background:var(--bg);border-bottom:1px solid var(--line)}.exoa-pack-motion-ads-page .cmp td{padding:15px 18px;color:var(--body);border-bottom:1px solid var(--line);line-height:1.5}.exoa-pack-motion-ads-page .cmp tr:last-child td{border-bottom:0}.exoa-pack-motion-ads-page .cmp td:first-child{font-weight:600;color:var(--ink)}.exoa-pack-motion-ads-page .cmp .yes{color:var(--lav2);font-weight:600}.exoa-pack-motion-ads-page .cmp-w{overflow-x:auto;border-radius:16px}@media(max-width:1000px){.exoa-pack-motion-ads-page .grid2{grid-template-columns:1fr}}\n.exoa-pack-motion-ads-page{margin-top:0 !important;padding-top:0 !important}\n.exoa-pack-motion-ads-page .btn-primary,.exoa-pack-motion-ads-page .btn.btn-primary{color:#060039 !important;background:#CDF980 !important;font-weight:700}\n\nbody .site-content,body #content,body #primary,body .site-main,body .ast-container,body .content-area,body .ast-plain-container,body .ast-separate-container,body .ast-separate-container .ast-article-single{padding-top:0 !important;margin-top:0 !important;background:#060039 !important}\nbody.ast-separate-container{background-color:#060039 !important}\nbody .entry-content,body .entry-header{margin:0 !important;padding:0 !important}\nbody .ast-article-single{padding:0 !important;margin:0 !important;border:none !important;background:transparent !important}\n\n.exoa-pack-motion-ads-page .resx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}\n.exoa-pack-motion-ads-page .resx-card{background:#fff;border:1px solid rgba(6,0,57,.1);border-radius:16px;padding:30px 26px}\n.exoa-pack-motion-ads-page .resx-num{font-size:34px;font-weight:800;color:#060039;line-height:1.1}\n.exoa-pack-motion-ads-page .resx-card h3{font-size:17px;font-weight:700;color:#060039;margin:6px 0 8px}\n.exoa-pack-motion-ads-page .resx-card p{font-size:14px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-pack-motion-ads-page .livr-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-pack-motion-ads-page .livr-card{background:#F7F7FB;border:1px solid rgba(6,0,57,.08);border-radius:14px;padding:24px 22px}\n.exoa-pack-motion-ads-page .livr-card h3{font-size:15.5px;font-weight:700;color:#060039;margin:0 0 6px}\n.exoa-pack-motion-ads-page .livr-card h3 span{color:#8CC63F;font-weight:800;margin-right:6px}\n.exoa-pack-motion-ads-page .livr-card p{font-size:13.5px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-pack-motion-ads-page .expx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-pack-motion-ads-page .expx-card{display:block;background:rgba(255,255,255,.05);border:1px solid rgba(255,255,255,.14);border-radius:14px;padding:22px;text-decoration:none}\n.exoa-pack-motion-ads-page .expx-card:hover{background:rgba(255,255,255,.1);border-color:rgba(205,249,128,.5)}\n.exoa-pack-motion-ads-page .expx-card strong{display:block;color:#fff;font-size:15.5px;font-weight:700;margin-bottom:4px}\n.exoa-pack-motion-ads-page .expx-card span{display:block;color:#C6C9E0;font-size:13px;line-height:1.5}\n.exoa-pack-motion-ads-page .expx-card em{display:inline-block;margin-top:10px;font-style:normal;color:#CDF980;font-size:12px;font-weight:700}\n.exoa-pack-motion-ads-page .hero h1{color:#fff !important}\n@media(max-width:900px){.exoa-pack-motion-ads-page .resx-grid,.exoa-pack-motion-ads-page .livr-grid,.exoa-pack-motion-ads-page .expx-grid{grid-template-columns:1fr 1fr}}\n@media(max-width:600px){.exoa-pack-motion-ads-page .resx-grid,.exoa-pack-motion-ads-page .livr-grid,.exoa-pack-motion-ads-page .expx-grid{grid-template-columns:1fr}}\n\/*exoa-landing-fix*\/\n.exoa-pack-motion-ads-page .sec-dk h2,.exoa-pack-motion-ads-page .sec-dk .h2,.exoa-pack-motion-ads-page .sec-dk h3,.exoa-pack-motion-ads-page .fin h2,.exoa-pack-motion-ads-page .fin h3{color:#fff !important}\n.exoa-pack-motion-ads-page .sec-dk .h2 em,.exoa-pack-motion-ads-page .fin h2 em{color:#CDF980 !important}\n.exoa-pack-motion-ads-page .sec:not(.sec-dk) h2,.exoa-pack-motion-ads-page .sec:not(.sec-dk) .h2{color:#060039}\n.exoa-pack-motion-ads-page .steps{border-top:0 !important;gap:16px !important}\n.exoa-pack-motion-ads-page .step{border-right:0 !important;background:#fff;border:1px solid #E4E6FF !important;border-radius:14px;padding:26px 22px !important;box-shadow:0 6px 18px rgba(6,0,57,.05)}\n.exoa-pack-motion-ads-page .step::before{position:static !important;display:block;width:44px;height:3px;border-radius:99px;margin-bottom:14px}\n@media(max-width:768px){\n  .exoa-pack-motion-ads-page .reel{display:none !important}\n  .exoa-pack-motion-ads-page .hero{padding-bottom:40px !important}\n  .exoa-pack-motion-ads-page .hero-note{margin-bottom:0 !important}\n}\n\/*exoa-width-fix*\/\n.exoa-pack-motion-ads-page .hero h1{max-width:24ch !important}\n.exoa-pack-motion-ads-page .h2{max-width:38ch !important}\n.exoa-pack-motion-ads-page .fin h2{max-width:28ch !important}\n.exoa-pack-motion-ads-page .sec .wrap > p,.exoa-pack-motion-ads-page .h2 + p,.exoa-pack-motion-ads-page .tag ~ p,.exoa-pack-motion-ads-page .fin p,.exoa-pack-motion-ads-page .sub p{max-width:76ch !important}\n.exoa-pack-motion-ads-page .hero-sub{max-width:70ch !important}\n\/*exoa-pack-align*\/\n.exoa-pack-motion-ads-page .packs{align-items:stretch !important}\n.exoa-pack-motion-ads-page .pack{height:100%}\n.exoa-pack-motion-ads-page .pack > p,.exoa-pack-motion-ads-page .pack .pack-for{min-height:3.4em}\n.exoa-pack-motion-ads-page .pack ul,.exoa-pack-motion-ads-page .pack .pack-l{flex:1}\n.exoa-pack-motion-ads-page .pack .btn,.exoa-pack-motion-ads-page .pack a[class*=\"btn\"]{margin-top:auto}\n@media(max-width:900px){.exoa-pack-motion-ads-page .pack > p,.exoa-pack-motion-ads-page .pack .pack-for{min-height:0}}\n<\/style>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"Service\",\"name\":\"Pack Motion & Ads \u2014 Exoa\",\"description\":\"Pack cr\u00e9atif scalable : vid\u00e9o motion de lancement ou promotion, carrousels publicitaires et diffusion des campagnes publicitaires.\",\"provider\":{\"@type\":\"Organization\",\"name\":\"Exoa\",\"url\":\"https:\/\/www.agence-exoa.com\"},\"areaServed\":[{\"@type\":\"Country\",\"name\":\"France\"},{\"@type\":\"Country\",\"name\":\"Luxembourg\"}],\"serviceType\":\"Motion design & publicit\u00e9 digitale\",\"offers\":[{\"@type\":\"Offer\",\"name\":\"Pack Impulse\",\"price\":\"890\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Pack Momentum\",\"price\":\"1690\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Pack Launch\",\"price\":\"2890\",\"priceCurrency\":\"EUR\"}]}\n<\/script>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"FAQPage\",\"mainEntity\":[{\"@type\":\"Question\",\"name\":\"En combien de temps la vid\u00e9o est-elle livr\u00e9e ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"10 jours ouvr\u00e9s \u00e0 partir de la validation du script pour le Pack Impulse, 15 jours pour Momentum et Launch. Deux allers-retours de retouches sont inclus dans chaque pack.\"}},{\"@type\":\"Question\",\"name\":\"Faut-il fournir des \u00e9l\u00e9ments graphiques ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Id\u00e9alement votre logo, vos couleurs et vos polices. Si vous n'avez pas de charte, nous construisons un mini univers graphique \u2014 inclus dans les packs Momentum et Launch.\"}},{\"@type\":\"Question\",\"name\":\"Qui \u00e9crit le script de la vid\u00e9o ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Nous. Le script, le storyboard et l'accroche sont r\u00e9dig\u00e9s par nos \u00e9quipes \u00e0 partir d'un brief de 30 minutes, puis valid\u00e9s par vous avant production.\"}},{\"@type\":\"Question\",\"name\":\"Le budget publicitaire est-il inclus dans les prix ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Non. Les tarifs affich\u00e9s couvrent la cr\u00e9ation et la gestion. Le budget m\u00e9dia (ce que vous versez \u00e0 Meta, Google ou TikTok) est r\u00e9gl\u00e9 directement depuis votre compte publicitaire \u2014 vous en gardez la ma\u00eetrise totale.\"}},{\"@type\":\"Question\",\"name\":\"Sur quelles plateformes les cr\u00e9ations sont-elles diffusables ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Toutes : Meta (Instagram, Facebook), TikTok, LinkedIn, YouTube, Pinterest, Snapchat et display programmatique. Chaque livrable est d\u00e9clin\u00e9 en 9:16, 1:1, 4:5 et 16:9.\"}},{\"@type\":\"Question\",\"name\":\"Pourquoi \u00eates-vous moins cher que les studios motion ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Parce que nous travaillons par packs standardis\u00e9s plut\u00f4t qu'en projet sur mesure : un cadre de production \u00e9prouv\u00e9, des templates propri\u00e9taires et un p\u00e9rim\u00e8tre clair. La qualit\u00e9 reste celle d'un studio, sans les co\u00fbts de la personnalisation totale.\"}},{\"@type\":\"Question\",\"name\":\"Que se passe-t-il si la vid\u00e9o ne me pla\u00eet pas ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Vous validez le script et le storyboard avant toute production, ce qui \u00e9limine l'essentiel du risque. Deux s\u00e9ries de retouches sont ensuite incluses sur l'animation.\"}},{\"@type\":\"Question\",\"name\":\"Peut-on d\u00e9marrer petit puis monter en puissance ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"C'est le principe : commencez par le Pack Impulse, puis basculez sur l'abonnement Motion Studio quand vous avez besoin de contenu r\u00e9gulier. Le montant du pack d\u00e9j\u00e0 r\u00e9gl\u00e9 est d\u00e9duit du premier mois d'abonnement.\"}}]}\n<\/script>\n<div class=\"exoa-pack-motion-ads-page\">\n<header class=\"hero\"><div class=\"wrap hero-in\">\n<div class=\"pill\"><span class=\"dot\"><\/span><span>Motion \u00b7 Carrousels \u00b7 Diffusion<\/span><\/div>\n<h1>Votre marque m\u00e9rite mieux qu&rsquo;un <em>post statique<\/em> et un budget <span class=\"v\">br\u00fbl\u00e9<\/span>.<\/h1>\n<p class=\"hero-sub\">Un pack unique qui couvre toute la cha\u00eene : la vid\u00e9o motion qui capte l&rsquo;attention, les carrousels publicitaires qui expliquent, et les campagnes qui les diffusent aux bonnes personnes. Livr\u00e9 en 10 jours, pr\u00eat \u00e0 publier \u2014 sans studio \u00e0 8 000\u20ac.<\/p>\n<div class=\"hero-cta\">\n<a href=\"#packs\" class=\"btn btn-lime\">Voir les packs \u2014 d\u00e8s 890\u20ac \u2192<\/a>\n<a href=\"#devis\" class=\"btn btn-ghost\">Parler de mon projet<\/a>\n<\/div>\n<div class=\"hero-note\"><span><b>10 jours<\/b> de production<\/span><span><b>4 formats<\/b> livr\u00e9s par cr\u00e9ation<\/span><span><b>Sans engagement<\/b> de dur\u00e9e<\/span><\/div>\n<div class=\"reel\">\n<div class=\"reel-frame\">\n<div class=\"reel-bar\"><i><\/i><i><\/i><i><\/i><em>SHOWREEL \u2014 EXOA MOTION 2026<\/em><\/div>\n<image-slot id=\"motion-reel\" shape=\"rect\" fit=\"cover\" placeholder=\"D\u00e9posez votre showreel (image ou GIF)\"><\/image-slot>\n<\/div>\n<\/div>\n<\/div><\/header>\n\n<div class=\"marq\"><div class=\"marq-t\">\n<span>Vid\u00e9o de lancement<\/span><span>Carrousel publicitaire<\/span><span>Promotion saisonni\u00e8re<\/span><span>Meta Ads<\/span><span>TikTok<\/span><span>LinkedIn<\/span><span>Programmatique<\/span><span>Habillage de marque<\/span>\n<span>Vid\u00e9o de lancement<\/span><span>Carrousel publicitaire<\/span><span>Promotion saisonni\u00e8re<\/span><span>Meta Ads<\/span><span>TikTok<\/span><span>LinkedIn<\/span><span>Programmatique<\/span><span>Habillage de marque<\/span>\n<\/div><\/div>\n\n<section class=\"sec sec-dk\">\n<div class=\"wrap\">\n<div class=\"tag\"><i><\/i><span>Le constat<\/span><\/div>\n<h2 class=\"h2\">La cr\u00e9a est devenue <em>le premier levier<\/em> de performance.<\/h2>\n<p class=\"lead\">Les plateformes publicitaires optimisent mieux que n&rsquo;importe quel expert. Ce qui reste entre vos mains, c&rsquo;est ce que les gens voient \u2014 et c&rsquo;est pr\u00e9cis\u00e9ment l\u00e0 que la plupart des budgets se perdent.<\/p>\n<div class=\"stats\">\n<div class=\"stat\"><div class=\"stat-n\">3 s<\/div><div class=\"stat-l\">C&rsquo;est le temps dont dispose une vid\u00e9o pour retenir l&rsquo;attention dans un fil d&rsquo;actualit\u00e9. Pass\u00e9 ce d\u00e9lai, le budget est d\u00e9pens\u00e9 pour rien.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">\u00d74<\/div><div class=\"stat-l\">Une m\u00eame campagne peut voir son co\u00fbt par r\u00e9sultat varier d&rsquo;un facteur quatre selon la seule qualit\u00e9 du visuel diffus\u00e9.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">1 seule<\/div><div class=\"stat-l\">Une cr\u00e9ation unique ne suffit plus : les algorithmes ont besoin de plusieurs variantes pour trouver celle qui fonctionne sur votre audience.<\/div><\/div>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec\" id=\"offre\">\n<div class=\"wrap\">\n<div class=\"head-row\">\n<div>\n<div class=\"tag\"><i><\/i><span>L&rsquo;offre en trois briques<\/span><\/div>\n<h2 class=\"h2\">Une cha\u00eene compl\u00e8te, <em>pas trois prestataires<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Chaque brique fonctionne seule. Assembl\u00e9es, elles forment un dispositif de lancement coh\u00e9rent \u2014 m\u00eame script, m\u00eame univers, m\u00eame message.<\/p>\n<\/div>\n<div class=\"bricks\">\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"brick-video\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel vid\u00e9o motion\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 01<\/span>\n<h3>Vid\u00e9o motion<\/h3>\n<p>Une vid\u00e9o anim\u00e9e de 15 \u00e0 45 secondes qui pr\u00e9sente votre offre, votre lancement ou votre promotion. \u00c9crite pour \u00eatre comprise sans le son.<\/p>\n<ul>\n<li>Script et storyboard r\u00e9dig\u00e9s par nos \u00e9quipes<\/li>\n<li>Animation, habillage, sous-titres int\u00e9gr\u00e9s<\/li>\n<li>Musique libre de droits incluse<\/li>\n<li>D\u00e9clin\u00e9e en 9:16, 1:1, 4:5 et 16:9<\/li>\n<\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"brick-carr\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel carrousel ads\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 02<\/span>\n<h3>Carrousels publicitaires<\/h3>\n<p>L\u00e0 o\u00f9 la vid\u00e9o accroche, le carrousel convainc : b\u00e9n\u00e9fices, preuves, offre, appel \u00e0 l&rsquo;action \u2014 une slide par id\u00e9e.<\/p>\n<ul>\n<li>3 \u00e0 6 slides par carrousel, textes inclus<\/li>\n<li>Plusieurs angles pour tester ce qui fonctionne<\/li>\n<li>Univers graphique align\u00e9 sur la vid\u00e9o<\/li>\n<li>Exports pr\u00eats \u00e0 t\u00e9l\u00e9verser dans le gestionnaire<\/li>\n<\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"brick-ads\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel campagnes ads\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 03<\/span>\n<h3>Diffusion &amp; campagnes<\/h3>\n<p>Les cr\u00e9ations ne servent \u00e0 rien dans un dossier. Nous configurons, lan\u00e7ons et pilotons les campagnes qui les mettent devant votre audience.<\/p>\n<ul>\n<li>Structure de campagne et audiences cibl\u00e9es<\/li>\n<li>Suivi des conversions install\u00e9 et v\u00e9rifi\u00e9<\/li>\n<li>Tests des variantes, arbitrage du budget<\/li>\n<li>Rapport clair : co\u00fbt par r\u00e9sultat, pas des courbes<\/li>\n<\/ul>\n<\/div>\n<\/article>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec sec-lt\" id=\"packs\">\n<div class=\"wrap\">\n<div class=\"head-row\">\n<div>\n<div class=\"tag\"><i><\/i><span>Les packs<\/span><\/div>\n<h2 class=\"h2\">Trois formules, <em>un seul devis<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Tarifs fermes, p\u00e9rim\u00e8tre \u00e9crit noir sur blanc. Vous savez exactement ce que vous recevez avant de signer.<\/p>\n<\/div>\n<div class=\"packs\">\n\n<div class=\"pack\">\n<span class=\"pack-n\">Pack 01<\/span>\n<h3>Impulse<\/h3>\n<p class=\"pack-for\">Pour tester le motion sur une offre pr\u00e9cise, sans engager un gros budget.<\/p>\n<div class=\"pack-p\"><b>890\u20ac<\/b><i>HT \u00b7 livr\u00e9 en 10 jours<\/i><\/div>\n<ul class=\"pack-l\">\n<li><b>1 vid\u00e9o motion<\/b> de 15 secondes<\/li>\n<li><b>1 carrousel<\/b> publicitaire de 4 slides<\/li>\n<li>Script et storyboard inclus<\/li>\n<li>4 formats par cr\u00e9ation<\/li>\n<li>2 s\u00e9ries de retouches<\/li>\n<\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Impulse<\/a>\n<\/div>\n\n<div class=\"pack hot\">\n<span class=\"pack-bdg\">Le plus choisi<\/span>\n<span class=\"pack-n\">Pack 02<\/span>\n<h3>Momentum<\/h3>\n<p class=\"pack-for\">Pour un lancement complet : de quoi alimenter les tests cr\u00e9atifs pendant six semaines.<\/p>\n<div class=\"pack-p\"><b>1 690\u20ac<\/b><i>HT \u00b7 livr\u00e9 en 15 jours<\/i><\/div>\n<ul class=\"pack-l\">\n<li><b>1 vid\u00e9o motion<\/b> de 30 secondes<\/li>\n<li><b>3 carrousels<\/b> publicitaires, angles diff\u00e9rents<\/li>\n<li><b>3 variantes d&rsquo;accroche<\/b> pour les tests<\/li>\n<li>Mini univers graphique si vous n&rsquo;avez pas de charte<\/li>\n<li>Configuration du suivi des conversions<\/li>\n<li>4 formats par cr\u00e9ation \u00b7 2 s\u00e9ries de retouches<\/li>\n<\/ul>\n<a href=\"#devis\" class=\"btn btn-lime\">Choisir Momentum<\/a>\n<\/div>\n\n<div class=\"pack\">\n<span class=\"pack-n\">Pack 03<\/span>\n<h3>Launch<\/h3>\n<p class=\"pack-for\">Pour confier tout le lancement : cr\u00e9ation, diffusion et pilotage sur un mois complet.<\/p>\n<div class=\"pack-p\"><b>2 890\u20ac<\/b><i>HT \u00b7 + budget m\u00e9dia<\/i><\/div>\n<ul class=\"pack-l\">\n<li><b>Tout le pack Momentum<\/b><\/li>\n<li class=\"plus\"><b>1 vid\u00e9o additionnelle<\/b> de 15 secondes<\/li>\n<li class=\"plus\"><b>Campagnes configur\u00e9es et lanc\u00e9es<\/b> (Meta, TikTok ou LinkedIn)<\/li>\n<li class=\"plus\"><b>Pilotage 30 jours<\/b> : tests, arbitrages, optimisation<\/li>\n<li class=\"plus\">Rapport de fin de campagne et recommandations<\/li>\n<\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Launch<\/a>\n<\/div>\n\n<\/div>\n\n<div class=\"sub\">\n<div class=\"sub-l\">\n<span class=\"mono\">Pour aller plus loin<\/span>\n<h3>Abonnement Motion Studio<\/h3>\n<p>Du contenu neuf chaque mois, sans repasser par un devis : deux vid\u00e9os motion, quatre carrousels et le pilotage continu de vos campagnes. R\u00e9siliable \u00e0 tout moment avec un mois de pr\u00e9avis \u2014 et le montant d&rsquo;un pack d\u00e9j\u00e0 r\u00e9gl\u00e9 est d\u00e9duit du premier mois.<\/p>\n<\/div>\n<div class=\"sub-r\">\n<b>1 290\u20ac<\/b>\n<i>HT \/ mois \u00b7 sans engagement de dur\u00e9e<\/i>\n<\/div>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec sec-dk\">\n<div class=\"wrap\">\n<div class=\"tag\"><i><\/i><span>Ce que vous recevez<\/span><\/div>\n<h2 class=\"h2\">Un dossier pr\u00eat \u00e0 t\u00e9l\u00e9verser, <em>pas des fichiers \u00e0 retravailler<\/em>.<\/h2>\n<p class=\"lead\">Chaque cr\u00e9ation est livr\u00e9e dans les quatre ratios utilis\u00e9s par les plateformes, export\u00e9e aux bonnes dimensions et au bon poids.<\/p>\n<div class=\"fmts\">\n<div class=\"fmt\"><div class=\"fmt-r\" style=\"aspect-ratio:9\/16;max-width:52px\"><\/div><b>9:16 \u2014 Vertical<\/b><small>Stories, Reels, TikTok, Shorts. 1080 \u00d7 1920 px.<\/small><\/div>\n<div class=\"fmt\"><div class=\"fmt-r\" style=\"aspect-ratio:1\/1;max-width:52px\"><\/div><b>1:1 \u2014 Carr\u00e9<\/b><small>Fil d&rsquo;actualit\u00e9 Instagram et Facebook. 1080 \u00d7 1080 px.<\/small><\/div>\n<div class=\"fmt\"><div class=\"fmt-r\" style=\"aspect-ratio:4\/5;max-width:52px\"><\/div><b>4:5 \u2014 Portrait<\/b><small>Le format qui occupe le plus d&rsquo;\u00e9cran en fil. 1080 \u00d7 1350 px.<\/small><\/div>\n<div class=\"fmt\"><div class=\"fmt-r\" style=\"aspect-ratio:16\/9;max-width:78px\"><\/div><b>16:9 \u2014 Paysage<\/b><small>YouTube, LinkedIn, site web, display. 1920 \u00d7 1080 px.<\/small><\/div>\n<\/div>\n<div class=\"gal\">\n<div class=\"gal-i\"><image-slot id=\"gal-1\" shape=\"rect\" fit=\"cover\" placeholder=\"Exemple 9:16\"><\/image-slot><\/div>\n<div class=\"gal-i\"><image-slot id=\"gal-2\" shape=\"rect\" fit=\"cover\" placeholder=\"Exemple 9:16\"><\/image-slot><\/div>\n<div class=\"gal-i wide\"><image-slot id=\"gal-3\" shape=\"rect\" fit=\"cover\" placeholder=\"Exemple 16:9\"><\/image-slot><\/div>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec\" id=\"process\">\n<div class=\"wrap\">\n<div class=\"tag\"><i><\/i><span>Comment \u00e7a se passe<\/span><\/div>\n<h2 class=\"h2\">Quatre \u00e9tapes, <em>z\u00e9ro surprise<\/em>.<\/h2>\n<div class=\"steps\">\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 01<\/span>\n<h3>Brief de 30 minutes<\/h3>\n<p>Un appel pour cadrer l&rsquo;offre \u00e0 mettre en avant, l&rsquo;audience vis\u00e9e et le message. Vous n&rsquo;avez rien \u00e0 pr\u00e9parer : nous posons les questions.<\/p>\n<em>JOUR 1<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 02<\/span>\n<h3>Script et storyboard<\/h3>\n<p>Nous r\u00e9digeons l&rsquo;accroche, le d\u00e9roul\u00e9 et le d\u00e9coupage image par image. Vous validez avant qu&rsquo;une seule seconde ne soit anim\u00e9e.<\/p>\n<em>JOURS 2 \u00c0 4<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 03<\/span>\n<h3>Production<\/h3>\n<p>Animation, habillage graphique, sous-titres, musique, d\u00e9clinaison dans les quatre formats. Deux s\u00e9ries de retouches sont incluses.<\/p>\n<em>JOURS 5 \u00c0 10<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 04<\/span>\n<h3>Diffusion<\/h3>\n<p>Livraison du dossier complet. Avec le pack Launch ou l&rsquo;abonnement, nous configurons et pilotons directement les campagnes.<\/p>\n<em>JOUR 10 ET APR\u00c8S<\/em>\n<\/div>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec sec-lt\">\n<div class=\"wrap\">\n<div class=\"head-row\">\n<div>\n<div class=\"tag\"><i><\/i><span>\u00c0 la carte<\/span><\/div>\n<h2 class=\"h2\">Besoin d&rsquo;un <em>\u00e9l\u00e9ment en plus<\/em> ?<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Chaque option s&rsquo;ajoute \u00e0 un pack ou se commande seule. Tarifs HT.<\/p>\n<\/div>\n<div class=\"adds\">\n<div class=\"add\"><div><b>Vid\u00e9o motion suppl\u00e9mentaire<\/b><small>15 secondes, 4 formats<\/small><\/div><i>390\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Carrousel suppl\u00e9mentaire<\/b><small>4 slides, textes inclus<\/small><\/div><i>190\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Voix off professionnelle<\/b><small>FR, DE ou EN<\/small><\/div><i>150\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Sous-titrage multilingue<\/b><small>Par langue additionnelle<\/small><\/div><i>90\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Version longue 60 s<\/b><small>Format YouTube ou site<\/small><\/div><i>490\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Banni\u00e8res display anim\u00e9es<\/b><small>Jeu de 5 formats HTML5<\/small><\/div><i>450\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Landing page d\u00e9di\u00e9e<\/b><small>Page de destination de campagne<\/small><\/div><i>750\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Mois de pilotage additionnel<\/b><small>Gestion des campagnes<\/small><\/div><i>590\u20ac<\/i><\/div>\n<\/div>\n<\/div>\n<\/section>\n\n<section class=\"sec\" id=\"faq\">\n<div class=\"wrap\">\n<div style=\"text-align:center;margin-bottom:44px\">\n<div class=\"tag\" style=\"justify-content:center\"><i><\/i><span>Questions fr\u00e9quentes<\/span><\/div>\n<h2 class=\"h2\" style=\"margin:0 auto\">Ce qu&rsquo;on nous demande <em>avant de signer<\/em>.<\/h2>\n<\/div>\n<div class=\"faq\">\n<details open><summary>En combien de temps la vid\u00e9o est-elle livr\u00e9e ?<\/summary><p>Dix jours ouvr\u00e9s \u00e0 partir de la validation du script pour le pack Impulse, quinze jours pour Momentum et Launch. <b>Deux allers-retours de retouches<\/b> sont inclus dans chaque pack.<\/p><\/details>\n<details><summary>Faut-il fournir des \u00e9l\u00e9ments graphiques ?<\/summary><p>Id\u00e9alement votre logo, vos couleurs et vos polices. Si vous n&rsquo;avez pas de charte graphique, nous construisons un mini univers visuel \u2014 <b>inclus dans les packs Momentum et Launch<\/b>.<\/p><\/details>\n<details><summary>Qui \u00e9crit le script de la vid\u00e9o ?<\/summary><p>Nous. Le script, le storyboard et l&rsquo;accroche sont r\u00e9dig\u00e9s par nos \u00e9quipes \u00e0 partir d&rsquo;un brief de trente minutes, <b>puis valid\u00e9s par vous avant production<\/b>. Vous ne d\u00e9couvrez jamais le r\u00e9sultat \u00e0 la fin.<\/p><\/details>\n<details><summary>Le budget publicitaire est-il inclus dans les prix ?<\/summary><p>Non. Les tarifs affich\u00e9s couvrent la cr\u00e9ation et la gestion. <b>Le budget m\u00e9dia est r\u00e9gl\u00e9 depuis votre propre compte publicitaire<\/b> \u2014 vous gardez la ma\u00eetrise et la propri\u00e9t\u00e9 des donn\u00e9es.<\/p><\/details>\n<details><summary>Sur quelles plateformes les cr\u00e9ations sont-elles diffusables ?<\/summary><p>Toutes : Meta (Instagram, Facebook), TikTok, LinkedIn, YouTube, Pinterest, Snapchat et display programmatique. Chaque livrable \u00e9tant d\u00e9clin\u00e9 en quatre ratios, <b>rien n&rsquo;est \u00e0 recadrer<\/b>.<\/p><\/details>\n<details><summary>Pourquoi \u00eates-vous moins cher que les studios motion ?<\/summary><p>Parce que nous travaillons par <b>packs standardis\u00e9s<\/b> plut\u00f4t qu&rsquo;en projet enti\u00e8rement sur mesure : cadre de production \u00e9prouv\u00e9, templates propri\u00e9taires, p\u00e9rim\u00e8tre \u00e9crit. La qualit\u00e9 reste celle d&rsquo;un studio, sans le co\u00fbt de la personnalisation totale.<\/p><\/details>\n<details><summary>Que se passe-t-il si la vid\u00e9o ne me pla\u00eet pas ?<\/summary><p>La validation du script et du storyboard en amont \u00e9limine l&rsquo;essentiel du risque : vous savez exactement ce qui sera produit. <b>Deux s\u00e9ries de retouches<\/b> sont ensuite incluses sur l&rsquo;animation.<\/p><\/details>\n<details><summary>Peut-on d\u00e9marrer petit puis monter en puissance ?<\/summary><p>C&rsquo;est le principe m\u00eame de l&rsquo;offre. Commencez par un pack, puis basculez sur l&rsquo;abonnement Motion Studio quand vous avez besoin de contenu r\u00e9gulier : <b>le montant du pack d\u00e9j\u00e0 r\u00e9gl\u00e9 est d\u00e9duit du premier mois<\/b>.<\/p><\/details>\n<\/div>\n<\/div>\n<\/section>\n\n\n<section style=\"background:#F7F7FB;padding:90px 20px\"><div style=\"max-width:1240px;margin:0 auto\">\n<div style=\"text-align:center;margin-bottom:48px\"><h2 style=\"color:#060039\">Des r\u00e9sultats <span style=\"color:#7B87E8\">mesurables<\/span><\/h2><\/div>\n<div class=\"resx-grid\"><div class=\"resx-card\"><div class=\"resx-num\">+80%<\/div><h3>de taux d&rsquo;attention<\/h3><p>Les vid\u00e9os motion retiennent l&rsquo;attention bien au-del\u00e0 des visuels statiques.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">10+<\/div><h3>d\u00e9clinaisons par concept<\/h3><p>Chaque cr\u00e9ation d\u00e9clin\u00e9e pour tous les formats et placements.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">14 jours<\/div><h3>de la commande \u00e0 la diffusion<\/h3><p>Un pack cr\u00e9atif complet livr\u00e9 et en ligne en deux semaines.<\/p><\/div><\/div><\/div><\/section>\n<section style=\"background:#fff;padding:90px 20px\"><div style=\"max-width:1240px;margin:0 auto\">\n<div style=\"text-align:center;margin-bottom:48px\"><h2 style=\"color:#060039\">Ce que nous livrons <span style=\"color:#7B87E8\">concr\u00e8tement<\/span><\/h2><p style=\"color:#5A5E80;max-width:640px;margin:12px auto 0\">Chaque accompagnement motion &#038; ads s&rsquo;appuie sur des livrables tangibles.<\/p><\/div>\n<div class=\"livr-grid\"><div class=\"livr-card\"><h3><span>\u2713<\/span>Concept cr\u00e9atif<\/h3><p>Direction artistique et messages align\u00e9s sur votre marque et votre offre.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Vid\u00e9os motion design<\/h3><p>Formats courts optimis\u00e9s pour le feed : 6, 15 et 30 secondes.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>D\u00e9clinaisons multi-formats<\/h3><p>Carr\u00e9, vertical, story, banni\u00e8res : tous les placements couverts.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Copywriting publicitaire<\/h3><p>Accroches et textes test\u00e9s en plusieurs variantes.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Mise en ligne des campagnes<\/h3><p>Diffusion sur Meta, TikTok, YouTube ou display selon le plan.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>It\u00e9rations data-driven<\/h3><p>Les cr\u00e9as gagnantes amplifi\u00e9es, les autres remplac\u00e9es.<\/p><\/div><\/div><\/div><\/section>\n<section style=\"background:#060039;padding:90px 20px\"><div style=\"max-width:1240px;margin:0 auto\">\n<div style=\"text-align:center;margin-bottom:48px\"><h2 style=\"color:#fff\">Un dispositif appuy\u00e9 sur <span style=\"color:#CDF980\">toutes nos expertises<\/span><\/h2><\/div>\n<div class=\"expx-grid\"><a class=\"expx-card\" href=\"\/audit-google-ads\/\"><strong>Audit Google Ads<\/strong><span>Structure, conversions, budgets : le diagnostic complet.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/google-search-ads\/\"><strong>Google Search Ads<\/strong><span>Capter l&rsquo;intention au moment o\u00f9 elle s&rsquo;exprime.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/remarketing\/\"><strong>Remarketing<\/strong><span>Reconvertir vos visiteurs sur tous les r\u00e9seaux.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/publicite-reseaux-sociaux\/\"><strong>Social Ads<\/strong><span>Meta, TikTok, LinkedIn : l&rsquo;acquisition sociale.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/agence-programmatique\/\"><strong>Programmatique<\/strong><span>Display, DOOH, CTV et audio en achat direct DSP.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/creation-landing-page\/\"><strong>Landing pages<\/strong><span>Des pages con\u00e7ues pour convertir votre trafic payant.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><\/div><\/div><\/section>\n<section class=\"fin\" id=\"devis\">\n<div class=\"wrap fin-in\">\n<div>\n<div class=\"tag\"><i><\/i><span>D\u00e9marrer<\/span><\/div>\n<h2>Dites-nous ce que vous lancez. <em>On s&rsquo;occupe du reste.<\/em><\/h2>\n<p>D\u00e9crivez votre projet en deux lignes : nous revenons sous vingt-quatre heures avec le pack adapt\u00e9, un d\u00e9lai ferme et un devis chiffr\u00e9.<\/p>\n<div class=\"fin-tr\">\n<div><b>\u2713<\/b><span>Devis ferme sous 24 h, sans rendez-vous obligatoire<\/span><\/div>\n<div><b>\u2713<\/b><span>P\u00e9rim\u00e8tre \u00e9crit : vous savez ce que vous recevez<\/span><\/div>\n<div><b>\u2713<\/b><span>Aucun engagement de dur\u00e9e, m\u00eame sur l&rsquo;abonnement<\/span><\/div>\n<div><b>\u2713<\/b><span>France et Luxembourg \u00b7 facturation HT<\/span><\/div>\n<\/div>\n<\/div>\n<div class=\"exoa-contact-redirect\" style=\"background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.16);border-radius:18px;padding:38px 32px;text-align:center\">\n<h3 style=\"color:#fff;font-size:22px;margin:0 0 10px\">Parlons de votre projet<\/h3>\n<p style=\"color:#C6C9E0;margin:0 0 22px;line-height:1.6\">R\u00e9ponse sous 24 heures ouvr\u00e9es \u2014 audit et premi\u00e8res recommandations offerts.<\/p>\n<a href=\"\/contact\/?sujet=Pack+Motion+Ads\" style=\"display:inline-block;background:#CDF980;color:#060039;font-weight:700;font-size:16px;border-radius:999px;padding:15px 32px;text-decoration:none\">Demander un devis gratuit \u2192<\/a>\n<p style=\"color:#9CA3C8;font-size:12.5px;margin:14px 0 0\">Vous serez redirig\u00e9 vers notre formulaire de contact s\u00e9curis\u00e9.<\/p>\n<\/div>\n<\/div>\n<\/section>\n\n<footer class=\"foot\"><div class=\"foot-in\">\n<span>\u00a9 2026 Exoa \u2014 Pack Motion &amp; Ads \u00b7 Tarifs HT, France et Luxembourg<\/span>\n<span><a href=\"\/offres\/\">Toutes les offres<\/a> \u00b7 <a href=\"\/regie-publicitaire-deleguee\/\">Publicit\u00e9<\/a> \u00b7 <a href=\"\/marketing-delegue\/\">Marketing d\u00e9l\u00e9gu\u00e9<\/a> \u00b7 <a href=\"\/expertise-acquisition\/\">Acquisition<\/a> \u00b7 <a href=\"\/expertise-conversion\/\">Conversion<\/a><\/span>\n<\/div><\/footer>\n\n\n<script>\nfunction mSend(e){e.preventDefault();document.getElementById('mFields').style.display='none';document.getElementById('mOk').classList.add('on');return false}\n<\/script>\n<\/div>\n<script>\/\/ @ds-adherence-ignore -- omelette starter scaffold (raw elements\/hex\/px by design)\n\/\/ Copied omelette starter. Re-running copy_starter_component with this kind overwrites this file with the latest version (page content is unaffected).\n\/* BEGIN USAGE *\/\n\/**\n * <image-slot> \u2014 user-fillable image placeholder.\n *\n * Drop this into a deck, mockup, or page wherever a design needs an image.\n * You control the slot's shape; it sizes to its container by default. When the search_stock_photos tool\n * is available, prefill the slot by default \u2014 write the photo's URL into\n * src (with credit\/credit-href); the user can still fill or replace it\n * by dragging an image file onto it (or clicking to browse). The dropped\n * image persists across reloads via a .image-slots.state.json sidecar \u2014\n * same read-via-fetch \/ write-via-window.omelette pattern as\n * design_canvas.jsx, so the filled slot shows on share links, downloaded\n * zips, and PPTX export. Outside the omelette runtime the slot is read-only.\n *\n * The sidecar is a SIBLING of the HTML file that uses this component: the\n * read is a document-relative fetch, and the host resolves the bridge's\n * sidecar writes into the previewed file's directory to match (same\n * contract as design_canvas.jsx). Pages in the same directory share one\n * sidecar; keep slot ids distinct across them.\n *\n * Attributes:\n *   id           Persistence key. REQUIRED for the drop to survive reload \u2014\n *                every slot on the page needs a distinct id.\n *   shape        'rect' | 'rounded' | 'circle' | 'pill'   (default 'rounded')\n *                'circle' applies 50% border-radius; on a non-square slot\n *                that's an ellipse \u2014 set equal width and height for a true\n *                circle.\n *   radius       Corner radius in px for 'rounded'.       (default 12)\n *   mask         Any CSS clip-path value. Overrides `shape` \u2014 use this for\n *                hexagons, blobs, arbitrary polygons.\n *   fit          Initial framing baseline: cover | contain.   (default 'cover')\n *                cover starts the image filling the frame (overflow cropped);\n *                contain starts it fully visible (letterboxed). Either way the\n *                user can always pan\/scale from there \u2014 double-click, or the\n *                Edit control, enters reframe mode (drag to move, scroll or\n *                corner-handles to scale; Escape \/ click-out commits). The\n *                crop persists alongside the image in the sidecar.\n *   placeholder  Empty-state caption.                      (default 'Drop an image')\n *   src          Optional initial\/fallback image URL. Prefill it with a real\n *                photo via search_stock_photos when that tool is available\n *                (set credit\/credit-href from the result). A user drop\n *                overrides it; clearing the drop reveals src again.\n *   credit       Attribution text shown as a small overlay at the\n *                bottom-left of the filled slot. REQUIRED whenever src\n *                points at any Unsplash host (images.unsplash.com,\n *                plus.unsplash.com, \u2026): an Unsplash src with no credit\n *                renders an error tile INSTEAD of the photo (Unsplash\n *                terms forbid showing their photos unattributed). Use the\n *                exact form 'Photo by {photographer name} on Unsplash' \u2014\n *                the overlay then links the name to credit-href and\n *                'Unsplash' to the Unsplash homepage, and links back to\n *                unsplash.com automatically get the required utm referral\n *                params appended at render time. The credit belongs to\n *                the src image, so it only shows while src is what's\n *                displayed \u2014 a user-dropped image hides it.\n *   credit-href  Link for the photographer's name in the credit overlay\n *                (their Unsplash profile URL from the stock-photo search\n *                results). http(s) URLs only \u2014 anything else renders the\n *                name as plain text.\n *\n * Sizing: the slot fills its container by default (width\/height 100%).\n * Put it in a sized wrapper \u2014 absolutely positioned, a grid cell, a fixed\n * frame \u2014 and it takes exactly that box. When the parent's height is\n * indefinite (ordinary flow), it falls back to full width at a 3:2 aspect\n * ratio instead of collapsing. In a shrink-to-fit parent (a float,\n * width:max-content, an unsized absolute wrapper), percentages have\n * nothing to resolve against \u2014 size the slot or its wrapper explicitly\n * there. For a fixed-size slot, set\n * width\/height on the element itself (inline style), which overrides the\n * default. When\n * layering content above a slot (full-bleed layouts), make the overlay\n * click-through \u2014 pointer-events: none on scrims\/text plates, re-enabled\n * on interactive children \u2014 so the slot's hover controls stay reachable.\n * Keep the slot's bottom-left corner visually clear as well: the credit\n * overlay renders there, and a dark fade or text plate covering it hides\n * the attribution Unsplash's terms require \u2014 end the fade above that\n * corner, or keep it nearly transparent where the credit sits.\n *\n * Usage:\n *   <div style=\"position:relative;width:100%;height:100%\">      <!-- full-bleed: -->\n *     <image-slot id=\"bg\" shape=\"rect\"><\/image-slot>            <!-- fills the wrapper -->\n *   <\/div>\n *   <image-slot id=\"hero\"   style=\"width:800px;height:450px\" shape=\"rounded\" radius=\"20\"\n *               placeholder=\"Drop a hero image\"><\/image-slot>\n *   <image-slot id=\"avatar\" style=\"width:120px;height:120px\" shape=\"circle\"><\/image-slot>\n *   <image-slot id=\"kite\"   style=\"width:300px;height:300px\"\n *               mask=\"polygon(50% 0, 100% 50%, 50% 100%, 0 50%)\"><\/image-slot>\n *\/\n\/* END USAGE *\/\n\n(() => {\n  const STATE_FILE = '.image-slots.state.json';\n\n  \/\/ Unsplash terms require visible attribution wherever their photos\n  \/\/ display, and every link back to unsplash.com must carry utm referral\n  \/\/ params. Two render-time rules enforce that here:\n  \/\/  - an Unsplash-src slot with NO credit attribute renders an error\n  \/\/    tile INSTEAD of the photo (an uncredited Unsplash photo on screen\n  \/\/    is itself the terms violation, so it never renders bare);\n  \/\/  - rendered credit links pointing at unsplash.com get the referral\n  \/\/    params appended when absent (credit-href values live in page\n  \/\/    content that can't be edited after the fact).\n  \/\/ Keep the utm_source value in sync with UTM_SOURCE in\n  \/\/ platform\/web-agent\/unsplash.ts \u2014 this file is a project-local\n  \/\/ artifact and cannot import it (equality is pinned by tests).\n  const UNSPLASH_HOMEPAGE_HREF =\n    'https:\/\/unsplash.com\/?utm_source=claude_design&utm_medium=referral';\n  \/\/ Host rule mirrors the hotlink validator that admits Unsplash srcs into\n  \/\/ pages in the first place (cdn$ in unsplash.ts: apex or any subdomain)\n  \/\/ \u2014 Unsplash+ results serve from plus.unsplash.com, not just images.*,\n  \/\/ and an admitted-but-uncredited photo must error whatever unsplash\n  \/\/ host it rides on.\n  \/\/ Trailing-dot FQDNs (images.unsplash.com.) are the same host to the\n  \/\/ browser but would miss the regex \u2014 strip one dot so the check fails\n  \/\/ CLOSED (unrecognized-but-real Unsplash srcs must error, not render).\n  const isUnsplashHost = (u) => {\n    try {\n      return \/(^|\\.)unsplash\\.com$\/.test(\n        new URL(u, document.baseURI).hostname.replace(\/\\.$\/, '')\n      );\n    } catch {\n      return false;\n    }\n  };\n  \/\/ Render-time referral normalization for links back to Unsplash:\n  \/\/ appends utm_source\/utm_medium when absent, preserves every existing\n  \/\/ query param, never overwrites an existing utm_source, and passes\n  \/\/ non-Unsplash URLs through untouched. Input is an ABSOLUTE validated\n  \/\/ http(s) URL (the credit render funnel resolves + validates first).\n  const withReferral = (href) => {\n    try {\n      const u = new URL(href);\n      if (!\/(^|\\.)unsplash\\.com$\/.test(u.hostname.replace(\/\\.$\/, ''))) {\n        return href;\n      }\n      if (!u.searchParams.has('utm_source')) {\n        u.searchParams.set('utm_source', 'claude_design');\n      }\n      if (!u.searchParams.has('utm_medium')) {\n        u.searchParams.set('utm_medium', 'referral');\n      }\n      return u.toString();\n    } catch (e) {\n      return href;\n    }\n  };\n  \/\/ 2\u00d7 a ~600px slot in a 1920-wide deck \u2014 retina-sharp without making the\n  \/\/ sidecar enormous. A 1200px WebP at q=0.85 is ~150-300KB.\n  const MAX_DIM = 1200;\n  \/\/ Raster formats only. SVG is excluded (can carry script; createImageBitmap\n  \/\/ on SVG blobs is inconsistent). GIF is excluded because the canvas\n  \/\/ re-encode keeps only the first frame, so an animated GIF would silently\n  \/\/ go still \u2014 better to reject than surprise.\n  const ACCEPT = ['image\/png', 'image\/jpeg', 'image\/webp', 'image\/avif'];\n\n  \/\/ \u2500\u2500 Shared sidecar store \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \/\/ One fetch + immediate write-on-change for every <image-slot> on the\n  \/\/ page. Reads via fetch() so viewing works anywhere the HTML and sidecar\n  \/\/ are served together; writes go through window.omelette.writeFile, which\n  \/\/ the host allowlists to *.state.json basenames only.\n  const subs = new Set();\n  let slots = {};\n  \/\/ ids explicitly cleared before the sidecar fetch resolved \u2014 otherwise\n  \/\/ the merge below can't tell \"never set\" from \"just deleted\" and would\n  \/\/ resurrect the sidecar's stale value.\n  const tombstones = new Set();\n  let loaded = false;\n  let loadP = null;\n\n  function load() {\n    if (loadP) return loadP;\n    loadP = fetch(STATE_FILE)\n      .then((r) => (r.ok ? r.json() : null))\n      .then((j) => {\n        \/\/ Merge: sidecar loses to any in-memory change that raced ahead of\n        \/\/ the fetch (drop or clear) so neither is clobbered by hydration.\n        if (j && typeof j === 'object') {\n          const merged = Object.assign({}, j, slots);\n          \/\/ A framing-only write that raced ahead of hydration must not\n          \/\/ drop a user image that's only on disk \u2014 inherit u from the\n          \/\/ sidecar for any in-memory entry that lacks one.\n          for (const k in slots) {\n            if (merged[k] && !merged[k].u && j[k]) {\n              merged[k].u = typeof j[k] === 'string' ? j[k] : j[k].u;\n            }\n          }\n          for (const id of tombstones) delete merged[id];\n          slots = merged;\n        }\n        tombstones.clear();\n      })\n      .catch(() => {})\n      .then(() => { loaded = true; subs.forEach((fn) => fn()); });\n    return loadP;\n  }\n\n  \/\/ Serialize writes so two near-simultaneous drops on different slots\n  \/\/ can't reorder at the backend and leave the sidecar with only the\n  \/\/ first. A save requested mid-flight just marks dirty and re-fires on\n  \/\/ completion with the then-current slots.\n  let saving = false;\n  let saveDirty = false;\n  \/\/ Unload-time flush: save()'s serialization defers a mid-RTT re-fire to a\n  \/\/ .then that never runs in an unloading document, silently dropping a\n  \/\/ pagehide commit. Post the current slots immediately instead \u2014 content\n  \/\/ is a superset snapshot of any in-flight save's, the write is a\n  \/\/ whole-file last-writer-wins replace, and postMessage FIFO delivers it\n  \/\/ to the host after the in-flight one, so a backend-side reorder at\n  \/\/ worst reproduces the dropped-commit outcome this flush improves on.\n  \/\/ Guarded on the initial sidecar read: pre-hydration slots can miss\n  \/\/ other slots' persisted entries, and flushing it would clobber them \u2014\n  \/\/ that narrow case stays best-effort (the in-memory merge in load()\n  \/\/ cannot happen in an unloading document anyway).\n  function flushNow() {\n    if (!loaded) return;\n    const w = window.omelette && window.omelette.writeFile;\n    if (!w) return;\n    try { Promise.resolve(w(STATE_FILE, JSON.stringify(slots))).catch(() => {}); } catch (e) {}\n  }\n  function save() {\n    if (saving) { saveDirty = true; return; }\n    const w = window.omelette && window.omelette.writeFile;\n    if (!w) return;\n    saving = true;\n    Promise.resolve(w(STATE_FILE, JSON.stringify(slots)))\n      .catch(() => {})\n      .then(() => { saving = false; if (saveDirty) { saveDirty = false; save(); } });\n  }\n\n  const S_MAX = 5;\n  const clampS = (s) => Math.max(1, Math.min(S_MAX, s));\n\n  \/\/ Normalize a stored slot value. Pre-reframe sidecars stored a bare\n  \/\/ data-URL string; newer ones store {u, s, x, y}. Either shape is valid.\n  function getSlot(id) {\n    const v = slots[id];\n    if (!v) return null;\n    return typeof v === 'string' ? { u: v, s: 1, x: 0, y: 0 } : v;\n  }\n\n  function setSlot(id, val) {\n    if (!id) return;\n    if (val) { slots[id] = val; tombstones.delete(id); }\n    else { delete slots[id]; if (!loaded) tombstones.add(id); }\n    subs.forEach((fn) => fn());\n    \/\/ A drop is rare + high-value \u2014 write immediately so nav-away can't lose\n    \/\/ it. Gate on the initial read so we don't overwrite a sidecar we haven't\n    \/\/ merged yet; the merge in load() keeps this change once the read lands.\n    if (loaded) save(); else load().then(save);\n  }\n\n  \/\/ \u2500\u2500 Image downscale \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  \/\/ Encode through a canvas so the sidecar carries resized bytes, not the\n  \/\/ raw upload. Longest side is capped at 2\u00d7 the slot's rendered width\n  \/\/ (retina) and at MAX_DIM. WebP keeps alpha and is ~10\u00d7 smaller than PNG\n  \/\/ for photos, so there's no need for per-image format picking.\n  async function toDataUrl(file, targetW) {\n    const bitmap = await createImageBitmap(file);\n    try {\n      const cap = Math.min(MAX_DIM, Math.max(1, Math.round(targetW * 2)) || MAX_DIM);\n      const scale = Math.min(1, cap \/ Math.max(bitmap.width, bitmap.height));\n      const w = Math.max(1, Math.round(bitmap.width * scale));\n      const h = Math.max(1, Math.round(bitmap.height * scale));\n      const canvas = document.createElement('canvas');\n      canvas.width = w; canvas.height = h;\n      canvas.getContext('2d').drawImage(bitmap, 0, 0, w, h);\n      return canvas.toDataURL('image\/webp', 0.85);\n    } finally {\n      bitmap.close && bitmap.close();\n    }\n  }\n\n  \/\/ \u2500\u2500 Custom element \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n  const stylesheet =\n    \/\/ Fill the container by default: slots are usually placed inside a\n    \/\/ sized wrapper (a hero frame, a grid cell, an inset:0 layer) and are\n    \/\/ expected to take that box \u2014 a fixed intrinsic size would render as\n    \/\/ a small tile in the corner of a full-bleed wrapper instead.\n    \/\/ aspect-ratio is the companion fallback that keeps a bare slot\n    \/\/ visible when the parent's height is indefinite: height:100%\n    \/\/ resolves to auto there, and the ratio then derives height from\n    \/\/ width instead of letting the slot collapse to zero height.\n    \/\/ Explicit width\/height on the element override all of this.\n    \/\/ color:inherit (not a fixed near-black): the placeholder chrome \u2014\n    \/\/ empty-state icon\/caption (currentColor) and the dashed ring \u2014 must\n    \/\/ read on dark decks too, and the slide's own text color is the one\n    \/\/ color guaranteed to contrast with the slide background. The soft\n    \/\/ look comes from opacity on those parts, not from a baked-in alpha.\n    ':host{display:block;position:relative;' +\n    '  font:13px\/1.3 system-ui,-apple-system,sans-serif;' +\n    '  width:100%;height:100%;aspect-ratio:3\/2}' +\n    '.empty .cap,.empty .sub{opacity:.75}' +\n    '.frame{position:absolute;inset:0;overflow:hidden;background:rgba(127,127,127,.08)}' +\n    \/\/ .frame img (clipped) and .spill (unclipped ghost + handles) share the\n    \/\/ same left\/top\/width\/height in frame-%, computed by _applyView(), so the\n    \/\/ inside-mask crop and the outside-mask spill stay pixel-aligned.\n    '.frame img{position:absolute;max-width:none;transform:translate(-50%,-50%);' +\n    '  -webkit-user-drag:none;user-select:none;touch-action:none}' +\n    \/\/ Reframe mode (double-click): the full image spills past the mask. The\n    \/\/ spill layer is sized to the IMAGE bounds so its corners are where the\n    \/\/ resize handles belong. The ghost <img> inside is translucent; the real\n    \/\/ clipped <img> underneath shows the opaque in-mask crop.\n    \/\/ popover=manual promotes the spill to the top layer on reframe, so it is\n    \/\/ not clipped by any overflow:hidden \/ clip-path \/ scroll-container\n    \/\/ ancestor (a plain z-index can't escape overflow clipping). UA popover\n    \/\/ defaults (inset:0;margin:auto) are reset; _applyView sets viewport px.\n    '.spill{position:fixed;margin:0;inset:auto;border:0;padding:0;background:transparent;' +\n    '  overflow:visible;transform:translate(-50%,-50%);z-index:1;cursor:grab;touch-action:none}' +\n    ':host([data-panning]) .spill{cursor:grabbing}' +\n    '.spill .ghost{position:absolute;inset:0;width:100%;height:100%;opacity:.35;' +\n    '  pointer-events:none;-webkit-user-drag:none;user-select:none;' +\n    '  box-shadow:0 0 0 1px rgba(0,0,0,.2),0 12px 32px rgba(0,0,0,.2)}' +\n    '.spill .handle{position:absolute;width:12px;height:12px;border-radius:50%;' +\n    '  background:#fff;box-shadow:0 0 0 1.5px #c96442,0 1px 3px rgba(0,0,0,.3);' +\n    '  transform:translate(-50%,-50%)}' +\n    '.spill .handle[data-c=nw]{left:0;top:0;cursor:nwse-resize}' +\n    '.spill .handle[data-c=ne]{left:100%;top:0;cursor:nesw-resize}' +\n    '.spill .handle[data-c=sw]{left:0;top:100%;cursor:nesw-resize}' +\n    '.spill .handle[data-c=se]{left:100%;top:100%;cursor:nwse-resize}' +\n    ':host([data-reframe]){z-index:10}' +\n    ':host([data-reframe]) .frame{box-shadow:0 0 0 2px #c96442}' +\n    '.empty{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;' +\n    '  justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +\n    '  cursor:pointer;user-select:none}' +\n    '.empty svg{opacity:.45}' +\n    '.empty .cap{max-width:90%;font-weight:500;letter-spacing:.01em}' +\n    '.empty .sub{font-size:11px}' +\n    '.empty .sub u{text-underline-offset:2px}' +\n    '.empty:hover .sub{opacity:1}' +\n    ':host([data-over]) .frame{outline:2px solid #c96442;outline-offset:-2px;' +\n    '  background:rgba(201,100,66,.10)}' +\n    '.ring{position:absolute;inset:0;pointer-events:none;border:1.5px dashed currentColor;' +\n    '  opacity:.35;transition:border-color .12s,opacity .12s}' +\n    ':host([data-over]) .ring{border-color:#c96442;opacity:1}' +\n    ':host([data-filled]) .ring{display:none}' +\n    \/\/ Controls overlay INSIDE the frame, pinned to the top-right corner, so\n    \/\/ a full-bleed slot in an overflow:hidden container still shows them\n    \/\/ (the old below-mask placement got clipped). Credit sits bottom-left,\n    \/\/ so top-right avoids collision. The blurred pill background keeps them\n    \/\/ legible over the image.\n    \/\/ The UA [popover] base rule styles the element in EVERY state (only\n    \/\/ display:none is gated on :not(:popover-open), and the display:flex\n    \/\/ below overrides that) \u2014 so the UA resets live HERE, like .spill's,\n    \/\/ or the ordinary hover-state strip renders as a bordered Canvas box\n    \/\/ centered by margin:auto. inset:auto precedes top\/right (shorthand).\n    '.ctl{position:absolute;inset:auto;top:8px;right:8px;margin:0;border:0;padding:0;' +\n    '  background:transparent;overflow:visible;' +\n    '  display:flex;gap:6px;opacity:0;pointer-events:none;transition:opacity .12s;z-index:2;' +\n    '  white-space:nowrap}' +\n    \/\/ While reframing, the spill owns the top layer and would swallow every\n    \/\/ click on the in-frame controls. Promoting .ctl into the top layer\n    \/\/ ABOVE the spill (shown after it \u2014 later popovers stack higher) keeps\n    \/\/ Edit-as-toggle and Replace clickable mid-reframe. _applyView pins it\n    \/\/ to the frame's top-right in viewport px (translateX(-100%)\n    \/\/ right-aligns against the computed left edge); inset:auto clears the\n    \/\/ base rule's top\/right so the inline left\/top position it alone.\n    '.ctl:popover-open{position:fixed;inset:auto;transform:translateX(-100%)}' +\n    ':host([data-filled][data-editable]:hover) .ctl,:host([data-reframe]) .ctl' +\n    '  {opacity:1;pointer-events:auto}' +\n    '.ctl button{appearance:none;border:0;border-radius:6px;padding:5px 10px;cursor:pointer;' +\n    '  background:rgba(0,0,0,.65);color:#fff;font:11px\/1 system-ui,-apple-system,sans-serif;' +\n    '  backdrop-filter:blur(6px)}' +\n    '.ctl button:hover{background:rgba(0,0,0,.8)}' +\n    '.err{position:absolute;left:8px;bottom:8px;right:8px;color:#b3261e;font-size:11px;' +\n    '  background:rgba(255,255,255,.85);padding:4px 6px;border-radius:5px;pointer-events:none}' +\n    \/\/ Replacement in flight: after a src swap the browser keeps painting\n    \/\/ the PREVIOUS image until the new one decodes, so a Replace would\n    \/\/ flash the old photo and then pop. Hide the stale frame (visibility,\n    \/\/ not display \u2014 _applyView geometry still applies) and spin until the\n    \/\/ new image reports in (load\/error clears data-swapping).\n    ':host([data-swapping]) .frame img{visibility:hidden}' +\n    '.loading{position:absolute;inset:0;display:none;align-items:center;' +\n    '  justify-content:center;pointer-events:none}' +\n    ':host([data-swapping]) .loading{display:flex}' +\n    '.loading::after{content:\"\";width:22px;height:22px;border-radius:50%;' +\n    '  border:2px solid rgba(127,127,127,.25);border-top-color:currentColor;' +\n    '  animation:om-slot-spin .7s linear infinite}' +\n    '@keyframes om-slot-spin{to{transform:rotate(360deg)}}' +\n    \/\/ Reduced motion: the static two-tone ring still reads as \"working\".\n    '@media (prefers-reduced-motion:reduce){.loading::after{animation:none}}' +\n    '.credit{position:absolute;left:6px;bottom:6px;max-width:calc(100% - 12px);display:none;' +\n    '  padding:3px 7px;border-radius:5px;background:rgba(0,0,0,.55);color:#fff;' +\n    '  font:10px\/1.2 system-ui,-apple-system,sans-serif;text-decoration:none;' +\n    '  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;backdrop-filter:blur(6px)}' +\n    \/\/ The credit is a SPAN holding one or two <a>s (Unsplash's prescribed\n    \/\/ form links the photographer AND Unsplash) \u2014 anchors style inline so\n    \/\/ the overlay reads as one line of text.\n    '.credit a{color:inherit;text-decoration:none}' +\n    '.credit a:hover,.credit a:focus-visible{text-decoration:underline}' +\n    ':host([data-filled][data-credit]) .credit{display:block}' +\n    \/\/ Exports must ship JUST the image \u2014 no hover controls, no credit chip\n    \/\/ (the host marks <html data-om-exporting> for the capture window; the\n    \/\/ page-level hide script can't reach shadow DOM, this rule can).\n    ':host-context([data-om-exporting]) .ctl,' +\n    ':host-context([data-om-exporting]) .credit{display:none !important}' +\n    \/\/ Print must ship just the image too: the hover-gated controls can be\n    \/\/ mid-hover when print() fires, and the credit chip is screen chrome \u2014\n    \/\/ the same rule the capture window gets, keyed on print media instead\n    \/\/ of the host's data-om-exporting mark (the print path sets no mark).\n    '@media print{.ctl,.credit{display:none !important}}' +\n    \/\/ No export-window mask rules here on purpose: the export capture\n    \/\/ releases the replacement mask by REMOVING data-swapping (the\n    \/\/ shadow-root pass in pages\/export\/shared.ts HIDE_EXPORT_CHROME_SCRIPT)\n    \/\/ \u2014 attribute removal works in every engine (:host-context is\n    \/\/ Chromium-only), is scoped by construction to slots actually\n    \/\/ mid-swap, and hides the spinner through the same gate. A masked img\n    \/\/ would otherwise be silently dropped from PPTX decks (the capture\n    \/\/ walk skips visibility:hidden imgs).\n    \/\/ Attribution error tile: REPLACES the photo when an Unsplash src has\n    \/\/ no credit attribute \u2014 rendering the photo uncredited is the terms\n    \/\/ violation, so the photo must not appear at all.\n    \/\/ Calm and neutral on purpose (review feedback): the tile informs the\n    \/\/ user; the fix instructions are machine-facing (usage docblock, tool\n    \/\/ description, and the turn-end scan's bounce copy name the attributes\n    \/\/ for the agent).\n    '.attr-error{position:absolute;inset:0;display:none;flex-direction:column;align-items:center;' +\n    '  justify-content:center;gap:6px;text-align:center;padding:12px;box-sizing:border-box;' +\n    '  background:#f2f1ef;color:#6e6c66;user-select:none;' +\n    '  font:13px\/1.45 system-ui,-apple-system,sans-serif}' +\n    '.attr-error svg{opacity:.55}' +\n    '.attr-error .cap{max-width:92%;font-weight:500;letter-spacing:.01em}' +\n    ':host([data-attribution-error]) .attr-error{display:flex}' +\n    ':host([data-attribution-error]) .ring{display:none}';\n\n  const icon =\n    '<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" ' +\n    'stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' +\n    '<rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><circle cx=\"8.5\" cy=\"8.5\" r=\"1.5\"\/>' +\n    '<path d=\"m21 15-5-5L5 21\"\/><\/svg>';\n\n  const warnIcon =\n    '<svg width=\"28\" height=\"28\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" ' +\n    'stroke-width=\"1.6\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' +\n    '<path d=\"m21.73 18-8-14a2 2 0 0 0-3.46 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"\/>' +\n    '<path d=\"M12 9v4\"\/><path d=\"M12 17h.01\"\/><\/svg>';\n\n  class ImageSlot extends HTMLElement {\n    static get observedAttributes() {\n      return ['shape', 'radius', 'mask', 'fit', 'placeholder', 'src', 'id', 'credit', 'credit-href'];\n    }\n\n    \/** Duplicate-slide hook (called by deck-stage, see its\n     *  _remintDuplicateIds): copy this id's stored image, if any, under a\n     *  freshly minted key and return that key \u2014 so a duplicated slide's\n     *  slot keeps its dropped photo instead of reverting to the\n     *  placeholder. 'isFree' is the caller's uniqueness check (document\n     *  ids); candidates must ALSO be unused in the sidecar, which can\n     *  hold keys from other pages sharing the project root. (An EMPTY\n     *  slot on another page leaves no sidecar entry, so its id is not\n     *  detectable here \u2014 a minted key can collide with it and that slot\n     *  would show this photo. Same blast radius as two pages reusing an\n     *  id by hand, which the shared sidecar already permits.) Returns null\n     *  when no id could be minted (caller strips the id, today's\n     *  behavior). *\/\n    static cloneSlot(fromId, isFree) {\n      if (typeof fromId !== 'string' || !fromId) return null;\n      \/\/ Pre-hydration the store can't veto candidates or source the copy\n      \/\/ \u2014 degrade to the strip (today's behavior) rather than mint\n      \/\/ against keys we can't see yet. Any rendered (= droppable) slot\n      \/\/ means load() has already settled.\n      if (!loaded) return null;\n      const stem = fromId.replace(\/-\\d+$\/, '') || fromId;\n      for (let n = 2; n < 100; n++) {\n        const toId = stem + '-' + n;\n        if (toId === fromId) continue;\n        if (slots[toId] !== undefined) {\n          \/\/ Reuse a key holding this exact value (bytes AND crop) if no\n          \/\/ live element here owns it \u2014 a duplicate op the host refused\n          \/\/ after minting leaves such a key behind, and reusing keeps\n          \/\/ refused retries from accumulating one orphaned copy per\n          \/\/ attempt. Full equality (not just bytes) so a byte-identical\n          \/\/ key another PAGE owns with its own crop is stepped past, not\n          \/\/ adopted or rewritten. (Entries without .u never match.)\n          const prev = getSlot(toId);\n          const cur = getSlot(fromId);\n          if (!(prev &#038;&#038; cur &#038;&#038; prev.u &#038;&#038; prev.u === cur.u &#038;&#038;\n                prev.s === cur.s &#038;&#038; prev.x === cur.x &#038;&#038; prev.y === cur.y &#038;&#038;\n                (typeof isFree !== 'function' || isFree(toId)))) continue;\n          return toId;\n        }\n        if (typeof isFree === 'function' &#038;&#038; !isFree(toId)) continue;\n        const v = getSlot(fromId);\n        if (v) setSlot(toId, Object.assign({}, v));\n        return toId;\n      }\n      return null;\n    }\n\n    constructor() {\n      super();\n      \/\/ clonable: rail thumbnails deep-clone slides and carry this shadow\n      \/\/ along; reuse an already-cloned root so upgrade-after-clone works.\n      \/\/ (Deliberately NOT serializable \u2014 a getHTML consumer would embed\n      \/\/ multi-MB sidecar data-URLs into serialized page HTML.)\n      const root = this.shadowRoot ||\n        this.attachShadow({ mode: 'open', clonable: true });\n      \/\/ .spill and .ctl sit OUTSIDE .frame so overflow:hidden + border-radius\n      \/\/ on the frame (circle, pill, rounded) can't clip them.\n      root.innerHTML =\n        '<style>' + stylesheet + '<\/style>' +\n        '<div class=\"frame\" part=\"frame\">' +\n        '  <img part=\"image\" alt=\"\" draggable=\"false\" style=\"display:none\">' +\n        '  <div class=\"empty\" part=\"empty\">' + icon +\n        '    <div class=\"cap\"><\/div>' +\n        '    <div class=\"sub\">or <u>browse files<\/u><\/div><\/div>' +\n        '  <div class=\"attr-error\" part=\"attribution-error\">' + warnIcon +\n        '    <div class=\"cap\">This photo needs attribution<\/div><\/div>' +\n        '  <div class=\"loading\" part=\"loading\"><\/div>' +\n        '  <div class=\"ring\" part=\"ring\"><\/div>' +\n        '<\/div>' +\n        \/\/ Outside .frame, like .spill\/.ctl \u2014 the frame's overflow:hidden +\n        \/\/ border-radius\/clip-path would cut the credit off on circle\/pill\/mask.\n        \/\/ A SPAN, not an <a>: the prescribed Unsplash credit holds two links\n        \/\/ (photographer + Unsplash), built per-render in _render().\n        '<span class=\"credit\" part=\"credit\"><\/span>' +\n        '<div class=\"spill\" popover=\"manual\" data-dc-edit-transparent>' +\n        '  <img class=\"ghost\" alt=\"\" draggable=\"false\">' +\n        '  <div class=\"handle\" data-c=\"nw\"><\/div><div class=\"handle\" data-c=\"ne\"><\/div>' +\n        '  <div class=\"handle\" data-c=\"sw\"><\/div><div class=\"handle\" data-c=\"se\"><\/div>' +\n        '<\/div>' +\n        \/\/ data-dc-edit-transparent: the DC editor's edit-mode picker lets\n        \/\/ clicks through for chrome marked with it (EDIT_TRANSPARENT_SEL)\n        \/\/ \u2014 without it, Replace\/Edit clicks in Edit mode are swallowed by\n        \/\/ element selection and the controls look dead.\n        '<div class=\"ctl\" popover=\"manual\" data-dc-edit-transparent><button data-act=\"replace\" title=\"Replace image\">Replace<\/button>' +\n        '  <button data-act=\"edit\" title=\"Reframe image\">Edit<\/button><\/div>' +\n        '<input type=\"file\" accept=\"' + ACCEPT.join(',') + '\" hidden>';\n      this._frame = root.querySelector('.frame');\n      this._ring = root.querySelector('.ring');\n      this._img = root.querySelector('.frame img');\n      this._empty = root.querySelector('.empty');\n      this._cap = root.querySelector('.cap');\n      this._sub = root.querySelector('.sub');\n      this._spill = root.querySelector('.spill');\n      this._ctl = root.querySelector('.ctl');\n      this._credit = root.querySelector('.credit');\n      this._attrError = root.querySelector('.attr-error');\n      \/\/ Credit clicks open the link, not browse\/reframe.\n      this._credit.addEventListener('click', (e) => e.stopPropagation());\n      this._credit.addEventListener('dblclick', (e) => e.stopPropagation());\n      this._ghost = root.querySelector('.ghost');\n      this._err = null;\n      this._input = root.querySelector('input');\n      this._depth = 0;\n      this._gen = 0;\n      \/\/ Encode-in-flight marker (the owning _ingest generation): while set,\n      \/\/ the same-src \"nothing in flight\" clear in _render must not fire \u2014\n      \/\/ the stored value still points at the OLD image until the encode\n      \/\/ lands, so that clear would unmask the stale image mid-replace.\n      this._swapGen = 0;\n      \/\/ Render-owned swap in flight: set when _render assigns a new src,\n      \/\/ cleared only by the img's own load\/error (or the empty branch).\n      \/\/ img.complete CANNOT stand in for this \u2014 setting src only QUEUES\n      \/\/ the current-request swap (a microtask), so synchronously after an\n      \/\/ assignment, complete still reports the OLD settled request. The\n      \/\/ pick path does exactly that: the host sets src, credit, and\n      \/\/ credit-href back-to-back in one task, and renders #2\/#3 would\n      \/\/ read the stale complete === true and drop the mask one render\n      \/\/ after it was set.\n      this._loadPending = false;\n      \/\/ See _render's empty branch: a transient attribution-error wipe of a\n      \/\/ showing image must make the follow-up render a replacement (spinner),\n      \/\/ not a first fill (blank frame).\n      this._hidShowing = false;\n      this._view = { s: 1, x: 0, y: 0 };\n      this._subFn = () => this._render();\n      \/\/ Shadow-DOM listeners live with the shadow DOM \u2014 bound once here so\n      \/\/ disconnect\/reconnect (e.g. React remount) doesn't stack handlers.\n      this._empty.addEventListener('click', () => this._input.click());\n      root.addEventListener('click', (e) => {\n        const act = e.target && e.target.getAttribute && e.target.getAttribute('data-act');\n        if (!act) return;\n        \/\/ The hidden controls are opacity-0 but still tabbable \u2014 without\n        \/\/ this gate a keyboard user could drive them on a read-only share\n        \/\/ link (mirrors the dblclick handler's editable gate).\n        if (!this.hasAttribute('data-editable')) return;\n        if (act === 'replace') {\n          this._exitReframe(true);\n          \/\/ Host-owned picker (Unsplash modal; it also offers local import).\n          this.dispatchEvent(new CustomEvent('image-slot:pick', {\n            bubbles: true, composed: true, detail: { id: this.id || null }\n          }));\n        }\n        if (act === 'edit') {\n          if (!this._reframes()) return;\n          if (this.hasAttribute('data-reframe')) this._exitReframe(true);\n          else this._enterReframe();\n        }\n      });\n      this._input.addEventListener('change', () => {\n        const f = this._input.files && this._input.files[0];\n        if (f) this._ingest(f);\n        this._input.value = '';\n      });\n      \/\/ naturalWidth\/Height aren't known until load \u2014 re-apply so the cover\n      \/\/ baseline is computed from real dimensions, not the 100%\u00d7100% fallback.\n      \/\/ load\/error also release the replacement-in-flight mask (via the\n      \/\/ single discipline in _releaseMask): the swap is only revealed once\n      \/\/ the new image can actually paint (on error the frame shows its\n      \/\/ background, same as a fresh slot with a broken src).\n      this._img.addEventListener('load', () => {\n        this._loadPending = false;\n        this._releaseMask(true);\n        this._applyView();\n      });\n      this._img.addEventListener('error', () => {\n        this._loadPending = false;\n        this._releaseMask(true);\n      });\n      \/\/ Gated only on editable \u2014 any filled slot can be repositioned\/scaled,\n      \/\/ regardless of fit. Share links (no writeFile) stay static.\n      this.addEventListener('dblclick', (e) => {\n        if (!this.hasAttribute('data-editable') || !this._reframes()) return;\n        e.preventDefault();\n        if (this.hasAttribute('data-reframe')) this._exitReframe(true);\n        else this._enterReframe();\n      });\n      \/\/ Pan + resize both originate on the spill layer. A handle pointerdown\n      \/\/ drives an aspect-locked resize anchored at the opposite corner; any\n      \/\/ other pointerdown on the spill pans. Offsets are frame-% so a\n      \/\/ reframed slot survives responsive resize \/ PPTX export.\n      this._spill.addEventListener('pointerdown', (e) => {\n        if (e.button !== 0 || !this.hasAttribute('data-reframe')) return;\n        e.preventDefault();\n        e.stopPropagation();\n        this._spill.setPointerCapture(e.pointerId);\n        const rect = this.getBoundingClientRect();\n        const fw = rect.width || 1, fh = rect.height || 1;\n        const corner = e.target.getAttribute && e.target.getAttribute('data-c');\n        let move;\n        if (corner) {\n          \/\/ Resize about the OPPOSITE corner. Viewport-px throughout (rect\n          \/\/ fw\/fh, not clientWidth) so the math survives a transform:scale()\n          \/\/ ancestor \u2014 deck_stage renders slides scaled-to-fit.\n          const iw = this._img.naturalWidth || 1, ih = this._img.naturalHeight || 1;\n          const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain';\n          const base = contain ? Math.min(fw \/ iw, fh \/ ih) : Math.max(fw \/ iw, fh \/ ih);\n          const sx = corner.includes('e') ? 1 : -1;\n          const sy = corner.includes('s') ? 1 : -1;\n          const s0 = this._view.s;\n          const w0 = iw * base * s0, h0 = ih * base * s0;\n          const cx0 = (50 + this._view.x) \/ 100 * fw;\n          const cy0 = (50 + this._view.y) \/ 100 * fh;\n          const ox = cx0 - sx * w0 \/ 2, oy = cy0 - sy * h0 \/ 2;\n          const diag0 = Math.hypot(w0, h0);\n          const ux = sx * w0 \/ diag0, uy = sy * h0 \/ diag0;\n          move = (ev) => {\n            const proj = (ev.clientX - rect.left - ox) * ux +\n                         (ev.clientY - rect.top - oy) * uy;\n            const s = clampS(s0 * proj \/ diag0);\n            const d = diag0 * s \/ s0;\n            this._view.s = s;\n            this._view.x = (ox + ux * d \/ 2) \/ fw * 100 - 50;\n            this._view.y = (oy + uy * d \/ 2) \/ fh * 100 - 50;\n            this._clampView();\n            this._applyView();\n          };\n        } else {\n          this.setAttribute('data-panning', '');\n          const start = { px: e.clientX, py: e.clientY, x: this._view.x, y: this._view.y };\n          move = (ev) => {\n            this._view.x = start.x + (ev.clientX - start.px) \/ fw * 100;\n            this._view.y = start.y + (ev.clientY - start.py) \/ fh * 100;\n            this._clampView();\n            this._applyView();\n          };\n        }\n        const up = () => {\n          try { this._spill.releasePointerCapture(e.pointerId); } catch {}\n          this._spill.removeEventListener('pointermove', move);\n          this._spill.removeEventListener('pointerup', up);\n          this._spill.removeEventListener('pointercancel', up);\n          this.removeAttribute('data-panning');\n          this._dragUp = null;\n        };\n        \/\/ Stashed so _exitReframe (Escape \/ outside-click mid-drag) can\n        \/\/ tear the capture + listeners down synchronously.\n        this._dragUp = up;\n        this._spill.addEventListener('pointermove', move);\n        this._spill.addEventListener('pointerup', up);\n        this._spill.addEventListener('pointercancel', up);\n      });\n      \/\/ Wheel zoom stays available inside reframe mode as a trackpad nicety \u2014\n      \/\/ zooms toward the cursor (offset' = cursor\u00b7(1-k) + offset\u00b7k).\n      this.addEventListener('wheel', (e) => {\n        if (!this.hasAttribute('data-reframe')) return;\n        e.preventDefault();\n        const r = this.getBoundingClientRect();\n        const cx = (e.clientX - r.left) \/ r.width * 100 - 50;\n        const cy = (e.clientY - r.top) \/ r.height * 100 - 50;\n        const prev = this._view.s;\n        const next = clampS(prev * Math.pow(1.0015, -e.deltaY));\n        if (next === prev) return;\n        const k = next \/ prev;\n        this._view.s = next;\n        this._view.x = cx * (1 - k) + this._view.x * k;\n        this._view.y = cy * (1 - k) + this._view.y * k;\n        this._clampView();\n        this._applyView();\n      }, { passive: false });\n    }\n\n    connectedCallback() {\n      \/\/ Warn once per page \u2014 an id-less slot works for the session but\n      \/\/ cannot persist, and two id-less slots would share nothing.\n      if (!this.id && !ImageSlot._warned) {\n        ImageSlot._warned = true;\n        console.warn('<image-slot> without an id will not persist its dropped image.');\n      }\n      this.addEventListener('dragenter', this);\n      this.addEventListener('dragover', this);\n      this.addEventListener('dragleave', this);\n      this.addEventListener('drop', this);\n      subs.add(this._subFn);\n      \/\/ The host may inject window.omelette.writeFile AFTER the first render;\n      \/\/ re-render on hover so the editable-gated controls reliably appear.\n      this.addEventListener('pointerenter', this._subFn);\n      \/\/ width%\/height% in _applyView encode the frame aspect at call time \u2014\n      \/\/ a host resize (responsive grid, pane divider) would stretch the\n      \/\/ image until the next _render. Re-render on size change: _render()\n      \/\/ re-seeds _view from stored before clamp\/apply, so a shrink\u2192grow\n      \/\/ cycle round-trips instead of ratcheting x\/y toward the narrower\n      \/\/ frame's clamp range.\n      this._ro = new ResizeObserver(() => this._render());\n      this._ro.observe(this);\n      load();\n      this._render();\n    }\n\n    disconnectedCallback() {\n      subs.delete(this._subFn);\n      this.removeEventListener('pointerenter', this._subFn);\n      this.removeEventListener('dragenter', this);\n      this.removeEventListener('dragover', this);\n      this.removeEventListener('dragleave', this);\n      this.removeEventListener('drop', this);\n      if (this._ro) { this._ro.disconnect(); this._ro = null; }\n      \/\/ commit=false: a disconnect is not a user intent \u2014 committing here\n      \/\/ would persist whatever half-finished drag a React remount or DOM\n      \/\/ splice happened to interrupt. Deliberate exits commit on their own\n      \/\/ paths (Escape\/click-out\/toggle), and unloads commit via pagehide.\n      this._exitReframe(false);\n    }\n\n    _enterReframe() {\n      if (this.hasAttribute('data-reframe')) return;\n      this.setAttribute('data-reframe', '');\n      this._signalReframe(true);\n      \/\/ Best-effort commit when the document unloads mid-reframe (a host\n      \/\/ navigation racing the enter signal, a manual reload, tab close):\n      \/\/ the sidecar write rides the host bridge, which outlives this\n      \/\/ document, so the crop survives even though the mode dies with the\n      \/\/ DOM. Held on the instance so _exitReframe detaches exactly what\n      \/\/ was attached.\n      this._pagehide = () => { this._exitReframe(true); flushNow(); };\n      window.addEventListener('pagehide', this._pagehide);\n      \/\/ Promote spill to the top layer, then keep it pinned over the frame:\n      \/\/ scroll\/resize cover the common cases, and a per-frame rect check\n      \/\/ catches layout shifts that fire neither (an image above finishing\n      \/\/ load, streamed DOM pushing the slot down, an ancestor transform\n      \/\/ change) so the overlay can't detach from the frame.\n      try { this._spill.showPopover(); } catch {}\n      \/\/ After the spill, so the controls stack above it in the top layer.\n      try { this._ctl.showPopover(); } catch {}\n      this._reposition = () => { if (this.hasAttribute('data-reframe')) this._applyView(); };\n      window.addEventListener('scroll', this._reposition, true);\n      window.addEventListener('resize', this._reposition);\n      this._lastRect = '';\n      this._watch = () => {\n        if (!this.hasAttribute('data-reframe')) return;\n        const r = this.getBoundingClientRect();\n        const key = r.left + ',' + r.top + ',' + r.width + ',' + r.height;\n        if (key !== this._lastRect) { this._lastRect = key; this._applyView(); }\n        this._watchId = requestAnimationFrame(this._watch);\n      };\n      this._watchId = requestAnimationFrame(this._watch);\n      this._applyView();\n      \/\/ Close on click outside (the spill handler stopPropagation()s so\n      \/\/ in-image drags don't reach this) and on Escape. Listeners are held\n      \/\/ on the instance so _exitReframe \/ disconnectedCallback can detach\n      \/\/ exactly what was attached.\n      this._outside = (e) => {\n        if (e.composedPath && e.composedPath().includes(this)) return;\n        this._exitReframe(true);\n      };\n      this._esc = (e) => { if (e.key === 'Escape') this._exitReframe(true); };\n      document.addEventListener('pointerdown', this._outside, true);\n      document.addEventListener('keydown', this._esc, true);\n    }\n\n    _exitReframe(commit) {\n      if (!this.hasAttribute('data-reframe')) return;\n      if (this._dragUp) this._dragUp();\n      this.removeAttribute('data-reframe');\n      this.removeAttribute('data-panning');\n      if (this._outside) document.removeEventListener('pointerdown', this._outside, true);\n      if (this._esc) document.removeEventListener('keydown', this._esc, true);\n      this._outside = this._esc = null;\n      if (this._reposition) {\n        window.removeEventListener('scroll', this._reposition, true);\n        window.removeEventListener('resize', this._reposition);\n        this._reposition = null;\n      }\n      if (this._watchId) { cancelAnimationFrame(this._watchId); this._watchId = 0; }\n      if (this._pagehide) {\n        window.removeEventListener('pagehide', this._pagehide);\n        this._pagehide = null;\n      }\n      try { this._spill.hidePopover(); } catch {}\n      try { this._ctl.hidePopover(); } catch {}\n      this._ctl.style.left = ''; this._ctl.style.top = '';\n      if (commit) this._commitView();\n      this._signalReframe(false);\n    }\n\n    \/\/ Reframe state lives only in this DOM until commit, invisible to the\n    \/\/ host's dirty signals \u2014 announce enter\/exit so the host can hold\n    \/\/ auto-reloads for exactly the gesture (the guest bundle forwards\n    \/\/ image-slot:reframe to the host as imageSlotReframe). Dispatched on\n    \/\/ the element (composed, so it escapes shadow roots) while connected;\n    \/\/ a disconnected exit (disconnectedCallback) falls back to document so\n    \/\/ the host still hears it.\n    _signalReframe(active) {\n      const target = this.isConnected ? this : document;\n      target.dispatchEvent(new CustomEvent('image-slot:reframe', {\n        bubbles: true, composed: true,\n        detail: { active: active, id: this.id || null }\n      }));\n    }\n\n    \/\/ Public: host's \"Import from computer\" calls this to run local browse.\n    openFilePicker() { this._exitReframe(true); this._input.click(); }\n\n    \/\/ A src write is a newer intent for this slot's content \u2014 the host\n    \/\/ pick path (setImageSlotImage) or an agent edit \u2014 so it must win\n    \/\/ over any encode still in flight from an earlier drop: left live,\n    \/\/ that encode lands later, passes _ingest's gen guard, and its\n    \/\/ setSlot silently overwrites the pick (the stored value shadows\n    \/\/ src in _render). Bumping _gen kills the encode before its own\n    \/\/ _swapGen clear runs, so clear the dead claim here too \u2014 otherwise\n    \/\/ _releaseMask (gated on !_swapGen) never fires and the pick's\n    \/\/ spinner is stranded. src ONLY: the pick sets credit\/credit-href\n    \/\/ in the same task, and clearing _swapGen on those would let the\n    \/\/ same-src branch unmask the old image mid-encode.\n    attributeChangedCallback(name, oldVal, newVal) {\n      if (name === 'src' && oldVal !== newVal) {\n        this._gen++;\n        this._swapGen = 0;\n      }\n      if (this.shadowRoot) this._render();\n    }\n\n    \/\/ handleEvent \u2014 one listener object for all four drag events keeps the\n    \/\/ add\/remove symmetric and the depth counter correct.\n    handleEvent(e) {\n      if (e.type === 'dragenter' || e.type === 'dragover') {\n        \/\/ Without preventDefault the browser never fires 'drop'.\n        e.preventDefault();\n        e.stopPropagation();\n        if (e.dataTransfer) e.dataTransfer.dropEffect = 'copy';\n        if (e.type === 'dragenter') this._depth++;\n        this.setAttribute('data-over', '');\n      } else if (e.type === 'dragleave') {\n        \/\/ dragenter\/leave fire for every descendant crossing \u2014 count depth\n        \/\/ so hovering the icon inside the empty state doesn't flicker.\n        if (--this._depth <= 0) { this._depth = 0; this.removeAttribute('data-over'); }\n      } else if (e.type === 'drop') {\n        e.preventDefault();\n        e.stopPropagation();\n        this._depth = 0;\n        this.removeAttribute('data-over');\n        const f = e.dataTransfer &#038;&#038; e.dataTransfer.files &#038;&#038; e.dataTransfer.files[0];\n        if (f) this._ingest(f);\n      }\n    }\n\n    async _ingest(file) {\n      this._setError(null);\n      if (!file || ACCEPT.indexOf(file.type) < 0) {\n        this._setError('Drop a PNG, JPEG, WebP, or AVIF image.');\n        return;\n      }\n      \/\/ toDataUrl can take hundreds of ms on a large photo. A Clear or a\n      \/\/ newer drop during that window would be clobbered when this await\n      \/\/ resumes \u2014 bump + capture a generation so stale encodes bail.\n      const gen = ++this._gen;\n      \/\/ Replacing a shown image: surface the swap through the encode too,\n      \/\/ not just the decode \u2014 otherwise the old photo sits there with no\n      \/\/ feedback while the canvas re-encode runs. An empty slot keeps its\n      \/\/ placeholder (no spinner) until the encode lands, as before.\n      \/\/ _swapGen guards the mask against re-renders DURING the encode\n      \/\/ (pointerenter, ResizeObserver, another slot's store write): the\n      \/\/ stored value still resolves to the old image there, so _render's\n      \/\/ same-src clear would otherwise unmask it mid-replace.\n      if (this.hasAttribute('data-filled')) {\n        this.setAttribute('data-swapping', '');\n        this._swapGen = gen;\n      }\n      try {\n        const w = this.clientWidth || this.offsetWidth || MAX_DIM;\n        const url = await toDataUrl(file, w);\n        if (gen !== this._gen) return;\n        \/\/ Only exit reframe once the new image is in hand \u2014 a rejected type\n        \/\/ or decode failure leaves the in-progress crop untouched.\n        this._exitReframe(false);\n        \/\/ Clear BEFORE setSlot: its synchronous re-render must see no\n        \/\/ pending encode, so a byte-identical re-upload (same data URL, no\n        \/\/ load event coming) still clears the mask via the complete branch.\n        this._swapGen = 0;\n        const val = { u: url, s: 1, x: 0, y: 0 };\n        setSlot(this.id || '', val);\n        \/\/ Keep a session-local copy for id-less slots so the drop still\n        \/\/ shows, even though it cannot persist.\n        if (!this.id) { this._local = val; this._render(); }\n      } catch (err) {\n        if (gen !== this._gen) return;\n        this._swapGen = 0;\n        \/\/ Reveal the kept old image \u2014 unless another replacement (a\n        \/\/ remote pick's src swap) is still in flight, in which case the\n        \/\/ mask stays until THAT image settles (its load\/error releases).\n        this._releaseMask();\n        this._setError('Could not read that image.');\n        console.warn('<image-slot> ingest failed:', err);\n      }\n    }\n\n    _setError(msg) {\n      if (this._err) { this._err.remove(); this._err = null; }\n      if (!msg) return;\n      const d = document.createElement('div');\n      d.className = 'err'; d.textContent = msg;\n      this.shadowRoot.appendChild(d);\n      this._err = d;\n      setTimeout(() => { if (this._err === d) { d.remove(); this._err = null; } }, 3000);\n    }\n\n    \/\/ Reframing (pan\/resize) is available on any filled slot \u2014 the user can\n    \/\/ always reposition\/scale. `fit` only sets the initial baseline (see\n    \/\/ _geom): contain starts fully-visible, cover starts frame-filling.\n    _reframes() {\n      return this.hasAttribute('data-filled');\n    }\n\n    \/\/ The single release discipline for the replacement-in-flight mask\n    \/\/ (data-swapping). The mask comes off only when BOTH hold:\n    \/\/  - no encode is pending (_swapGen) \u2014 mid-encode the stored value\n    \/\/    still resolves to the old image, so any reveal paints it;\n    \/\/  - the frame img has settled on its current src \u2014 an unsettled src\n    \/\/    means some replacement is still in flight (e.g. a remote pick),\n    \/\/    whoever started it, and revealing would paint the previous\n    \/\/    frame. The load\/error listeners pass settled=true (the event IS\n    \/\/    the settlement signal, per spec complete is true by then);\n    \/\/    other callers rely on the complete flag (covers loaded AND\n    \/\/    failed).\n    \/\/ Every release path funnels through here EXCEPT _render's empty\n    \/\/ branch (the img is being cleared \u2014 nothing will ever settle).\n    _releaseMask(settled) {\n      if (\n        !this._swapGen &&\n        !this._loadPending &&\n        (settled || this._img.complete)\n      ) {\n        this.removeAttribute('data-swapping');\n      }\n    }\n\n    \/\/ Baseline geometry, shared by clamp\/apply\/resize. `base` is the scale at\n    \/\/ view-scale s=1: cover = fill the frame (overflow on the looser axis),\n    \/\/ contain = fit fully inside (letterboxed). Zooming a contain image past\n    \/\/ s where it overflows naturally becomes a crop. Null until the img has\n    \/\/ loaded (naturalWidth is 0 before that) or when the slot has no layout\n    \/\/ box \u2014 ResizeObserver fires with a 0\u00d70 rect under display:none, and\n    \/\/ clamping against a degenerate 1\u00d71 frame would silently pull the stored\n    \/\/ pan toward zero.\n    _geom() {\n      const iw = this._img.naturalWidth, ih = this._img.naturalHeight;\n      const fw = this.clientWidth, fh = this.clientHeight;\n      if (!iw || !ih || !fw || !fh) return null;\n      const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain';\n      const base = contain\n        ? Math.min(fw \/ iw, fh \/ ih)\n        : Math.max(fw \/ iw, fh \/ ih);\n      return { iw, ih, fw, fh, base };\n    }\n\n    _clampView() {\n      \/\/ Pan range on each axis is half the overflow past the frame edge.\n      const g = this._geom();\n      if (!g) return;\n      const mx = Math.max(0, (g.iw * g.base * this._view.s \/ g.fw - 1) * 50);\n      const my = Math.max(0, (g.ih * g.base * this._view.s \/ g.fh - 1) * 50);\n      this._view.x = Math.max(-mx, Math.min(mx, this._view.x));\n      this._view.y = Math.max(-my, Math.min(my, this._view.y));\n    }\n\n    _applyView() {\n      const g = this._geom();\n      \/\/ Top-layer controls: pin to the frame's top-right in viewport px\n      \/\/ (the same 8px inset as the in-frame layout; unscaled \u2014 top-layer UI\n      \/\/ reads as chrome, not page content). BEFORE the geometry branch:\n      \/\/ placement needs only the frame rect, and a not-yet-loaded or broken\n      \/\/ src must not leave the promoted strip floating unpositioned. Gated\n      \/\/ on the popover actually being open: without the Popover API,\n      \/\/ showPopover() threw (swallowed in _enterReframe), .ctl stays in\n      \/\/ its in-frame absolute layout, and viewport-px coordinates would\n      \/\/ shove it off-frame \u2014 and matches(':popover-open') itself throws\n      \/\/ there (unknown pseudo-class), hence the try\/catch.\n      if (this.hasAttribute('data-reframe')) {\n        let onTop = false;\n        try { onTop = this._ctl.matches(':popover-open'); } catch {}\n        if (onTop) {\n          const r = this.getBoundingClientRect();\n          this._ctl.style.left = (r.right - 8) + 'px';\n          this._ctl.style.top = (r.top + 8) + 'px';\n        }\n      }\n      if (!g) {\n        \/\/ Dimensions not known yet (before img load) \u2014 centered fit so there\n        \/\/ is no flash of an unpositioned image before the geometry lands.\n        const contain = (this.getAttribute('fit') || 'cover').toLowerCase() === 'contain';\n        this._img.style.width = '100%';\n        this._img.style.height = '100%';\n        this._img.style.left = '50%';\n        this._img.style.top = '50%';\n        this._img.style.objectFit = contain ? 'contain' : 'cover';\n        return;\n      }\n      \/\/ Baseline (cover-fill or contain-fit) \u00d7 view scale. Width\/height and\n      \/\/ left\/top are all frame-% \u2014 depends only on the frame aspect ratio, so\n      \/\/ a responsive resize keeps the same crop. The spill layer mirrors the\n      \/\/ same box so its corners = image corners.\n      const k = g.base * this._view.s;\n      const w = (g.iw * k \/ g.fw * 100) + '%';\n      const h = (g.ih * k \/ g.fh * 100) + '%';\n      const l = (50 + this._view.x) + '%';\n      const t = (50 + this._view.y) + '%';\n      this._img.style.width = w; this._img.style.height = h;\n      this._img.style.left = l; this._img.style.top = t;\n      this._img.style.objectFit = '';\n      if (this.hasAttribute('data-reframe')) {\n        \/\/ Top-layer spill: position in viewport px over the frame. The top\n        \/\/ layer escapes ancestor transforms entirely, so EVERY term must be\n        \/\/ in viewport units: getBoundingClientRect gives the frame's scaled\n        \/\/ origin AND size, and the rect\/layout ratio rescales the ghost \u2014\n        \/\/ sizing from layout px alone renders it 1\/scale too large under a\n        \/\/ scaled deck slide. Inner ghost + handles stay box-relative.\n        const r = this.getBoundingClientRect();\n        const sx = g.fw ? r.width \/ g.fw : 1;\n        const sy = g.fh ? r.height \/ g.fh : 1;\n        this._spill.style.width = (g.iw * k * sx) + 'px';\n        this._spill.style.height = (g.ih * k * sy) + 'px';\n        this._spill.style.left = (r.left + (50 + this._view.x) \/ 100 * r.width) + 'px';\n        this._spill.style.top = (r.top + (50 + this._view.y) \/ 100 * r.height) + 'px';\n      }\n    }\n\n    _commitView() {\n      const v = { s: this._view.s, x: this._view.x, y: this._view.y };\n      if (this._userUrl) v.u = this._userUrl;\n      \/\/ Framing-only (no u) persists too so an author-src slot remembers its\n      \/\/ crop; clearing the sidecar still falls through to src=.\n      if (this.id) setSlot(this.id, v);\n      else { this._local = v; }\n    }\n\n    _render() {\n      \/\/ Shape \/ mask. Presets use border-radius so the dashed ring can\n      \/\/ follow the rounded outline; clip-path is only applied for an\n      \/\/ explicit `mask` (the ring is hidden there since a rectangle\n      \/\/ dashed border chopped by an arbitrary polygon looks broken).\n      const mask = this.getAttribute('mask');\n      const shape = (this.getAttribute('shape') || 'rounded').toLowerCase();\n      let radius = '';\n      if (shape === 'circle') radius = '50%';\n      else if (shape === 'pill') radius = '9999px';\n      else if (shape === 'rounded') {\n        const n = parseFloat(this.getAttribute('radius'));\n        radius = (Number.isFinite(n) ? n : 12) + 'px';\n      }\n      this._frame.style.borderRadius = mask ? '' : radius;\n      this._frame.style.clipPath = mask || '';\n      this._ring.style.borderRadius = mask ? '' : radius;\n      this._ring.style.display = mask ? 'none' : '';\n\n      \/\/ Controls and reframe entry gate on this so share links stay read-only.\n      const editable = !!(window.omelette && window.omelette.writeFile);\n      this.toggleAttribute('data-editable', editable);\n      this._sub.style.display = editable ? '' : 'none';\n\n      \/\/ Content. The sidecar is also writable by the agent's write_file\n      \/\/ tool, so its value isn't guaranteed canvas-originated \u2014 only accept\n      \/\/ data:image\/ URLs from it. The `src` attribute is author-controlled\n      \/\/ (Claude wrote it into the HTML) so it passes through unchanged.\n      let stored = this.id ? getSlot(this.id) : this._local;\n      if (stored && stored.u && !\/^data:image\\\/\/i.test(stored.u)) stored = null;\n      const srcAttr = this.getAttribute('src') || '';\n      this._userUrl = (stored && stored.u) || null;\n      const url = this._userUrl || srcAttr;\n      \/\/ Don't clobber an in-flight reframe with a store-triggered re-render.\n      if (!this.hasAttribute('data-reframe')) {\n        this._view = {\n          s: stored && Number.isFinite(stored.s) ? clampS(stored.s) : 1,\n          x: stored && Number.isFinite(stored.x) ? stored.x : 0,\n          y: stored && Number.isFinite(stored.y) ? stored.y : 0,\n        };\n      }\n      this._cap.textContent = this.getAttribute('placeholder') || 'Drop an image';\n      \/\/ Toggle via style.display \u2014 the [hidden] attribute alone loses to\n      \/\/ the display:flex \/ display:block rules in the stylesheet above.\n      \/\/ An Unsplash src with no credit attribute must NOT render \u2014 showing\n      \/\/ the photo uncredited is the Unsplash-terms violation itself. The\n      \/\/ error tile replaces the photo until the credit is written. A\n      \/\/ user-dropped image is the user's own content and always renders.\n      \/\/ Trimmed: credit is agent\/user-editable content, and a whitespace-\n      \/\/ only value must count as missing \u2014 otherwise it would suppress the\n      \/\/ error tile AND render an empty credit box (no text, no links),\n      \/\/ exactly the unattributed state this gate exists to prevent.\n      const credit = (this.getAttribute('credit') || '').trim();\n      const attrError = !!(\n        !credit && !this._userUrl && srcAttr && isUnsplashHost(srcAttr)\n      );\n      this.toggleAttribute('data-attribution-error', attrError);\n      if (url && !attrError) {\n        const prev = this._img.getAttribute('src');\n        if (prev !== url) {\n          \/\/ Replacing an already-shown image: mark the swap BEFORE setting\n          \/\/ src so the stale frame is never revealed (see the data-swapping\n          \/\/ stylesheet rules). First fill (prev empty) keeps the existing\n          \/\/ placeholder-until-load behavior \u2014 no spinner. _hidShowing\n          \/\/ covers the pick path's transient attribution-error wipe: prev\n          \/\/ is gone, but an image WAS showing, so this is a replacement.\n          if (prev || this._hidShowing) this.setAttribute('data-swapping', '');\n          \/\/ Mark the swap BEFORE assigning src: complete keeps reporting\n          \/\/ the old settled request until the browser's\n          \/\/ update-the-image-data microtask runs, so same-task re-renders\n          \/\/ (the pick path's credit\/credit-href setAttributes) need this\n          \/\/ flag, not complete, to know a load is in flight.\n          this._loadPending = true;\n          this._img.src = url;\n          this._ghost.src = url;\n        } else {\n          \/\/ Same-src re-render \u2014 release if settled, so an ingest-set\n          \/\/ spinner can't stick after a byte-identical re-upload (same\n          \/\/ data URL, no further load event ever fires).\n          this._releaseMask();\n        }\n        this._hidShowing = false;\n        this._img.style.display = 'block';\n        this._empty.style.display = 'none';\n        this.setAttribute('data-filled', '');\n        this._clampView();\n        this._applyView();\n      } else {\n        this.removeAttribute('data-swapping');\n        \/\/ The src is being removed \u2014 no load\/error will ever fire for it.\n        this._loadPending = false;\n        \/\/ A transient attribution-error wipe of a showing image happens on\n        \/\/ the pick path: the host sets src one setAttribute before credit,\n        \/\/ so render N hides the old image (attrError) and render N+1\n        \/\/ restores a URL. Remember the wipe so that restore renders as a\n        \/\/ replacement (spinner), not a first fill (blank frame).\n        this._hidShowing = attrError && !!this._img.getAttribute('src');\n        this._img.style.display = 'none';\n        this._img.removeAttribute('src');\n        this._ghost.removeAttribute('src');\n        \/\/ The error tile owns the blocked-photo state; .empty stays for\n        \/\/ the genuinely-empty slot.\n        this._empty.style.display = attrError ? 'none' : 'flex';\n        this.removeAttribute('data-filled');\n      }\n\n      \/\/ Credit belongs to the author src, so a user drop hides it.\n      \/\/ textContent + the http(s)-only funnel keep external strings inert.\n      const showCredit = !!(url && credit && !this._userUrl && !attrError);\n      this._credit.textContent = '';\n      if (showCredit) {\n        \/\/ Validate once (resolved against the document, http(s) only),\n        \/\/ then append the terms-required utm referral params to links\n        \/\/ that point back at unsplash.com.\n        let href = '';\n        const rawHref = this.getAttribute('credit-href') || '';\n        if (rawHref) {\n          try {\n            const u = new URL(rawHref, document.baseURI);\n            if (u.protocol === 'http:' || u.protocol === 'https:') {\n              href = withReferral(u.href);\n            }\n          } catch {}\n        }\n        const mkLink = (text, linkHref) => {\n          const a = document.createElement('a');\n          a.setAttribute('target', '_blank');\n          a.setAttribute('rel', 'noopener noreferrer');\n          a.setAttribute('href', linkHref);\n          a.textContent = text;\n          return a;\n        };\n        \/\/ Unsplash's prescribed credit is TWO links \u2014 the photographer's\n        \/\/ name to their profile (credit-href) and 'Unsplash' to the\n        \/\/ homepage. Render that split whenever the text has the canonical\n        \/\/ shape; other text keeps the legacy single-link rendering.\n        const m = \/^Photo by (.+) on Unsplash$\/.exec(credit);\n        if (m) {\n          this._credit.appendChild(document.createTextNode('Photo by '));\n          this._credit.appendChild(\n            href ? mkLink(m[1], href) : document.createTextNode(m[1])\n          );\n          this._credit.appendChild(document.createTextNode(' on '));\n          this._credit.appendChild(mkLink('Unsplash', UNSPLASH_HOMEPAGE_HREF));\n        } else if (href) {\n          this._credit.appendChild(mkLink(credit, href));\n        } else {\n          this._credit.textContent = credit;\n        }\n      }\n      this.toggleAttribute('data-credit', showCredit);\n    }\n  }\n\n  if (!customElements.get('image-slot')) {\n    customElements.define('image-slot', ImageSlot);\n  }\n})();\n<\/script>\n","protected":false},"excerpt":{"rendered":"<p>Motion \u00b7 Carrousels \u00b7 Diffusion Votre marque m\u00e9rite mieux qu&rsquo;un post statique et un budget br\u00fbl\u00e9. Un pack unique qui couvre toute la cha\u00eene : la vid\u00e9o motion qui capte l&rsquo;attention, les carrousels publicitaires qui expliquent, et les campagnes qui les diffusent aux bonnes personnes. Livr\u00e9 en 10 jours, pr\u00eat \u00e0 publier \u2014 sans studio \u00e0 8 000\u20ac. Voir les packs \u2014 d\u00e8s 890\u20ac \u2192 Parler de mon projet 10 jours de production4 formats livr\u00e9s par cr\u00e9ationSans engagement de dur\u00e9e SHOWREEL \u2014 EXOA MOTION 2026 Vid\u00e9o de lancementCarrousel publicitairePromotion saisonni\u00e8reMeta AdsTikTokLinkedInProgrammatiqueHabillage de marque Vid\u00e9o de lancementCarrousel publicitairePromotion saisonni\u00e8reMeta AdsTikTokLinkedInProgrammatiqueHabillage de marque Le constat La cr\u00e9a est devenue le premier levier de performance. Les plateformes publicitaires optimisent mieux que n&rsquo;importe quel expert. Ce qui reste entre vos mains, c&rsquo;est ce que les gens voient \u2014 et c&rsquo;est pr\u00e9cis\u00e9ment l\u00e0 que la plupart des budgets se perdent. 3 s C&rsquo;est le temps dont dispose une vid\u00e9o pour retenir l&rsquo;attention dans un fil d&rsquo;actualit\u00e9. Pass\u00e9 ce d\u00e9lai, le budget est d\u00e9pens\u00e9 pour rien. \u00d74 Une m\u00eame campagne peut voir son co\u00fbt par r\u00e9sultat varier d&rsquo;un facteur quatre selon la seule qualit\u00e9 du visuel diffus\u00e9. 1 seule Une cr\u00e9ation unique ne suffit plus : les algorithmes ont besoin de plusieurs variantes pour trouver celle qui fonctionne sur votre audience. L&rsquo;offre en trois briques Une cha\u00eene compl\u00e8te, pas trois prestataires. Chaque brique fonctionne seule. Assembl\u00e9es, elles forment un dispositif de lancement coh\u00e9rent \u2014 m\u00eame script, m\u00eame univers, m\u00eame message. BRIQUE 01 Vid\u00e9o motion Une vid\u00e9o anim\u00e9e de 15 \u00e0 45 secondes qui pr\u00e9sente votre offre, votre lancement ou votre promotion. \u00c9crite pour \u00eatre comprise sans le son. Script et storyboard r\u00e9dig\u00e9s par nos \u00e9quipes Animation, habillage, sous-titres int\u00e9gr\u00e9s Musique libre de droits incluse D\u00e9clin\u00e9e en 9:16, 1:1, 4:5 et 16:9 BRIQUE 02 Carrousels publicitaires L\u00e0 o\u00f9 la vid\u00e9o accroche, le carrousel convainc : b\u00e9n\u00e9fices, preuves, offre, appel \u00e0 l&rsquo;action \u2014 une slide par id\u00e9e. 3 \u00e0 6 slides par carrousel, textes inclus Plusieurs angles pour tester ce qui fonctionne Univers graphique align\u00e9 sur la vid\u00e9o Exports pr\u00eats \u00e0 t\u00e9l\u00e9verser dans le gestionnaire BRIQUE 03 Diffusion &amp; campagnes Les cr\u00e9ations ne servent \u00e0 rien dans un dossier. Nous configurons, lan\u00e7ons et pilotons les campagnes qui les mettent devant votre audience. Structure de campagne et audiences cibl\u00e9es Suivi des conversions install\u00e9 et v\u00e9rifi\u00e9 Tests des variantes, arbitrage du budget Rapport clair : co\u00fbt par r\u00e9sultat, pas des courbes Les packs Trois formules, un seul devis. Tarifs fermes, p\u00e9rim\u00e8tre \u00e9crit noir sur blanc. Vous savez exactement ce que vous recevez avant de signer. Pack 01 Impulse Pour tester le motion sur une offre pr\u00e9cise, sans engager un gros budget. 890\u20acHT \u00b7 livr\u00e9 en 10 jours 1 vid\u00e9o motion de 15 secondes 1 carrousel publicitaire de 4 slides Script et storyboard inclus 4 formats par cr\u00e9ation 2 s\u00e9ries de retouches Choisir Impulse Le plus choisi Pack 02 Momentum Pour un lancement complet : de quoi alimenter les tests cr\u00e9atifs pendant six semaines. 1 690\u20acHT \u00b7 livr\u00e9 en 15 jours 1 vid\u00e9o motion de 30 secondes 3 carrousels publicitaires, angles diff\u00e9rents 3 variantes d&rsquo;accroche pour les tests Mini univers graphique si vous n&rsquo;avez pas de charte Configuration du suivi des conversions 4 formats par cr\u00e9ation \u00b7 2 s\u00e9ries de retouches Choisir Momentum Pack 03 Launch Pour confier tout le lancement : cr\u00e9ation, diffusion et pilotage sur un mois complet. 2 890\u20acHT \u00b7 + budget m\u00e9dia Tout le pack Momentum 1 vid\u00e9o additionnelle de 15 secondes Campagnes configur\u00e9es et lanc\u00e9es (Meta, TikTok ou LinkedIn) Pilotage 30 jours : tests, arbitrages, optimisation Rapport de fin de campagne et recommandations Choisir Launch Pour aller plus loin Abonnement Motion Studio Du contenu neuf chaque mois, sans repasser par un devis : deux vid\u00e9os motion, quatre carrousels et le pilotage continu de vos campagnes. R\u00e9siliable \u00e0 tout moment avec un mois de pr\u00e9avis \u2014 et le montant d&rsquo;un pack d\u00e9j\u00e0 r\u00e9gl\u00e9 est d\u00e9duit du premier mois. 1 290\u20ac HT \/ mois \u00b7 sans engagement de dur\u00e9e Ce que vous recevez Un dossier pr\u00eat \u00e0 t\u00e9l\u00e9verser, pas des fichiers \u00e0 retravailler. Chaque cr\u00e9ation est livr\u00e9e dans les quatre ratios utilis\u00e9s par les plateformes, export\u00e9e aux bonnes dimensions et au bon poids. 9:16 \u2014 VerticalStories, Reels, TikTok, Shorts. 1080 \u00d7 1920 px. 1:1 \u2014 Carr\u00e9Fil d&rsquo;actualit\u00e9 Instagram et Facebook. 1080 \u00d7 1080 px. 4:5 \u2014 PortraitLe format qui occupe le plus d&rsquo;\u00e9cran en fil. 1080 \u00d7 1350 px. 16:9 \u2014 PaysageYouTube, LinkedIn, site web, display. 1920 \u00d7 1080 px. Comment \u00e7a se passe Quatre \u00e9tapes, z\u00e9ro surprise. \u00c9TAPE 01 Brief de 30 minutes Un appel pour cadrer l&rsquo;offre \u00e0 mettre en avant, l&rsquo;audience vis\u00e9e et le message. Vous n&rsquo;avez rien \u00e0 pr\u00e9parer : nous posons les questions. JOUR 1 \u00c9TAPE 02 Script et storyboard Nous r\u00e9digeons l&rsquo;accroche, le d\u00e9roul\u00e9 et le d\u00e9coupage image par image. Vous validez avant qu&rsquo;une seule seconde ne soit anim\u00e9e. JOURS 2 \u00c0 4 \u00c9TAPE 03 Production Animation, habillage graphique, sous-titres, musique, d\u00e9clinaison dans les quatre formats. Deux s\u00e9ries de retouches sont incluses. JOURS 5 \u00c0 10 \u00c9TAPE 04 Diffusion Livraison du dossier complet. Avec le pack Launch ou l&rsquo;abonnement, nous configurons et pilotons directement les campagnes. JOUR 10 ET APR\u00c8S \u00c0 la carte Besoin d&rsquo;un \u00e9l\u00e9ment en plus ? Chaque option s&rsquo;ajoute \u00e0 un pack ou se commande seule. Tarifs HT. Vid\u00e9o motion suppl\u00e9mentaire15 secondes, 4 formats 390\u20ac Carrousel suppl\u00e9mentaire4 slides, textes inclus 190\u20ac Voix off professionnelleFR, DE ou EN 150\u20ac Sous-titrage multilinguePar langue additionnelle 90\u20ac Version longue 60 sFormat YouTube ou site 490\u20ac Banni\u00e8res display anim\u00e9esJeu de 5 formats HTML5 450\u20ac Landing page d\u00e9di\u00e9ePage de destination de campagne 750\u20ac Mois de pilotage additionnelGestion des campagnes 590\u20ac Questions fr\u00e9quentes Ce qu&rsquo;on nous demande avant de signer. En combien de temps la vid\u00e9o est-elle livr\u00e9e ? Dix jours ouvr\u00e9s \u00e0 partir de la validation du script pour le pack Impulse, quinze jours pour Momentum et Launch. Deux allers-retours de retouches sont inclus dans chaque pack. Faut-il fournir des \u00e9l\u00e9ments graphiques ? Id\u00e9alement votre logo, vos couleurs et vos polices. Si vous n&rsquo;avez<\/p>","protected":false},"author":4,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"_sitemap_exclude":false,"_sitemap_priority":"","_sitemap_frequency":"","rank_math_title":"Pack Motion & Ads \u2014 Vid\u00e9os + Campagnes | Exoa","rank_math_description":"Pack Motion & Ads : vid\u00e9os motion design, d\u00e9clinaisons multi-formats et mise en ligne des campagnes en 14 jours. Cr\u00e9ativit\u00e9 + performance.","rank_math_focus_keyword":"pack motion ads","rank_math_canonical_url":"","rank_math_facebook_title":"","rank_math_facebook_description":"","rank_math_twitter_title":"","rank_math_twitter_description":"","rank_math_robots":["index"],"rank_math_advanced_robots":[],"site-post-title":"disabled","site-content-layout":"page-builder","site-sidebar-layout":"no-sidebar","ast-featured-img":"disabled","ast-breadcrumbs-content":"","footnotes":""},"class_list":["post-14788","page","type-page","status-publish","hentry"],"_yoast_wpseo_focuskw":null,"_yoast_wpseo_metadesc":null,"_links":{"self":[{"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/pages\/14788","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/comments?post=14788"}],"version-history":[{"count":6,"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/pages\/14788\/revisions"}],"predecessor-version":[{"id":16643,"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/pages\/14788\/revisions\/16643"}],"wp:attachment":[{"href":"https:\/\/www.agence-exoa.com\/de\/wp-json\/wp\/v2\/media?parent=14788"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}