{"id":14786,"date":"2026-08-01T14:19:27","date_gmt":"2026-08-01T14:19:27","guid":{"rendered":"https:\/\/www.agence-exoa.com\/?page_id=14786"},"modified":"2026-08-03T16:41:12","modified_gmt":"2026-08-03T16:41:12","slug":"conversion-expertise","status":"publish","type":"page","link":"https:\/\/www.agence-exoa.com\/en\/expertise-conversion\/","title":{"rendered":"Expertise conversion \u2014 Transformer votre trafic en clients | 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-expertise-conversion-page *{margin:0;padding:0;box-sizing:border-box}html{scroll-behavior:smooth}.exoa-expertise-conversion-page{font-family:'DM Sans',sans-serif;color:var(--ink);background:#fff;-webkit-font-smoothing:antialiased}.exoa-expertise-conversion-page a{color:var(--lav2);text-decoration:none}.exoa-expertise-conversion-page a:hover{color:var(--violet)}.exoa-expertise-conversion-page .wrap{max-width:1200px;margin:0 auto;padding:0 32px}.exoa-expertise-conversion-page .mono{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700}.exoa-expertise-conversion-page h1, .exoa-expertise-conversion-page h2, .exoa-expertise-conversion-page h3, .exoa-expertise-conversion-page .disp{font-family:'Bricolage Grotesque',sans-serif;font-weight:800;letter-spacing:-.02em;line-height:1.02}.exoa-expertise-conversion-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-expertise-conversion-page .nav-in{max-width:1200px;margin:0 auto;padding:14px 32px;display:flex;align-items:center;justify-content:space-between;gap:20px}.exoa-expertise-conversion-page .logo{display:flex;align-items:center;gap:9px;color:#fff;font-weight:700;font-size:15px;letter-spacing:.02em}.exoa-expertise-conversion-page .logo svg{width:17px;height:17px}.exoa-expertise-conversion-page .nav-mid{display:flex;gap:26px}.exoa-expertise-conversion-page .nav-mid a{color:#C6C9E0;font-size:14px;font-weight:500}.exoa-expertise-conversion-page .nav-mid a:hover{color:#fff}.exoa-expertise-conversion-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-expertise-conversion-page .btn-lime{background:var(--lime);color:var(--navy)}.exoa-expertise-conversion-page .btn-lime:hover{background:#dcff9c;color:var(--navy);transform:translateY(-1px)}.exoa-expertise-conversion-page .btn-ghost{background:rgba(255,255,255,.08);color:#fff;border:1px solid rgba(255,255,255,.2)}.exoa-expertise-conversion-page .btn-ghost:hover{background:rgba(255,255,255,.16);color:#fff}.exoa-expertise-conversion-page .btn-navy{background:var(--navy);color:#fff}.exoa-expertise-conversion-page .btn-navy:hover{background:var(--navy2);color:#fff;transform:translateY(-1px)}.exoa-expertise-conversion-page .btn-out{background:#fff;color:var(--navy);border:1.5px solid var(--line)}.exoa-expertise-conversion-page .btn-out:hover{border-color:var(--lav);color:var(--navy)}.exoa-expertise-conversion-page .hero{background:var(--navy);color:#fff;padding:88px 0 0;position:relative;overflow:hidden}.exoa-expertise-conversion-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-expertise-conversion-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-expertise-conversion-page .hero-in{position:relative;z-index:2}.exoa-expertise-conversion-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-expertise-conversion-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-expertise-conversion-page .pill span{font-family:'JetBrains Mono',monospace;font-size:10.5px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:#E8EAF6}.exoa-expertise-conversion-page .hero h1{font-size:clamp(40px,5.6vw,76px);max-width:15ch}.exoa-expertise-conversion-page .hero h1 em{font-style:normal;color:var(--lime)}.exoa-expertise-conversion-page .hero h1 .v{font-style:normal;color:var(--violet)}.exoa-expertise-conversion-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-expertise-conversion-page .hero-cta{display:flex;gap:12px;flex-wrap:wrap;align-items:center}.exoa-expertise-conversion-page .hero-note{font-size:13px;color:#9498b8;margin-top:18px;display:flex;gap:18px;flex-wrap:wrap}.exoa-expertise-conversion-page .hero-note b{color:#E8EAF6;font-weight:600}.exoa-expertise-conversion-page .reel{margin-top:56px;position:relative;z-index:2}.exoa-expertise-conversion-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-expertise-conversion-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-expertise-conversion-page .reel-bar i{width:9px;height:9px;border-radius:50%;background:rgba(255,255,255,.22)}.exoa-expertise-conversion-page .reel-bar em{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:.1em;color:#9498b8;margin-left:8px}.exoa-expertise-conversion-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-expertise-conversion-page .marq-t{display:inline-flex;gap:44px;animation:sc 26s linear infinite;padding-right:44px}.exoa-expertise-conversion-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-expertise-conversion-page .marq-t span::after{content:'\u2726';opacity:.45}@keyframes sc{to{transform:translateX(-50%)}}.exoa-expertise-conversion-page .sec{padding:104px 0}.exoa-expertise-conversion-page .sec-lt{background:var(--bg)}.exoa-expertise-conversion-page .sec-dk{background:var(--navy);color:#fff}.exoa-expertise-conversion-page .tag{display:inline-flex;align-items:center;gap:8px;margin-bottom:16px}.exoa-expertise-conversion-page .tag i{width:22px;height:2px;background:var(--lav);border-radius:2px}.exoa-expertise-conversion-page .tag span{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.14em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-expertise-conversion-page .sec-dk .tag span{color:var(--lime)}.exoa-expertise-conversion-page .sec-dk .tag i{background:var(--lime)}.exoa-expertise-conversion-page .h2{font-size:clamp(30px,3.6vw,50px);max-width:22ch}.exoa-expertise-conversion-page .h2 em{font-style:normal;color:var(--lav2)}.exoa-expertise-conversion-page .sec-dk .h2 em{color:var(--lime)}.exoa-expertise-conversion-page .lead{font-size:17px;color:var(--body);line-height:1.65;max-width:64ch;margin-top:18px;text-wrap:pretty}.exoa-expertise-conversion-page .sec-dk .lead{color:#C6C9E0}.exoa-expertise-conversion-page .head-row{display:flex;justify-content:space-between;align-items:flex-end;gap:40px;margin-bottom:52px;flex-wrap:wrap}.exoa-expertise-conversion-page .stats{display:grid;grid-template-columns:repeat(3,1fr);gap:20px;margin-top:52px}.exoa-expertise-conversion-page .stat{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:18px;padding:30px}.exoa-expertise-conversion-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-expertise-conversion-page .stat-l{font-size:14.5px;color:#C6C9E0;line-height:1.55;margin-top:12px}.exoa-expertise-conversion-page .bricks{display:grid;grid-template-columns:repeat(3,1fr);gap:22px}.exoa-expertise-conversion-page .brick{background:#fff;border:1px solid var(--line);border-radius:20px;overflow:hidden;display:flex;flex-direction:column;transition:all .22s}.exoa-expertise-conversion-page .brick:hover{border-color:var(--lav);transform:translateY(-3px);box-shadow:0 16px 40px -20px rgba(6,0,57,.22)}.exoa-expertise-conversion-page .brick-vis{width:100%;min-width:0;aspect-ratio:4\/3;background:var(--bg);border-bottom:1px solid var(--line);position:relative}.exoa-expertise-conversion-page .brick>*{min-width:0}.exoa-expertise-conversion-page .brick-b{padding:26px;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-expertise-conversion-page .brick-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.12em;font-weight:700;color:var(--lav2)}.exoa-expertise-conversion-page .brick h3{font-size:21px;letter-spacing:-.01em}.exoa-expertise-conversion-page .brick p{font-size:14.5px;color:var(--body);line-height:1.62}.exoa-expertise-conversion-page .brick ul{list-style:none;display:flex;flex-direction:column;gap:8px;margin-top:4px}.exoa-expertise-conversion-page .brick li{font-size:13.5px;color:var(--body);padding-left:20px;position:relative;line-height:1.5}.exoa-expertise-conversion-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-expertise-conversion-page .packs{display:grid;grid-template-columns:repeat(3,1fr);gap:22px;align-items:start}.exoa-expertise-conversion-page .pack{background:#fff;border:1px solid var(--line);border-radius:22px;padding:32px;display:flex;flex-direction:column;gap:18px;position:relative}.exoa-expertise-conversion-page .pack.hot{background:var(--navy);border-color:var(--navy);color:#fff;box-shadow:0 24px 60px -28px rgba(6,0,57,.5)}.exoa-expertise-conversion-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-expertise-conversion-page .pack-n{font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:.13em;text-transform:uppercase;font-weight:700;color:var(--lav2)}.exoa-expertise-conversion-page .pack.hot .pack-n{color:var(--lime)}.exoa-expertise-conversion-page .pack h3{font-size:25px}.exoa-expertise-conversion-page .pack-for{font-size:13.5px;color:var(--body);line-height:1.55}.exoa-expertise-conversion-page .pack.hot .pack-for{color:#C6C9E0}.exoa-expertise-conversion-page .pack-p{display:flex;align-items:baseline;gap:7px;padding-top:6px;border-top:1px solid var(--line)}.exoa-expertise-conversion-page .pack.hot .pack-p{border-color:rgba(255,255,255,.15)}.exoa-expertise-conversion-page .pack-p b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;letter-spacing:-.03em}.exoa-expertise-conversion-page .pack.hot .pack-p b{color:var(--lime)}.exoa-expertise-conversion-page .pack-p i{font-style:normal;font-size:13px;color:var(--body);font-weight:500}.exoa-expertise-conversion-page .pack.hot .pack-p i{color:#9CA0C4}.exoa-expertise-conversion-page .pack-l{list-style:none;display:flex;flex-direction:column;gap:11px;flex:1}.exoa-expertise-conversion-page .pack-l li{font-size:14px;line-height:1.5;padding-left:24px;position:relative;color:var(--body)}.exoa-expertise-conversion-page .pack.hot .pack-l li{color:#DDE0F0}.exoa-expertise-conversion-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-expertise-conversion-page .pack-l li b{color:var(--ink);font-weight:700}.exoa-expertise-conversion-page .pack.hot .pack-l li b{color:#fff}.exoa-expertise-conversion-page .pack-l li.plus::before{background:var(--violet);box-shadow:none}.exoa-expertise-conversion-page .pack .btn{justify-content:center;width:100%}.exoa-expertise-conversion-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-expertise-conversion-page .sub-l{flex:1;min-width:280px}.exoa-expertise-conversion-page .sub .mono{color:rgba(255,255,255,.75);display:block;margin-bottom:9px}.exoa-expertise-conversion-page .sub h3{font-size:26px;margin-bottom:10px}.exoa-expertise-conversion-page .sub p{font-size:14.5px;color:rgba(255,255,255,.9);line-height:1.6;max-width:58ch}.exoa-expertise-conversion-page .sub-r{text-align:right}.exoa-expertise-conversion-page .sub-r b{font-family:'Bricolage Grotesque',sans-serif;font-size:40px;font-weight:800;display:block;letter-spacing:-.03em}.exoa-expertise-conversion-page .sub-r i{font-style:normal;font-size:13px;color:rgba(255,255,255,.8)}.exoa-expertise-conversion-page .fmts{display:grid;grid-template-columns:repeat(4,1fr);gap:18px;margin-top:12px}.exoa-expertise-conversion-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-expertise-conversion-page .fmt-r{border:1.5px dashed rgba(205,249,128,.5);border-radius:7px;background:rgba(205,249,128,.07)}.exoa-expertise-conversion-page .fmt b{font-size:15px;color:#fff}.exoa-expertise-conversion-page .fmt small{font-size:12.5px;color:#9CA0C4;line-height:1.5;display:block}.exoa-expertise-conversion-page .steps{display:grid;grid-template-columns:repeat(4,1fr);gap:0;margin-top:12px;border-top:1px solid var(--line)}.exoa-expertise-conversion-page .step{padding:30px 26px 30px 0;border-right:1px solid var(--line);position:relative}.exoa-expertise-conversion-page .step:last-child{border-right:0;padding-right:0}.exoa-expertise-conversion-page .step::before{content:'';position:absolute;top:-1px;left:0;width:38%;height:3px;background:var(--lime)}.exoa-expertise-conversion-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-expertise-conversion-page .step h3{font-size:18px;margin-bottom:9px;letter-spacing:-.01em}.exoa-expertise-conversion-page .step p{font-size:14px;color:var(--body);line-height:1.6}.exoa-expertise-conversion-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-expertise-conversion-page .adds{display:grid;grid-template-columns:repeat(auto-fit,minmax(230px,1fr));gap:14px}.exoa-expertise-conversion-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-expertise-conversion-page .add:hover{border-color:var(--lav)}.exoa-expertise-conversion-page .add b{font-size:14.5px;font-weight:600;display:block;margin-bottom:3px}.exoa-expertise-conversion-page .add small{font-size:12.5px;color:var(--body)}.exoa-expertise-conversion-page .add i{font-style:normal;font-family:'JetBrains Mono',monospace;font-size:13px;font-weight:700;color:var(--lav2);white-space:nowrap}.exoa-expertise-conversion-page .gal{display:grid;grid-template-columns:repeat(4,1fr);gap:16px;margin-top:12px}.exoa-expertise-conversion-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-expertise-conversion-page .gal-i.wide{grid-column:span 2;aspect-ratio:16\/10}.exoa-expertise-conversion-page .faq{max-width:840px;margin:0 auto;display:flex;flex-direction:column;gap:10px}.exoa-expertise-conversion-page details{background:#fff;border:1px solid var(--line);border-radius:14px;overflow:hidden;transition:border-color .2s}.exoa-expertise-conversion-page details[open]{border-color:var(--lav)}.exoa-expertise-conversion-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-expertise-conversion-page summary::-webkit-details-marker{display:none}.exoa-expertise-conversion-page summary::after{content:'+';font-family:'Bricolage Grotesque',sans-serif;font-size:22px;color:var(--lav2);flex-shrink:0;line-height:1}.exoa-expertise-conversion-page details[open] summary::after{content:'\u2212'}.exoa-expertise-conversion-page details p{padding:0 24px 22px;font-size:14.5px;color:var(--body);line-height:1.7}.exoa-expertise-conversion-page details p b{color:var(--ink)}.exoa-expertise-conversion-page .fin{background:var(--navy);color:#fff;padding:104px 0;position:relative;overflow:hidden}.exoa-expertise-conversion-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-expertise-conversion-page .fin-in{position:relative;z-index:2;display:grid;grid-template-columns:1fr .9fr;gap:56px;align-items:start}.exoa-expertise-conversion-page .fin h2{font-size:clamp(32px,4vw,54px);max-width:16ch}.exoa-expertise-conversion-page .fin h2 em{font-style:normal;color:var(--lime)}.exoa-expertise-conversion-page .fin p{font-size:16.5px;color:#C6C9E0;line-height:1.65;margin-top:20px;max-width:50ch}.exoa-expertise-conversion-page .fin-tr{display:flex;flex-direction:column;gap:12px;margin-top:30px}.exoa-expertise-conversion-page .fin-tr div{display:flex;gap:11px;align-items:flex-start;font-size:14.5px;color:#DDE0F0;line-height:1.5}.exoa-expertise-conversion-page .fin-tr b{color:var(--lime);flex-shrink:0}.exoa-expertise-conversion-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-expertise-conversion-page .form h3{font-size:20px;margin-bottom:6px}.exoa-expertise-conversion-page .form>p{font-size:13.5px;color:#9CA0C4;margin-bottom:22px}.exoa-expertise-conversion-page .fg{display:grid;grid-template-columns:1fr 1fr;gap:13px}.exoa-expertise-conversion-page .fld{display:flex;flex-direction:column;gap:6px}.exoa-expertise-conversion-page .fld.full{grid-column:1\/-1}.exoa-expertise-conversion-page .fld label{font-size:12.5px;font-weight:600;color:#DDE0F0}.exoa-expertise-conversion-page .fld input, .exoa-expertise-conversion-page .fld select, .exoa-expertise-conversion-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-expertise-conversion-page .fld input::placeholder, .exoa-expertise-conversion-page .fld textarea::placeholder{color:#7C81A6}.exoa-expertise-conversion-page .fld select option{background:var(--navy);color:#fff}.exoa-expertise-conversion-page .fld input:focus, .exoa-expertise-conversion-page .fld select:focus, .exoa-expertise-conversion-page .fld textarea:focus{outline:0;border-color:var(--lime)}.exoa-expertise-conversion-page .chk{display:flex;gap:9px;align-items:flex-start;font-size:12px;color:#9CA0C4;line-height:1.5;margin-top:14px}.exoa-expertise-conversion-page .chk input{margin-top:2px;accent-color:var(--lime)}.exoa-expertise-conversion-page .ok{display:none;text-align:center;padding:36px 12px}.exoa-expertise-conversion-page .ok.on{display:block}.exoa-expertise-conversion-page .ok h3{font-size:23px;color:var(--lime);margin-bottom:10px}.exoa-expertise-conversion-page .ok p{font-size:14.5px;color:#C6C9E0;line-height:1.6}.exoa-expertise-conversion-page .foot{background:#04002B;color:#7C81A6;padding:34px 0;font-size:13px}.exoa-expertise-conversion-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-expertise-conversion-page .foot a{color:#9CA0C4}.exoa-expertise-conversion-page .foot a:hover{color:#fff}@media(max-width:1000px){.exoa-expertise-conversion-page .bricks, .exoa-expertise-conversion-page .packs, .exoa-expertise-conversion-page .stats{grid-template-columns:1fr 1fr}.exoa-expertise-conversion-page .fmts{grid-template-columns:1fr 1fr}.exoa-expertise-conversion-page .steps{grid-template-columns:1fr 1fr}.exoa-expertise-conversion-page .step{border-right:0;border-bottom:1px solid var(--line);padding-right:26px}.exoa-expertise-conversion-page .fin-in{grid-template-columns:1fr}.exoa-expertise-conversion-page .gal{grid-template-columns:1fr 1fr}.exoa-expertise-conversion-page .gal-i.wide{grid-column:span 2}}@media(max-width:700px){.exoa-expertise-conversion-page .wrap{padding:0 20px}.exoa-expertise-conversion-page .nav-mid{display:none}.exoa-expertise-conversion-page .sec{padding:64px 0}.exoa-expertise-conversion-page .bricks, .exoa-expertise-conversion-page .packs, .exoa-expertise-conversion-page .stats, .exoa-expertise-conversion-page .fmts, .exoa-expertise-conversion-page .steps, .exoa-expertise-conversion-page .gal, .exoa-expertise-conversion-page .fg{grid-template-columns:1fr}.exoa-expertise-conversion-page .gal-i.wide{grid-column:span 1}.exoa-expertise-conversion-page .hero{padding-top:56px}.exoa-expertise-conversion-page .hero-cta{flex-direction:column;align-items:stretch}.exoa-expertise-conversion-page .hero-cta .btn{justify-content:center}.exoa-expertise-conversion-page .sub{padding:28px 24px}.exoa-expertise-conversion-page .sub-r{text-align:left}.exoa-expertise-conversion-page .head-row{margin-bottom:36px}.exoa-expertise-conversion-page .foot-in{flex-direction:column;align-items:flex-start}}.exoa-expertise-conversion-page .pack-l li.neu::before{background:var(--lav);box-shadow:none}.exoa-expertise-conversion-page .grid2{display:grid;grid-template-columns:repeat(2,1fr);gap:22px}.exoa-expertise-conversion-page .lev{background:rgba(255,255,255,.045);border:1px solid rgba(255,255,255,.13);border-radius:16px;padding:24px}.exoa-expertise-conversion-page .lev b{font-size:16px;color:#fff;display:block;margin-bottom:7px}.exoa-expertise-conversion-page .lev small{font-size:13.5px;color:#9CA0C4;line-height:1.6;display:block}.exoa-expertise-conversion-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-expertise-conversion-page .cmp{width:100%;border-collapse:collapse;font-size:14.5px;background:#fff;border:1px solid var(--line);border-radius:16px;overflow:hidden}.exoa-expertise-conversion-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-expertise-conversion-page .cmp td{padding:15px 18px;color:var(--body);border-bottom:1px solid var(--line);line-height:1.5}.exoa-expertise-conversion-page .cmp tr:last-child td{border-bottom:0}.exoa-expertise-conversion-page .cmp td:first-child{font-weight:600;color:var(--ink)}.exoa-expertise-conversion-page .cmp .yes{color:var(--lav2);font-weight:600}.exoa-expertise-conversion-page .cmp-w{overflow-x:auto;border-radius:16px}@media(max-width:1000px){.exoa-expertise-conversion-page .grid2{grid-template-columns:1fr}}\n.exoa-expertise-conversion-page{margin-top:0 !important;padding-top:0 !important}\n.exoa-expertise-conversion-page .btn-primary,.exoa-expertise-conversion-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-expertise-conversion-page .resx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:8px}\n.exoa-expertise-conversion-page .resx-card{background:#fff;border:1px solid rgba(6,0,57,.1);border-radius:16px;padding:30px 26px}\n.exoa-expertise-conversion-page .resx-num{font-size:34px;font-weight:800;color:#060039;line-height:1.1}\n.exoa-expertise-conversion-page .resx-card h3{font-size:17px;font-weight:700;color:#060039;margin:6px 0 8px}\n.exoa-expertise-conversion-page .resx-card p{font-size:14px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-expertise-conversion-page .livr-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-expertise-conversion-page .livr-card{background:#F7F7FB;border:1px solid rgba(6,0,57,.08);border-radius:14px;padding:24px 22px}\n.exoa-expertise-conversion-page .livr-card h3{font-size:15.5px;font-weight:700;color:#060039;margin:0 0 6px}\n.exoa-expertise-conversion-page .livr-card h3 span{color:#8CC63F;font-weight:800;margin-right:6px}\n.exoa-expertise-conversion-page .livr-card p{font-size:13.5px;color:#5A5E80;line-height:1.6;margin:0}\n.exoa-expertise-conversion-page .expx-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:8px}\n.exoa-expertise-conversion-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-expertise-conversion-page .expx-card:hover{background:rgba(255,255,255,.1);border-color:rgba(205,249,128,.5)}\n.exoa-expertise-conversion-page .expx-card strong{display:block;color:#fff;font-size:15.5px;font-weight:700;margin-bottom:4px}\n.exoa-expertise-conversion-page .expx-card span{display:block;color:#C6C9E0;font-size:13px;line-height:1.5}\n.exoa-expertise-conversion-page .expx-card em{display:inline-block;margin-top:10px;font-style:normal;color:#CDF980;font-size:12px;font-weight:700}\n.exoa-expertise-conversion-page .hero h1{color:#fff !important}\n@media(max-width:900px){.exoa-expertise-conversion-page .resx-grid,.exoa-expertise-conversion-page .livr-grid,.exoa-expertise-conversion-page .expx-grid{grid-template-columns:1fr 1fr}}\n@media(max-width:600px){.exoa-expertise-conversion-page .resx-grid,.exoa-expertise-conversion-page .livr-grid,.exoa-expertise-conversion-page .expx-grid{grid-template-columns:1fr}}\n\/*exoa-landing-fix*\/\n.exoa-expertise-conversion-page .sec-dk h2,.exoa-expertise-conversion-page .sec-dk .h2,.exoa-expertise-conversion-page .sec-dk h3,.exoa-expertise-conversion-page .fin h2,.exoa-expertise-conversion-page .fin h3{color:#fff !important}\n.exoa-expertise-conversion-page .sec-dk .h2 em,.exoa-expertise-conversion-page .fin h2 em{color:#CDF980 !important}\n.exoa-expertise-conversion-page .sec:not(.sec-dk) h2,.exoa-expertise-conversion-page .sec:not(.sec-dk) .h2{color:#060039}\n.exoa-expertise-conversion-page .steps{border-top:0 !important;gap:16px !important}\n.exoa-expertise-conversion-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-expertise-conversion-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-expertise-conversion-page .reel{display:none !important}\n  .exoa-expertise-conversion-page .hero{padding-bottom:40px !important}\n  .exoa-expertise-conversion-page .hero-note{margin-bottom:0 !important}\n}\n\/*exoa-width-fix*\/\n.exoa-expertise-conversion-page .hero h1{max-width:24ch !important}\n.exoa-expertise-conversion-page .h2{max-width:38ch !important}\n.exoa-expertise-conversion-page .fin h2{max-width:28ch !important}\n.exoa-expertise-conversion-page .sec .wrap > p,.exoa-expertise-conversion-page .h2 + p,.exoa-expertise-conversion-page .tag ~ p,.exoa-expertise-conversion-page .fin p,.exoa-expertise-conversion-page .sub p{max-width:76ch !important}\n.exoa-expertise-conversion-page .hero-sub{max-width:70ch !important}\n\/*exoa-pack-align*\/\n.exoa-expertise-conversion-page .packs{align-items:stretch !important}\n.exoa-expertise-conversion-page .pack{height:100%}\n.exoa-expertise-conversion-page .pack > p,.exoa-expertise-conversion-page .pack .pack-for{min-height:3.4em}\n.exoa-expertise-conversion-page .pack ul,.exoa-expertise-conversion-page .pack .pack-l{flex:1}\n.exoa-expertise-conversion-page .pack .btn,.exoa-expertise-conversion-page .pack a[class*=\"btn\"]{margin-top:auto}\n@media(max-width:900px){.exoa-expertise-conversion-page .pack > p,.exoa-expertise-conversion-page .pack .pack-for{min-height:0}}\n<\/style>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"Service\",\"name\":\"Expertise conversion \u2014 Exoa\",\"description\":\"Optimisation de la conversion : pages, tunnel, formulaires, relances, suivi. Faire mieux avec le trafic que vous avez d\u00e9j\u00e0. Diagnostic d\u00e8s 1 750\u20ac, suivi d\u00e8s 1 490\u20ac\/mois.\",\"provider\":{\"@type\":\"Organization\",\"name\":\"Exoa\",\"url\":\"https:\/\/www.agence-exoa.com\"},\"areaServed\":[{\"@type\":\"Country\",\"name\":\"France\"},{\"@type\":\"Country\",\"name\":\"Luxembourg\"}],\"serviceType\":\"Optimisation de la conversion\",\"offers\":[{\"@type\":\"Offer\",\"name\":\"Diagnostic\",\"price\":\"1750\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Optimisation\",\"price\":\"1490\",\"priceCurrency\":\"EUR\"},{\"@type\":\"Offer\",\"name\":\"Tunnel complet\",\"price\":\"2900\",\"priceCurrency\":\"EUR\"}]}\n<\/script>\n<script type=\"application\/ld+json\">\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"FAQPage\",\"mainEntity\":[{\"@type\":\"Question\",\"name\":\"Faut-il refaire tout mon site ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Presque jamais. Quelques pages et le tunnel suffisent g\u00e9n\u00e9ralement \u00e0 produire l'essentiel du gain.\"}},{\"@type\":\"Question\",\"name\":\"Combien de trafic faut-il pour que ce soit utile ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"En dessous de 1 000 visiteurs par mois, les tests A\/B ne sont pas exploitables : nous travaillons sur les bonnes pratiques. Au-del\u00e0, les tests deviennent pertinents.\"}},{\"@type\":\"Question\",\"name\":\"Quel gain peut-on esp\u00e9rer ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Le gain d\u00e9pend du point de d\u00e9part. Un suivi r\u00e9par\u00e9 et un formulaire simplifi\u00e9 produisent presque toujours un effet mesurable d\u00e8s le premier mois.\"}},{\"@type\":\"Question\",\"name\":\"Intervenez-vous directement sur mon site ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Oui, si vous nous donnez l'acc\u00e8s. Sinon nous livrons des sp\u00e9cifications pr\u00e9cises \u00e0 votre d\u00e9veloppeur.\"}},{\"@type\":\"Question\",\"name\":\"Est-ce compatible avec mes campagnes en cours ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Oui. Am\u00e9liorer la conversion fait baisser le co\u00fbt par acquisition de toutes vos campagnes existantes.\"}},{\"@type\":\"Question\",\"name\":\"Le diagnostic est-il utilisable sans vous ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Oui, c'est un document autonome et actionnable. Le montant est d\u00e9duit de la premi\u00e8re mensualit\u00e9 si vous poursuivez avec nous.\"}},{\"@type\":\"Question\",\"name\":\"Combien de temps avant de voir un effet ?\",\"acceptedAnswer\":{\"@type\":\"Answer\",\"text\":\"Les correctifs de friction produisent un effet en quelques semaines. Comptez trois mois pour une progression nette et durable.\"}}]}\n<\/script>\n<div class=\"exoa-expertise-conversion-page\">\n<header class=\"hero\"><div class=\"wrap hero-in\">\n<div class=\"pill\"><span class=\"dot\"><\/span><span>Pages \u00b7 Tunnel \u00b7 Relances \u00b7 Mesure<\/span><\/div>\n<h1>Vous n&rsquo;avez pas un probl\u00e8me de trafic. Vous avez un probl\u00e8me de <em>tunnel<\/em>.<\/h1>\n<p class=\"hero-sub\">Doubler votre trafic co\u00fbte cher. Doubler votre taux de conversion co\u00fbte bien moins, et produit exactement le m\u00eame r\u00e9sultat. Nous travaillons sur ce que vous avez d\u00e9j\u00e0 : les pages, les formulaires, les relances et la mesure.<\/p>\n<div class=\"hero-cta\">\n<a href=\"#packs\" class=\"btn btn-lime\">Voir les formules \u2014 d\u00e8s 1 490\u20ac\/mois \u2192<\/a>\n<a href=\"#devis\" class=\"btn btn-ghost\">Faire diagnostiquer mon tunnel<\/a>\n<\/div>\n<div class=\"hero-note\"><span><b>Diagnostic<\/b> en 2 semaines<\/span><span><b>Sur le trafic<\/b> que vous avez d\u00e9j\u00e0<\/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>TUNNEL DE CONVERSION \u2014 \u00c9TAPES ET FUITES<\/em><\/div>\n<image-slot id=\"cnv-reel\" shape=\"rect\" fit=\"cover\" placeholder=\"Capture de votre tunnel ou de vos pages\"><\/image-slot>\n<\/div>\n<\/div>\n<\/div><\/header>\n\n<div class=\"marq\"><div class=\"marq-t\">\n<span>Pages de destination<\/span><span>Formulaires<\/span><span>Tunnel de commande<\/span><span>Relances panier<\/span><span>Preuve sociale<\/span><span>Tests A\/B<\/span><span>Suivi des conversions<\/span><span>Automatisation<\/span>\n<span>Pages de destination<\/span><span>Formulaires<\/span><span>Tunnel de commande<\/span><span>Relances panier<\/span><span>Preuve sociale<\/span><span>Tests A\/B<\/span><span>Suivi des conversions<\/span><span>Automatisation<\/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\">Chaque point de conversion gagn\u00e9 vaut <em>des mois de trafic<\/em>.<\/h2>\n<p class=\"lead\">Passer de 1 % \u00e0 2 % de conversion revient \u00e0 doubler votre trafic \u2014 sans d\u00e9penser un euro de plus en publicit\u00e9. Pourtant la plupart des budgets partent dans l&rsquo;acquisition, sur des pages qui perdent l&rsquo;essentiel des visiteurs en cours de route.<\/p>\n<div class=\"stats\">\n<div class=\"stat\"><div class=\"stat-n\">\u00d7 2<\/div><div class=\"stat-l\">L&rsquo;effet d&rsquo;un taux de conversion doubl\u00e9 : exactement le m\u00eame r\u00e9sultat qu&rsquo;un budget publicitaire doubl\u00e9, pour une fraction du co\u00fbt.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">7 sur 10<\/div><div class=\"stat-l\">La proportion de paniers abandonn\u00e9s en ligne \u2014 et la plupart du temps, aucune relance n&rsquo;est d\u00e9clench\u00e9e.<\/div><\/div>\n<div class=\"stat\"><div class=\"stat-n\">1 champ<\/div><div class=\"stat-l\">Chaque champ retir\u00e9 d&rsquo;un formulaire augmente mesurablement le nombre de personnes qui le terminent.<\/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 chantier<\/span><\/div>\n<h2 class=\"h2\">Trois zones de <em>d\u00e9perdition<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">Nous ne redessinons pas votre site par go\u00fbt : nous corrigeons les endroits pr\u00e9cis o\u00f9 les visiteurs abandonnent.<\/p>\n<\/div>\n<div class=\"bricks\">\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"cnv-b1\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel pages\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 01<\/span>\n<h3>Pages &#038; messages<\/h3>\n<p>La premi\u00e8re seconde d\u00e9cide de tout : ce que vous proposez doit \u00eatre compris sans effort, et la raison de vous choisir imm\u00e9diatement visible.<\/p>\n<ul><li>R\u00e9\u00e9criture des accroches et des arguments<\/li><li>Structure des pages et hi\u00e9rarchie visuelle<\/li><li>Preuves : avis, r\u00e9sultats, r\u00e9f\u00e9rences<\/li><li>Pages de destination d\u00e9di\u00e9es par campagne<\/li><\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"cnv-b2\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel tunnel\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 02<\/span>\n<h3>Tunnel &#038; formulaires<\/h3>\n<p>Chaque \u00e9tape superflue, chaque champ inutile, chaque h\u00e9sitation non lev\u00e9e fait perdre des clients d\u00e9j\u00e0 convaincus.<\/p>\n<ul><li>Simplification des \u00e9tapes et des champs<\/li><li>R\u00e9assurance aux moments d&rsquo;h\u00e9sitation<\/li><li>Tunnel de commande ou de prise de contact<\/li><li>Version mobile trait\u00e9e en priorit\u00e9<\/li><\/ul>\n<\/div>\n<\/article>\n<article class=\"brick\">\n<div class=\"brick-vis\"><image-slot id=\"cnv-b3\" shape=\"rect\" fit=\"cover\" placeholder=\"Visuel relances\"><\/image-slot><\/div>\n<div class=\"brick-b\">\n<span class=\"brick-n\">BRIQUE 03<\/span>\n<h3>Relances &#038; mesure<\/h3>\n<p>La majorit\u00e9 des visiteurs ne convertissent pas au premier passage. Ce qui compte, c&rsquo;est ce que vous d\u00e9clenchez ensuite.<\/p>\n<ul><li>Relances panier et devis abandonn\u00e9s<\/li><li>S\u00e9quences de reprise de contact<\/li><li>Suivi des conversions fiabilis\u00e9<\/li><li>Tests A\/B et d\u00e9cisions document\u00e9es<\/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\">Un diagnostic, <em>puis un suivi<\/em>.<\/h2>\n<\/div>\n<p class=\"lead\" style=\"margin:0;max-width:38ch\">On commence toujours par mesurer. Ensuite seulement, on d\u00e9cide s&rsquo;il faut un chantier ponctuel ou un suivi continu.<\/p>\n<\/div>\n<div class=\"packs\">\n<div class=\"pack\">\n\n<span class=\"pack-n\">\u00c9tape 01<\/span>\n<h3>Diagnostic<\/h3>\n<p class=\"pack-for\">Pour savoir pr\u00e9cis\u00e9ment o\u00f9 et pourquoi vous perdez des visiteurs.<\/p>\n<div class=\"pack-p\"><b>1 750\u20ac<\/b><i>HT \u00b7 une seule fois<\/i><\/div>\n<ul class=\"pack-l\"><li><b>Analyse chiffr\u00e9e<\/b> du tunnel, \u00e9tape par \u00e9tape<\/li><li>Enregistrements de sessions et cartes de clics<\/li><li><b>Liste prioris\u00e9e<\/b> des correctifs, par impact<\/li><li>V\u00e9rification du suivi des conversions<\/li><li>Restitution d&rsquo;1 heure avec votre \u00e9quipe<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Diagnostic<\/a>\n<\/div>\n<div class=\"pack hot\">\n<span class=\"pack-bdg\">Le plus choisi<\/span>\n<span class=\"pack-n\">\u00c9tape 02<\/span>\n<h3>Optimisation<\/h3>\n<p class=\"pack-for\">Pour corriger en continu et faire progresser le taux mois apr\u00e8s mois.<\/p>\n<div class=\"pack-p\"><b>1 490\u20ac<\/b><i>HT \/ mois<\/i><\/div>\n<ul class=\"pack-l\"><li><b>Tout le diagnostic<\/b> la premi\u00e8re fois<\/li><li><b>2 chantiers<\/b> de correction par mois<\/li><li><b>1 test A\/B<\/b> men\u00e9 et document\u00e9 par mois<\/li><li>Relances panier et s\u00e9quences de reprise<\/li><li>Tableau de bord de conversion partag\u00e9<\/li><li>Point mensuel de 45 minutes<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-lime\">Choisir Optimisation<\/a>\n<\/div>\n<div class=\"pack\">\n\n<span class=\"pack-n\">\u00c9tape 03<\/span>\n<h3>Tunnel complet<\/h3>\n<p class=\"pack-for\">Pour refondre enti\u00e8rement un parcours qui ne fonctionne plus.<\/p>\n<div class=\"pack-p\"><b>2 900\u20ac<\/b><i>HT \u00b7 projet<\/i><\/div>\n<ul class=\"pack-l\"><li><b>Refonte du parcours<\/b> de bout en bout<\/li><li class=\"plus\"><b>Pages de destination<\/b> con\u00e7ues et int\u00e9gr\u00e9es<\/li><li class=\"plus\"><b>Tunnel<\/b> de commande ou de contact reconstruit<\/li><li class=\"plus\"><b>S\u00e9quences d&rsquo;e-mails<\/b> compl\u00e8tes install\u00e9es<\/li><li class=\"plus\">Suivi des conversions et tableaux de bord<\/li><li class=\"plus\">30 jours de r\u00e9glages apr\u00e8s mise en ligne<\/li><\/ul>\n<a href=\"#devis\" class=\"btn btn-out\">Choisir Tunnel complet<\/a>\n<\/div>\n<\/div>\n\n<div class=\"sub\">\n<div class=\"sub-l\">\n<span class=\"mono\">\u00c0 savoir<\/span>\n<h3>Suivi &#038; conformit\u00e9 de la mesure<\/h3>\n<p>Impossible d&rsquo;optimiser ce qui est mal mesur\u00e9. Nous installons ou r\u00e9parons le suivi des conversions, le gestionnaire de balises, la gestion du consentement et les tableaux de bord \u2014 puis nous v\u00e9rifions chaque \u00e9v\u00e9nement un par un. Inclus dans la formule Optimisation, disponible seul.<\/p>\n<\/div>\n<div class=\"sub-r\">\n<b>850\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 points de fuite<\/span><\/div>\n<h2 class=\"h2\">L\u00e0 o\u00f9 l&rsquo;argent <em>se perd<\/em>.<\/h2>\n<p class=\"lead\">Six endroits que nous examinons syst\u00e9matiquement, dans cet ordre \u2014 du plus rentable \u00e0 corriger au plus co\u00fbteux.<\/p>\n<div class=\"grid2\" style=\"margin-top:44px\">\n<div class=\"lev\"><span class=\"lev-t\">MESURE<\/span><b>Suivi des conversions cass\u00e9<\/b><small>Le plus fr\u00e9quent et le plus grave : des \u00e9v\u00e9nements dupliqu\u00e9s ou manquants faussent toutes les d\u00e9cisions, y compris celles des plateformes publicitaires.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">CLART\u00c9<\/span><b>Proposition illisible<\/b><small>Le visiteur ne comprend pas en trois secondes ce que vous vendez ni pourquoi vous. Aucun budget publicitaire ne compense ce d\u00e9faut.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">FRICTION<\/span><b>Formulaires trop longs<\/b><small>Chaque champ non indispensable \u00e9carte des personnes d\u00e9j\u00e0 d\u00e9cid\u00e9es. Le plus grand gain pour le moins d&rsquo;effort.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">CONFIANCE<\/span><b>Absence de preuve<\/b><small>Pas d&rsquo;avis, pas de r\u00e9sultats, pas de r\u00e9f\u00e9rences : le visiteur h\u00e9site au moment pr\u00e9cis o\u00f9 il fallait le rassurer.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">MOBILE<\/span><b>Parcours mobile n\u00e9glig\u00e9<\/b><small>La majorit\u00e9 du trafic vient du mobile, la majorit\u00e9 des tests se font sur ordinateur. L&rsquo;\u00e9cart se paie en conversions perdues.<\/small><\/div>\n<div class=\"lev\"><span class=\"lev-t\">RELANCE<\/span><b>Aucune reprise de contact<\/b><small>Panier abandonn\u00e9, devis non sign\u00e9, formulaire commenc\u00e9 : sans relance automatique, ces occasions disparaissent d\u00e9finitivement.<\/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\">Deux semaines pour <em>savoir o\u00f9 \u00e7a fuit<\/em>.<\/h2>\n<div class=\"steps\">\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 01<\/span>\n<h3>Mesure<\/h3>\n<p>Nous v\u00e9rifions d&rsquo;abord que les chiffres sont justes. Un tunnel analys\u00e9 sur des donn\u00e9es fausses ne m\u00e8ne nulle part.<\/p>\n<em>JOURS 1 \u00c0 4<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 02<\/span>\n<h3>Analyse<\/h3>\n<p>\u00c9tape par \u00e9tape : o\u00f9 les visiteurs partent, sur quel appareil, \u00e0 quel moment. Enregistrements de sessions et cartes de clics \u00e0 l&rsquo;appui.<\/p>\n<em>JOURS 5 \u00c0 10<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 03<\/span>\n<h3>Priorisation<\/h3>\n<p>Chaque correctif est class\u00e9 par impact estim\u00e9 et par effort. Vous savez quoi faire en premier, et ce que \u00e7a devrait rapporter.<\/p>\n<em>JOURS 11 \u00c0 14<\/em>\n<\/div>\n<div class=\"step\">\n<span class=\"step-n\">\u00c9TAPE 04<\/span>\n<h3>Correction<\/h3>\n<p>Nous ex\u00e9cutons, puis nous mesurons l&rsquo;effet r\u00e9el. Un test par mois, une d\u00e9cision document\u00e9e \u00e0 chaque fois.<\/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>chantier pr\u00e9cis<\/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>Diagnostic de conversion<\/b><small>Analyse compl\u00e8te et plan prioris\u00e9<\/small><\/div><i>1 750\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Landing page d\u00e9di\u00e9e<\/b><small>Con\u00e7ue, r\u00e9dig\u00e9e, int\u00e9gr\u00e9e<\/small><\/div><i>750\u20ac<\/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>S\u00e9quences d&rsquo;e-mails<\/b><small>Relances panier ou devis<\/small><\/div><i>d\u00e8s 690\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Refonte d&rsquo;un formulaire<\/b><small>Simplification et int\u00e9gration<\/small><\/div><i>450\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Preuve sociale<\/b><small>Collecte et mise en sc\u00e8ne des avis<\/small><\/div><i>590\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Test A\/B additionnel<\/b><small>Men\u00e9 et document\u00e9<\/small><\/div><i>390\u20ac<\/i><\/div>\n<div class=\"add\"><div><b>Optimisation mobile<\/b><small>Parcours mobile repris de bout en bout<\/small><\/div><i>890\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>Faut-il refaire tout mon site ?<\/summary><p>Presque jamais. Dans la grande majorit\u00e9 des cas, <b>quelques pages et le tunnel suffisent<\/b> \u00e0 produire l&rsquo;essentiel du gain. La refonte compl\u00e8te n&rsquo;arrive qu&rsquo;en dernier recours, et nous le dirons franchement si c&rsquo;est le cas.<\/p><\/details>\n<details><summary>Combien de trafic faut-il pour que ce soit utile ?<\/summary><p>En dessous de <b>1 000 visiteurs par mois<\/b>, les tests A\/B ne sont pas statistiquement exploitables \u2014 nous travaillons alors sur les bonnes pratiques et la clart\u00e9, ce qui reste tr\u00e8s rentable. Au-del\u00e0, les tests deviennent pertinents.<\/p><\/details>\n<details><summary>Quel gain peut-on esp\u00e9rer ?<\/summary><p>Nous ne promettons pas de chiffre : cela d\u00e9pend de votre point de d\u00e9part. En revanche, un suivi de conversions r\u00e9par\u00e9 et un formulaire simplifi\u00e9 produisent <b>presque toujours<\/b> un effet mesurable d\u00e8s le premier mois.<\/p><\/details>\n<details><summary>Intervenez-vous directement sur mon site ?<\/summary><p>Oui, si vous nous en donnez l&rsquo;acc\u00e8s \u2014 WordPress, Shopify, Framer, Webflow, PrestaShop, Magento et la plupart des solutions courantes. Sinon, nous livrons des <b>sp\u00e9cifications pr\u00e9cises<\/b> \u00e0 votre d\u00e9veloppeur.<\/p><\/details>\n<details><summary>Est-ce compatible avec mes campagnes en cours ?<\/summary><p>C&rsquo;est m\u00eame l&rsquo;ordre logique : am\u00e9liorer la conversion <b>fait baisser le co\u00fbt par acquisition<\/b> de toutes vos campagnes existantes, sans y toucher. Les deux chantiers se renforcent.<\/p><\/details>\n<details><summary>Le diagnostic est-il utilisable sans vous ?<\/summary><p>Enti\u00e8rement. C&rsquo;est un document <b>autonome et actionnable<\/b> : votre \u00e9quipe ou un autre prestataire peut l&rsquo;ex\u00e9cuter. Si vous nous confiez la suite, le montant est d\u00e9duit de la premi\u00e8re mensualit\u00e9.<\/p><\/details>\n<details><summary>Combien de temps avant de voir un effet ?<\/summary><p>Les correctifs de friction produisent un effet en <b>quelques semaines<\/b>. Un test A\/B demande <b>deux \u00e0 quatre semaines<\/b> pour \u00eatre concluant. Comptez trois mois pour une progression nette et durable.<\/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\">+35%<\/div><h3>de taux de conversion<\/h3><p>Gain moyen constat\u00e9 sur les sites que nous optimisons en 6 mois.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">x2<\/div><h3>de valeur par visiteur<\/h3><p>Doublement moyen du revenu g\u00e9n\u00e9r\u00e9 par visiteur via CRO et parcours.<\/p><\/div><div class=\"resx-card\"><div class=\"resx-num\">A\/B<\/div><h3>d\u00e9cisions par les donn\u00e9es<\/h3><p>Chaque changement test\u00e9 et mesur\u00e9 avant d&rsquo;\u00eatre g\u00e9n\u00e9ralis\u00e9.<\/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 conversion s&rsquo;appuie sur des livrables tangibles.<\/p><\/div>\n<div class=\"livr-grid\"><div class=\"livr-card\"><h3><span>\u2713<\/span>Audit CRO complet<\/h3><p>Analytics, heatmaps, sessions : o\u00f9 et pourquoi vos visiteurs abandonnent.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Plan de tests prioris\u00e9<\/h3><p>Les hypoth\u00e8ses class\u00e9es par impact attendu et facilit\u00e9 de mise en \u0153uvre.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Optimisation des pages cl\u00e9s<\/h3><p>Home, fiches, tunnel, formulaires : les pages qui p\u00e8sent sur le CA.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>A\/B testing<\/h3><p>Mise en place, suivi et analyse statistique des tests.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>UX mobile<\/h3><p>La majorit\u00e9 de votre trafic m\u00e9rite mieux qu&rsquo;une version r\u00e9duite du desktop.<\/p><\/div><div class=\"livr-card\"><h3><span>\u2713<\/span>Mesure continue<\/h3><p>Taux de conversion, panier, valeur par visiteur : le tableau de bord CRO.<\/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-strategique\/\"><strong>Audit strat\u00e9gique<\/strong><span>\u00c9tat des lieux complet de votre acquisition.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/agence-seo\/\"><strong>SEO<\/strong><span>Le canal au meilleur ROI long terme.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/agence-sea\/\"><strong>SEA &#038; Google Ads<\/strong><span>Des r\u00e9sultats imm\u00e9diats et pilot\u00e9s.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/generation-de-leads\/\"><strong>G\u00e9n\u00e9ration de leads<\/strong><span>Un flux de prospects qualifi\u00e9s et mesurable.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/automatisation-marketing\/\"><strong>Automatisation<\/strong><span>Nurturing, scoring et sc\u00e9narios qui travaillent 24h\/24.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><a class=\"expx-card\" href=\"\/strategie-360\/\"><strong>Strat\u00e9gie 360<\/strong><span>L&rsquo;orchestration compl\u00e8te de vos canaux.<\/span><em>D\u00c9COUVRIR \u2192<\/em><\/a><\/div><\/div><\/section>\n<section class=\"fin\" id=\"devis\">\n<div class=\"wrap fin-in\">\n<div>\n<div class=\"tag\"><i><\/i><span>D\u00e9marrer<\/span><\/div>\n<h2>M\u00eames visiteurs. <em>Deux fois plus de clients.<\/em><\/h2>\n<p>Envoyez-nous l&rsquo;adresse de votre site et vos chiffres actuels : nous revenons sous vingt-quatre heures avec les fuites que nous voyons d\u00e9j\u00e0 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>Diagnostic autonome et actionnable<\/span><\/div>\n<div><b>\u2713<\/b><span>Nous mesurons avant de recommander<\/span><\/div>\n<div><b>\u2713<\/b><span>Intervention directe ou sp\u00e9cifications<\/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=Conversion\" 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 Expertise conversion \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>Pages \u00b7 Tunnel \u00b7 Relances \u00b7 Mesure Vous n&rsquo;avez pas un probl\u00e8me de trafic. Vous avez un probl\u00e8me de tunnel. Doubler votre trafic co\u00fbte cher. Doubler votre taux de conversion co\u00fbte bien moins, et produit exactement le m\u00eame r\u00e9sultat. Nous travaillons sur ce que vous avez d\u00e9j\u00e0 : les pages, les formulaires, les relances et la mesure. Voir les formules \u2014 d\u00e8s 1 490\u20ac\/mois \u2192 Faire diagnostiquer mon tunnel Diagnostic en 2 semainesSur le trafic que vous avez d\u00e9j\u00e0Sans engagement de dur\u00e9e TUNNEL DE CONVERSION \u2014 \u00c9TAPES ET FUITES Pages de destinationFormulairesTunnel de commandeRelances panierPreuve socialeTests A\/BSuivi des conversionsAutomatisation Pages de destinationFormulairesTunnel de commandeRelances panierPreuve socialeTests A\/BSuivi des conversionsAutomatisation Le constat Chaque point de conversion gagn\u00e9 vaut des mois de trafic. Passer de 1 % \u00e0 2 % de conversion revient \u00e0 doubler votre trafic \u2014 sans d\u00e9penser un euro de plus en publicit\u00e9. Pourtant la plupart des budgets partent dans l&rsquo;acquisition, sur des pages qui perdent l&rsquo;essentiel des visiteurs en cours de route. \u00d7 2 L&rsquo;effet d&rsquo;un taux de conversion doubl\u00e9 : exactement le m\u00eame r\u00e9sultat qu&rsquo;un budget publicitaire doubl\u00e9, pour une fraction du co\u00fbt. 7 sur 10 La proportion de paniers abandonn\u00e9s en ligne \u2014 et la plupart du temps, aucune relance n&rsquo;est d\u00e9clench\u00e9e. 1 champ Chaque champ retir\u00e9 d&rsquo;un formulaire augmente mesurablement le nombre de personnes qui le terminent. Le chantier Trois zones de d\u00e9perdition. Nous ne redessinons pas votre site par go\u00fbt : nous corrigeons les endroits pr\u00e9cis o\u00f9 les visiteurs abandonnent. BRIQUE 01 Pages &#038; messages La premi\u00e8re seconde d\u00e9cide de tout : ce que vous proposez doit \u00eatre compris sans effort, et la raison de vous choisir imm\u00e9diatement visible. R\u00e9\u00e9criture des accroches et des arguments Structure des pages et hi\u00e9rarchie visuelle Preuves : avis, r\u00e9sultats, r\u00e9f\u00e9rences Pages de destination d\u00e9di\u00e9es par campagne BRIQUE 02 Tunnel &#038; formulaires Chaque \u00e9tape superflue, chaque champ inutile, chaque h\u00e9sitation non lev\u00e9e fait perdre des clients d\u00e9j\u00e0 convaincus. Simplification des \u00e9tapes et des champs R\u00e9assurance aux moments d&rsquo;h\u00e9sitation Tunnel de commande ou de prise de contact Version mobile trait\u00e9e en priorit\u00e9 BRIQUE 03 Relances &#038; mesure La majorit\u00e9 des visiteurs ne convertissent pas au premier passage. Ce qui compte, c&rsquo;est ce que vous d\u00e9clenchez ensuite. Relances panier et devis abandonn\u00e9s S\u00e9quences de reprise de contact Suivi des conversions fiabilis\u00e9 Tests A\/B et d\u00e9cisions document\u00e9es Les formules Un diagnostic, puis un suivi. On commence toujours par mesurer. Ensuite seulement, on d\u00e9cide s&rsquo;il faut un chantier ponctuel ou un suivi continu. \u00c9tape 01 Diagnostic Pour savoir pr\u00e9cis\u00e9ment o\u00f9 et pourquoi vous perdez des visiteurs. 1 750\u20acHT \u00b7 une seule fois Analyse chiffr\u00e9e du tunnel, \u00e9tape par \u00e9tape Enregistrements de sessions et cartes de clics Liste prioris\u00e9e des correctifs, par impact V\u00e9rification du suivi des conversions Restitution d&rsquo;1 heure avec votre \u00e9quipe Choisir Diagnostic Le plus choisi \u00c9tape 02 Optimisation Pour corriger en continu et faire progresser le taux mois apr\u00e8s mois. 1 490\u20acHT \/ mois Tout le diagnostic la premi\u00e8re fois 2 chantiers de correction par mois 1 test A\/B men\u00e9 et document\u00e9 par mois Relances panier et s\u00e9quences de reprise Tableau de bord de conversion partag\u00e9 Point mensuel de 45 minutes Choisir Optimisation \u00c9tape 03 Tunnel complet Pour refondre enti\u00e8rement un parcours qui ne fonctionne plus. 2 900\u20acHT \u00b7 projet Refonte du parcours de bout en bout Pages de destination con\u00e7ues et int\u00e9gr\u00e9es Tunnel de commande ou de contact reconstruit S\u00e9quences d&rsquo;e-mails compl\u00e8tes install\u00e9es Suivi des conversions et tableaux de bord 30 jours de r\u00e9glages apr\u00e8s mise en ligne Choisir Tunnel complet \u00c0 savoir Suivi &#038; conformit\u00e9 de la mesure Impossible d&rsquo;optimiser ce qui est mal mesur\u00e9. Nous installons ou r\u00e9parons le suivi des conversions, le gestionnaire de balises, la gestion du consentement et les tableaux de bord \u2014 puis nous v\u00e9rifions chaque \u00e9v\u00e9nement un par un. Inclus dans la formule Optimisation, disponible seul. 850\u20ac HT \u00b7 une seule fois Les points de fuite L\u00e0 o\u00f9 l&rsquo;argent se perd. Six endroits que nous examinons syst\u00e9matiquement, dans cet ordre \u2014 du plus rentable \u00e0 corriger au plus co\u00fbteux. MESURESuivi des conversions cass\u00e9Le plus fr\u00e9quent et le plus grave : des \u00e9v\u00e9nements dupliqu\u00e9s ou manquants faussent toutes les d\u00e9cisions, y compris celles des plateformes publicitaires. CLART\u00c9Proposition illisibleLe visiteur ne comprend pas en trois secondes ce que vous vendez ni pourquoi vous. Aucun budget publicitaire ne compense ce d\u00e9faut. FRICTIONFormulaires trop longsChaque champ non indispensable \u00e9carte des personnes d\u00e9j\u00e0 d\u00e9cid\u00e9es. Le plus grand gain pour le moins d&rsquo;effort. CONFIANCEAbsence de preuvePas d&rsquo;avis, pas de r\u00e9sultats, pas de r\u00e9f\u00e9rences : le visiteur h\u00e9site au moment pr\u00e9cis o\u00f9 il fallait le rassurer. MOBILEParcours mobile n\u00e9glig\u00e9La majorit\u00e9 du trafic vient du mobile, la majorit\u00e9 des tests se font sur ordinateur. L&rsquo;\u00e9cart se paie en conversions perdues. RELANCEAucune reprise de contactPanier abandonn\u00e9, devis non sign\u00e9, formulaire commenc\u00e9 : sans relance automatique, ces occasions disparaissent d\u00e9finitivement. Comment \u00e7a se passe Deux semaines pour savoir o\u00f9 \u00e7a fuit. \u00c9TAPE 01 Mesure Nous v\u00e9rifions d&rsquo;abord que les chiffres sont justes. Un tunnel analys\u00e9 sur des donn\u00e9es fausses ne m\u00e8ne nulle part. JOURS 1 \u00c0 4 \u00c9TAPE 02 Analyse \u00c9tape par \u00e9tape : o\u00f9 les visiteurs partent, sur quel appareil, \u00e0 quel moment. Enregistrements de sessions et cartes de clics \u00e0 l&rsquo;appui. JOURS 5 \u00c0 10 \u00c9TAPE 03 Priorisation Chaque correctif est class\u00e9 par impact estim\u00e9 et par effort. Vous savez quoi faire en premier, et ce que \u00e7a devrait rapporter. JOURS 11 \u00c0 14 \u00c9TAPE 04 Correction Nous ex\u00e9cutons, puis nous mesurons l&rsquo;effet r\u00e9el. Un test par mois, une d\u00e9cision document\u00e9e \u00e0 chaque fois. EN CONTINU \u00c0 la carte Besoin d&rsquo;un chantier pr\u00e9cis ? Chaque option s&rsquo;ajoute \u00e0 une formule ou se commande seule. Tarifs HT. Diagnostic de conversionAnalyse compl\u00e8te et plan prioris\u00e9 1 750\u20ac Landing page d\u00e9di\u00e9eCon\u00e7ue, r\u00e9dig\u00e9e, int\u00e9gr\u00e9e 750\u20ac Suivi &#038; conformit\u00e9GA4, GTM, consentement RGPD 850\u20ac S\u00e9quences d&rsquo;e-mailsRelances panier ou devis d\u00e8s 690\u20ac Refonte d&rsquo;un formulaireSimplification et int\u00e9gration 450\u20ac Preuve socialeCollecte et mise en sc\u00e8ne des avis 590\u20ac Test A\/B additionnelMen\u00e9 et document\u00e9 390\u20ac Optimisation mobileParcours mobile repris de bout en bout 890\u20ac Questions fr\u00e9quentes Ce qu&rsquo;on<\/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":"Expertise Conversion \u2014 Trafic en Clients | Exoa","rank_math_description":"Expertise conversion (CRO) : audit, tests A\/B et optimisation des pages cl\u00e9s. Transformez plus de visiteurs en clients, sans acheter plus de trafic.","rank_math_focus_keyword":"expertise conversion","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-14786","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\/14786","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=14786"}],"version-history":[{"count":6,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages\/14786\/revisions"}],"predecessor-version":[{"id":16642,"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/pages\/14786\/revisions\/16642"}],"wp:attachment":[{"href":"https:\/\/www.agence-exoa.com\/en\/wp-json\/wp\/v2\/media?parent=14786"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}