{"id":14789,"date":"2026-08-01T14:19:21","date_gmt":"2026-08-01T14:19:21","guid":{"rendered":"https:\/\/www.agence-exoa.com\/?page_id=14789"},"modified":"2026-08-03T16:41:19","modified_gmt":"2026-08-03T16:41:19","slug":"outsourced-advertising-management","status":"publish","type":"page","link":"https:\/\/www.agence-exoa.com\/en\/regie-publicitaire-deleguee\/","title":{"rendered":"R\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e \u2014 Google, Meta, TikTok et programmatique | 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-regie-publicitaire-deleguee-page *{margin:0;padding:0;box-sizing:border-box}html{scroll-behavior:smooth}.exoa-regie-publicitaire-deleguee-page{font-family:'DM Sans',sans-serif;color:var(--ink);background:#fff;-webkit-font-smoothing:antialiased}.exoa-regie-publicitaire-deleguee-page a{color:var(--lav2);text-decoration:none}.exoa-regie-publicitaire-deleguee-page a:hover{color:var(--violet)}.exoa-regie-publicitaire-deleguee-page .wrap{max-width:1200px;margin:0 auto;padding:0 32px}.exoa-regie-publicitaire-deleguee-page .mono{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}.exoa-regie-publicitaire-deleguee-page h1, .exoa-regie-publicitaire-deleguee-page h2, .exoa-regie-publicitaire-deleguee-page h3, .exoa-regie-publicitaire-deleguee-page .disp{font-family:'Bricolage Grotesque',sans-serif;font-weight:800;letter-spacing:-.02em;line-height:1.02}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .nav-in{max-width:1200px;margin:0 auto;padding:14px 32px;display:flex;align-items:center;justify-content:space-between;gap:20px}.exoa-regie-publicitaire-deleguee-page .logo{display:flex;align-items:center;gap:9px;color:#fff;font-weight:700;font-size:15px;letter-spacing:.02em}.exoa-regie-publicitaire-deleguee-page .logo svg{width:17px;height:17px}.exoa-regie-publicitaire-deleguee-page .nav-mid{display:flex;gap:26px}.exoa-regie-publicitaire-deleguee-page .nav-mid a{color:#C6C9E0;font-size:14px;font-weight:500}.exoa-regie-publicitaire-deleguee-page .nav-mid a:hover{color:#fff}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .btn-lime{background:var(--lime);color:var(--navy)}.exoa-regie-publicitaire-deleguee-page .btn-lime:hover{background:#dcff9c;color:var(--navy);transform:translateY(-1px)}.exoa-regie-publicitaire-deleguee-page .btn-ghost{background:rgba(255,255,255,.08);color:#fff;border:1px solid rgba(255,255,255,.2)}.exoa-regie-publicitaire-deleguee-page .btn-ghost:hover{background:rgba(255,255,255,.16);color:#fff}.exoa-regie-publicitaire-deleguee-page .btn-navy{background:var(--navy);color:#fff}.exoa-regie-publicitaire-deleguee-page .btn-navy:hover{background:var(--navy2);color:#fff;transform:translateY(-1px)}.exoa-regie-publicitaire-deleguee-page .btn-out{background:#fff;color:var(--navy);border:1.5px solid var(--line)}.exoa-regie-publicitaire-deleguee-page .btn-out:hover{border-color:var(--lav);color:var(--navy)}.exoa-regie-publicitaire-deleguee-page .hero{background:var(--navy);color:#fff;padding:88px 0 0;position:relative;overflow:hidden}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .hero-in{position:relative;z-index:2}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .pill span{font-family:'JetBrains Mono',monospace;font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:#E8EAF6}.exoa-regie-publicitaire-deleguee-page .hero h1{font-size:clamp(40px,5.6vw,76px);max-width:15ch}.exoa-regie-publicitaire-deleguee-page .hero h1 em{font-style:normal;color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .hero h1 .v{font-style:normal;color:var(--violet)}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .hero-cta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.exoa-regie-publicitaire-deleguee-page .hero-note{font-size:13px;color:#9498b8;margin-top:18px;display:flex;gap:18px;flex-wrap:wrap}.exoa-regie-publicitaire-deleguee-page .hero-note b{color:#E8EAF6;font-weight:600}.exoa-regie-publicitaire-deleguee-page .reel{margin-top:56px;position:relative;z-index:2}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .reel-bar i{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.22)}.exoa-regie-publicitaire-deleguee-page .reel-bar em{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.1em;color:#9498b8;margin-left:8px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .marq-t{display:inline-flex;gap:44px;animation:sc 26s linear infinite;padding-right:44px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .marq-t span::after{content:'\u2726';opacity:.45}@keyframes sc{to{transform:translateX(-50%)}}.exoa-regie-publicitaire-deleguee-page .sec{padding:104px 0}.exoa-regie-publicitaire-deleguee-page .sec-lt{background:var(--bg)}.exoa-regie-publicitaire-deleguee-page .sec-dk{background:var(--navy);color:#fff}.exoa-regie-publicitaire-deleguee-page .tag{display:inline-flex;align-items:center;gap:8px;margin-bottom:16px}.exoa-regie-publicitaire-deleguee-page .tag i{width:22px;height:2px;background:var(--lav);border-radius:2px}.exoa-regie-publicitaire-deleguee-page .tag span{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-regie-publicitaire-deleguee-page .sec-dk .tag span{color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .sec-dk .tag i{background:var(--lime)}.exoa-regie-publicitaire-deleguee-page .h2{font-size:clamp(30px,3.6vw,50px);max-width:22ch}.exoa-regie-publicitaire-deleguee-page .h2 em{font-style:normal;color:var(--lav2)}.exoa-regie-publicitaire-deleguee-page .sec-dk .h2 em{color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .lead{font-size:17px;color:var(--body);line-height:1.65;max-width:64ch;margin-top:18px;text-wrap:pretty}.exoa-regie-publicitaire-deleguee-page .sec-dk .lead{color:#C6C9E0}.exoa-regie-publicitaire-deleguee-page .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:40px;margin-bottom:52px;flex-wrap:wrap}.exoa-regie-publicitaire-deleguee-page .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:52px}.exoa-regie-publicitaire-deleguee-page .stat{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:18px;padding:30px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .stat-l{font-size:14.5px;color:#C6C9E0;line-height:1.55;margin-top:12px}.exoa-regie-publicitaire-deleguee-page .bricks{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.exoa-regie-publicitaire-deleguee-page .brick{background:#fff;border:1px solid var(--line);border-radius:20px;overflow:hidden;display:flex;flex-direction:column;transition:all .22s}.exoa-regie-publicitaire-deleguee-page .brick:hover{border-color:var(--lav);transform:translateY(-3px);box-shadow:0 16px 40px -20px rgba(6,0,57,.22)}.exoa-regie-publicitaire-deleguee-page .brick-vis{width:100%;min-width:0;aspect-ratio:4\/3;background:var(--bg);border-bottom:1px solid var(--line);position:relative}.exoa-regie-publicitaire-deleguee-page .brick>*{min-width:0}.exoa-regie-publicitaire-deleguee-page .brick-b{padding:26px;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-regie-publicitaire-deleguee-page .brick-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.12em;font-weight:700;color:var(--lav2)}.exoa-regie-publicitaire-deleguee-page .brick h3{font-size:21px;letter-spacing:-.01em}.exoa-regie-publicitaire-deleguee-page .brick p{font-size:14.5px;color:var(--body);line-height:1.62}.exoa-regie-publicitaire-deleguee-page .brick ul{list-style:none;display:flex;flex-direction:column;gap:8px;margin-top:4px}.exoa-regie-publicitaire-deleguee-page .brick li{font-size:13.5px;color:var(--body);padding-left:20px;position:relative;line-height:1.5}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .packs{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;align-items:start}.exoa-regie-publicitaire-deleguee-page .pack{background:#fff;border:1px solid var(--line);border-radius:22px;padding:32px;display:flex;flex-direction:column;gap:18px;position:relative}.exoa-regie-publicitaire-deleguee-page .pack.hot{background:var(--navy);border-color:var(--navy);color:#fff;box-shadow:0 24px 60px -28px rgba(6,0,57,.5)}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .pack-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-n{color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .pack h3{font-size:25px}.exoa-regie-publicitaire-deleguee-page .pack-for{font-size:13.5px;color:var(--body);line-height:1.55}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-for{color:#C6C9E0}.exoa-regie-publicitaire-deleguee-page .pack-p{display:flex;align-items:baseline;gap:7px;padding-top:6px;border-top:1px solid var(--line)}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-p{border-color:rgba(255,255,255,.15)}.exoa-regie-publicitaire-deleguee-page .pack-p b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;letter-spacing:-.03em}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-p b{color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .pack-p i{font-style:normal;font-size:13px;color:var(--body);font-weight:500}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-p i{color:#9CA0C4}.exoa-regie-publicitaire-deleguee-page .pack-l{list-style:none;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-regie-publicitaire-deleguee-page .pack-l li{font-size:14px;line-height:1.5;padding-left:24px;position:relative;color:var(--body)}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-l li{color:#DDE0F0}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .pack-l li b{color:var(--ink);font-weight:700}.exoa-regie-publicitaire-deleguee-page .pack.hot .pack-l li b{color:#fff}.exoa-regie-publicitaire-deleguee-page .pack-l li.plus::before{background:var(--violet);box-shadow:none}.exoa-regie-publicitaire-deleguee-page .pack .btn{justify-content:center;width:100%}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .sub-l{flex:1;min-width:280px}.exoa-regie-publicitaire-deleguee-page .sub .mono{color:rgba(255,255,255,.75);display:block;margin-bottom:9px}.exoa-regie-publicitaire-deleguee-page .sub h3{font-size:26px;margin-bottom:10px}.exoa-regie-publicitaire-deleguee-page .sub p{font-size:14.5px;color:rgba(255,255,255,.9);line-height:1.6;max-width:58ch}.exoa-regie-publicitaire-deleguee-page .sub-r{text-align:right}.exoa-regie-publicitaire-deleguee-page .sub-r b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;display:block;letter-spacing:-.03em}.exoa-regie-publicitaire-deleguee-page .sub-r i{font-style:normal;font-size:13px;color:rgba(255,255,255,.8)}.exoa-regie-publicitaire-deleguee-page .fmts{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:12px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .fmt-r{border:1.5px dashed rgba(205,249,128,.5);border-radius:7px;background:rgba(205,249,128,.07)}.exoa-regie-publicitaire-deleguee-page .fmt b{font-size:15px;color:#fff}.exoa-regie-publicitaire-deleguee-page .fmt small{font-size:12.5px;color:#9CA0C4;line-height:1.5;display:block}.exoa-regie-publicitaire-deleguee-page .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:12px;border-top:1px solid var(--line)}.exoa-regie-publicitaire-deleguee-page .step{padding:30px 26px 30px 0;border-right:1px solid var(--line);position:relative}.exoa-regie-publicitaire-deleguee-page .step:last-child{border-right:0;padding-right:0}.exoa-regie-publicitaire-deleguee-page .step::before{content:'';position:absolute;top:-1px;left:0;width:38%;height:3px;background:var(--lime)}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .step h3{font-size:18px;margin-bottom:9px;letter-spacing:-.01em}.exoa-regie-publicitaire-deleguee-page .step p{font-size:14px;color:var(--body);line-height:1.6}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .adds{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .add:hover{border-color:var(--lav)}.exoa-regie-publicitaire-deleguee-page .add b{font-size:14.5px;font-weight:600;display:block;margin-bottom:3px}.exoa-regie-publicitaire-deleguee-page .add small{font-size:12.5px;color:var(--body)}.exoa-regie-publicitaire-deleguee-page .add i{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:var(--lav2);white-space:nowrap}.exoa-regie-publicitaire-deleguee-page .gal{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:12px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .gal-i.wide{grid-column:span 2;aspect-ratio:16\/10}.exoa-regie-publicitaire-deleguee-page .faq{max-width:840px;margin:0 auto;display:flex;flex-direction:column;gap:10px}.exoa-regie-publicitaire-deleguee-page details{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden;transition:border-color .2s}.exoa-regie-publicitaire-deleguee-page details[open]{border-color:var(--lav)}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page summary::-webkit-details-marker{display:none}.exoa-regie-publicitaire-deleguee-page summary::after{content:'+';font-family:'Bricolage Grotesque',sans-serif;font-size:22px;color:var(--lav2);flex-shrink:0;line-height:1}.exoa-regie-publicitaire-deleguee-page details[open] summary::after{content:'\u2212'}.exoa-regie-publicitaire-deleguee-page details p{padding:0 24px 22px;font-size:14.5px;color:var(--body);line-height:1.7}.exoa-regie-publicitaire-deleguee-page details p b{color:var(--ink)}.exoa-regie-publicitaire-deleguee-page .fin{background:var(--navy);color:#fff;padding:104px 0;position:relative;overflow:hidden}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .fin-in{position:relative;z-index:2;display:grid;grid-template-columns:1fr .9fr;gap:56px;align-items:start}.exoa-regie-publicitaire-deleguee-page .fin h2{font-size:clamp(32px,4vw,54px);max-width:16ch}.exoa-regie-publicitaire-deleguee-page .fin h2 em{font-style:normal;color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .fin p{font-size:16.5px;color:#C6C9E0;line-height:1.65;margin-top:20px;max-width:50ch}.exoa-regie-publicitaire-deleguee-page .fin-tr{display:flex;flex-direction:column;gap:12px;margin-top:30px}.exoa-regie-publicitaire-deleguee-page .fin-tr div{display:flex;gap:11px;align-items:flex-start;font-size:14.5px;color:#DDE0F0;line-height:1.5}.exoa-regie-publicitaire-deleguee-page .fin-tr b{color:var(--lime);flex-shrink:0}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .form h3{font-size:20px;margin-bottom:6px}.exoa-regie-publicitaire-deleguee-page .form>p{font-size:13.5px;color:#9CA0C4;margin-bottom:22px}.exoa-regie-publicitaire-deleguee-page .fg{display:grid;grid-template-columns:1fr 1fr;gap:13px}.exoa-regie-publicitaire-deleguee-page .fld{display:flex;flex-direction:column;gap:6px}.exoa-regie-publicitaire-deleguee-page .fld.full{grid-column:1\/-1}.exoa-regie-publicitaire-deleguee-page .fld label{font-size:12.5px;font-weight:600;color:#DDE0F0}.exoa-regie-publicitaire-deleguee-page .fld input, .exoa-regie-publicitaire-deleguee-page .fld select, .exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .fld input::placeholder, .exoa-regie-publicitaire-deleguee-page .fld textarea::placeholder{color:#7C81A6}.exoa-regie-publicitaire-deleguee-page .fld select option{background:var(--navy);color:#fff}.exoa-regie-publicitaire-deleguee-page .fld input:focus, .exoa-regie-publicitaire-deleguee-page .fld select:focus, .exoa-regie-publicitaire-deleguee-page .fld textarea:focus{outline:0;border-color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .chk{display:flex;gap:9px;align-items:flex-start;font-size:12px;color:#9CA0C4;line-height:1.5;margin-top:14px}.exoa-regie-publicitaire-deleguee-page .chk input{margin-top:2px;accent-color:var(--lime)}.exoa-regie-publicitaire-deleguee-page .ok{display:none;text-align:center;padding:36px 12px}.exoa-regie-publicitaire-deleguee-page .ok.on{display:block}.exoa-regie-publicitaire-deleguee-page .ok h3{font-size:23px;color:var(--lime);margin-bottom:10px}.exoa-regie-publicitaire-deleguee-page .ok p{font-size:14.5px;color:#C6C9E0;line-height:1.6}.exoa-regie-publicitaire-deleguee-page .foot{background:#04002B;color:#7C81A6;padding:34px 0;font-size:13px}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .foot a{color:#9CA0C4}.exoa-regie-publicitaire-deleguee-page .foot a:hover{color:#fff}@media(max-width:1000px){.exoa-regie-publicitaire-deleguee-page .bricks, .exoa-regie-publicitaire-deleguee-page .packs, .exoa-regie-publicitaire-deleguee-page .stats{grid-template-columns:1fr 1fr}.exoa-regie-publicitaire-deleguee-page .fmts{grid-template-columns:1fr 1fr}.exoa-regie-publicitaire-deleguee-page .steps{grid-template-columns:1fr 1fr}.exoa-regie-publicitaire-deleguee-page .step{border-right:0;border-bottom:1px solid var(--line);padding-right:26px}.exoa-regie-publicitaire-deleguee-page .fin-in{grid-template-columns:1fr}.exoa-regie-publicitaire-deleguee-page .gal{grid-template-columns:1fr 1fr}.exoa-regie-publicitaire-deleguee-page .gal-i.wide{grid-column:span 2}}@media(max-width:700px){.exoa-regie-publicitaire-deleguee-page .wrap{padding:0 20px}.exoa-regie-publicitaire-deleguee-page .nav-mid{display:none}.exoa-regie-publicitaire-deleguee-page .sec{padding:64px 0}.exoa-regie-publicitaire-deleguee-page .bricks, .exoa-regie-publicitaire-deleguee-page .packs, .exoa-regie-publicitaire-deleguee-page .stats, .exoa-regie-publicitaire-deleguee-page .fmts, .exoa-regie-publicitaire-deleguee-page .steps, .exoa-regie-publicitaire-deleguee-page .gal, .exoa-regie-publicitaire-deleguee-page .fg{grid-template-columns:1fr}.exoa-regie-publicitaire-deleguee-page .gal-i.wide{grid-column:span 1}.exoa-regie-publicitaire-deleguee-page .hero{padding-top:56px}.exoa-regie-publicitaire-deleguee-page .hero-cta{flex-direction:column;align-items:stretch}.exoa-regie-publicitaire-deleguee-page .hero-cta .btn{justify-content:center}.exoa-regie-publicitaire-deleguee-page .sub{padding:28px 24px}.exoa-regie-publicitaire-deleguee-page .sub-r{text-align:left}.exoa-regie-publicitaire-deleguee-page .head-row{margin-bottom:36px}.exoa-regie-publicitaire-deleguee-page .foot-in{flex-direction:column;align-items:flex-start}}.exoa-regie-publicitaire-deleguee-page .pack-l li.neu::before{background:var(--lav);box-shadow:none}.exoa-regie-publicitaire-deleguee-page .grid2{display:grid;grid-template-columns:repeat(2,1fr);gap:22px}.exoa-regie-publicitaire-deleguee-page .lev{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:16px;padding:24px}.exoa-regie-publicitaire-deleguee-page .lev b{font-size:16px;color:#fff;display:block;margin-bottom:7px}.exoa-regie-publicitaire-deleguee-page .lev small{font-size:13.5px;color:#9CA0C4;line-height:1.6;display:block}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .cmp{width:100%;border-collapse:collapse;font-size:14.5px;background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden}.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .cmp td{padding:15px 18px;color:var(--body);border-bottom:1px solid var(--line);line-height:1.5}.exoa-regie-publicitaire-deleguee-page .cmp tr:last-child td{border-bottom:0}.exoa-regie-publicitaire-deleguee-page .cmp td:first-child{font-weight:600;color:var(--ink)}.exoa-regie-publicitaire-deleguee-page .cmp .yes{color:var(--lav2);font-weight:600}.exoa-regie-publicitaire-deleguee-page .cmp-w{overflow-x:auto;border-radius:16px}@media(max-width:1000px){.exoa-regie-publicitaire-deleguee-page .grid2{grid-template-columns:1fr}}\n.exoa-regie-publicitaire-deleguee-page{margin-top:0 !important;padding-top:0 !important}\n.exoa-regie-publicitaire-deleguee-page .btn-primary,.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .resx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}\n.exoa-regie-publicitaire-deleguee-page .resx-card{background:#fff;border:1px solid rgba(6,0,57,.1);border-radius:16px;padding:30px 26px}\n.exoa-regie-publicitaire-deleguee-page .resx-num{font-size:34px;font-weight:800;color:#060039;line-height:1.1}\n.exoa-regie-publicitaire-deleguee-page .resx-card h3{font-size:17px;font-weight:700;color:#060039;margin:6px 0 8px}\n.exoa-regie-publicitaire-deleguee-page .resx-card p{font-size:14px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-regie-publicitaire-deleguee-page .livr-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-regie-publicitaire-deleguee-page .livr-card{background:#F7F7FB;border:1px solid rgba(6,0,57,.08);border-radius:14px;padding:24px 22px}\n.exoa-regie-publicitaire-deleguee-page .livr-card h3{font-size:15.5px;font-weight:700;color:#060039;margin:0 0 6px}\n.exoa-regie-publicitaire-deleguee-page .livr-card h3 span{color:#8CC63F;font-weight:800;margin-right:6px}\n.exoa-regie-publicitaire-deleguee-page .livr-card p{font-size:13.5px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-regie-publicitaire-deleguee-page .expx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .expx-card:hover{background:rgba(255,255,255,.1);border-color:rgba(205,249,128,.5)}\n.exoa-regie-publicitaire-deleguee-page .expx-card strong{display:block;color:#fff;font-size:15.5px;font-weight:700;margin-bottom:4px}\n.exoa-regie-publicitaire-deleguee-page .expx-card span{display:block;color:#C6C9E0;font-size:13px;line-height:1.5}\n.exoa-regie-publicitaire-deleguee-page .expx-card em{display:inline-block;margin-top:10px;font-style:normal;color:#CDF980;font-size:12px;font-weight:700}\n.exoa-regie-publicitaire-deleguee-page .hero h1{color:#fff !important}\n@media(max-width:900px){.exoa-regie-publicitaire-deleguee-page .resx-grid,.exoa-regie-publicitaire-deleguee-page .livr-grid,.exoa-regie-publicitaire-deleguee-page .expx-grid{grid-template-columns:1fr 1fr}}\n@media(max-width:600px){.exoa-regie-publicitaire-deleguee-page .resx-grid,.exoa-regie-publicitaire-deleguee-page .livr-grid,.exoa-regie-publicitaire-deleguee-page .expx-grid{grid-template-columns:1fr}}\n\/*exoa-landing-fix*\/\n.exoa-regie-publicitaire-deleguee-page .sec-dk h2,.exoa-regie-publicitaire-deleguee-page .sec-dk .h2,.exoa-regie-publicitaire-deleguee-page .sec-dk h3,.exoa-regie-publicitaire-deleguee-page .fin h2,.exoa-regie-publicitaire-deleguee-page .fin h3{color:#fff !important}\n.exoa-regie-publicitaire-deleguee-page .sec-dk .h2 em,.exoa-regie-publicitaire-deleguee-page .fin h2 em{color:#CDF980 !important}\n.exoa-regie-publicitaire-deleguee-page .sec:not(.sec-dk) h2,.exoa-regie-publicitaire-deleguee-page .sec:not(.sec-dk) .h2{color:#060039}\n.exoa-regie-publicitaire-deleguee-page .steps{border-top:0 !important;gap:16px !important}\n.exoa-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-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-regie-publicitaire-deleguee-page .reel{display:none !important}\n  .exoa-regie-publicitaire-deleguee-page .hero{padding-bottom:40px !important}\n  .exoa-regie-publicitaire-deleguee-page .hero-note{margin-bottom:0 !important}\n}\n\/*exoa-width-fix*\/\n.exoa-regie-publicitaire-deleguee-page .hero h1{max-width:24ch !important}\n.exoa-regie-publicitaire-deleguee-page .h2{max-width:38ch !important}\n.exoa-regie-publicitaire-deleguee-page .fin h2{max-width:28ch !important}\n.exoa-regie-publicitaire-deleguee-page .sec .wrap > p,.exoa-regie-publicitaire-deleguee-page .h2 + p,.exoa-regie-publicitaire-deleguee-page .tag ~ p,.exoa-regie-publicitaire-deleguee-page .fin p,.exoa-regie-publicitaire-deleguee-page .sub p{max-width:76ch !important}\n.exoa-regie-publicitaire-deleguee-page .hero-sub{max-width:70ch !important}\n\/*exoa-pack-align*\/\n.exoa-regie-publicitaire-deleguee-page .packs{align-items:stretch !important}\n.exoa-regie-publicitaire-deleguee-page .pack{height:100%}\n.exoa-regie-publicitaire-deleguee-page .pack > p,.exoa-regie-publicitaire-deleguee-page .pack .pack-for{min-height:3.4em}\n.exoa-regie-publicitaire-deleguee-page .pack ul,.exoa-regie-publicitaire-deleguee-page .pack .pack-l{flex:1}\n.exoa-regie-publicitaire-deleguee-page .pack .btn,.exoa-regie-publicitaire-deleguee-page .pack a[class*=\"btn\"]{margin-top:auto}\n@media(max-width:900px){.exoa-regie-publicitaire-deleguee-page .pack > p,.exoa-regie-publicitaire-deleguee-page .pack .pack-for{min-height:0}}\n<\/style>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"Service\",\"name\":\"R\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e \u2014 Exoa\",\"description\":\"Confiez toute votre publicit\u00e9 digitale \u00e0 une seule \u00e9quipe : Google Ads, Meta, TikTok, LinkedIn, display et DOOH. Cr\u00e9ation incluse, budget ma\u00eetris\u00e9, d\u00e8s 690\u20ac\/mois.\",\"provider\":{\"@type\":\"Organization\",\"name\":\"Exoa\",\"url\":\"https:\/\/www.agence-exoa.com\"},\"areaServed\":[{\"@type\":\"Country\",\"name\":\"France\"},{\"@type\":\"Country\",\"name\":\"Luxembourg\"}],\"serviceType\":\"Publicit\u00e9 digitale\",\"offers\":[{\"@type\":\"Offer\",\"name\":\"Amorce\",\"price\":\"690\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Traction\",\"price\":\"1190\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Omnicanal\",\"price\":\"1990\",\"priceCurrency\":\"EUR\"}]}\n<\/script>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"FAQPage\",\"mainEntity\":[{\"@type\":\"Question\",\"name\":\"Le budget publicitaire est-il inclus dans vos tarifs ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Non. Les montants affich\u00e9s sont des frais de gestion couvrant la strat\u00e9gie, la cr\u00e9ation et le pilotage. Le budget m\u00e9dia est r\u00e9gl\u00e9 depuis votre propre compte publicitaire.\"}},{\"@type\":\"Question\",\"name\":\"Quel budget m\u00e9dia minimum faut-il pr\u00e9voir ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"En dessous de 800\u20ac \u00e0 1 000\u20ac par mois de budget m\u00e9dia, les plateformes n'ont pas assez de donn\u00e9es pour optimiser correctement.\"}},{\"@type\":\"Question\",\"name\":\"Qui est propri\u00e9taire des comptes publicitaires ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Vous, toujours. Nous travaillons sur vos comptes avec un acc\u00e8s d\u00e9l\u00e9gu\u00e9. Si la collaboration s'arr\u00eate, vous conservez l'historique, les audiences et les campagnes.\"}},{\"@type\":\"Question\",\"name\":\"Les cr\u00e9ations sont-elles vraiment incluses ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Oui. Vid\u00e9os motion, carrousels et banni\u00e8res sont produits en interne et compris dans les frais de gestion.\"}},{\"@type\":\"Question\",\"name\":\"En combien de temps voit-on des r\u00e9sultats ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Les premi\u00e8res conversions arrivent dans les deux premi\u00e8res semaines. Comptez six \u00e0 huit semaines pour un co\u00fbt d'acquisition stabilis\u00e9.\"}},{\"@type\":\"Question\",\"name\":\"Y a-t-il un engagement de dur\u00e9e ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Aucun engagement de dur\u00e9e. R\u00e9siliation avec un mois de pr\u00e9avis.\"}},{\"@type\":\"Question\",\"name\":\"Travaillez-vous avec des concurrents directs ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Non. Sur un m\u00eame march\u00e9 et une m\u00eame zone g\u00e9ographique, nous n'accompagnons qu'un seul annonceur.\"}}]}\n<\/script>\n<div class=\"exoa-regie-publicitaire-deleguee-page\">\n<header class=\"hero\"><div class=\"wrap hero-in\">\n<div class=\"pill\"><span class=\"dot\"><\/span><span>Search \u00b7 Social \u00b7 Programmatique \u00b7 DOOH<\/span><\/div>\n<h1>Votre publicit\u00e9 pilot\u00e9e par une <em>\u00e9quipe enti\u00e8re<\/em>, pas par un tableau de bord.<\/h1>\n<p class=\"hero-sub\">Google Ads, Meta, TikTok, LinkedIn, display, audio, affichage num\u00e9rique : un seul interlocuteur pour tous vos leviers payants. Nous concevons les cr\u00e9ations, structurons les campagnes, arbitrons le budget et vous rendons des comptes en euros \u2014 pas en impressions.<\/p>\n<div class=\"hero-cta\">\n<a href=\"#packs\" class=\"btn btn-lime\">Voir les formules \u2014 d\u00e8s 690\u20ac\/mois \u2192<\/a>\n<a href=\"#devis\" class=\"btn btn-ghost\">Auditer mes campagnes actuelles<\/a>\n<\/div>\n<div class=\"hero-note\"><span><b>Setup en 7 jours<\/b> apr\u00e8s validation<\/span><span><b>Cr\u00e9ations incluses<\/b> dans chaque formule<\/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>TABLEAU DE BORD \u2014 CAMPAGNES EXOA<\/em><\/div>\n<image-slot id=\"pub-reel\" shape=\"rect\" fit=\"cover\" placeholder=\"Capture de votre tableau de bord ou d'une campagne\"><\/image-slot>\n<\/div>\n<\/div>\n<\/div><\/header>\n\n<div class=\"marq\"><div class=\"marq-t\">\n<span>Google Ads<\/span><span>Meta<\/span><span>TikTok<\/span><span>LinkedIn<\/span><span>Display<\/span><span>Audio digital<\/span><span>DOOH<\/span><span>Retargeting<\/span><span>Shopping<\/span><span>YouTube<\/span>\n<span>Google Ads<\/span><span>Meta<\/span><span>TikTok<\/span><span>LinkedIn<\/span><span>Display<\/span><span>Audio digital<\/span><span>DOOH<\/span><span>Retargeting<\/span><span>Shopping<\/span><span>YouTube<\/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\">Le probl\u00e8me n&rsquo;est presque jamais <em>le budget<\/em>.<\/h2>\n<p class=\"lead\">La plupart des comptes publicitaires que nous reprenons ne souffrent pas d&rsquo;un manque d&rsquo;argent, mais d&rsquo;une structure improvis\u00e9e, d&rsquo;un suivi de conversions cass\u00e9 et de cr\u00e9ations jamais renouvel\u00e9es. Trois d\u00e9fauts qui co\u00fbtent bien plus cher qu&rsquo;une hausse de budget.<\/p>\n<div class=\"stats\">\n<div class=\"stat\"><div class=\"stat-n\">1 sur 3<\/div><div class=\"stat-l\">Environ un compte publicitaire sur trois que nous auditons remonte des conversions incompl\u00e8tes ou dupliqu\u00e9es \u2014 les d\u00e9cisions sont donc prises sur des chiffres faux.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">70 %<\/div><div class=\"stat-l\">La part de la performance qui d\u00e9pend aujourd&rsquo;hui de la cr\u00e9ation et du message, d\u00e9sormais que les plateformes g\u00e8rent seules le ciblage et les ench\u00e8res.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">0 test<\/div><div class=\"stat-l\">Le nombre de variantes test\u00e9es dans la majorit\u00e9 des comptes que nous reprenons : une seule annonce, un seul visuel, aucune alternative.<\/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>Le p\u00e9rim\u00e8tre<\/span><\/div>\n<h2 class=\"h2\">Trois m\u00e9tiers, <em>une seule \u00e9quipe<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Strat\u00e9gie, cr\u00e9ation et pilotage sont indissociables. Les s\u00e9parer entre plusieurs prestataires, c&rsquo;est garantir la d\u00e9perdition.<\/p>\n<\/div>\n<div class=\"bricks\">\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"pub-b1\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel strat\u00e9gie m\u00e9dia\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 01<\/span>\n<h3>Strat\u00e9gie &#038; structure<\/h3>\n<p>Avant de d\u00e9penser, nous cadrons : qui viser, avec quel message, sur quel levier et \u00e0 quel co\u00fbt d&rsquo;acquisition cible.<\/p>\n<ul><li>Choix des leviers selon votre march\u00e9 et vos marges<\/li><li>Architecture de campagnes et logique d&rsquo;audiences<\/li><li>Suivi des conversions install\u00e9 et v\u00e9rifi\u00e9<\/li><li>Co\u00fbt par acquisition cible d\u00e9fini avec vous<\/li><\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"pub-b2\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel cr\u00e9ations\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 02<\/span>\n<h3>Cr\u00e9ations publicitaires<\/h3>\n<p>Vid\u00e9os, carrousels, banni\u00e8res, annonces textuelles : toutes les cr\u00e9ations sont produites en interne, jamais sous-trait\u00e9es.<\/p>\n<ul><li>Vid\u00e9os motion et carrousels d\u00e9clin\u00e9s en 4 formats<\/li><li>Variantes d&rsquo;accroche pour alimenter les tests<\/li><li>Banni\u00e8res display anim\u00e9es et statiques<\/li><li>Renouvellement r\u00e9gulier pour \u00e9viter l&rsquo;usure<\/li><\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"pub-b3\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel pilotage\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 03<\/span>\n<h3>Pilotage &#038; arbitrage<\/h3>\n<p>Le travail r\u00e9el commence apr\u00e8s le lancement : couper ce qui ne fonctionne pas, renforcer ce qui performe, semaine apr\u00e8s semaine.<\/p>\n<ul><li>Arbitrage du budget entre leviers et audiences<\/li><li>Tests cr\u00e9atifs continus, d\u00e9cisions document\u00e9es<\/li><li>Rapport mensuel lisible : co\u00fbt, volume, retour<\/li><li>Un interlocuteur unique, joignable<\/li><\/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 formules<\/span><\/div>\n<h2 class=\"h2\">Trois formules, <em>selon vos leviers<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Les frais de gestion couvrent la strat\u00e9gie, la cr\u00e9ation et le pilotage. Le budget m\u00e9dia reste sur votre compte publicitaire.<\/p>\n<\/div>\n<div class=\"packs\">\n<div class=\"pack\">\n\n<span class=\"pack-n\">Formule 01<\/span>\n<h3>Amorce<\/h3>\n<p class=\"pack-for\">Pour d\u00e9marrer proprement sur un seul levier, avec une base saine.<\/p>\n<div class=\"pack-p\"><b>690\u20ac<\/b><i>HT \/ mois \u00b7 1 levier<\/i><\/div>\n<ul class=\"pack-l\"><li><b>1 levier<\/b> au choix (Google, Meta ou TikTok)<\/li><li>Structure de campagnes et audiences<\/li><li><b>2 cr\u00e9ations<\/b> par mois<\/li><li>Suivi des conversions install\u00e9<\/li><li>Rapport mensuel comment\u00e9<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Amorce<\/a>\n<\/div>\n<div class=\"pack hot\">\n<span class=\"pack-bdg\">Le plus choisi<\/span>\n<span class=\"pack-n\">Formule 02<\/span>\n<h3>Traction<\/h3>\n<p class=\"pack-for\">Pour combiner intention et d\u00e9couverte : capter la demande existante et en cr\u00e9er.<\/p>\n<div class=\"pack-p\"><b>1 190\u20ac<\/b><i>HT \/ mois \u00b7 2 \u00e0 3 leviers<\/i><\/div>\n<ul class=\"pack-l\"><li><b>2 \u00e0 3 leviers<\/b> combin\u00e9s<\/li><li><b>5 cr\u00e9ations<\/b> par mois, tests inclus<\/li><li>Retargeting et audiences similaires<\/li><li>Optimisation hebdomadaire<\/li><li>Point mensuel de 45 minutes<\/li><li>Tableau de bord partag\u00e9 en continu<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-lime\">Choisir Traction<\/a>\n<\/div>\n<div class=\"pack\">\n\n<span class=\"pack-n\">Formule 03<\/span>\n<h3>Omnicanal<\/h3>\n<p class=\"pack-for\">Pour couvrir tout le parcours, du grand public \u00e0 la reprise de contact.<\/p>\n<div class=\"pack-p\"><b>1 990\u20ac<\/b><i>HT \/ mois \u00b7 tous leviers<\/i><\/div>\n<ul class=\"pack-l\"><li><b>Tous les leviers<\/b>, y compris programmatique<\/li><li class=\"plus\"><b>Display, audio digital et affichage num\u00e9rique<\/b><\/li><li class=\"plus\"><b>Cr\u00e9ations illimit\u00e9es<\/b> dans le cadre d\u00e9fini<\/li><li class=\"plus\"><b>Bilan trimestriel<\/b> et plan m\u00e9dia \u00e0 3 mois<\/li><li class=\"plus\">Acc\u00e8s prioritaire \u00e0 l&rsquo;\u00e9quipe<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Omnicanal<\/a>\n<\/div>\n<\/div>\n\n<div class=\"sub\">\n<div class=\"sub-l\">\n<span class=\"mono\">Avant de vous engager<\/span>\n<h3>Audit publicitaire complet<\/h3>\n<p>Nous reprenons vos comptes existants, v\u00e9rifions le suivi des conversions, la structure, les audiences et les cr\u00e9ations. Vous recevez un rapport \u00e9crit avec les corrections prioritaires \u2014 utilisable m\u00eame si vous ne travaillez pas avec nous ensuite. Le montant est d\u00e9duit de la premi\u00e8re mensualit\u00e9 si vous nous confiez la suite.<\/p>\n<\/div>\n<div class=\"sub-r\">\n<b>750\u20ac<\/b>\n<i>HT \u00b7 une seule fois<\/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>Les leviers<\/span><\/div>\n<h2 class=\"h2\">Tout ce que nous <em>diffusons pour vous<\/em>.<\/h2>\n<p class=\"lead\">Nous ne poussons jamais un levier parce qu&rsquo;il est \u00e0 la mode, mais parce qu&rsquo;il correspond \u00e0 votre march\u00e9, votre panier moyen et votre cycle de d\u00e9cision.<\/p>\n<div class=\"grid2\" style=\"margin-top:44px\">\n<div class=\"lev\"><span class=\"lev-t\">INTENTION<\/span><b>Google Ads \u2014 Search &#038; Shopping<\/b><small>Capter les personnes qui cherchent d\u00e9j\u00e0 ce que vous vendez. Le levier au meilleur retour imm\u00e9diat, \u00e0 condition que la structure et les mots-cl\u00e9s soient tenus.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">D\u00c9COUVERTE<\/span><b>Meta \u2014 Instagram &#038; Facebook<\/b><small>Cr\u00e9er la demande aupr\u00e8s d&rsquo;audiences qui ne vous connaissent pas encore, puis reprendre contact avec celles qui ont montr\u00e9 de l&rsquo;int\u00e9r\u00eat.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">ATTENTION<\/span><b>TikTok &#038; Reels<\/b><small>Le format qui donne aujourd&rsquo;hui le plus de visibilit\u00e9 pour un budget donn\u00e9, \u00e0 condition d&rsquo;accepter un registre cr\u00e9atif natif plut\u00f4t qu&rsquo;une publicit\u00e9 classique.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">B2B<\/span><b>LinkedIn Ads<\/b><small>Cibler par fonction, secteur et taille d&rsquo;entreprise. Co\u00fbt par clic \u00e9lev\u00e9, mais pertinent d\u00e8s que la valeur d&rsquo;un client le justifie.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">COUVERTURE<\/span><b>Display &#038; programmatique<\/b><small>\u00catre pr\u00e9sent sur les sites que votre audience consulte, avec un ciblage par centres d&rsquo;int\u00e9r\u00eat, zone g\u00e9ographique ou comportement d&rsquo;achat.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">PHYSIQUE<\/span><b>Audio digital &#038; affichage num\u00e9rique<\/b><small>Podcasts, radios en ligne, \u00e9crans en points de vente et en rue. Le levier le plus sous-exploit\u00e9 pour asseoir une pr\u00e9sence locale.<\/small><\/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\">Sept jours pour <em>reprendre la main<\/em>.<\/h2>\n<div class=\"steps\">\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 01<\/span>\n<h3>Audit et cadrage<\/h3>\n<p>Nous ouvrons vos comptes existants, mesurons ce qui fonctionne, identifions les pertes. Puis nous fixons ensemble un co\u00fbt d&rsquo;acquisition cible.<\/p>\n<em>JOURS 1 \u00c0 3<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 02<\/span>\n<h3>Correctifs et structure<\/h3>\n<p>Suivi des conversions r\u00e9par\u00e9, campagnes restructur\u00e9es, audiences reconstruites. Les premi\u00e8res cr\u00e9ations entrent en production.<\/p>\n<em>JOURS 4 \u00c0 7<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 03<\/span>\n<h3>Lancement et tests<\/h3>\n<p>Mise en ligne progressive, plusieurs variantes en parall\u00e8le. Nous laissons les plateformes apprendre avant d&rsquo;arbitrer.<\/p>\n<em>SEMAINES 2 \u00c0 4<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 04<\/span>\n<h3>Pilotage continu<\/h3>\n<p>Chaque semaine : couper, renforcer, renouveler les cr\u00e9ations. Chaque mois : un rapport clair et un point avec vous.<\/p>\n<em>EN CONTINU<\/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 une formule ou se commande seule. Tarifs HT.<\/p>\n<\/div>\n<div class=\"adds\">\n<div class=\"add\"><div><b>Audit publicitaire complet<\/b><small>Rapport \u00e9crit, corrections prioris\u00e9es<\/small><\/div><i>750\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Landing page de campagne<\/b><small>Page de destination d\u00e9di\u00e9e<\/small><\/div><i>750\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Vid\u00e9o motion additionnelle<\/b><small>15 secondes, 4 formats<\/small><\/div><i>390\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>Levier suppl\u00e9mentaire<\/b><small>Ajout \u00e0 une formule en cours<\/small><\/div><i>290\u20ac\/mois<\/i><\/div>\n<div class=\"add\"><div><b>Suivi &#038; conformit\u00e9<\/b><small>GA4, GTM, consentement RGPD<\/small><\/div><i>850\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Flux produits e-commerce<\/b><small>Optimisation du catalogue Shopping<\/small><\/div><i>690\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Formation de votre \u00e9quipe<\/b><small>Demi-journ\u00e9e, en visio ou sur site<\/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>Le budget publicitaire est-il inclus dans vos tarifs ?<\/summary><p>Non. Les montants affich\u00e9s sont des <b>frais de gestion<\/b> couvrant la strat\u00e9gie, la cr\u00e9ation et le pilotage. Le budget m\u00e9dia est r\u00e9gl\u00e9 depuis votre propre compte publicitaire : vous en gardez la ma\u00eetrise, la propri\u00e9t\u00e9 et l&rsquo;historique.<\/p><\/details>\n<details><summary>Quel budget m\u00e9dia minimum faut-il pr\u00e9voir ?<\/summary><p>En dessous de <b>800\u20ac \u00e0 1 000\u20ac par mois<\/b> de budget m\u00e9dia, les plateformes n&rsquo;ont pas assez de donn\u00e9es pour optimiser correctement. Nous pr\u00e9f\u00e9rons vous le dire avant qu&rsquo;apr\u00e8s.<\/p><\/details>\n<details><summary>Qui est propri\u00e9taire des comptes publicitaires ?<\/summary><p>Vous, toujours. Nous travaillons sur <b>vos<\/b> comptes, avec un acc\u00e8s d\u00e9l\u00e9gu\u00e9. Si la collaboration s&rsquo;arr\u00eate, vous conservez l&rsquo;int\u00e9gralit\u00e9 de l&rsquo;historique, des audiences et des campagnes.<\/p><\/details>\n<details><summary>Les cr\u00e9ations sont-elles vraiment incluses ?<\/summary><p>Oui, et c&rsquo;est ce qui nous distingue de la plupart des agences m\u00e9dia qui facturent la cr\u00e9ation \u00e0 part. Vid\u00e9os motion, carrousels, banni\u00e8res : <b>tout est produit en interne<\/b> et compris dans les frais de gestion.<\/p><\/details>\n<details><summary>En combien de temps voit-on des r\u00e9sultats ?<\/summary><p>Les premi\u00e8res conversions arrivent g\u00e9n\u00e9ralement dans les <b>deux premi\u00e8res semaines<\/b>. Il faut compter <b>six \u00e0 huit semaines<\/b> pour un co\u00fbt d&rsquo;acquisition stabilis\u00e9, le temps que les tests cr\u00e9atifs livrent leurs enseignements.<\/p><\/details>\n<details><summary>Y a-t-il un engagement de dur\u00e9e ?<\/summary><p>Aucun. R\u00e9siliation avec <b>un mois de pr\u00e9avis<\/b>. Nous pr\u00e9f\u00e9rons \u00eatre gard\u00e9s parce que les r\u00e9sultats sont l\u00e0, pas parce qu&rsquo;un contrat l&rsquo;impose.<\/p><\/details>\n<details><summary>Travaillez-vous avec des concurrents directs ?<\/summary><p>Non. Sur un m\u00eame march\u00e9 et une m\u00eame zone g\u00e9ographique, nous nous engageons \u00e0 <b>n&rsquo;accompagner qu&rsquo;un seul annonceur<\/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\">1<\/div><h3>interlocuteur, toutes r\u00e9gies<\/h3><p>Google, Meta, TikTok, LinkedIn, programmatique : un seul pilote.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">-25%<\/div><h3>de co\u00fbt m\u00e9dia moyen<\/h3><p>Optimisation crois\u00e9e des budgets entre plateformes selon la performance.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">100%<\/div><h3>de transparence<\/h3><p>Comptes \u00e0 votre nom, co\u00fbts r\u00e9els visibles, aucune marge cach\u00e9e.<\/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 r\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e s&rsquo;appuie sur des livrables tangibles.<\/p><\/div>\n<div class=\"livr-grid\"><div class=\"livr-card\"><h3><span>\u2713<\/span>Audit des comptes existants<\/h3><p>Structure, conversions, historiques : le diagnostic multi-r\u00e9gies.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Plan m\u00e9dia unifi\u00e9<\/h3><p>Budgets r\u00e9partis selon la performance r\u00e9elle, pas les habitudes.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Gestion quotidienne<\/h3><p>Ench\u00e8res, audiences, cr\u00e9as et tests sur toutes les plateformes.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Cr\u00e9ations publicitaires<\/h3><p>Les formats requis par chaque r\u00e9gie, produits par notre studio.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Tracking consolid\u00e9<\/h3><p>Une seule v\u00e9rit\u00e9 : conversions d\u00e9dupliqu\u00e9es et CA attribu\u00e9.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Comit\u00e9 mensuel<\/h3><p>R\u00e9sultats, enseignements et arbitrages du mois suivant.<\/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\n<section class=\"exoa-trf-note\" style=\"background:#F7F7FB;padding:40px 20px\"><div style=\"max-width:900px;margin:0 auto;background:#fff;border:1px solid rgba(6,0,57,.1);border-left:4px solid #CDF980;border-radius:14px;padding:24px 28px\">\n<strong style=\"color:#060039;font-size:16px\">&#x1f4b6; Tarification transparente et coh\u00e9rente<\/strong>\n<p style=\"color:#5A5E80;margin:8px 0 12px;line-height:1.6\">Nos honoraires de gestion d\u00e9marrent \u00e0 500 \u20ac HT\/mois et s&rsquo;ajoutent \u00e0 votre budget m\u00e9dia, factur\u00e9 \u00e0 votre nom et visible en co\u00fbts r\u00e9els. Les rep\u00e8res tarifaires sont les m\u00eames sur tout le site \u2014 pas de prix diff\u00e9rent selon la page.<\/p>\n<a href=\"\/offres\/#tarifs\" style=\"display:inline-block;background:#CDF980;color:#060039;font-weight:700;font-size:13px;border-radius:20px;padding:8px 18px;text-decoration:none\">Voir tous nos rep\u00e8res tarifaires \u2192<\/a>\n<\/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 vendez. <em>On s&rsquo;occupe de la diffusion.<\/em><\/h2>\n<p>D\u00e9crivez votre activit\u00e9 et votre situation actuelle : nous revenons sous vingt-quatre heures avec les leviers recommand\u00e9s, un budget m\u00e9dia conseill\u00e9 et une proposition chiffr\u00e9e.<\/p>\n<div class=\"fin-tr\">\n<div><b>\u2713<\/b><span>Proposition chiffr\u00e9e sous 24 h<\/span><\/div>\n<div><b>\u2713<\/b><span>Vous restez propri\u00e9taire de vos comptes<\/span><\/div>\n<div><b>\u2713<\/b><span>Cr\u00e9ations incluses, jamais factur\u00e9es \u00e0 part<\/span><\/div>\n<div><b>\u2713<\/b><span>Un seul annonceur par march\u00e9 et par zone<\/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=R\u00e9gie+publicitaire\" 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 R\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e \u00b7 Tarifs HT, France et Luxembourg<\/span>\n<span><a href=\"\/\">Accueil<\/a> \u00b7 <a href=\"\/pack-motion-ads\/\">Motion &amp; Ads<\/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>Search \u00b7 Social \u00b7 Programmatique \u00b7 DOOH Votre publicit\u00e9 pilot\u00e9e par une \u00e9quipe enti\u00e8re, pas par un tableau de bord. Google Ads, Meta, TikTok, LinkedIn, display, audio, affichage num\u00e9rique : un seul interlocuteur pour tous vos leviers payants. Nous concevons les cr\u00e9ations, structurons les campagnes, arbitrons le budget et vous rendons des comptes en euros \u2014 pas en impressions. Voir les formules \u2014 d\u00e8s 690\u20ac\/mois \u2192 Auditer mes campagnes actuelles Setup en 7 jours apr\u00e8s validationCr\u00e9ations incluses dans chaque formuleSans engagement de dur\u00e9e TABLEAU DE BORD \u2014 CAMPAGNES EXOA Google AdsMetaTikTokLinkedInDisplayAudio digitalDOOHRetargetingShoppingYouTube Google AdsMetaTikTokLinkedInDisplayAudio digitalDOOHRetargetingShoppingYouTube Le constat Le probl\u00e8me n&rsquo;est presque jamais le budget. La plupart des comptes publicitaires que nous reprenons ne souffrent pas d&rsquo;un manque d&rsquo;argent, mais d&rsquo;une structure improvis\u00e9e, d&rsquo;un suivi de conversions cass\u00e9 et de cr\u00e9ations jamais renouvel\u00e9es. Trois d\u00e9fauts qui co\u00fbtent bien plus cher qu&rsquo;une hausse de budget. 1 sur 3 Environ un compte publicitaire sur trois que nous auditons remonte des conversions incompl\u00e8tes ou dupliqu\u00e9es \u2014 les d\u00e9cisions sont donc prises sur des chiffres faux. 70 % La part de la performance qui d\u00e9pend aujourd&rsquo;hui de la cr\u00e9ation et du message, d\u00e9sormais que les plateformes g\u00e8rent seules le ciblage et les ench\u00e8res. 0 test Le nombre de variantes test\u00e9es dans la majorit\u00e9 des comptes que nous reprenons : une seule annonce, un seul visuel, aucune alternative. Le p\u00e9rim\u00e8tre Trois m\u00e9tiers, une seule \u00e9quipe. Strat\u00e9gie, cr\u00e9ation et pilotage sont indissociables. Les s\u00e9parer entre plusieurs prestataires, c&rsquo;est garantir la d\u00e9perdition. BRIQUE 01 Strat\u00e9gie &#038; structure Avant de d\u00e9penser, nous cadrons : qui viser, avec quel message, sur quel levier et \u00e0 quel co\u00fbt d&rsquo;acquisition cible. Choix des leviers selon votre march\u00e9 et vos marges Architecture de campagnes et logique d&rsquo;audiences Suivi des conversions install\u00e9 et v\u00e9rifi\u00e9 Co\u00fbt par acquisition cible d\u00e9fini avec vous BRIQUE 02 Cr\u00e9ations publicitaires Vid\u00e9os, carrousels, banni\u00e8res, annonces textuelles : toutes les cr\u00e9ations sont produites en interne, jamais sous-trait\u00e9es. Vid\u00e9os motion et carrousels d\u00e9clin\u00e9s en 4 formats Variantes d&rsquo;accroche pour alimenter les tests Banni\u00e8res display anim\u00e9es et statiques Renouvellement r\u00e9gulier pour \u00e9viter l&rsquo;usure BRIQUE 03 Pilotage &#038; arbitrage Le travail r\u00e9el commence apr\u00e8s le lancement : couper ce qui ne fonctionne pas, renforcer ce qui performe, semaine apr\u00e8s semaine. Arbitrage du budget entre leviers et audiences Tests cr\u00e9atifs continus, d\u00e9cisions document\u00e9es Rapport mensuel lisible : co\u00fbt, volume, retour Un interlocuteur unique, joignable Les formules Trois formules, selon vos leviers. Les frais de gestion couvrent la strat\u00e9gie, la cr\u00e9ation et le pilotage. Le budget m\u00e9dia reste sur votre compte publicitaire. Formule 01 Amorce Pour d\u00e9marrer proprement sur un seul levier, avec une base saine. 690\u20acHT \/ mois \u00b7 1 levier 1 levier au choix (Google, Meta ou TikTok) Structure de campagnes et audiences 2 cr\u00e9ations par mois Suivi des conversions install\u00e9 Rapport mensuel comment\u00e9 Choisir Amorce Le plus choisi Formule 02 Traction Pour combiner intention et d\u00e9couverte : capter la demande existante et en cr\u00e9er. 1 190\u20acHT \/ mois \u00b7 2 \u00e0 3 leviers 2 \u00e0 3 leviers combin\u00e9s 5 cr\u00e9ations par mois, tests inclus Retargeting et audiences similaires Optimisation hebdomadaire Point mensuel de 45 minutes Tableau de bord partag\u00e9 en continu Choisir Traction Formule 03 Omnicanal Pour couvrir tout le parcours, du grand public \u00e0 la reprise de contact. 1 990\u20acHT \/ mois \u00b7 tous leviers Tous les leviers, y compris programmatique Display, audio digital et affichage num\u00e9rique Cr\u00e9ations illimit\u00e9es dans le cadre d\u00e9fini Bilan trimestriel et plan m\u00e9dia \u00e0 3 mois Acc\u00e8s prioritaire \u00e0 l&rsquo;\u00e9quipe Choisir Omnicanal Avant de vous engager Audit publicitaire complet Nous reprenons vos comptes existants, v\u00e9rifions le suivi des conversions, la structure, les audiences et les cr\u00e9ations. Vous recevez un rapport \u00e9crit avec les corrections prioritaires \u2014 utilisable m\u00eame si vous ne travaillez pas avec nous ensuite. Le montant est d\u00e9duit de la premi\u00e8re mensualit\u00e9 si vous nous confiez la suite. 750\u20ac HT \u00b7 une seule fois Les leviers Tout ce que nous diffusons pour vous. Nous ne poussons jamais un levier parce qu&rsquo;il est \u00e0 la mode, mais parce qu&rsquo;il correspond \u00e0 votre march\u00e9, votre panier moyen et votre cycle de d\u00e9cision. INTENTIONGoogle Ads \u2014 Search &#038; ShoppingCapter les personnes qui cherchent d\u00e9j\u00e0 ce que vous vendez. Le levier au meilleur retour imm\u00e9diat, \u00e0 condition que la structure et les mots-cl\u00e9s soient tenus. D\u00c9COUVERTEMeta \u2014 Instagram &#038; FacebookCr\u00e9er la demande aupr\u00e8s d&rsquo;audiences qui ne vous connaissent pas encore, puis reprendre contact avec celles qui ont montr\u00e9 de l&rsquo;int\u00e9r\u00eat. ATTENTIONTikTok &#038; ReelsLe format qui donne aujourd&rsquo;hui le plus de visibilit\u00e9 pour un budget donn\u00e9, \u00e0 condition d&rsquo;accepter un registre cr\u00e9atif natif plut\u00f4t qu&rsquo;une publicit\u00e9 classique. B2BLinkedIn AdsCibler par fonction, secteur et taille d&rsquo;entreprise. Co\u00fbt par clic \u00e9lev\u00e9, mais pertinent d\u00e8s que la valeur d&rsquo;un client le justifie. COUVERTUREDisplay &#038; programmatique\u00catre pr\u00e9sent sur les sites que votre audience consulte, avec un ciblage par centres d&rsquo;int\u00e9r\u00eat, zone g\u00e9ographique ou comportement d&rsquo;achat. PHYSIQUEAudio digital &#038; affichage num\u00e9riquePodcasts, radios en ligne, \u00e9crans en points de vente et en rue. Le levier le plus sous-exploit\u00e9 pour asseoir une pr\u00e9sence locale. Comment \u00e7a se passe Sept jours pour reprendre la main. \u00c9TAPE 01 Audit et cadrage Nous ouvrons vos comptes existants, mesurons ce qui fonctionne, identifions les pertes. Puis nous fixons ensemble un co\u00fbt d&rsquo;acquisition cible. JOURS 1 \u00c0 3 \u00c9TAPE 02 Correctifs et structure Suivi des conversions r\u00e9par\u00e9, campagnes restructur\u00e9es, audiences reconstruites. Les premi\u00e8res cr\u00e9ations entrent en production. JOURS 4 \u00c0 7 \u00c9TAPE 03 Lancement et tests Mise en ligne progressive, plusieurs variantes en parall\u00e8le. Nous laissons les plateformes apprendre avant d&rsquo;arbitrer. SEMAINES 2 \u00c0 4 \u00c9TAPE 04 Pilotage continu Chaque semaine : couper, renforcer, renouveler les cr\u00e9ations. Chaque mois : un rapport clair et un point avec vous. EN CONTINU \u00c0 la carte Besoin d&rsquo;un \u00e9l\u00e9ment en plus ? Chaque option s&rsquo;ajoute \u00e0 une formule ou se commande seule. Tarifs HT. Audit publicitaire completRapport \u00e9crit, corrections prioris\u00e9es 750\u20ac Landing page de campagnePage de destination d\u00e9di\u00e9e 750\u20ac Vid\u00e9o motion additionnelle15 secondes, 4 formats 390\u20ac Banni\u00e8res display anim\u00e9esJeu de 5 formats HTML5 450\u20ac Levier suppl\u00e9mentaireAjout \u00e0 une formule en cours 290\u20ac\/mois Suivi &#038;<\/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":"R\u00e9gie Publicitaire D\u00e9l\u00e9gu\u00e9e \u2014 Toutes R\u00e9gies | Exoa","rank_math_description":"R\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e : Google, Meta, TikTok, LinkedIn et programmatique pilot\u00e9s par un seul interlocuteur. Transparence totale. Audit gratuit.","rank_math_focus_keyword":"r\u00e9gie publicitaire d\u00e9l\u00e9gu\u00e9e","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-14789","page","type-page","status-publish","hentry"],"_yoast_wpseo_focuskw":null,"_yoast_wpseo_metadesc":null,"_links":{"self":[{"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages\/14789","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/users\/4"}],"replies":[{"embeddable":true,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/comments?post=14789"}],"version-history":[{"count":8,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages\/14789\/revisions"}],"predecessor-version":[{"id":16644,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages\/14789\/revisions\/16644"}],"wp:attachment":[{"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/media?parent=14789"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}