{"id":15727,"date":"2026-10-16T22:37:06","date_gmt":"2026-10-16T20:37:06","guid":{"rendered":"https:\/\/www.ok3d.fr\/?page_id=15727"},"modified":"2026-10-16T23:07:05","modified_gmt":"2026-10-16T21:07:05","slug":"convertisseur-en-ligne","status":"publish","type":"page","link":"https:\/\/www.ok3d.fr\/en\/convertisseur-en-ligne\/","title":{"rendered":"Online converter"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"15727\" class=\"elementor elementor-15727\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-bbe5d3c e-con e-atomic-element e-flexbox-base e-default-div e-ba436fd \" data-id=\"bbe5d3c\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"bbe5d3c\" data-e-type=\"e-flexbox\" data-id=\"bbe5d3c\">\n    \t\t<div class=\"elementor-element elementor-element-c1d60fd elementor-widget elementor-widget-html\" data-id=\"c1d60fd\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!-- Format Lab by OK3D \u2014 contenu centr\u00e9, sans header int\u00e9gr\u00e9.\r\nColler tout ce fichier dans le widget HTML existant.\r\nCadre : 1600px, marges comprises, comme les autres labs OK3D.\r\nEspace haut : 94 + 54px sur ordinateur ; 78 + 43px sur mobile.\r\nPage Elementor pleine largeur ; conteneur sans padding, d\u00e9bordement visible.\r\n-->\r\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\r\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\r\n<link rel=\"stylesheet\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Unbounded:wght@500;700;800&family=Geist:wght@400;500;600;700;800&family=Geist+Mono:wght@500;600&display=swap\">\r\n<style>\r\n.okk-scope[data-okk=\"format\"]{display:block;font-family:var(--f-body);font-size:15px;line-height:1.5;color:var(--ink);background:var(--bg);text-align:left;-webkit-font-smoothing:antialiased}\r\n.okk-scope[data-okk=\"format\"] *,.okk-scope[data-okk=\"format\"] *::before,.okk-scope[data-okk=\"format\"] *::after{box-sizing:border-box}\r\n.okk-scope[data-okk=\"format\"] [hidden]{display:none!important}\r\n.okk-scope[data-okk=\"format\"] > .okk{min-height:100vh;min-height:100svh}\r\n.okk-scope[data-okk=\"format\"] img{max-width:100%;height:auto}\r\n.okk-scope[data-okk=\"format\"] button,.okk-scope[data-okk=\"format\"] input{font-family:inherit;text-transform:none;letter-spacing:normal;min-height:0;box-shadow:none;margin:0}\r\n.okk-scope[data-okk=\"format\"] h1,.okk-scope[data-okk=\"format\"] h2,.okk-scope[data-okk=\"format\"] h3{color:inherit;text-transform:none;padding:0}\r\n.okk-scope[data-okk=\"format\"] p{margin:0}\r\n.okk-scope[data-okk=\"format\"] a{text-decoration:none}\r\n.okk-scope[data-okk=\"format\"]{\r\n  color-scheme:dark;\r\n  --bg:#06080C; --ink:#EEF2F8; --muted:#8792A6; --faint:#566074; --line:#1C2433; --line2:#2A3549; --panel:#0B0F17;\r\n  --brand:#2E6BFF; --brand-hi:#4F86FF; --brand2:#7CC4FF; --glow:rgba(46,107,255,.45); --good:#35E0A1; --bad:#FF6B6B;\r\n  --c-img:#7CC4FF; --c-vec:#B892FF; --c-pdf:#FF8A7A; --c-m3d:#35E0A1; --c-vid:#FFB547; --c-font:#E9EEF6;\r\n  --f-display:\"Unbounded\",\"Arial Black\",system-ui,sans-serif; --f-body:\"Geist\",system-ui,-apple-system,\"Segoe UI\",sans-serif; --f-mono:\"Geist Mono\",ui-monospace,Menlo,Consolas,monospace;\r\n}\r\n.okk-scope[data-okk=\"format\"]{background:var(--bg);color:var(--ink)}\r\n.okk-scope[data-okk=\"format\"]{font-family:var(--f-body);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}\r\n.okk-scope[data-okk=\"format\"] *{box-sizing:border-box}\r\n.okk-scope[data-okk=\"format\"] [hidden]{display:none!important}\r\n.okk-scope[data-okk=\"format\"] button{font:inherit;color:inherit;cursor:pointer;background:none;border:0;padding:0}\r\n.okk-scope[data-okk=\"format\"] button:disabled{cursor:default;opacity:.45}\r\n.okk-scope[data-okk=\"format\"] button:hover,.okk-scope[data-okk=\"format\"] button:focus{background-color:transparent;color:inherit}\r\n.okk-scope[data-okk=\"format\"] select{width:auto}\r\n.okk-scope[data-okk=\"format\"] img{border:0;box-shadow:none;border-radius:0}\r\n.okk-scope[data-okk=\"format\"] h4{color:inherit}\r\n.okk-scope[data-okk=\"format\"] a{color:inherit}\r\n.okk-scope[data-okk=\"format\"] a.btn{text-decoration:none}\r\n.okk-scope[data-okk=\"format\"] :focus-visible{outline:2px solid var(--brand2);outline-offset:3px;border-radius:6px}\r\n.okk-scope[data-okk=\"format\"] .okk{position:relative;overflow:hidden;min-height:100%;padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:clamp(14px,3vw,34px) 48px;\r\n  background:radial-gradient(900px 520px at 88% -12%,rgba(46,107,255,.22),transparent 62%),radial-gradient(760px 460px at -5% 8%,rgba(124,196,255,.08),transparent 60%),var(--bg)}\r\n.okk-scope[data-okk=\"format\"] .wrap{position:relative;max-width:1280px;margin:0 auto}\r\n.okk-scope[data-okk=\"format\"] .eyebrow{font:600 11px var(--f-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--brand2);display:inline-flex;align-items:center;gap:8px}\r\n.okk-scope[data-okk=\"format\"] .eyebrow::before{content:\"\";width:6px;height:6px;border-radius:2px;background:var(--brand2);box-shadow:0 0 10px var(--brand2)}\r\n.okk-scope[data-okk=\"format\"] h1,.okk-scope[data-okk=\"format\"] h2,.okk-scope[data-okk=\"format\"] h3{margin:0;font-family:var(--f-display);letter-spacing:-.035em;text-wrap:balance}\r\n.okk-scope[data-okk=\"format\"] .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:46px;padding:0 22px;border-radius:999px;font-weight:600;font-size:14.5px;transition:transform .15s,border-color .2s,opacity .2s;white-space:nowrap}\r\n.okk-scope[data-okk=\"format\"] .btn.pri{background:linear-gradient(180deg,var(--brand-hi),#2152E0);color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 12px 30px -10px var(--glow),0 0 0 1px rgba(46,107,255,.6)}\r\n.okk-scope[data-okk=\"format\"] .btn.pri:not(:disabled):hover{transform:translateY(-1px)}\r\n.okk-scope[data-okk=\"format\"] .btn.gho{border:1px solid var(--line2);background:rgba(255,255,255,.02)}\r\n.okk-scope[data-okk=\"format\"] .btn.gho:not(:disabled):hover{border-color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .btn.sm{height:36px;padding:0 14px;font-size:13px}\r\n.okk-scope[data-okk=\"format\"] .btn.ok{background:rgba(53,224,161,.12);color:var(--good);box-shadow:inset 0 0 0 1px rgba(53,224,161,.4)}\r\n.okk-scope[data-okk=\"format\"] .glass{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.012));border:1px solid var(--line);border-radius:26px;box-shadow:inset 0 1px 0 rgba(255,255,255,.045)}\r\n.okk-scope[data-okk=\"format\"] .lbl{font:600 10px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:10px;display:flex;justify-content:space-between;gap:8px;align-items:center}\r\n.okk-scope[data-okk=\"format\"] .lbl em{font-style:normal;color:var(--muted);letter-spacing:.06em;text-transform:none;font-size:11px}\r\n.okk-scope[data-okk=\"format\"] .intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;padding-block:6px 26px}\r\n.okk-scope[data-okk=\"format\"] .intro h1{font-size:clamp(34px,5.4vw,70px);line-height:1.02;font-weight:800;margin-top:16px}\r\n.okk-scope[data-okk=\"format\"] .intro h1 .c{background:linear-gradient(100deg,#4F86FF,#7CC4FF 55%,#E9F5FF);-webkit-background-clip:text;background-clip:text;color:transparent}\r\n.okk-scope[data-okk=\"format\"] .intro p{color:var(--muted);max-width:54ch;margin:16px 0 0;font-size:clamp(15px,1.3vw,16.5px)}\r\n.okk-scope[data-okk=\"format\"] .kpis{display:flex;gap:10px}\r\n.okk-scope[data-okk=\"format\"] .kpis div{padding:12px 16px;border-radius:18px;border:1px solid var(--line);background:rgba(255,255,255,.025);min-width:96px}\r\n.okk-scope[data-okk=\"format\"] .kpis b{display:block;font:700 22px var(--f-display);letter-spacing:-.03em}\r\n.okk-scope[data-okk=\"format\"] .kpis span{font:500 11px var(--f-mono);color:var(--muted);letter-spacing:.06em}\r\n.okk-scope[data-okk=\"format\"] .drop{position:relative;overflow:hidden;min-height:clamp(320px,40vw,440px);display:grid;place-items:center;text-align:center;padding:28px 18px;cursor:pointer;transition:border-color .25s,box-shadow .25s,min-height .4s}\r\n.okk-scope[data-okk=\"format\"] .drop::before{content:\"\";position:absolute;inset:12px;border-radius:18px;border:1.5px dashed var(--line2);pointer-events:none;transition:border-color .25s}\r\n.okk-scope[data-okk=\"format\"] .drop.over{border-color:rgba(124,196,255,.6);box-shadow:0 0 0 6px rgba(46,107,255,.12),0 30px 80px -30px var(--glow)}\r\n.okk-scope[data-okk=\"format\"] .drop.over::before{border-color:var(--brand2)}\r\n.okk-scope[data-okk=\"format\"] .marq{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:space-evenly;pointer-events:none;opacity:.55;\r\n  -webkit-mask-image:radial-gradient(ellipse 46% 50% at 50% 50%,transparent 45%,#000 85%);mask-image:radial-gradient(ellipse 46% 50% at 50% 50%,transparent 45%,#000 85%)}\r\n.okk-scope[data-okk=\"format\"] .mrow{overflow:hidden;white-space:nowrap}\r\n.okk-scope[data-okk=\"format\"] .mtrack{display:inline-flex;gap:10px;animation:mq 46s linear infinite;padding-right:10px}\r\n.okk-scope[data-okk=\"format\"] .mrow.r .mtrack{animation-direction:reverse;animation-duration:58s}\r\n.okk-scope[data-okk=\"format\"] .mrow:nth-child(3) .mtrack{animation-duration:52s}\r\n@keyframes mq{to{transform:translateX(-50%)}}\r\n.okk-scope[data-okk=\"format\"] .fb{display:inline-flex;align-items:center;gap:6px;font:600 12px var(--f-mono);padding:7px 11px;border-radius:10px;border:1px solid var(--line2);background:rgba(11,15,23,.8);color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .fb i{width:6px;height:6px;border-radius:2px;background:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .dz-in{position:relative;z-index:2;display:flex;flex-direction:column;align-items:center;gap:12px;max-width:560px}\r\n.okk-scope[data-okk=\"format\"] .orb{width:78px;height:78px;border-radius:24px;display:grid;place-items:center;background:linear-gradient(150deg,#4F86FF,#1F49D8);box-shadow:0 18px 50px -12px var(--glow),inset 0 1px 0 rgba(255,255,255,.35);position:relative}\r\n.okk-scope[data-okk=\"format\"] .orb::after{content:\"\";position:absolute;inset:-8px;border-radius:30px;border:1px solid rgba(124,196,255,.35);animation:pulse 2.6s ease-out infinite}\r\n@keyframes pulse{0%{transform:scale(.92);opacity:1}100%{transform:scale(1.25);opacity:0}}\r\n.okk-scope[data-okk=\"format\"] .orb svg{width:36px;height:36px}\r\n.okk-scope[data-okk=\"format\"] .drop.over .orb{transform:scale(1.08)}\r\n.okk-scope[data-okk=\"format\"] .dz-in h2{font-size:clamp(22px,3vw,32px);font-weight:700;margin-top:6px}\r\n.okk-scope[data-okk=\"format\"] .dz-in p{margin:0;color:var(--muted);font-size:14px}\r\n.okk-scope[data-okk=\"format\"] .priv{display:inline-flex;align-items:center;gap:8px;font:500 12px var(--f-mono);color:var(--good);padding:6px 12px;border-radius:999px;background:rgba(53,224,161,.08);margin-top:4px}\r\n.okk-scope[data-okk=\"format\"] .priv svg{width:14px;height:14px}\r\n.okk-scope[data-okk=\"format\"] .drop.mini{min-height:0;padding:18px 22px;text-align:left}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .marq,.okk-scope[data-okk=\"format\"] .drop.mini .dz-in h2,.okk-scope[data-okk=\"format\"] .drop.mini .priv{display:none}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .dz-in{flex-direction:row;max-width:none;width:100%;gap:16px}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .orb{width:46px;height:46px;border-radius:15px;flex:none}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .orb::after{display:none}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .orb svg{width:22px;height:22px}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .dz-in .gotxt{flex:1}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .dz-in .gotxt::before{content:\"Ajoute d'autres fichiers\";display:block;font:700 16px var(--f-display);letter-spacing:-.02em;margin-bottom:2px}\r\n.okk-scope[data-okk=\"format\"] .kbonly{display:inline}\r\n@media (hover:none){.okk-scope[data-okk=\"format\"] .kbonly{display:none}}\r\n.okk-scope[data-okk=\"format\"] .work{display:grid;grid-template-columns:minmax(0,1.22fr) minmax(320px,.78fr);gap:16px;align-items:start;margin-top:16px}\r\n.okk-scope[data-okk=\"format\"] .qpanel{padding:18px;display:flex;flex-direction:column;gap:14px}\r\n.okk-scope[data-okk=\"format\"] .qhead{display:flex;justify-content:space-between;align-items:center;gap:10px;flex-wrap:wrap}\r\n.okk-scope[data-okk=\"format\"] .qhead .lbl{margin:0 0 2px}\r\n.okk-scope[data-okk=\"format\"] .qhead b{font:700 18px var(--f-display);letter-spacing:-.02em}\r\n.okk-scope[data-okk=\"format\"] .qtools{display:flex;gap:8px}\r\n.okk-scope[data-okk=\"format\"] .queue{display:flex;flex-direction:column;gap:8px}\r\n.okk-scope[data-okk=\"format\"] .row{position:relative;display:grid;grid-template-columns:58px minmax(0,1fr) auto;gap:14px;align-items:center;padding:10px 12px 10px 10px;border-radius:18px;border:1px solid var(--line);background:rgba(255,255,255,.018);cursor:pointer;transition:border-color .2s,background .2s;animation:rin .45s cubic-bezier(.2,.9,.3,1) both;overflow:hidden}\r\n@keyframes rin{from{opacity:0;transform:translateY(8px)}}\r\n.okk-scope[data-okk=\"format\"] .row:hover{border-color:var(--line2)}\r\n.okk-scope[data-okk=\"format\"] .row.sel{border-color:rgba(124,196,255,.55);background:rgba(46,107,255,.07)}\r\n.okk-scope[data-okk=\"format\"] .th{width:58px;height:58px;border-radius:13px;overflow:hidden;display:grid;place-items:center;background:var(--panel);border:1px solid var(--line);position:relative}\r\n.okk-scope[data-okk=\"format\"] .th img,.okk-scope[data-okk=\"format\"] .th canvas{width:100%;height:100%;object-fit:contain;display:block}\r\n.okk-scope[data-okk=\"format\"] .th.chk{background:conic-gradient(#1a2130 25%,#121825 0 50%,#1a2130 0 75%,#121825 0) 0 0\/12px 12px}\r\n.okk-scope[data-okk=\"format\"] .th .gl{font:700 13px var(--f-mono);color:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .th .spin{position:absolute;inset:0;display:grid;place-items:center;background:rgba(6,8,12,.55)}\r\n.okk-scope[data-okk=\"format\"] .spin i,.okk-scope[data-okk=\"format\"] .busyi{width:18px;height:18px;border-radius:50%;border:2px solid rgba(255,255,255,.2);border-top-color:#fff;animation:sp .8s linear infinite;display:inline-block}\r\n@keyframes sp{to{transform:rotate(360deg)}}\r\n.okk-scope[data-okk=\"format\"] .mid{min-width:0}\r\n.okk-scope[data-okk=\"format\"] .nm{font-weight:600;font-size:14px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\r\n.okk-scope[data-okk=\"format\"] .mt{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px;color:var(--muted);font-size:12.5px}\r\n.okk-scope[data-okk=\"format\"] .tag{font:700 10.5px var(--f-mono);letter-spacing:.06em;padding:3px 7px;border-radius:6px;color:#06080C;background:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .res{font:600 12px var(--f-mono);color:var(--good)}\r\n.okk-scope[data-okk=\"format\"] .res.big{color:var(--c-vid)}\r\n.okk-scope[data-okk=\"format\"] .err{font-size:12.5px;color:var(--bad)}\r\n.okk-scope[data-okk=\"format\"] .pb{position:absolute;left:0;bottom:0;height:3px;width:0;background:linear-gradient(90deg,var(--brand),var(--brand2));transition:width .25s;box-shadow:0 0 12px var(--brand2)}\r\n.okk-scope[data-okk=\"format\"] .ctl{display:flex;align-items:center;gap:8px}\r\n.okk-scope[data-okk=\"format\"] .arr{color:var(--faint);font-family:var(--f-mono)}\r\n.okk-scope[data-okk=\"format\"] .fsel{width:auto;line-height:normal;appearance:none;-webkit-appearance:none;height:36px;padding:0 30px 0 12px;border-radius:11px;border:1px solid var(--line2);background:var(--panel) url(\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238792A6' stroke-width='1.6' fill='none' stroke-linecap='round'\/%3E%3C\/svg%3E\") no-repeat right 11px center;color:var(--ink);font:600 12.5px var(--f-mono);cursor:pointer;max-width:170px}\r\n.okk-scope[data-okk=\"format\"] .fsel:hover{border-color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .ib{width:36px;height:36px;border-radius:11px;display:grid;place-items:center;border:1px solid var(--line2);color:var(--muted);flex:none;transition:border-color .2s,color .2s,background .2s}\r\n.okk-scope[data-okk=\"format\"] .ib:hover{color:var(--ink);border-color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .ib svg{width:16px;height:16px}\r\n.okk-scope[data-okk=\"format\"] .ib.go{background:linear-gradient(180deg,var(--brand-hi),#2152E0);color:#fff;border-color:transparent}\r\n.okk-scope[data-okk=\"format\"] .ib.dl{background:rgba(53,224,161,.14);color:var(--good);border-color:rgba(53,224,161,.45)}\r\n.okk-scope[data-okk=\"format\"] .qfoot{display:flex;gap:10px;flex-wrap:wrap;padding-top:4px}\r\n.okk-scope[data-okk=\"format\"] .qfoot .btn{flex:1 1 200px}\r\n.okk-scope[data-okk=\"format\"] .side{display:flex;flex-direction:column;gap:16px;min-width:0}\r\n.okk-scope[data-okk=\"format\"] .pv{padding:16px}\r\n.okk-scope[data-okk=\"format\"] .pvbox{position:relative;aspect-ratio:4\/3;border-radius:18px;overflow:hidden;background:var(--panel);border:1px solid var(--line);display:grid;place-items:center}\r\n.okk-scope[data-okk=\"format\"] .pvbox.chk{background:conic-gradient(#161c28 25%,#10151f 0 50%,#161c28 0 75%,#10151f 0) 0 0\/18px 18px}\r\n.okk-scope[data-okk=\"format\"] .pvbox>img,.okk-scope[data-okk=\"format\"] .pvbox>video{position:absolute;inset:0;width:100%;height:100%;object-fit:contain;display:block}\r\n.okk-scope[data-okk=\"format\"] .pvbox .three{position:absolute;inset:0;width:100%!important;height:100%!important}\r\n.okk-scope[data-okk=\"format\"] .pvbox .ph{color:var(--faint);font-size:13px;text-align:center;padding:20px}\r\n.okk-scope[data-okk=\"format\"] .cmp{position:absolute;inset:0}\r\n.okk-scope[data-okk=\"format\"] .cmp img{position:absolute;inset:0;width:100%;height:100%;object-fit:contain}\r\n.okk-scope[data-okk=\"format\"] .cmp .after{clip-path:inset(0 0 0 var(--x,50%))}\r\n.okk-scope[data-okk=\"format\"] .cmp .ln{position:absolute;top:0;bottom:0;left:var(--x,50%);width:2px;margin-left:-1px;background:#fff;box-shadow:0 0 12px rgba(0,0,0,.6);pointer-events:none}\r\n.okk-scope[data-okk=\"format\"] .cmp .ln::after{content:\"\";position:absolute;top:50%;left:50%;width:30px;height:30px;margin:-15px 0 0 -15px;border-radius:50%;background:#fff url(\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='16' height='10'%3E%3Cpath d='M5 1L1 5l4 4M11 1l4 4-4 4' stroke='%2306080C' stroke-width='1.8' fill='none' stroke-linecap='round' stroke-linejoin='round'\/%3E%3C\/svg%3E\") no-repeat center}\r\n.okk-scope[data-okk=\"format\"] .cmp input{position:absolute;inset:0;width:100%;height:100%;opacity:0;cursor:ew-resize;margin:0}\r\n.okk-scope[data-okk=\"format\"] .cmp .cap{position:absolute;top:10px;font:600 10.5px var(--f-mono);letter-spacing:.08em;text-transform:uppercase;padding:4px 8px;border-radius:7px;background:rgba(6,8,12,.75);pointer-events:none}\r\n.okk-scope[data-okk=\"format\"] .cmp .cap.l{left:10px}\r\n.okk-scope[data-okk=\"format\"] .cmp .cap.r{right:10px;color:var(--good)}\r\n.okk-scope[data-okk=\"format\"] .pvstats{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}\r\n.okk-scope[data-okk=\"format\"] .pvstats span{font:500 11.5px var(--f-mono);padding:5px 9px;border-radius:8px;background:rgba(255,255,255,.05);color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .pvstats b{color:var(--ink);font-weight:600}\r\n.okk-scope[data-okk=\"format\"] .ftest{width:100%;height:100%;display:flex;flex-direction:column;justify-content:center;gap:6px;padding:20px 22px;overflow:hidden;text-align:left}\r\n.okk-scope[data-okk=\"format\"] .ftest .aa{font-size:clamp(44px,5.4vw,72px);line-height:1;white-space:nowrap}\r\n.okk-scope[data-okk=\"format\"] .ftest .pg{font-size:19px;line-height:1.3;outline:none;color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .ftest .ab{font-size:13px;color:var(--faint);letter-spacing:.04em}\r\n.okk-scope[data-okk=\"format\"] .code{margin-top:12px;position:relative}\r\n.okk-scope[data-okk=\"format\"] .code pre{margin:0;padding:12px 14px;border-radius:12px;background:var(--panel);border:1px solid var(--line);font:500 11.5px\/1.55 var(--f-mono);color:var(--muted);white-space:pre-wrap;word-break:break-all;max-height:150px;overflow:auto}\r\n.okk-scope[data-okk=\"format\"] .code button{position:absolute;top:8px;right:8px}\r\n.okk-scope[data-okk=\"format\"] .set{padding:18px;display:flex;flex-direction:column;gap:18px}\r\n.okk-scope[data-okk=\"format\"] .set .sec{display:flex;flex-direction:column;gap:10px}\r\n.okk-scope[data-okk=\"format\"] .set .sec+.sec{padding-top:16px;border-top:1px solid var(--line)}\r\n.okk-scope[data-okk=\"format\"] .set h4{margin:0;font:700 13px var(--f-display);letter-spacing:-.01em;display:flex;align-items:center;gap:8px}\r\n.okk-scope[data-okk=\"format\"] .set h4 i{width:8px;height:8px;border-radius:3px;background:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .chips{display:flex;flex-wrap:wrap;gap:6px}\r\n.okk-scope[data-okk=\"format\"] .chip{font:600 12px var(--f-mono);padding:7px 11px;border-radius:10px;border:1px solid var(--line2);color:var(--muted);transition:all .15s}\r\n.okk-scope[data-okk=\"format\"] .chip:hover{color:var(--ink);border-color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .chip.on{color:#06080C;background:var(--c,var(--brand2));border-color:transparent}\r\n.okk-scope[data-okk=\"format\"] .fld{display:grid;grid-template-columns:118px minmax(0,1fr);gap:10px;align-items:center;font-size:13px;color:var(--muted)}\r\n.okk-scope[data-okk=\"format\"] .fld .val{font:600 12px var(--f-mono);color:var(--ink);min-width:42px;text-align:right}\r\n.okk-scope[data-okk=\"format\"] .fld .ctl2{display:flex;align-items:center;gap:10px;min-width:0}\r\n.okk-scope[data-okk=\"format\"] .fld select{width:100%;max-width:none}\r\n.okk-scope[data-okk=\"format\"] input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:4px;border-radius:4px;background:linear-gradient(90deg,var(--brand2) var(--p,50%),var(--line2) var(--p,50%));outline:none;margin:0}\r\n.okk-scope[data-okk=\"format\"] input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:18px;height:18px;border-radius:50%;background:#fff;box-shadow:0 2px 8px rgba(0,0,0,.5);cursor:pointer}\r\n.okk-scope[data-okk=\"format\"] input[type=range]::-moz-range-thumb{width:18px;height:18px;border:0;border-radius:50%;background:#fff;cursor:pointer}\r\n.okk-scope[data-okk=\"format\"] .sw{display:flex;gap:6px;align-items:center}\r\n.okk-scope[data-okk=\"format\"] .sw button{width:26px;height:26px;border-radius:8px;border:1px solid var(--line2)}\r\n.okk-scope[data-okk=\"format\"] .sw button.on{box-shadow:0 0 0 2px var(--bg),0 0 0 4px var(--brand2)}\r\n.okk-scope[data-okk=\"format\"] .sw input{width:30px;height:28px;border:0;padding:0;background:none;cursor:pointer}\r\n.okk-scope[data-okk=\"format\"] .tg{display:flex;align-items:center;gap:10px;cursor:pointer;color:var(--ink);font-size:13px}\r\n.okk-scope[data-okk=\"format\"] .tg input{display:none}\r\n.okk-scope[data-okk=\"format\"] .tg i{width:36px;height:21px;border-radius:12px;background:var(--line2);position:relative;transition:background .2s;flex:none}\r\n.okk-scope[data-okk=\"format\"] .tg i::after{content:\"\";position:absolute;top:3px;left:3px;width:15px;height:15px;border-radius:50%;background:#fff;transition:transform .2s}\r\n.okk-scope[data-okk=\"format\"] .tg input:checked+i{background:var(--brand)}\r\n.okk-scope[data-okk=\"format\"] .tg input:checked+i::after{transform:translateX(15px)}\r\n.okk-scope[data-okk=\"format\"] .note{font-size:12px;color:var(--faint);margin:0}\r\n.okk-scope[data-okk=\"format\"] .cats{margin-top:56px}\r\n.okk-scope[data-okk=\"format\"] .cats>.head{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:18px}\r\n.okk-scope[data-okk=\"format\"] .cats>.head h2{font-size:clamp(22px,3vw,34px);font-weight:700;margin-top:12px}\r\n.okk-scope[data-okk=\"format\"] .cats>.head p{margin:0;color:var(--muted);max-width:44ch;font-size:14px}\r\n.okk-scope[data-okk=\"format\"] .cgrid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}\r\n.okk-scope[data-okk=\"format\"] .cat{padding:20px;text-align:left;display:flex;flex-direction:column;gap:14px;transition:border-color .2s,transform .2s;position:relative;overflow:hidden}\r\n.okk-scope[data-okk=\"format\"] .cat::after{content:\"\";position:absolute;right:-60px;top:-60px;width:160px;height:160px;border-radius:50%;background:radial-gradient(circle,var(--c),transparent 70%);opacity:.12;transition:opacity .3s}\r\n.okk-scope[data-okk=\"format\"] .cat:hover{border-color:var(--line2);transform:translateY(-2px)}\r\n.okk-scope[data-okk=\"format\"] .cat:hover::after{opacity:.25}\r\n.okk-scope[data-okk=\"format\"] .cat .ct{display:flex;align-items:center;gap:12px}\r\n.okk-scope[data-okk=\"format\"] .cat .ci{width:42px;height:42px;border-radius:13px;display:grid;place-items:center;background:rgba(255,255,255,.04);border:1px solid var(--line2);color:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .cat .ci svg{width:21px;height:21px}\r\n.okk-scope[data-okk=\"format\"] .cat h3{font-size:17px;font-weight:700;letter-spacing:-.02em}\r\n.okk-scope[data-okk=\"format\"] .cat .ct small{display:block;font:500 11.5px var(--f-mono);color:var(--muted);margin-top:2px}\r\n.okk-scope[data-okk=\"format\"] .cat .io{display:grid;grid-template-columns:62px minmax(0,1fr);gap:8px;align-items:start}\r\n.okk-scope[data-okk=\"format\"] .cat .io>span{font:600 10px var(--f-mono);letter-spacing:.12em;color:var(--faint);padding-top:5px}\r\n.okk-scope[data-okk=\"format\"] .cat .fl{display:flex;flex-wrap:wrap;gap:4px}\r\n.okk-scope[data-okk=\"format\"] .cat .fl b{font:600 11px var(--f-mono);padding:3px 7px;border-radius:6px;background:rgba(255,255,255,.05);color:var(--ink)}\r\n.okk-scope[data-okk=\"format\"] .cat .fl.o b{background:color-mix(in srgb,var(--c) 16%,transparent);color:var(--c)}\r\n.okk-scope[data-okk=\"format\"] .cta{margin-top:18px;padding:clamp(22px,4vw,34px);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:20px;align-items:center;background:linear-gradient(140deg,rgba(46,107,255,.16),rgba(46,107,255,.02))}\r\n.okk-scope[data-okk=\"format\"] .cta h3{font-size:clamp(19px,2.4vw,26px);font-weight:700}\r\n.okk-scope[data-okk=\"format\"] .cta p{margin:8px 0 0;color:var(--muted);max-width:58ch}\r\n.okk-scope[data-okk=\"format\"] .toast{position:fixed;left:50%;bottom:24px;transform:translateX(-50%);padding:10px 16px;border-radius:999px;background:var(--ink);color:var(--bg);font-weight:600;font-size:13px;z-index:9999;max-width:90%;text-align:center;animation:rin .3s both}\r\n.okk-scope[data-okk=\"format\"] .vh{position:absolute!important;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}\r\n@media (max-width:1020px){.okk-scope[data-okk=\"format\"] .work{grid-template-columns:1fr}\r\n.okk-scope[data-okk=\"format\"] .cgrid{grid-template-columns:repeat(2,minmax(0,1fr))}}\r\n@media (max-width:640px){.okk-scope[data-okk=\"format\"] .kpis{width:100%}\r\n.okk-scope[data-okk=\"format\"] .kpis div{flex:1;min-width:0}\r\n.okk-scope[data-okk=\"format\"] .row{grid-template-columns:50px minmax(0,1fr);gap:12px}\r\n.okk-scope[data-okk=\"format\"] .th{width:50px;height:50px}\r\n.okk-scope[data-okk=\"format\"] .row .ctl{grid-column:1 \/ -1;justify-content:flex-end}\r\n.okk-scope[data-okk=\"format\"] .row .ctl .fsel{flex:1;max-width:none}\r\n.okk-scope[data-okk=\"format\"] .cgrid{grid-template-columns:1fr}\r\n.okk-scope[data-okk=\"format\"] .cta{grid-template-columns:1fr}\r\n.okk-scope[data-okk=\"format\"] .fld{grid-template-columns:100px minmax(0,1fr)}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .dz-in{flex-wrap:wrap}\r\n.okk-scope[data-okk=\"format\"] .drop.mini .btn{width:100%}\r\n.okk-scope[data-okk=\"format\"] .btn{height:44px}}\r\n@media (prefers-reduced-motion:reduce){.okk-scope[data-okk=\"format\"] *{animation:none!important;transition:none!important}}\r\n\/* Format Lab : m\u00eames dimensions que les autres exp\u00e9riences OK3D. *\/\r\n.okk-scope[data-okk=\"format\"] {\r\n  --okk-content-max: 1600px;\r\n  --okk-gutter: clamp(22px, 5vw, 88px);\r\n  --okk-header-offset: 94px;\r\n  --okk-top-gap: 54px;\r\n  position: relative;\r\n  left: var(--okk-shift, 0px);\r\n  width: var(--okk-width, 100%) !important;\r\n  max-width: none !important;\r\n  min-width: 0;\r\n  margin: 0 !important;\r\n  padding: 0 !important;\r\n  isolation: isolate;\r\n}\r\n.okk-scope[data-okk=\"format\"] > .okk {\r\n  width: 100%;\r\n  min-height: 100vh;\r\n  min-height: 100dvh;\r\n  overflow: clip;\r\n  padding-inline: 0;\r\n  padding-top: calc(var(--okk-header-offset) + var(--okk-top-gap));\r\n  padding-bottom: max(28px, env(safe-area-inset-bottom, 0px));\r\n}\r\n.okk-scope[data-okk=\"format\"] > .okk > .wrap {\r\n  width: 100%;\r\n  max-width: var(--okk-content-max);\r\n  margin-inline: auto;\r\n  padding-left: max(var(--okk-gutter), env(safe-area-inset-left, 0px));\r\n  padding-right: max(var(--okk-gutter), env(safe-area-inset-right, 0px));\r\n}\r\n.okk-scope[data-okk=\"format\"] .intro { padding-top: 0; }\r\n.okk-scope[data-okk=\"format\"] :is(.intro, .work, .cta, .cgrid) > * { min-width: 0; }\r\n.okk-scope[data-okk=\"format\"] .work { scroll-margin-top: calc(var(--okk-header-offset) + 24px); }\r\n.okk-scope[data-okk=\"format\"] .btn {\r\n  max-width: 100%;\r\n  height: auto;\r\n  min-height: 44px;\r\n  padding-block: 10px;\r\n  white-space: normal;\r\n  text-align: center;\r\n}\r\n.okk-scope[data-okk=\"format\"] .btn.sm { min-height: 36px; padding-block: 7px; }\r\n.okk-scope[data-okk=\"format\"] :is(.priv, .kpis) { max-width: 100%; }\r\n.okk-scope[data-okk=\"format\"] :is(.kpis span, .intro h1, .priv) { overflow-wrap: anywhere; }\r\n@media (max-width: 700px) {\r\n  .okk-scope[data-okk=\"format\"] { --okk-header-offset: 78px; --okk-top-gap: 43px; }\r\n}\r\n@media (max-width: 640px) {\r\n  .okk-scope[data-okk=\"format\"] .kpis div { padding: 12px 10px; }\r\n  .okk-scope[data-okk=\"format\"] .kpis b { font-size: clamp(16px, 4.6vw, 22px); white-space: nowrap; }\r\n  .okk-scope[data-okk=\"format\"] .kpis span { font-size: clamp(9px, 2.5vw, 11px); white-space: nowrap; }\r\n}\r\n<\/style>\r\n<div class=\"okk-scope\" data-okk=\"format\">\r\n<div class=\"okk\" id=\"okk\">\r\n  <div class=\"wrap\">\r\n    <section class=\"intro\">\r\n      <div><span class=\"eyebrow\">OK3D Lab \u00b7 Convertisseur<\/span>\r\n        <h1>Tous tes fichiers.<br><span class=\"c\">Le bon format.<\/span><\/h1>\r\n        <p>Images, vectoriel, PDF, vid\u00e9os, polices et mod\u00e8les 3D : d\u00e9pose, choisis, t\u00e9l\u00e9charge. Tout se passe dans ton navigateur, tes fichiers ne quittent jamais ton appareil.<\/p><\/div>\r\n      <div class=\"kpis\"><div><b id=\"kfmt\">40+<\/b><span>formats<\/span><\/div><div><b>100 %<\/b><span>local<\/span><\/div><div><b>0<\/b><span>inscription<\/span><\/div><\/div>\r\n    <\/section>\r\n\r\n    <section class=\"glass drop\" id=\"drop\" role=\"button\" tabindex=\"0\" aria-label=\"D\u00e9poser ou choisir des fichiers \u00e0 convertir\">\r\n      <div class=\"marq\" aria-hidden=\"true\"><div class=\"mrow\"><div class=\"mtrack\" id=\"m1\"><\/div><\/div><div class=\"mrow r\"><div class=\"mtrack\" id=\"m2\"><\/div><\/div><div class=\"mrow\"><div class=\"mtrack\" id=\"m3\"><\/div><\/div><\/div>\r\n      <div class=\"dz-in\">\r\n        <div class=\"orb\"><svg viewBox=\"0 0 36 36\" fill=\"none\" stroke=\"#fff\" stroke-width=\"2.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 14h17l-5-5\"\/><path d=\"M28 22H11l5 5\"\/><\/svg><\/div>\r\n        <h2>D\u00e9pose tes fichiers ici<\/h2>\r\n        <p class=\"gotxt\">Plusieurs fichiers \u00e0 la fois, de tous types<span class=\"kbonly\"> \u00b7 ou colle une image avec Ctrl+V<\/span><\/p>\r\n        <button class=\"btn pri\" id=\"pick\" type=\"button\">Choisir des fichiers<\/button>\r\n        <div class=\"priv\"><svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.6\"><rect x=\"3\" y=\"7\" width=\"10\" height=\"7\" rx=\"2\"\/><path d=\"M5.5 7V5a2.5 2.5 0 015 0v2\"\/><\/svg>Aucun envoi : la conversion se fait sur ton appareil<\/div>\r\n      <\/div>\r\n      <input type=\"file\" id=\"file\" multiple class=\"vh\" tabindex=\"-1\" aria-hidden=\"true\">\r\n    <\/section>\r\n\r\n    <section class=\"work\" id=\"work\" hidden>\r\n      <div class=\"glass qpanel\">\r\n        <div class=\"qhead\"><div><div class=\"lbl\">File de conversion<\/div><b id=\"qcount\">0 fichier<\/b><\/div>\r\n          <div class=\"qtools\"><button class=\"btn gho sm\" id=\"clear\" type=\"button\">Tout vider<\/button><\/div><\/div>\r\n        <div class=\"queue\" id=\"queue\"><\/div>\r\n        <div class=\"qfoot\"><button class=\"btn pri\" id=\"runall\" type=\"button\">Tout convertir<\/button><button class=\"btn gho\" id=\"zipall\" type=\"button\" disabled>Tout t\u00e9l\u00e9charger (.zip)<\/button><\/div>\r\n      <\/div>\r\n      <aside class=\"side\">\r\n        <div class=\"glass pv\"><div class=\"lbl\"><span>Aper\u00e7u<\/span><em id=\"pvinfo\"><\/em><\/div><div class=\"pvbox\" id=\"pvbox\"><div class=\"ph\">S\u00e9lectionne un fichier<\/div><\/div><div class=\"pvstats\" id=\"pvstats\"><\/div><div id=\"pvx\"><\/div><\/div>\r\n        <div class=\"glass set\" id=\"set\"><\/div>\r\n      <\/aside>\r\n    <\/section>\r\n\r\n    <section class=\"cats\">\r\n      <div class=\"head\"><div><span class=\"eyebrow\">Formats pris en charge<\/span><h2>Six familles, un seul geste.<\/h2><\/div><p>Touche une famille pour choisir directement les fichiers concern\u00e9s.<\/p><\/div>\r\n      <div class=\"cgrid\" id=\"cgrid\"><\/div>\r\n    <\/section>\r\n\r\n    <section class=\"glass cta\"><div><span class=\"eyebrow\">Studio OK3D<\/span><h3 style=\"margin-top:12px\">Un fichier, c'est le d\u00e9but. Le rendu, c'est notre m\u00e9tier.<\/h3>\r\n      <p>OK3D cr\u00e9e des visuels 3D, des animations et des identit\u00e9s qui font la diff\u00e9rence. Un projet en t\u00eate ?<\/p><\/div>\r\n      <a class=\"btn pri\" id=\"ctalink\" href=\"#\" target=\"_blank\" rel=\"noopener\">Parlons-en \u2192<\/a><\/section>\r\n  <\/div>\r\n<\/div>\r\n<\/div>\r\n\r\n<script>\r\n(function(){\r\nconst OKK=document.querySelector('[data-okk=\"format\"]');\r\nif(!OKK) return;\r\n\r\n\r\n\/* ---------- FBX 7.4 binary writer (meshes baked in world space, Phong materials, embedded diffuse textures) ---------- *\/\r\nconst FBXW=(()=>{\r\n  const TE=new TextEncoder();\r\n  const P=(t,v)=>({t,v}), I=v=>P(\"I\",v), D=v=>P(\"D\",v), L=v=>P(\"L\",v), S=v=>P(\"S\",typeof v===\"string\"?TE.encode(v):v), C=v=>P(\"C\",v?1:0), R=v=>P(\"R\",v), AI=v=>P(\"i\",v), AD=v=>P(\"d\",v);\r\n  const N=(name,props=[],kids=[])=>({name:TE.encode(name),props,kids});\r\n  const SENT=13;\r\n  const psize=p=>{ switch(p.t){ case \"C\":return 2; case \"Y\":return 3; case \"I\":case \"F\":return 5; case \"D\":case \"L\":return 9;\r\n    case \"S\":case \"R\":return 5+p.v.length; case \"i\":case \"f\":return 13+p.v.length*4; case \"d\":case \"l\":return 13+p.v.length*8 } throw new Error(\"prop \"+p.t) };\r\n  const nsize=n=>{ let s=13+n.name.length; for(const p of n.props) s+=psize(p); for(const k of n.kids) s+=nsize(k); if(n.kids.length||!n.props.length) s+=SENT; n._s=s; return s };\r\n  function writeNode(dv,u8,o,n){\r\n    const start=o, end=start+n._s; dv.setUint32(o,end,true); dv.setUint32(o+4,n.props.length,true);\r\n    let pl=0; for(const p of n.props) pl+=psize(p); dv.setUint32(o+8,pl,true); dv.setUint8(o+12,n.name.length); u8.set(n.name,o+13); o+=13+n.name.length;\r\n    for(const p of n.props){ dv.setUint8(o,p.t.charCodeAt(0)); o++;\r\n      switch(p.t){\r\n        case \"C\": dv.setUint8(o,p.v); o+=1; break;\r\n        case \"I\": dv.setInt32(o,p.v,true); o+=4; break;\r\n        case \"D\": dv.setFloat64(o,p.v,true); o+=8; break;\r\n        case \"L\": dv.setBigInt64(o,BigInt(p.v),true); o+=8; break;\r\n        case \"S\": case \"R\": dv.setUint32(o,p.v.length,true); u8.set(p.v,o+4); o+=4+p.v.length; break;\r\n        case \"i\": case \"d\": { const es=p.t===\"i\"?4:8; dv.setUint32(o,p.v.length,true); dv.setUint32(o+4,0,true); dv.setUint32(o+8,p.v.length*es,true); o+=12;\r\n          for(let i=0;i<p.v.length;i++){ if(es===4) dv.setInt32(o,p.v[i],true); else dv.setFloat64(o,p.v[i],true); o+=es } break }\r\n      } }\r\n    for(const k of n.kids) o=writeNode(dv,u8,o,k);\r\n    if(n.kids.length||!n.props.length) o+=SENT;\r\n    return o;\r\n  }\r\n  const HEAD=TE.encode(\"Kaydara FBX Binary  \\0\");\r\n  const FILE_ID=new Uint8Array([0x28,0xb3,0x2a,0xeb,0xb6,0x24,0xcc,0xc2,0xbf,0xc8,0xb0,0x2a,0xa9,0x2b,0xfc,0xf1]);\r\n  const FOOT_ID=new Uint8Array([0xfa,0xbc,0xab,0x09,0xd0,0xc8,0xd4,0x66,0xb1,0x76,0xfb,0x83,0x1c,0xf7,0x26,0x7e]);\r\n  const FOOT_MAGIC=new Uint8Array([0xf8,0x5a,0x8c,0x6a,0xde,0xf5,0xd9,0x7e,0xec,0xe9,0x0c,0xe3,0x75,0x8f,0x29,0x0b]);\r\n  function serialize(top){\r\n    let size=27; for(const n of top) size+=nsize(n); size+=SENT;\r\n    const pad0=size+16+4; let pad=((pad0+15)&~15)-pad0; if(pad===0) pad=16;\r\n    const total=pad0+pad+4+120+16;\r\n    const buf=new ArrayBuffer(total), dv=new DataView(buf), u8=new Uint8Array(buf);\r\n    u8.set(HEAD,0); u8[21]=0x1a; u8[22]=0; dv.setUint32(23,7400,true);\r\n    let o=27; for(const n of top) o=writeNode(dv,u8,o,n); o+=SENT;\r\n    u8.set(FOOT_ID,o); o+=16+4+pad; dv.setUint32(o,7400,true); o+=4+120; u8.set(FOOT_MAGIC,o);\r\n    return u8;\r\n  }\r\n  const P70=(...ps)=>N(\"Properties70\",[],ps.map(a=>N(\"P\",a)));\r\n  const nm=(n,cls)=>S(TE.encode(n.replace(\/\\0\/g,\"\")+\"\\0\\x01\"+cls));\r\n\r\n  async function texPNG(tex){\r\n    const im=tex&&tex.image; if(!im) return null; const flip=tex.flipY===false;\r\n    try{\r\n      const w=im.width||im.videoWidth, h=im.height||im.videoHeight; if(!w||!h) return null;\r\n      const c=document.createElement(\"canvas\"); c.width=w; c.height=h; const x=c.getContext(\"2d\");\r\n      if(im.data){ const id=x.createImageData(w,h); const d=im.data; if(d.length===w*h*4) id.data.set(d); else if(d.length===w*h*3){ for(let i=0;i<w*h;i++){ id.data[i*4]=d[i*3]; id.data[i*4+1]=d[i*3+1]; id.data[i*4+2]=d[i*3+2]; id.data[i*4+3]=255 } } else return null; x.putImageData(id,0,0) }\r\n      else { if(flip){ x.translate(0,h); x.scale(1,-1) } x.drawImage(im,0,0) }\r\n      const b=await new Promise(r=>c.toBlob(r,\"image\/png\")); return b?new Uint8Array(await b.arrayBuffer()):null;\r\n    }catch(e){ return null }\r\n  }\r\n\r\n  async function exportFBX(root,opt={}){\r\n    root.updateMatrixWorld(true);\r\n    let nextId=1000000; const id=()=>nextId++;\r\n    const objects=[], conns=[], matMap=new Map(); let nGeo=0,nModel=0,nMat=0,nTex=0,nVid=0;\r\n    const usedNames={}; const uname=b=>{ b=(b||\"Objet\").slice(0,60); if(!usedNames[b]){ usedNames[b]=1; return b } return b+\".\"+String(usedNames[b]++).padStart(3,\"0\") };\r\n    async function material(m){\r\n      if(matMap.has(m.uuid)) return matMap.get(m.uuid);\r\n      const mid=id(); nMat++; const c=m.color||{r:.8,g:.8,b:.8}, ei=m.emissiveIntensity==null?1:m.emissiveIntensity, e0=m.emissive||{r:0,g:0,b:0}, e={r:e0.r*ei,g:e0.g*ei,b:e0.b*ei}, op=m.transparent?m.opacity:1;\r\n      const sp=m.specular||{r:.2,g:.2,b:.2}, sh=m.shininess!=null?m.shininess:(m.roughness!=null?Math.max(2,(1-m.roughness)*100):20);\r\n      objects.push(N(\"Material\",[L(mid),nm(uname(m.name||\"Materiau\"),\"Material\"),S(\"\")],[\r\n        N(\"Version\",[I(102)]), N(\"ShadingModel\",[S(\"Phong\")]), N(\"MultiLayer\",[I(0)]),\r\n        P70([S(\"DiffuseColor\"),S(\"Color\"),S(\"\"),S(\"A\"),D(c.r),D(c.g),D(c.b)],[S(\"DiffuseFactor\"),S(\"Number\"),S(\"\"),S(\"A\"),D(1)],\r\n            [S(\"EmissiveColor\"),S(\"Color\"),S(\"\"),S(\"A\"),D(e.r),D(e.g),D(e.b)],[S(\"EmissiveFactor\"),S(\"Number\"),S(\"\"),S(\"A\"),D(1)],\r\n            [S(\"SpecularColor\"),S(\"Color\"),S(\"\"),S(\"A\"),D(sp.r),D(sp.g),D(sp.b)],[S(\"SpecularFactor\"),S(\"Number\"),S(\"\"),S(\"A\"),D(m.metalness!=null?.25+.5*m.metalness:.5)],\r\n            [S(\"ShininessExponent\"),S(\"Number\"),S(\"\"),S(\"A\"),D(sh)],[S(\"TransparencyFactor\"),S(\"Number\"),S(\"\"),S(\"A\"),D(1-op)],[S(\"Opacity\"),S(\"Number\"),S(\"\"),S(\"A\"),D(op)])\r\n      ]));\r\n      if(opt.textures!==false&&m.map){ const png=await texPNG(m.map); if(png){\r\n        const tid=id(), vid=id(), fn=uname((m.map.name||m.name||\"texture\").replace(\/[^\\w.-]+\/g,\"_\"))+\".png\"; nTex++; nVid++;\r\n        objects.push(N(\"Video\",[L(vid),nm(fn,\"Video\"),S(\"Clip\")],[N(\"Type\",[S(\"Clip\")]),P70([S(\"Path\"),S(\"KString\"),S(\"XRefUrl\"),S(\"\"),S(fn)]),N(\"UseMipMap\",[I(0)]),N(\"Filename\",[S(fn)]),N(\"RelativeFilename\",[S(fn)]),N(\"Content\",[R(png)])]));\r\n        objects.push(N(\"Texture\",[L(tid),nm(fn,\"Texture\"),S(\"\")],[N(\"Type\",[S(\"TextureVideoClip\")]),N(\"Version\",[I(202)]),N(\"TextureName\",[nm(fn,\"Texture\")]),\r\n          P70([S(\"UseMaterial\"),S(\"bool\"),S(\"\"),S(\"\"),I(1)]),N(\"Media\",[nm(fn,\"Video\")]),N(\"FileName\",[S(fn)]),N(\"RelativeFilename\",[S(fn)]),\r\n          N(\"ModelUVTranslation\",[D(0),D(0)]),N(\"ModelUVScaling\",[D(1),D(1)]),N(\"Texture_Alpha_Source\",[S(\"None\")]),N(\"Cropping\",[I(0),I(0),I(0),I(0)])]));\r\n        conns.push(N(\"C\",[S(\"OO\"),L(vid),L(tid)]), N(\"C\",[S(\"OP\"),L(tid),L(mid),S(\"DiffuseColor\")]));\r\n      } }\r\n      matMap.set(m.uuid,mid); return mid;\r\n    }\r\n    const meshes=[]; root.traverse(o=>{ if(o.isMesh&&o.geometry&&o.geometry.attributes.position&&o.visible!==false) meshes.push(o) });\r\n    const v=new THREE.Vector3(), nmx=new THREE.Matrix3();\r\n    for(const o of meshes){\r\n      const g=o.geometry, pos=g.attributes.position, nor=g.attributes.normal, uv=g.attributes.uv, col=g.attributes.color, idx=g.index;\r\n      const nv=pos.count, nt=idx?Math.floor(idx.count\/3):Math.floor(nv\/3); if(!nt) continue;\r\n      const mw=o.matrixWorld; nmx.getNormalMatrix(mw); const flip=mw.determinant()<0;\r\n      const V=new Float64Array(nv*3); for(let i=0;i<nv;i++){ v.fromBufferAttribute(pos,i).applyMatrix4(mw); V[i*3]=v.x; V[i*3+1]=v.y; V[i*3+2]=v.z }\r\n      const PV=new Int32Array(nt*3), corner=new Int32Array(nt*3);\r\n      for(let t=0;t<nt;t++){ let a=idx?idx.getX(t*3):t*3, b=idx?idx.getX(t*3+1):t*3+1, c=idx?idx.getX(t*3+2):t*3+2; if(flip){ const k=b; b=c; c=k }\r\n        corner[t*3]=a; corner[t*3+1]=b; corner[t*3+2]=c; PV[t*3]=a; PV[t*3+1]=b; PV[t*3+2]=~c }\r\n      const kids=[N(\"Properties70\"),N(\"GeometryVersion\",[I(124)]),N(\"Vertices\",[AD(V)]),N(\"PolygonVertexIndex\",[AI(PV)])];\r\n      const layer=[N(\"Version\",[I(100)])];\r\n      if(nor){ const NN=new Float64Array(nt*9); for(let k=0;k<nt*3;k++){ v.fromBufferAttribute(nor,corner[k]).applyMatrix3(nmx).normalize(); NN[k*3]=v.x; NN[k*3+1]=v.y; NN[k*3+2]=v.z }\r\n        kids.push(N(\"LayerElementNormal\",[I(0)],[N(\"Version\",[I(101)]),N(\"Name\",[S(\"\")]),N(\"MappingInformationType\",[S(\"ByPolygonVertex\")]),N(\"ReferenceInformationType\",[S(\"Direct\")]),N(\"Normals\",[AD(NN)])]));\r\n        layer.push(N(\"LayerElement\",[],[N(\"Type\",[S(\"LayerElementNormal\")]),N(\"TypedIndex\",[I(0)])])) }\r\n      if(col){ const CC=new Float64Array(nv*4); for(let i=0;i<nv;i++){ CC[i*4]=col.getX(i); CC[i*4+1]=col.getY(i); CC[i*4+2]=col.getZ(i); CC[i*4+3]=col.itemSize>3?col.getW(i):1 }\r\n        kids.push(N(\"LayerElementColor\",[I(0)],[N(\"Version\",[I(101)]),N(\"Name\",[S(\"Col\")]),N(\"MappingInformationType\",[S(\"ByPolygonVertex\")]),N(\"ReferenceInformationType\",[S(\"IndexToDirect\")]),N(\"Colors\",[AD(CC)]),N(\"ColorIndex\",[AI(corner)])]));\r\n        layer.push(N(\"LayerElement\",[],[N(\"Type\",[S(\"LayerElementColor\")]),N(\"TypedIndex\",[I(0)])])) }\r\n      if(uv){ const UU=new Float64Array(nv*2); for(let i=0;i<nv;i++){ UU[i*2]=uv.getX(i); UU[i*2+1]=uv.getY(i) }\r\n        kids.push(N(\"LayerElementUV\",[I(0)],[N(\"Version\",[I(101)]),N(\"Name\",[S(\"UVMap\")]),N(\"MappingInformationType\",[S(\"ByPolygonVertex\")]),N(\"ReferenceInformationType\",[S(\"IndexToDirect\")]),N(\"UV\",[AD(UU)]),N(\"UVIndex\",[AI(corner)])]));\r\n        layer.push(N(\"LayerElement\",[],[N(\"Type\",[S(\"LayerElementUV\")]),N(\"TypedIndex\",[I(0)])])) }\r\n      const mats=(Array.isArray(o.material)?o.material:[o.material]).filter(Boolean);\r\n      const MI=new Int32Array(Array.isArray(o.material)?nt:1);\r\n      if(Array.isArray(o.material)&&g.groups.length){ for(const gr of g.groups){ const t0=Math.floor(gr.start\/3), t1=Math.min(nt,Math.floor((gr.start+gr.count)\/3)); for(let t=t0;t<t1;t++) MI[t]=Math.min(gr.materialIndex||0,mats.length-1) } }\r\n      kids.push(N(\"LayerElementMaterial\",[I(0)],[N(\"Version\",[I(101)]),N(\"Name\",[S(\"\")]),N(\"MappingInformationType\",[S(MI.length===1?\"AllSame\":\"ByPolygon\")]),N(\"ReferenceInformationType\",[S(\"IndexToDirect\")]),N(\"Materials\",[AI(MI)])]));\r\n      layer.push(N(\"LayerElement\",[],[N(\"Type\",[S(\"LayerElementMaterial\")]),N(\"TypedIndex\",[I(0)])]));\r\n      kids.push(N(\"Layer\",[I(0)],layer));\r\n      const gid=id(), mid=id(), name=uname(o.name||(o.parent&&o.parent.name)||\"Objet\"); nGeo++; nModel++;\r\n      objects.push(N(\"Geometry\",[L(gid),nm(name,\"Geometry\"),S(\"Mesh\")],kids));\r\n      objects.push(N(\"Model\",[L(mid),nm(name,\"Model\"),S(\"Mesh\")],[N(\"Version\",[I(232)]),\r\n        P70([S(\"Lcl Translation\"),S(\"Lcl Translation\"),S(\"\"),S(\"A\"),D(0),D(0),D(0)],[S(\"Lcl Rotation\"),S(\"Lcl Rotation\"),S(\"\"),S(\"A\"),D(0),D(0),D(0)],[S(\"Lcl Scaling\"),S(\"Lcl Scaling\"),S(\"\"),S(\"A\"),D(1),D(1),D(1)],[S(\"DefaultAttributeIndex\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(0)]),\r\n        N(\"MultiLayer\",[I(0)]),N(\"MultiTake\",[I(0)]),N(\"Shading\",[C(true)]),N(\"Culling\",[S(\"CullingOff\")])]));\r\n      conns.push(N(\"C\",[S(\"OO\"),L(mid),L(0)]), N(\"C\",[S(\"OO\"),L(gid),L(mid)]));\r\n      for(const m of (mats.length?mats:[new THREE.MeshPhongMaterial({color:0xcccccc})])){ const mm=await material(m); conns.push(N(\"C\",[S(\"OO\"),L(mm),L(mid)])) }\r\n    }\r\n    if(!nModel) throw new Error(\"Aucun maillage \u00e0 exporter\");\r\n    const now=new Date(), app=opt.creator||\"OK3D Format Lab\";\r\n    const def=(t,c)=>N(\"ObjectType\",[S(t)],[N(\"Count\",[I(c)])]);\r\n    const defs=[def(\"GlobalSettings\",1),def(\"Model\",nModel),def(\"Geometry\",nGeo),def(\"Material\",nMat)]; if(nTex) defs.push(def(\"Texture\",nTex),def(\"Video\",nVid));\r\n    const top=[\r\n      N(\"FBXHeaderExtension\",[],[N(\"FBXHeaderVersion\",[I(1003)]),N(\"FBXVersion\",[I(7400)]),N(\"EncryptionType\",[I(0)]),\r\n        N(\"CreationTimeStamp\",[],[N(\"Version\",[I(1000)]),N(\"Year\",[I(now.getFullYear())]),N(\"Month\",[I(now.getMonth()+1)]),N(\"Day\",[I(now.getDate())]),N(\"Hour\",[I(now.getHours())]),N(\"Minute\",[I(now.getMinutes())]),N(\"Second\",[I(now.getSeconds())]),N(\"Millisecond\",[I(0)])]),\r\n        N(\"Creator\",[S(app)]),\r\n        N(\"SceneInfo\",[nm(\"GlobalInfo\",\"SceneInfo\"),S(\"UserData\")],[N(\"Type\",[S(\"UserData\")]),N(\"Version\",[I(100)]),\r\n          N(\"MetaData\",[],[N(\"Version\",[I(100)]),N(\"Title\",[S(\"\")]),N(\"Subject\",[S(\"\")]),N(\"Author\",[S(\"\")]),N(\"Keywords\",[S(\"\")]),N(\"Revision\",[S(\"\")]),N(\"Comment\",[S(\"\")])]),\r\n          P70([S(\"DocumentUrl\"),S(\"KString\"),S(\"Url\"),S(\"\"),S(opt.fileName||\"export.fbx\")],[S(\"SrcDocumentUrl\"),S(\"KString\"),S(\"Url\"),S(\"\"),S(opt.fileName||\"export.fbx\")],\r\n            [S(\"Original\"),S(\"Compound\"),S(\"\"),S(\"\")],[S(\"Original|ApplicationVendor\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"OK3D\")],[S(\"Original|ApplicationName\"),S(\"KString\"),S(\"\"),S(\"\"),S(app)],\r\n            [S(\"Original|ApplicationVersion\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"1.0\")],[S(\"Original|DateTime_GMT\"),S(\"DateTime\"),S(\"\"),S(\"\"),S(\"01\/01\/1970 00:00:00.000\")],[S(\"Original|FileName\"),S(\"KString\"),S(\"\"),S(\"\"),S(opt.fileName||\"export.fbx\")],\r\n            [S(\"LastSaved\"),S(\"Compound\"),S(\"\"),S(\"\")],[S(\"LastSaved|ApplicationVendor\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"OK3D\")],[S(\"LastSaved|ApplicationName\"),S(\"KString\"),S(\"\"),S(\"\"),S(app)],\r\n            [S(\"LastSaved|ApplicationVersion\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"1.0\")],[S(\"LastSaved|DateTime_GMT\"),S(\"DateTime\"),S(\"\"),S(\"\"),S(\"01\/01\/1970 00:00:00.000\")])])]),\r\n      N(\"FileId\",[R(FILE_ID)]), N(\"CreationTime\",[S(\"1970-01-01 10:00:00:000\")]), N(\"Creator\",[S(app)]),\r\n      N(\"GlobalSettings\",[],[N(\"Version\",[I(1000)]),P70(\r\n        [S(\"UpAxis\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(1)],[S(\"UpAxisSign\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(1)],[S(\"FrontAxis\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(2)],[S(\"FrontAxisSign\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(1)],\r\n        [S(\"CoordAxis\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(0)],[S(\"CoordAxisSign\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(1)],[S(\"OriginalUpAxis\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(-1)],[S(\"OriginalUpAxisSign\"),S(\"int\"),S(\"Integer\"),S(\"\"),I(1)],\r\n        [S(\"UnitScaleFactor\"),S(\"double\"),S(\"Number\"),S(\"\"),D(opt.unitScale||100)],[S(\"OriginalUnitScaleFactor\"),S(\"double\"),S(\"Number\"),S(\"\"),D(opt.unitScale||100)],\r\n        [S(\"AmbientColor\"),S(\"ColorRGB\"),S(\"Color\"),S(\"\"),D(0),D(0),D(0)],[S(\"DefaultCamera\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"Producer Perspective\")],[S(\"TimeMode\"),S(\"enum\"),S(\"\"),S(\"\"),I(11)],\r\n        [S(\"TimeSpanStart\"),S(\"KTime\"),S(\"Time\"),S(\"\"),L(0)],[S(\"TimeSpanStop\"),S(\"KTime\"),S(\"Time\"),S(\"\"),L(46186158000)],[S(\"CustomFrameRate\"),S(\"double\"),S(\"Number\"),S(\"\"),D(24)])]),\r\n      N(\"Documents\",[],[N(\"Count\",[I(1)]),N(\"Document\",[L(id()),S(\"\"),S(\"Scene\")],[P70([S(\"SourceObject\"),S(\"object\"),S(\"\"),S(\"\")],[S(\"ActiveAnimStackName\"),S(\"KString\"),S(\"\"),S(\"\"),S(\"\")]),N(\"RootNode\",[L(0)])])]),\r\n      N(\"References\"),\r\n      N(\"Definitions\",[],[N(\"Version\",[I(100)]),N(\"Count\",[I(defs.reduce((a,d)=>a+d.kids[0].props[0].v,0))]),...defs]),\r\n      N(\"Objects\",[],objects),\r\n      N(\"Connections\",[],conns),\r\n      N(\"Takes\",[],[N(\"Current\",[S(\"\")])])\r\n    ];\r\n    return serialize(top);\r\n  }\r\n  return {exportFBX};\r\n})();\r\n\r\n\/* =====================================================================\r\n   CONFIG\r\n   ===================================================================== *\/\r\nconst CONFIG={ brand:\"OK3D\", contactUrl:\"https:\/\/www.ok3d.fr\/#contact\", cdn:\"https:\/\/cdn.jsdelivr.net\/npm\/\", maxFileMB:600 };\r\n\r\nconst $=s=>OKK.querySelector(s);\r\nconst clamp=(v,a,b)=>Math.max(a,Math.min(b,v));\r\nconst esc=s=>String(s).replace(\/[&<>\"']\/g,c=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\",\"'\":\"&#39;\"}[c]));\r\nconst sleep=ms=>new Promise(r=>setTimeout(r,ms));\r\nconst nextFrame=()=>new Promise(r=>requestAnimationFrame(()=>setTimeout(r,0)));\r\nconst fmtB=n=>n<1024?n+\" o\":n<1048576?(n\/1024).toFixed(n<10240?1:0).replace(\".\",\",\")+\" Ko\":(n\/1048576).toFixed(n<10485760?1:0).replace(\".\",\",\")+\" Mo\";\r\nconst extOf=n=>(n.includes(\".\")?n.split(\".\").pop():\"\").toLowerCase();\r\nconst baseOf=n=>n.replace(\/\\.[^.]+$\/,\"\")||\"fichier\";\r\nconst msg=e=>(e&&e.message)?e.message:\"Conversion impossible\";\r\nfunction toast(t){ const d=document.createElement(\"div\"); d.className=\"toast\"; d.textContent=t; $(\"#okk\").appendChild(d); setTimeout(()=>d.remove(),3200) }\r\nfunction mkCanvas(w,h){ const c=document.createElement(\"canvas\"); c.width=Math.max(1,Math.round(w)); c.height=Math.max(1,Math.round(h)); return c }\r\nconst blobOf=(c,type,q)=>new Promise((res,rej)=>{ try{ c.toBlob(b=>b?res(b):rej(new Error(\"Encodage impossible (image trop grande ?)\")),type,q) }catch(e){ rej(e) } });\r\nconst ICON={\r\n  go:'<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 8h10M9 4l4 4-4 4\"\/><\/svg>',\r\n  dl:'<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M8 2.5v8M4.5 7.5L8 11l3.5-3.5M3 13.5h10\"\/><\/svg>',\r\n  rm:'<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\"><path d=\"M4 4l8 8M12 4l-8 8\"\/><\/svg>',\r\n  retry:'<svg viewBox=\"0 0 16 16\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.8\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M13 8a5 5 0 11-1.5-3.6M13 2.5v3h-3\"\/><\/svg>'\r\n};\r\n\r\n\/* =====================================================================\r\n   FORMATS\r\n   ===================================================================== *\/\r\nconst CATS={\r\n  img:{name:\"Images\",sub:\"Photos, rendus, textures, HDRI\",c:\"var(--c-img)\",ins:[\"jpg\",\"jpeg\",\"png\",\"webp\",\"avif\",\"heic\",\"heif\",\"gif\",\"bmp\",\"tif\",\"tiff\",\"psd\",\"tga\",\"ico\",\"hdr\",\"exr\"],outs:[\"png\",\"jpg\",\"webp\",\"avif\",\"gif\",\"bmp\",\"tiff\",\"tga\",\"ico\",\"psd\",\"pdf\",\"svg\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"4\" width=\"18\" height=\"16\" rx=\"3\"\/><circle cx=\"9\" cy=\"10\" r=\"2\"\/><path d=\"M4 18l5-5 4 4 3-3 4 4\"\/><\/svg>'},\r\n  vec:{name:\"Vectoriel\",sub:\"Logos, ic\u00f4nes, illustrations\",c:\"var(--c-vec)\",ins:[\"svg\"],outs:[\"png\",\"jpg\",\"webp\",\"avif\",\"pdf\",\"svg\",\"ico\",\"gif\",\"tiff\",\"psd\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linejoin=\"round\"><path d=\"M4 19C8 7 16 17 20 5\"\/><rect x=\"2.5\" y=\"17.5\" width=\"3\" height=\"3\" rx=\".6\"\/><rect x=\"18.5\" y=\"3.5\" width=\"3\" height=\"3\" rx=\".6\"\/><path d=\"M4 19l6-11\"\/><\/svg>'},\r\n  pdf:{name:\"PDF & Illustrator\",sub:\"Documents, maquettes, fichiers .ai\",c:\"var(--c-pdf)\",ins:[\"pdf\",\"ai\"],outs:[\"png\",\"jpg\",\"webp\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linejoin=\"round\"><path d=\"M6 2.5h8l5 5V20a1.5 1.5 0 01-1.5 1.5h-11A1.5 1.5 0 015 20V4a1.5 1.5 0 011-1.5z\"\/><path d=\"M14 2.5V8h5M8.5 13h7M8.5 16.5h5\"\/><\/svg>'},\r\n  m3d:{name:\"Mod\u00e8les 3D\",sub:\"Blender, Maya, C4D, Unity, impression 3D\",c:\"var(--c-m3d)\",ins:[\"fbx\",\"obj\",\"glb\",\"gltf\",\"stl\",\"ply\",\"dae\",\"3ds\",\"3mf\"],outs:[\"glb\",\"gltf\",\"fbx\",\"obj\",\"stl\",\"ply\",\"dae\",\"usdz\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linejoin=\"round\"><path d=\"M12 2.5l8.5 4.8v9.4L12 21.5l-8.5-4.8V7.3z\"\/><path d=\"M3.5 7.3L12 12l8.5-4.7M12 12v9.5\"\/><\/svg>'},\r\n  vid:{name:\"Vid\u00e9o\",sub:\"Motion, animations, rushes\",c:\"var(--c-vid)\",ins:[\"mp4\",\"mov\",\"webm\",\"m4v\"],outs:[\"gif\",\"mp4\",\"webm\",\"png\",\"jpg\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linejoin=\"round\"><rect x=\"2.5\" y=\"5\" width=\"14\" height=\"14\" rx=\"3\"\/><path d=\"M16.5 10l5-3v10l-5-3\"\/><\/svg>'},\r\n  font:{name:\"Polices\",sub:\"Typographies pour le web et le print\",c:\"var(--c-font)\",ins:[\"ttf\",\"otf\",\"woff\"],outs:[\"woff\",\"ttf\",\"otf\",\"zip\"],\r\n    icon:'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 20L10 4h1l6 16M6.5 14h8\"\/><path d=\"M17 9.5c.6-.9 1.5-1.4 2.6-1.2 1 .2 1.4 1 1.4 2V20M21 14.5c-2.6-.4-4.6.3-4.6 2.8 0 1.6 1.2 2.6 2.5 2.4 1.3-.2 2.1-1.3 2.1-2.8\"\/><\/svg>'}\r\n};\r\nconst ALIAS=[\"jpeg\",\"heif\",\"tif\",\"m4v\"];\r\nconst catOf=e=>Object.keys(CATS).find(k=>CATS[k].ins.includes(e));\r\nconst OUTX={jpg:\"jpg\",tiff:\"tif\",zip:\"zip\"};\r\nconst CAN={};\r\nconst okOut=f=>!(f in CAN)||CAN[f];\r\nfunction outLabel(it,f){\r\n  if(f===\"svg\") return it.cat===\"vec\"?\"SVG optimis\u00e9\":\"SVG vectoris\u00e9\";\r\n  if(f===\"zip\") return \"Kit web\";\r\n  if(it.cat===\"vid\"&&(f===\"png\"||f===\"jpg\")) return f.toUpperCase()+\" (image)\";\r\n  if(it.cat===\"pdf\"&&it.pages>1&&S.pages===\"all\") return f.toUpperCase()+\" \u00d7\"+it.pages;\r\n  return f===\"tiff\"?\"TIFF\":f.toUpperCase();\r\n}\r\nfunction outsFor(it){\r\n  let o=CATS[it.cat].outs.filter(okOut);\r\n  if(it.cat===\"font\") o=o.filter(f=>f!==(it.font&&it.font.kind===\"otf\"?\"ttf\":\"otf\"));\r\n  return o;\r\n}\r\nfunction defTarget(it){\r\n  const e=it.ext;\r\n  switch(it.cat){\r\n    case \"img\": return [\"heic\",\"heif\"].includes(e)?\"jpg\":[\"jpg\",\"jpeg\"].includes(e)?\"png\":e===\"png\"?\"jpg\":\"png\";\r\n    case \"vec\": case \"pdf\": return \"png\";\r\n    case \"m3d\": return e===\"glb\"?\"fbx\":\"glb\";\r\n    case \"vid\": return \"gif\";\r\n    case \"font\": return e===\"woff\"?\"ttf\":\"woff\";\r\n  }\r\n}\r\n\r\n\/* =====================================================================\r\n   LIBRARIES (lazy, loaded only when a format needs them)\r\n   ===================================================================== *\/\r\nconst V3=\"three@0.128.0\/\";\r\nconst LIB={ three:[V3+\"build\/three.min.js\"], fflate:[V3+\"examples\/js\/libs\/fflate.min.js\"], utif:[\"pako@2.1.0\/dist\/pako.min.js\",\"utif@3.1.0\/UTIF.js\"],\r\n  heic:[\"heic2any@0.0.4\/dist\/heic2any.min.js\"], psd:[\"ag-psd@24.0.0\/dist\/bundle.js\"], jspdf:[\"jspdf@2.5.1\/dist\/jspdf.umd.min.js\"],\r\n  svg2pdf:[\"svg2pdf.js@2.2.3\/dist\/svg2pdf.umd.min.js\"], pdfjs:[\"pdfjs-dist@3.11.174\/build\/pdf.min.js\"], tracer:[\"imagetracerjs@1.2.6\/imagetracer_v1.2.6.js\"] };\r\nconst scriptP={};\r\nfunction loadScript(u){\r\n  u=\/^https?:\/.test(u)?u:CONFIG.cdn+u;\r\n  return scriptP[u]||(scriptP[u]=new Promise((res,rej)=>{\r\n    const sc=document.createElement(\"script\"); sc.src=u; sc.async=true;\r\n    const amd=window.define&&window.define.amd?window.define:null; if(amd) window.define=undefined; \/\/ keep UMD bundles global on sites using AMD\r\n    const fin=()=>{ if(amd) window.define=amd };\r\n    sc.onload=()=>{ fin(); res() }; sc.onerror=()=>{ fin(); delete scriptP[u]; rej(new Error(\"Module de conversion indisponible (connexion ?)\")) };\r\n    document.head.appendChild(sc) }));\r\n}\r\nasync function need(...names){ for(const n of names) for(const u of LIB[n]) await loadScript(u) }\r\nasync function needEx(...paths){ await need(\"three\"); for(const p of paths) await loadScript(V3+\"examples\/js\/\"+p) }\r\nlet GIFE=null; const gifenc=async()=>GIFE||(GIFE=await import(CONFIG.cdn+\"gifenc@1.0.3\/dist\/gifenc.esm.js\"));\r\n\r\n\/* =====================================================================\r\n   SETTINGS\r\n   ===================================================================== *\/\r\nconst S={ q:88, size:\"orig\", bg:\"#FFFFFF\", ico:[16,32,48,256], colors:16, exposure:0, dpi:150, pages:\"all\", pdfAlpha:false,\r\n  scale:1, up:\"keep\", ground:false, anim:true, fps:15, gw:480, start:0, dur:5 };\r\n\r\n\/* =====================================================================\r\n   IMAGE DECODING\r\n   ===================================================================== *\/\r\nfunction imgToCanvas(b){ return new Promise((res,rej)=>{ const u=URL.createObjectURL(b), im=new Image();\r\n  im.onload=()=>{ const w=im.naturalWidth||1024,h=im.naturalHeight||1024; if(w*h>200e6){ URL.revokeObjectURL(u); rej(new Error(\"Image trop grande pour le navigateur\")); return }\r\n    const c=mkCanvas(w,h); c.getContext(\"2d\").drawImage(im,0,0,w,h); URL.revokeObjectURL(u); res(c) };\r\n  im.onerror=()=>{ URL.revokeObjectURL(u); rej(new Error(\"Ce navigateur ne sait pas lire ce fichier\")) }; im.src=u }) }\r\nfunction decodeTGA(buf){\r\n  const d=new Uint8Array(buf), idl=d[0], cmt=d[1], type=d[2], w=d[12]|d[13]<<8, h=d[14]|d[15]<<8, bpp=d[16], desc=d[17];\r\n  if(cmt||![2,3,10,11].includes(type)) throw new Error(\"Variante de TGA non prise en charge\");\r\n  const Bp=bpp>>3, gray=type===3||type===11, out=new Uint8ClampedArray(w*h*4); let p=18+idl, k=0; const n=w*h;\r\n  const put=o=>{ if(gray){ out[o]=out[o+1]=out[o+2]=d[p]; out[o+3]=Bp===2?d[p+1]:255 }\r\n    else if(Bp===2){ const v=d[p]|d[p+1]<<8; out[o]=((v>>10)&31)*8.226; out[o+1]=((v>>5)&31)*8.226; out[o+2]=(v&31)*8.226; out[o+3]=255 }\r\n    else { out[o]=d[p+2]; out[o+1]=d[p+1]; out[o+2]=d[p]; out[o+3]=Bp===4?d[p+3]:255 } };\r\n  if(type<9){ for(;k<n;k++){ put(k*4); p+=Bp } }\r\n  else { while(k<n&&p<d.length){ const hd=d[p++], cnt=(hd&127)+1; if(hd&128){ for(let j=0;j<cnt&&k<n;j++){ put(k*4); k++ } p+=Bp } else { for(let j=0;j<cnt&&k<n;j++){ put(k*4); p+=Bp; k++ } } } }\r\n  if(Bp===4){ let any=false; for(let i=3;i<out.length;i+=4) if(out[i]){ any=true; break } if(!any) for(let i=3;i<out.length;i+=4) out[i]=255 }\r\n  const t=mkCanvas(w,h); t.getContext(\"2d\").putImageData(new ImageData(out,w,h),0,0); if(desc&32) return t;\r\n  const c=mkCanvas(w,h), x=c.getContext(\"2d\"); x.translate(0,h); x.scale(1,-1); x.drawImage(t,0,0); return c;\r\n}\r\nfunction readPSD(buf){\r\n  let psd=agPsd.readPsd(buf,{skipLayerImageData:true,skipThumbnail:true}); if(psd.canvas) return psd.canvas;\r\n  psd=agPsd.readPsd(buf,{skipThumbnail:true}); const c=mkCanvas(psd.width,psd.height), x=c.getContext(\"2d\");\r\n  const draw=ls=>{ for(const l of ls||[]){ if(l.hidden) continue; if(l.children){ draw(l.children); continue } if(l.canvas){ x.globalAlpha=l.opacity==null?1:l.opacity; x.drawImage(l.canvas,l.left||0,l.top||0) } } x.globalAlpha=1 };\r\n  draw(psd.children); return c;\r\n}\r\nasync function decodeHDR(it){\r\n  if(it.hdr) return it.hdr; const exr=it.ext===\"exr\";\r\n  if(exr) await need(\"fflate\"); await needEx(exr?\"loaders\/EXRLoader.js\":\"loaders\/RGBELoader.js\");\r\n  const L=exr?new THREE.EXRLoader():new THREE.RGBELoader(); L.setDataType(THREE.FloatType);\r\n  const r=L.parse(await it.file.arrayBuffer()); if(!r||!r.data) throw new Error(\"Fichier HDR illisible\");\r\n  return it.hdr={w:r.width,h:r.height,data:r.data,ch:Math.round(r.data.length\/(r.width*r.height)),flip:exr};\r\n}\r\nfunction toneMap(h,ev){\r\n  const k=Math.pow(2,ev)*.6, c=mkCanvas(h.w,h.h), x=c.getContext(\"2d\"), id=x.createImageData(h.w,h.h), o=id.data, d=h.data, ch=h.ch;\r\n  const lut=new Uint8ClampedArray(4096); for(let i=0;i<4096;i++){ const v=i\/4095; lut[i]=(v<=.0031308?v*12.92:1.055*Math.pow(v,1\/2.4)-.055)*255+.5 }\r\n  const tm=v=>{ v*=k; if(!(v>0)) return 0; const r=(v*(2.51*v+.03))\/(v*(2.43*v+.59)+.14); return lut[(r>1?1:r)*4095|0] };\r\n  for(let y=0;y<h.h;y++){ const sy=h.flip?h.h-1-y:y; for(let xx=0;xx<h.w;xx++){ const s=(sy*h.w+xx)*ch, t=(y*h.w+xx)*4; o[t]=tm(d[s]); o[t+1]=tm(d[s+1]); o[t+2]=tm(d[s+2]); o[t+3]=255 } }\r\n  x.putImageData(id,0,0); return c;\r\n}\r\nasync function decodeImage(it){\r\n  const e=it.ext;\r\n  if(e===\"hdr\"||e===\"exr\") return toneMap(await decodeHDR(it),S.exposure);\r\n  if(it.cache) return it.cache;\r\n  let c;\r\n  if(e===\"tif\"||e===\"tiff\"){ await need(\"utif\"); const buf=await it.file.arrayBuffer(); const ifds=UTIF.decode(buf); if(!ifds.length) throw new Error(\"TIFF illisible\");\r\n    let best=ifds[0]; ifds.forEach(f=>{ const a=(f.t256?f.t256[0]:0)*(f.t257?f.t257[0]:0), b=(best.t256?best.t256[0]:0)*(best.t257?best.t257[0]:0); if(a>b) best=f });\r\n    UTIF.decodeImage(buf,best); const rgba=UTIF.toRGBA8(best); c=mkCanvas(best.width,best.height); c.getContext(\"2d\").putImageData(new ImageData(new Uint8ClampedArray(rgba.buffer,rgba.byteOffset,best.width*best.height*4),best.width,best.height),0,0) }\r\n  else if(e===\"heic\"||e===\"heif\"){ try{ c=await imgToCanvas(it.file) }catch(_){ await need(\"heic\"); let b=await heic2any({blob:it.file,toType:\"image\/png\"}); if(Array.isArray(b)) b=b[0]; c=await imgToCanvas(b) } }\r\n  else if(e===\"psd\"){ await need(\"psd\"); c=readPSD(await it.file.arrayBuffer()) }\r\n  else if(e===\"tga\"){ c=decodeTGA(await it.file.arrayBuffer()) }\r\n  else c=await imgToCanvas(it.file);\r\n  return it.cache=c;\r\n}\r\n\r\n\/* ---------- SVG ---------- *\/\r\nconst UNIT={px:1,pt:4\/3,pc:16,mm:96\/25.4,cm:96\/2.54,in:96,\"\":1};\r\nfunction svgLen(v){ const m=\/^\\s*([\\d.]+)\\s*(px|pt|pc|mm|cm|in)?\\s*$\/.exec(v||\"\"); return m?parseFloat(m[1])*UNIT[m[2]||\"\"]:0 }\r\nasync function svgInfo(it){\r\n  if(it.svg) return it.svg;\r\n  const text=await it.file.text(); const doc=new DOMParser().parseFromString(text,\"image\/svg+xml\"); const el=doc.documentElement;\r\n  if(!el||el.nodeName.toLowerCase()!==\"svg\"||doc.getElementsByTagName(\"parsererror\").length) throw new Error(\"SVG invalide\");\r\n  doc.querySelectorAll(\"script,foreignObject\").forEach(n=>n.remove());\r\n  doc.querySelectorAll(\"*\").forEach(n=>{ for(const a of [...n.attributes]) if(\/^on\/i.test(a.name)) n.removeAttribute(a.name) });\r\n  const vb=(el.getAttribute(\"viewBox\")||\"\").split(\/[\\s,]+\/).map(Number);\r\n  let w=svgLen(el.getAttribute(\"width\")), h=svgLen(el.getAttribute(\"height\"));\r\n  if(vb.length===4&&vb[2]>0&&vb[3]>0){ if(!w&&!h){ const k=1024\/Math.max(vb[2],vb[3]); w=vb[2]*k; h=vb[3]*k } else if(!w) w=h*vb[2]\/vb[3]; else if(!h) h=w*vb[3]\/vb[2] }\r\n  if(!w||!h){ w=w||1024; h=h||1024 }\r\n  if(Math.max(w,h)<64){ const k=512\/Math.max(w,h); w*=k; h*=k }\r\n  if(!el.getAttribute(\"viewBox\")&&svgLen(el.getAttribute(\"width\"))&&svgLen(el.getAttribute(\"height\"))) el.setAttribute(\"viewBox\",`0 0 ${svgLen(el.getAttribute(\"width\"))} ${svgLen(el.getAttribute(\"height\"))}`);\r\n  return it.svg={doc,text,w:Math.round(w),h:Math.round(h)};\r\n}\r\nasync function rasterSVG(it,opt){\r\n  const info=await svgInfo(it); const [W,H]=sizeTarget(info.w,info.h,opt);\r\n  const el=info.doc.documentElement.cloneNode(true); el.setAttribute(\"width\",W); el.setAttribute(\"height\",H);\r\n  const blob=new Blob([new XMLSerializer().serializeToString(el)],{type:\"image\/svg+xml\"});\r\n  const u=URL.createObjectURL(blob);\r\n  try{ const im=await new Promise((res,rej)=>{ const i=new Image(); i.onload=()=>res(i); i.onerror=()=>rej(new Error(\"Ce SVG ne peut pas \u00eatre rendu\")); i.src=u });\r\n    const c=mkCanvas(W,H); c.getContext(\"2d\").drawImage(im,0,0,c.width,c.height); return c } finally{ URL.revokeObjectURL(u) }\r\n}\r\nfunction optimizeSVG(info){\r\n  const doc=info.doc.cloneNode(true), root=doc.documentElement;\r\n  const PFX=\/^(sodipodi|inkscape|i|x|graph|sketch|serif|figma|a|adobe|dc|cc|rdf)[:]\/i;\r\n  const round=v=>v.replace(\/(-?\\d*\\.\\d{3})\\d+\/g,(m,k)=>String(+k));\r\n  const NUM=\/^(d|points|transform|x|y|x1|x2|y1|y2|cx|cy|r|rx|ry|width|height|viewBox|offset|stroke-width|gradientTransform|fx|fy)$\/;\r\n  const attrs=n=>{ for(const a of [...n.attributes]){ if(PFX.test(a.name)||a.name===\"xml:space\"||a.name===\"data-name\"||a.name===\"enable-background\"||(\/^xmlns:\/.test(a.name)&&a.name!==\"xmlns:xlink\")) n.removeAttribute(a.name); else if(NUM.test(a.name)) n.setAttribute(a.name,round(a.value)) } };\r\n  const walk=n=>{ for(const ch of [...n.childNodes]){\r\n    if(ch.nodeType===8||ch.nodeType===7){ ch.remove(); continue }\r\n    if(ch.nodeType===3){ if(!ch.textContent.trim()&&!\/^(text|tspan|textPath)$\/i.test(n.nodeName)) ch.remove(); continue }\r\n    if(ch.nodeType===1){ if(\/^(metadata|title|desc)$\/i.test(ch.localName)||PFX.test(ch.nodeName)){ ch.remove(); continue } attrs(ch); walk(ch) } } };\r\n  attrs(root); walk(root);\r\n  return new XMLSerializer().serializeToString(root).replace(\/>\\s+<\/g,\"><\");\r\n}\r\n\r\n\/* =====================================================================\r\n   IMAGE ENCODING\r\n   ===================================================================== *\/\r\nfunction sizeTarget(w,h,opt){\r\n  let k=1; if(opt===\"x2\") k=2; else if(opt===\"x4\") k=4; else if(opt===\"half\") k=.5; else if(opt!==\"orig\"){ const m=+opt; k=Math.min(1,m\/Math.max(w,h)) }\r\n  k=Math.min(k,16384\/Math.max(w,h)); return [Math.max(1,Math.round(w*k)),Math.max(1,Math.round(h*k))];\r\n}\r\nfunction scaleTo(c,W,H){\r\n  if(W===c.width&&H===c.height) return c; let src=c;\r\n  while(src.width>W*2&&src.height>H*2){ const t=mkCanvas(src.width\/2,src.height\/2), x=t.getContext(\"2d\"); x.imageSmoothingQuality=\"high\"; x.drawImage(src,0,0,t.width,t.height); src=t }\r\n  const o=mkCanvas(W,H), x=o.getContext(\"2d\"); x.imageSmoothingQuality=\"high\"; x.drawImage(src,0,0,W,H); return o;\r\n}\r\nconst resize=(c,opt)=>{ const [W,H]=sizeTarget(c.width,c.height,opt); return scaleTo(c,W,H) };\r\nconst pixels=c=>c.getContext(\"2d\").getImageData(0,0,c.width,c.height).data;\r\nfunction alphaIn(d){ for(let i=3;i<d.length;i+=4) if(d[i]<255) return true; return false }\r\nfunction flatten(c){ const o=mkCanvas(c.width,c.height), x=o.getContext(\"2d\"); x.fillStyle=S.bg; x.fillRect(0,0,o.width,o.height); x.drawImage(c,0,0); return o }\r\nfunction encBMP(c){\r\n  const w=c.width,h=c.height,d=pixels(c),a=alphaIn(d);\r\n  if(!a){ const row=(w*3+3)&~3, size=54+row*h, b=new ArrayBuffer(size), v=new DataView(b), u=new Uint8Array(b);\r\n    v.setUint16(0,0x4D42,true); v.setUint32(2,size,true); v.setUint32(10,54,true); v.setUint32(14,40,true); v.setInt32(18,w,true); v.setInt32(22,h,true); v.setUint16(26,1,true); v.setUint16(28,24,true); v.setUint32(34,row*h,true); v.setInt32(38,2835,true); v.setInt32(42,2835,true);\r\n    for(let y=0;y<h;y++){ let o=54+(h-1-y)*row; for(let x=0;x<w;x++){ const s=(y*w+x)*4; u[o++]=d[s+2]; u[o++]=d[s+1]; u[o++]=d[s] } }\r\n    return new Blob([b],{type:\"image\/bmp\"}) }\r\n  const off=122, size=off+w*h*4, b=new ArrayBuffer(size), v=new DataView(b), u=new Uint8Array(b);\r\n  v.setUint16(0,0x4D42,true); v.setUint32(2,size,true); v.setUint32(10,off,true); v.setUint32(14,108,true); v.setInt32(18,w,true); v.setInt32(22,h,true); v.setUint16(26,1,true); v.setUint16(28,32,true); v.setUint32(30,3,true); v.setUint32(34,w*h*4,true);\r\n  v.setInt32(38,2835,true); v.setInt32(42,2835,true); v.setUint32(54,0x00FF0000,true); v.setUint32(58,0x0000FF00,true); v.setUint32(62,0x000000FF,true); v.setUint32(66,0xFF000000,true); v.setUint32(70,0x73524742,true);\r\n  for(let y=0;y<h;y++){ let o=off+(h-1-y)*w*4; for(let x=0;x<w;x++){ const s=(y*w+x)*4; u[o++]=d[s+2]; u[o++]=d[s+1]; u[o++]=d[s]; u[o++]=d[s+3] } }\r\n  return new Blob([b],{type:\"image\/bmp\"});\r\n}\r\nfunction encTGA(c){\r\n  const w=c.width,h=c.height,d=pixels(c),u=new Uint8Array(18+w*h*4); u[2]=2; u[12]=w&255; u[13]=w>>8; u[14]=h&255; u[15]=h>>8; u[16]=32; u[17]=0x28;\r\n  for(let i=0,o=18;i<w*h;i++){ const s=i*4; u[o++]=d[s+2]; u[o++]=d[s+1]; u[o++]=d[s]; u[o++]=d[s+3] }\r\n  return new Blob([u],{type:\"image\/x-tga\"});\r\n}\r\nasync function encICO(c){\r\n  const sizes=[...S.ico].sort((a,b)=>a-b); if(!sizes.length) sizes.push(32); const pngs=[];\r\n  for(const s of sizes){ const k=s\/Math.max(c.width,c.height), r=scaleTo(c,Math.max(1,Math.round(c.width*k)),Math.max(1,Math.round(c.height*k))), t=mkCanvas(s,s), x=t.getContext(\"2d\");\r\n    x.imageSmoothingQuality=\"high\"; x.drawImage(r,Math.round((s-r.width)\/2),Math.round((s-r.height)\/2)); pngs.push(new Uint8Array(await (await blobOf(t,\"image\/png\")).arrayBuffer())) }\r\n  const n=pngs.length, head=6+16*n, u=new Uint8Array(head+pngs.reduce((a,p)=>a+p.length,0)), v=new DataView(u.buffer); v.setUint16(2,1,true); v.setUint16(4,n,true); let off=head;\r\n  pngs.forEach((p,i)=>{ const s=sizes[i], e=6+16*i; u[e]=s>=256?0:s; u[e+1]=s>=256?0:s; v.setUint16(e+4,1,true); v.setUint16(e+6,32,true); v.setUint32(e+8,p.length,true); v.setUint32(e+12,off,true); u.set(p,off); off+=p.length });\r\n  return new Blob([u],{type:\"image\/x-icon\"});\r\n}\r\nasync function encGIF(c){\r\n  const {GIFEncoder,quantize,applyPalette}=await gifenc(); const d=pixels(c), a=alphaIn(d), fm=a?\"rgba4444\":\"rgb565\";\r\n  const pal=quantize(d,256,{format:fm,oneBitAlpha:a}), idx=applyPalette(d,pal,fm), g=GIFEncoder(), ti=a?pal.findIndex(p=>p[3]===0):-1;\r\n  g.writeFrame(idx,c.width,c.height,{palette:pal,transparent:ti>=0,transparentIndex:Math.max(0,ti)}); g.finish(); return new Blob([g.bytes()],{type:\"image\/gif\"});\r\n}\r\nasync function encPDF(canvases){\r\n  await need(\"jspdf\"); const {jsPDF}=window.jspdf; let doc=null;\r\n  for(const c of canvases){ const w=c.width*.75,h=c.height*.75, o=w>h?\"l\":\"p\";\r\n    if(!doc) doc=new jsPDF({orientation:o,unit:\"pt\",format:[w,h],compress:true}); else doc.addPage([w,h],o);\r\n    if(alphaIn(pixels(c))) doc.addImage(c,\"PNG\",0,0,w,h,undefined,\"FAST\"); else doc.addImage(flatten(c).toDataURL(\"image\/jpeg\",S.q\/100),\"JPEG\",0,0,w,h) }\r\n  return doc.output(\"blob\");\r\n}\r\nasync function encSVGtrace(c){\r\n  await need(\"tracer\"); const s=Math.max(c.width,c.height)>900?resize(c,\"900\"):c;\r\n  const svg=ImageTracer.imagedataToSVG(s.getContext(\"2d\").getImageData(0,0,s.width,s.height),{numberofcolors:S.colors,colorquantcycles:3,ltres:1,qtres:1,pathomit:8,blurradius:0,strokewidth:0,linefilter:true,roundcoords:1,scale:1,viewbox:true,desc:false});\r\n  return new Blob([svg.replace(\"<svg \",`<svg width=\"${c.width}\" height=\"${c.height}\" `)],{type:\"image\/svg+xml\"});\r\n}\r\nasync function encodeRaster(c,f){\r\n  const q=S.q\/100;\r\n  switch(f){\r\n    case \"png\": return blobOf(c,\"image\/png\");\r\n    case \"jpg\": return blobOf(flatten(c),\"image\/jpeg\",q);\r\n    case \"webp\": return blobOf(c,\"image\/webp\",q);\r\n    case \"avif\": return blobOf(c,\"image\/avif\",q);\r\n    case \"bmp\": return encBMP(c);\r\n    case \"tga\": return encTGA(c);\r\n    case \"tiff\": { await need(\"utif\"); const d=pixels(c); return new Blob([UTIF.encodeImage(new Uint8Array(d.buffer,d.byteOffset,d.length),c.width,c.height)],{type:\"image\/tiff\"}) }\r\n    case \"gif\": return encGIF(c);\r\n    case \"ico\": return encICO(c);\r\n    case \"psd\": { await need(\"psd\"); return new Blob([agPsd.writePsd({width:c.width,height:c.height,canvas:c,children:[{name:\"Calque 1\",canvas:c}]})],{type:\"image\/vnd.adobe.photoshop\"}) }\r\n    case \"pdf\": return encPDF([c]);\r\n    case \"svg\": return encSVGtrace(c);\r\n  }\r\n  throw new Error(\"Format de sortie inconnu\");\r\n}\r\n\r\n\/* =====================================================================\r\n   PDF \/ AI\r\n   ===================================================================== *\/\r\nasync function pdfDoc(it){\r\n  if(it.pdf) return it.pdf; await need(\"pdfjs\"); const lib=window.pdfjsLib;\r\n  lib.GlobalWorkerOptions.workerSrc=CONFIG.cdn+\"pdfjs-dist@3.11.174\/build\/pdf.worker.min.js\";\r\n  try{ it.pdf=await lib.getDocument({data:new Uint8Array(await it.file.arrayBuffer()),isEvalSupported:false}).promise }\r\n  catch(e){ throw new Error(it.ext===\"ai\"?\"Ce .ai n'est pas compatible PDF : r\u00e9enregistre-le avec \u00ab Cr\u00e9er un fichier compatible PDF \u00bb\":\"PDF illisible ou prot\u00e9g\u00e9 par mot de passe\") }\r\n  return it.pdf;\r\n}\r\nasync function renderPage(pdf,i,dpi,alpha){\r\n  const p=await pdf.getPage(i); let vp=p.getViewport({scale:dpi\/72}); const m=Math.max(vp.width,vp.height); if(m>12000) vp=p.getViewport({scale:dpi\/72*12000\/m});\r\n  const c=mkCanvas(Math.ceil(vp.width),Math.ceil(vp.height)); await p.render({canvasContext:c.getContext(\"2d\"),viewport:vp,background:alpha?\"rgba(0,0,0,0)\":\"#ffffff\"}).promise; return c;\r\n}\r\n\r\n\/* =====================================================================\r\n   3D\r\n   ===================================================================== *\/\r\nconst M3={glb:[\"GLTFLoader\"],gltf:[\"GLTFLoader\"],obj:[\"OBJLoader\"],fbx:[\"FBXLoader\"],stl:[\"STLLoader\"],ply:[\"PLYLoader\"],dae:[\"ColladaLoader\"],\"3ds\":[\"TDSLoader\"],\"3mf\":[\"ThreeMFLoader\",\"3MFLoader\"]};\r\nconst M3DEPS={GLTFLoader:[\"libs\/meshopt_decoder.js\",\"loaders\/DRACOLoader.js\"],OBJLoader:[\"loaders\/MTLLoader.js\"],FBXLoader:[\"libs\/fflate.min.js\",\"curves\/NURBSUtils.js\",\"curves\/NURBSCurve.js\"],ColladaLoader:[\"loaders\/TGALoader.js\"],ThreeMFLoader:[\"libs\/fflate.min.js\"]};\r\nfunction fbxUnit(buf){ \/\/ GlobalSettings \u203a UnitScaleFactor (centimetres per unit); three's FBXLoader ignores it\r\n  const u=new Uint8Array(buf), key=new TextEncoder().encode(\"UnitScaleFactor\");\r\n  if(String.fromCharCode(...u.subarray(0,18))!==\"Kaydara FBX Binary\"){ const m=\/\"UnitScaleFactor\"\\s*,\\s*\"double\"\\s*,\\s*\"Number\"\\s*,\\s*\"[^\"]*\"\\s*,\\s*([-\\d.eE+]+)\/.exec(new TextDecoder().decode(u.subarray(0,Math.min(u.length,400000)))); return m?+m[1]:0 }\r\n  outer: for(let i=0;i<u.length-key.length;i++){ for(let j=0;j<key.length;j++) if(u[i+j]!==key[j]) continue outer;\r\n    let p=i+key.length; const dv=new DataView(buf); for(let k=0;k<3;k++){ if(u[p]!==83) return 0; p+=5+dv.getUint32(p+1,true) } if(u[p]!==68) return 0; return dv.getFloat64(p+1,true) }\r\n  return 0 }\r\nasync function load3D(it){\r\n  if(it.obj) return it.obj;\r\n  const [L,file]=M3[it.ext]; await need(\"three\"); for(const d of M3DEPS[L]||[]){ try{ await loadScript(V3+\"examples\/js\/\"+d) }catch(e){} }\r\n  if(!THREE[L]) await loadScript(V3+\"examples\/js\/loaders\/\"+(file||L)+\".js\");\r\n  const urls={}; [it.file,...it.aux].forEach(f=>urls[f.name.toLowerCase()]=URL.createObjectURL(f)); it.urls=Object.values(urls);\r\n  const mgr=new THREE.LoadingManager(); mgr.setURLModifier(u=>{ if(\/^(blob|data):\/.test(u)) return u; const n=decodeURIComponent(u.split(\/[\\\\\/]\/).pop().split(\"?\")[0]).toLowerCase(); return urls[n]||u });\r\n  const done=new Promise(r=>{ mgr.onLoad=r; setTimeout(r,8000) });\r\n  const buf=await it.file.arrayBuffer(), text=()=>new TextDecoder().decode(buf); let obj;\r\n  if(L===\"GLTFLoader\"){ const gl=new THREE.GLTFLoader(mgr); if(window.MeshoptDecoder) gl.setMeshoptDecoder(window.MeshoptDecoder);\r\n    if(THREE.DRACOLoader){ const dr=new THREE.DRACOLoader(); dr.setDecoderPath(\"https:\/\/www.gstatic.com\/draco\/versioned\/decoders\/1.4.1\/\"); gl.setDRACOLoader(dr) }\r\n    const cib=window.createImageBitmap; try{ window.createImageBitmap=undefined }catch(_){}\r\n    try{ const g=await new Promise((res,rej)=>gl.parse(it.ext===\"glb\"?buf:text(),\"\",res,rej)); obj=g.scene; obj.animations=g.animations||[] } finally{ try{ window.createImageBitmap=cib }catch(_){} } }\r\n  else if(L===\"OBJLoader\"){ const ol=new THREE.OBJLoader(mgr); const mtl=it.aux.find(f=>extOf(f.name)===\"mtl\"); if(mtl&&THREE.MTLLoader){ const m=new THREE.MTLLoader(mgr).parse(await mtl.text(),\"\"); m.preload(); ol.setMaterials(m) } obj=ol.parse(text()) }\r\n  else if(L===\"FBXLoader\"){ obj=new THREE.FBXLoader(mgr).parse(buf,\"\"); const u=fbxUnit(buf); if(u&&Math.abs(u-100)>1e-6){ const w=new THREE.Group(); w.add(obj); w.scale.setScalar(u\/100); w.animations=obj.animations; obj=w } }\r\n  else if(L===\"ColladaLoader\"){ const r=new THREE.ColladaLoader(mgr).parse(text(),\"\"); obj=r.scene; obj.animations=r.animations||[] }\r\n  else if(L===\"TDSLoader\") obj=new THREE.TDSLoader(mgr).parse(buf,\"\");\r\n  else if(L===\"ThreeMFLoader\") obj=new THREE.ThreeMFLoader(mgr).parse(buf);\r\n  else { const geo=new THREE[L]().parse(buf); if(!geo.attributes.normal) geo.computeVertexNormals(); obj=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0xC9CED6,roughness:.55,vertexColors:!!geo.attributes.color})); obj.name=it.base }\r\n  let meshes=0; obj.traverse(o=>{ if(o.isMesh){ meshes++; if(!o.geometry.attributes.normal) o.geometry.computeVertexNormals() } });\r\n  if(!meshes) throw new Error(\"Aucun maillage trouv\u00e9 dans ce fichier\");\r\n  await Promise.race([done,sleep(10)]);\r\n  if(!obj.animations) obj.animations=[];\r\n  it.obj=obj; it.inner=new THREE.Group(); it.inner.add(obj); it.holder=new THREE.Group(); it.holder.name=it.base; it.holder.add(it.inner);\r\n  if(obj.animations.length){ it.mixer=new THREE.AnimationMixer(obj); it.mixer.clipAction(obj.animations[0]).play() }\r\n  apply3D(it); return obj;\r\n}\r\nfunction apply3D(it){\r\n  if(!it.holder) return; const inn=it.inner;\r\n  inn.rotation.set(S.up===\"zy\"?-Math.PI\/2:S.up===\"yz\"?Math.PI\/2:0,0,0); inn.scale.setScalar(S.scale); inn.position.set(0,0,0); it.holder.updateMatrixWorld(true);\r\n  if(S.ground){ const b=new THREE.Box3().setFromObject(it.holder), c=b.getCenter(new THREE.Vector3()); inn.position.set(-c.x,-b.min.y,-c.z); it.holder.updateMatrixWorld(true) }\r\n}\r\nfunction stats3D(it){\r\n  let tris=0,verts=0,meshes=0; const mats=new Set(), tex=new Set();\r\n  it.obj.traverse(o=>{ if(!o.isMesh) return; meshes++; const g=o.geometry; verts+=g.attributes.position.count; tris+=Math.floor((g.index?g.index.count:g.attributes.position.count)\/3);\r\n    (Array.isArray(o.material)?o.material:[o.material]).forEach(m=>{ if(!m) return; mats.add(m.uuid); [\"map\",\"normalMap\",\"roughnessMap\",\"metalnessMap\",\"emissiveMap\",\"aoMap\",\"specularMap\",\"bumpMap\",\"alphaMap\"].forEach(k=>m[k]&&tex.add(m[k].uuid)) }) });\r\n  const b=new THREE.Box3().setFromObject(it.holder), s=b.getSize(new THREE.Vector3());\r\n  return {tris,verts,meshes,mats:mats.size,tex:tex.size,anims:it.obj.animations.length,size:s};\r\n}\r\nconst MATCACHE=new Map();\r\nfunction toStd(m){\r\n  if(!m||m.isMeshStandardMaterial) return m; if(MATCACHE.has(m.uuid)) return MATCACHE.get(m.uuid);\r\n  const s=new THREE.MeshStandardMaterial({name:m.name,color:m.color?m.color.clone():new THREE.Color(1,1,1),map:m.map||null,normalMap:m.normalMap||null,emissive:m.emissive?m.emissive.clone():new THREE.Color(0,0,0),emissiveIntensity:m.emissiveIntensity==null?1:m.emissiveIntensity,\r\n    emissiveMap:m.emissiveMap||null,aoMap:m.aoMap||null,alphaMap:m.alphaMap||null,opacity:m.opacity,transparent:m.transparent,alphaTest:m.alphaTest||0,side:m.side,vertexColors:m.vertexColors,\r\n    roughness:m.shininess!=null?clamp(1-Math.sqrt(m.shininess\/100),.12,1):.6,metalness:0,skinning:!!m.skinning,morphTargets:!!m.morphTargets,morphNormals:!!m.morphNormals,flatShading:!!m.flatShading});\r\n  MATCACHE.set(m.uuid,s); return s;\r\n}\r\nconst TEXK=[\"map\",\"normalMap\",\"emissiveMap\",\"aoMap\",\"alphaMap\",\"roughnessMap\",\"metalnessMap\",\"bumpMap\",\"specularMap\",\"lightMap\",\"displacementMap\"];\r\nconst texOK=t=>{ const i=t&&t.image; return !!(i&&((i.width||i.videoWidth||i.naturalWidth)>0)) };\r\nfunction cleanTex(m){ if(!m||!TEXK.some(k=>m[k]&&!texOK(m[k]))) return m; const c=m.clone(); TEXK.forEach(k=>{ if(c[k]&&!texOK(c[k])) c[k]=null }); return c }\r\nfunction swapStd(root,std=true){ const back=[]; root.traverse(o=>{ if(o.isMesh){ back.push([o,o.material]); const f=m=>cleanTex(std?toStd(m):m); o.material=Array.isArray(o.material)?o.material.map(f):f(o.material) } }); return ()=>back.forEach(([o,m])=>o.material=m) }\r\nfunction flatStd(root){\r\n  const G=new THREE.Group(), ni=new Map();\r\n  root.traverse(o=>{ if(!o.isMesh) return; const g=o.geometry, ms=Array.isArray(o.material)?o.material:[o.material], parts=[];\r\n    if(Array.isArray(o.material)&&g.groups.length){ let src=ni.get(g.uuid); if(!src){ src=g.index?g.toNonIndexed():g; ni.set(g.uuid,src) }\r\n      for(const gr of g.groups){ const sg=new THREE.BufferGeometry(); for(const k in src.attributes){ const a=src.attributes[k]; if(!a.array) continue; sg.setAttribute(k,new THREE.BufferAttribute(a.array.slice(gr.start*a.itemSize,(gr.start+gr.count)*a.itemSize),a.itemSize,a.normalized)) } parts.push([sg,ms[gr.materialIndex||0]]) } }\r\n    else parts.push([g,ms[0]]);\r\n    for(const [geo,m] of parts){ const mm=new THREE.Mesh(geo,cleanTex(toStd(m))||new THREE.MeshStandardMaterial()); mm.name=o.name; o.matrixWorld.decompose(mm.position,mm.quaternion,mm.scale); G.add(mm) } });\r\n  G.updateMatrixWorld(true); return G;\r\n}\r\nasync function export3D(it,f,prog){\r\n  await load3D(it); apply3D(it); const H=it.holder; H.updateMatrixWorld(true); const EX=\"exporters\/\";\r\n  prog(.15,\"Pr\u00e9paration de l'export\u2026\"); await nextFrame();\r\n  switch(f){\r\n    case \"glb\": case \"gltf\": { await needEx(EX+\"GLTFExporter.js\"); const restore=swapStd(H);\r\n      try{ const r=await new Promise((res,rej)=>{ try{ new THREE.GLTFExporter().parse(H,res,{binary:f===\"glb\",embedImages:true,onlyVisible:true,animations:S.anim?it.obj.animations:[],maxTextureSize:4096}) }catch(e){ rej(e) } });\r\n        return f===\"glb\"?new Blob([r],{type:\"model\/gltf-binary\"}):new Blob([JSON.stringify(r)],{type:\"model\/gltf+json\"}) } finally{ restore() } }\r\n    case \"obj\": await needEx(EX+\"OBJExporter.js\"); return new Blob([new THREE.OBJExporter().parse(H)],{type:\"text\/plain\"});\r\n    case \"stl\": { await needEx(EX+\"STLExporter.js\"); const dv=new THREE.STLExporter().parse(H,{binary:true}); return new Blob([dv.buffer||dv],{type:\"model\/stl\"}) }\r\n    case \"ply\": { await needEx(EX+\"PLYExporter.js\"); const r=await new Promise(res=>{ const o=new THREE.PLYExporter().parse(H,res,{binary:true}); if(o) res(o) }); return new Blob([r],{type:\"application\/octet-stream\"}) }\r\n    case \"dae\": { await needEx(EX+\"ColladaExporter.js\"); const rs=swapStd(H,false); let r; try{ r=new THREE.ColladaExporter().parse(H,undefined,{textureDirectory:\"textures\"}) } finally{ rs() }\r\n      const dae=new Blob([r.data],{type:\"model\/vnd.collada+xml\"}); if(!r.textures||!r.textures.length) return dae;\r\n      return zipBlob([{name:it.base+\".dae\",blob:dae},...r.textures.map(t=>({name:(t.directory||\"\")+t.name+\".\"+t.ext,blob:new Blob([t.data])}))]) }\r\n    case \"usdz\": { await needEx(\"libs\/fflate.min.js\",EX+\"USDZExporter.js\"); const G=flatStd(H); const r=await new THREE.USDZExporter().parse(G); return new Blob([r],{type:\"model\/vnd.usdz+zip\"}) }\r\n    case \"fbx\": { const r=await FBXW.exportFBX(H,{fileName:it.base+\".fbx\"}); return new Blob([r],{type:\"application\/octet-stream\"}) }\r\n  }\r\n  throw new Error(\"Format 3D inconnu\");\r\n}\r\n\/* ---------- 3D preview ---------- *\/\r\nlet R3=null;\r\nasync function ensureR3(){\r\n  if(R3) return R3; await needEx(\"controls\/OrbitControls.js\",\"environments\/RoomEnvironment.js\");\r\n  let r; try{ r=new THREE.WebGLRenderer({antialias:true,alpha:true,preserveDrawingBuffer:true}) }catch(e){ throw new Error(\"Aper\u00e7u 3D indisponible (WebGL d\u00e9sactiv\u00e9)\") }\r\n  r.setPixelRatio(Math.min(2,window.devicePixelRatio||1)); r.outputEncoding=THREE.sRGBEncoding; r.toneMapping=THREE.ACESFilmicToneMapping; r.domElement.className=\"three\";\r\n  const sc=new THREE.Scene(), pm=new THREE.PMREMGenerator(r); sc.environment=pm.fromScene(new THREE.RoomEnvironment(),.04).texture;\r\n  sc.add(new THREE.HemisphereLight(0xffffff,0x2a3549,.7)); const dl=new THREE.DirectionalLight(0xffffff,.9); dl.position.set(3,5,4); sc.add(dl);\r\n  const grid=new THREE.GridHelper(2,20,0x3a4a66,0x1c2433); sc.add(grid);\r\n  const cam=new THREE.PerspectiveCamera(35,4\/3,.01,1000), ctl=new THREE.OrbitControls(cam,r.domElement); ctl.enableDamping=true; ctl.autoRotate=true; ctl.autoRotateSpeed=1.4;\r\n  r.domElement.addEventListener(\"pointerdown\",()=>ctl.autoRotate=false);\r\n  return R3={r,sc,cam,ctl,grid,cur:null,raf:0,clock:new THREE.Clock(),it:null};\r\n}\r\nfunction frame3D(){\r\n  const R=R3; if(!R.cur) return; const b=new THREE.Box3().setFromObject(R.cur); if(b.isEmpty()) return; const s=b.getBoundingSphere(new THREE.Sphere()), rad=s.radius||1;\r\n  R.grid.scale.setScalar(rad*1.6); R.grid.position.set(s.center.x,b.min.y,s.center.z);\r\n  R.cam.near=rad\/200; R.cam.far=rad*200; R.cam.position.copy(s.center).add(new THREE.Vector3(1,.62,1.3).normalize().multiplyScalar(rad*2.7)); R.ctl.target.copy(s.center); R.cam.updateProjectionMatrix(); R.ctl.update();\r\n}\r\nfunction put3D(it){ const R=R3; if(R.cur) R.sc.remove(R.cur); R.cur=it.holder; R.it=it; R.sc.add(it.holder); frame3D() }\r\nfunction loop3D(){ const R=R3; cancelAnimationFrame(R.raf); const el=R.r.domElement;\r\n  const tick=()=>{ if(!el.isConnected){ R.raf=0; return } const w=el.parentNode.clientWidth,h=el.parentNode.clientHeight; if(w&&h&&(el.width!==Math.round(w*R.r.getPixelRatio())||el.height!==Math.round(h*R.r.getPixelRatio()))){ R.r.setSize(w,h,false); R.cam.aspect=w\/h; R.cam.updateProjectionMatrix() }\r\n    const dt=R.clock.getDelta(); if(R.it&&R.it.mixer) R.it.mixer.update(dt); R.ctl.update(); R.r.render(R.sc,R.cam); R.raf=requestAnimationFrame(tick) }; tick() }\r\nasync function thumb3D(it){ const R=await ensureR3(), prev=R.it; put3D(it); const el=R.r.domElement, ow=el.width, oh=el.height; R.r.setSize(160,160,false); R.cam.aspect=1; R.cam.updateProjectionMatrix(); frame3D(); R.r.render(R.sc,R.cam);\r\n  const u=el.toDataURL(\"image\/png\"); R.r.setSize(ow\/R.r.getPixelRatio()||400,oh\/R.r.getPixelRatio()||300,false); if(prev&&prev!==it) put3D(prev); else if(!prev){ R.sc.remove(it.holder); R.cur=null; R.it=null } return u }\r\n\r\n\/* =====================================================================\r\n   VIDEO\r\n   ===================================================================== *\/\r\nfunction loadVideo(file){ return new Promise((res,rej)=>{ const v=document.createElement(\"video\"); v.muted=true; v.playsInline=true; v.preload=\"auto\"; v.src=URL.createObjectURL(file);\r\n  v.onloadeddata=()=>res(v); v.onerror=()=>{ URL.revokeObjectURL(v.src); rej(new Error(\"Vid\u00e9o illisible par ce navigateur (codec non pris en charge)\")) } }) }\r\nconst seek=(v,t)=>new Promise(r=>{ let ok=false; const done=()=>{ if(ok) return; ok=true; v.removeEventListener(\"seeked\",done); r() }; v.addEventListener(\"seeked\",done); v.currentTime=Math.max(0,Math.min(t,(v.duration||t+1)-.04)); setTimeout(done,4000) });\r\nfunction vRange(it){ const d=isFinite(it.dur)&&it.dur>0?it.dur:10, t0=Math.min(S.start,Math.max(0,d-.2)); return [t0,S.dur===\"all\"?d:Math.min(d,t0+S.dur)] }\r\nfunction vSize(v,maxW){ let W=v.videoWidth||640, H=v.videoHeight||360; if(maxW&&W>maxW){ H=H*maxW\/W; W=maxW } return [Math.round(W\/2)*2||2,Math.round(H\/2)*2||2] }\r\nasync function videoGIF(it,prog){\r\n  const {GIFEncoder,quantize,applyPalette}=await gifenc(); const v=await loadVideo(it.file);\r\n  try{ const [W,H]=vSize(v,S.gw===\"orig\"?0:+S.gw), c=mkCanvas(W,H), x=c.getContext(\"2d\",{willReadFrequently:true}), [t0,t1]=vRange(it), fps=S.fps, n=Math.min(600,Math.max(1,Math.round((t1-t0)*fps))), g=GIFEncoder();\r\n    for(let i=0;i<n;i++){ await seek(v,t0+i\/fps); x.drawImage(v,0,0,W,H); const d=x.getImageData(0,0,W,H).data, pal=quantize(d,256), idx=applyPalette(d,pal); g.writeFrame(idx,W,H,{palette:pal,delay:Math.round(1000\/fps)}); prog((i+1)\/n,`Image ${i+1} \/ ${n}`); if(i%4===3) await sleep(0) }\r\n    g.finish(); return new Blob([g.bytes()],{type:\"image\/gif\"}) } finally{ URL.revokeObjectURL(v.src) }\r\n}\r\nasync function videoRecord(it,f,prog){\r\n  const v=await loadVideo(it.file), [W,H]=vSize(v,S.gw===\"orig\"?0:+S.gw), c=mkCanvas(W,H), x=c.getContext(\"2d\"), stream=c.captureStream(30); let ac=null;\r\n  try{ ac=new (window.AudioContext||window.webkitAudioContext)(); const src=ac.createMediaElementSource(v), dst=ac.createMediaStreamDestination(); src.connect(dst); dst.stream.getAudioTracks().forEach(t=>stream.addTrack(t)); v.muted=false; await ac.resume() }catch(e){ ac=null; v.muted=true }\r\n  const types=f===\"mp4\"?[\"video\/mp4;codecs=avc1.42E01E,mp4a.40.2\",\"video\/mp4;codecs=avc1.42E01E\",\"video\/mp4;codecs=avc1\"]:[\"video\/webm;codecs=vp9,opus\",\"video\/webm;codecs=vp8,opus\",\"video\/webm\"];\r\n  const mime=types.find(t=>MediaRecorder.isTypeSupported(t)); if(!mime) throw new Error(\"Ce navigateur ne sait pas encoder en \"+f.toUpperCase());\r\n  const rec=new MediaRecorder(stream,{mimeType:mime,videoBitsPerSecond:Math.round(clamp(W*H*30*.1,1.5e6,16e6))}), chunks=[]; rec.ondataavailable=e=>e.data&&e.data.size&&chunks.push(e.data);\r\n  const [t0,t1]=vRange(it); await seek(v,t0); x.drawImage(v,0,0,W,H); rec.start(400);\r\n  try{ await v.play() }catch(e){ v.muted=true; await v.play() }\r\n  await new Promise(res=>{ const step=()=>{ x.drawImage(v,0,0,W,H); prog((v.currentTime-t0)\/Math.max(.01,t1-t0),\"Enregistrement en temps r\u00e9el\u2026\"); if(v.currentTime>=t1||v.ended){ res(); return } requestAnimationFrame(step) }; step() });\r\n  v.pause(); const stop=new Promise(r=>rec.onstop=r); rec.stop(); await stop; if(ac) ac.close(); URL.revokeObjectURL(v.src);\r\n  return new Blob(chunks,{type:mime.split(\";\")[0]});\r\n}\r\nasync function videoFrame(it){ const v=await loadVideo(it.file); try{ await seek(v,vRange(it)[0]); const c=mkCanvas(v.videoWidth,v.videoHeight); c.getContext(\"2d\").drawImage(v,0,0); return c } finally{ URL.revokeObjectURL(v.src) } }\r\n\r\n\/* =====================================================================\r\n   FONTS (TTF \/ OTF \/ WOFF)\r\n   ===================================================================== *\/\r\nfunction woffToSfnt(w){\r\n  const dv=new DataView(w.buffer,w.byteOffset,w.byteLength), flavor=dv.getUint32(4), n=dv.getUint16(12), tabs=[];\r\n  for(let i=0;i<n;i++){ const b=44+i*20; tabs.push({tag:dv.getUint32(b),o:dv.getUint32(b+4),cl:dv.getUint32(b+8),ol:dv.getUint32(b+12),ck:dv.getUint32(b+16)}) }\r\n  let size=12+16*n; tabs.forEach(t=>size+=(t.ol+3)&~3); const out=new Uint8Array(size), od=new DataView(out.buffer); let es=1,lg=0; while(es*2<=n){ es*=2; lg++ }\r\n  od.setUint32(0,flavor); od.setUint16(4,n); od.setUint16(6,es*16); od.setUint16(8,lg); od.setUint16(10,n*16-es*16); let off=12+16*n;\r\n  tabs.forEach((t,i)=>{ let data=w.subarray(t.o,t.o+t.cl); if(t.cl<t.ol) data=fflate.unzlibSync(data); const r=12+i*16; od.setUint32(r,t.tag); od.setUint32(r+4,t.ck); od.setUint32(r+8,off); od.setUint32(r+12,t.ol); out.set(data.subarray(0,t.ol),off); off+=(t.ol+3)&~3 });\r\n  return out;\r\n}\r\nfunction sfntToWoff(s){\r\n  const dv=new DataView(s.buffer,s.byteOffset,s.byteLength), flavor=dv.getUint32(0), n=dv.getUint16(4), tabs=[];\r\n  for(let i=0;i<n;i++){ const r=12+i*16; tabs.push({tag:dv.getUint32(r),ck:dv.getUint32(r+4),o:dv.getUint32(r+8),l:dv.getUint32(r+12)}) }\r\n  tabs.sort((a,b)=>a.tag-b.tag); let sfntSize=12+16*n; tabs.forEach(t=>sfntSize+=(t.l+3)&~3);\r\n  const comp=tabs.map(t=>{ const raw=s.subarray(t.o,t.o+t.l), z=fflate.zlibSync(raw,{level:9}); return z.length<raw.length?z:raw });\r\n  let size=44+20*n; comp.forEach(c=>size+=(c.length+3)&~3); const out=new Uint8Array(size), od=new DataView(out.buffer);\r\n  od.setUint32(0,0x774F4646); od.setUint32(4,flavor); od.setUint32(8,size); od.setUint16(12,n); od.setUint32(16,sfntSize); od.setUint16(20,1);\r\n  let off=44+20*n; tabs.forEach((t,i)=>{ const b=44+i*20, c=comp[i]; od.setUint32(b,t.tag); od.setUint32(b+4,off); od.setUint32(b+8,c.length); od.setUint32(b+12,t.l); od.setUint32(b+16,t.ck); out.set(c,off); off+=(c.length+3)&~3 });\r\n  return out;\r\n}\r\nfunction fontNames(s){\r\n  const dv=new DataView(s.buffer,s.byteOffset,s.byteLength), n=dv.getUint16(4); let nm=null;\r\n  for(let i=0;i<n;i++){ const r=12+i*16; if(dv.getUint32(r)===0x6E616D65) nm=dv.getUint32(r+8) } if(nm==null) return {};\r\n  const cnt=dv.getUint16(nm+2), so=nm+dv.getUint16(nm+4), out={}, pri={};\r\n  for(let i=0;i<cnt;i++){ const r=nm+6+i*12, pid=dv.getUint16(r), eid=dv.getUint16(r+2), lid=dv.getUint16(r+4), id=dv.getUint16(r+6), len=dv.getUint16(r+8), o=so+dv.getUint16(r+10);\r\n    if(![1,2,4,16,17].includes(id)) continue; let str=\"\", p=0;\r\n    if(pid===3||pid===0){ for(let k=0;k+1<len;k+=2) str+=String.fromCharCode(dv.getUint16(o+k)); p=pid===3&&(lid===0x409||lid===0x40C)?3:2 } else if(pid===1&&eid===0){ for(let k=0;k<len;k++) str+=String.fromCharCode(dv.getUint8(o+k)); p=1 } else continue;\r\n    if(str&&(!(id in pri)||p>pri[id])){ out[id]=str; pri[id]=p } }\r\n  return {family:out[16]||out[1]||\"\",style:out[17]||out[2]||\"Regular\",full:out[4]||\"\"};\r\n}\r\nasync function fontParse(it){\r\n  if(it.font) return it.font; const u=new Uint8Array(await it.file.arrayBuffer()), sig=String.fromCharCode(u[0],u[1],u[2],u[3]);\r\n  if(sig===\"wOF2\") throw new Error(\"Le WOFF2 n'est pas pris en charge : utilise le TTF ou l'OTF d'origine\");\r\n  if(sig===\"ttcf\") throw new Error(\"Les collections de polices (.ttc) ne sont pas prises en charge\");\r\n  let sfnt=u; if(sig===\"wOFF\"){ await need(\"fflate\"); sfnt=woffToSfnt(u) }\r\n  const fl=new DataView(sfnt.buffer,sfnt.byteOffset).getUint32(0); if(![0x00010000,0x4F54544F,0x74727565].includes(fl)) throw new Error(\"Fichier de police invalide\");\r\n  const names=fontNames(sfnt), fam=\"okf\"+it.id;\r\n  try{ const ff=new FontFace(fam,sfnt.slice().buffer); await ff.load(); document.fonts.add(ff) }catch(e){ throw new Error(\"Police refus\u00e9e par le navigateur (fichier endommag\u00e9 ?)\") }\r\n  return it.font={sfnt,kind:fl===0x4F54544F?\"otf\":\"ttf\",names,fam};\r\n}\r\nfunction fontCSS(it,files){\r\n  const n=it.font.names, st=(n.style||\"\").toLowerCase(), W={thin:100,hairline:100,extralight:200,ultralight:200,light:300,regular:400,book:400,medium:500,semibold:600,demibold:600,bold:700,extrabold:800,ultrabold:800,black:900,heavy:900};\r\n  let wt=400; Object.keys(W).forEach(k=>{ if(st.replace(\/[\\s-]\/g,\"\").includes(k)) wt=W[k] }); if(st.replace(\/[\\s-]\/g,\"\").includes(\"extrabold\")) wt=800; if(st.replace(\/[\\s-]\/g,\"\").includes(\"semibold\")) wt=600;\r\n  const fam=n.family||it.base;\r\n  return `@font-face {\\n  font-family: \"${fam}\";\\n  src: url(\"${files[0]}\") format(\"woff\")${files[1]?`,\\n       url(\"${files[1]}\") format(\"${it.font.kind===\"otf\"?\"opentype\":\"truetype\"}\")`:\"\"};\\n  font-weight: ${wt};\\n  font-style: ${\/italic|oblique\/.test(st)?\"italic\":\"normal\"};\\n  font-display: swap;\\n}\\n\\n\/* Utilisation *\/\\n.titre { font-family: \"${fam}\", system-ui, sans-serif; }\\n`;\r\n}\r\n\r\n\/* =====================================================================\r\n   ZIP + DOWNLOAD\r\n   ===================================================================== *\/\r\nasync function zipBlob(files){\r\n  await need(\"fflate\"); const o={};\r\n  for(const f of files){ let n=f.name, k=2; while(o[n]){ n=f.name.replace(\/(\\.[^.]+)?$\/,m=>\"-\"+k+m); k++ }\r\n    o[n]=[new Uint8Array(await f.blob.arrayBuffer()),{level:\/\\.(png|jpe?g|webp|avif|gif|mp4|webm|glb|usdz|zip|woff|pdf)$\/i.test(n)?0:6}] }\r\n  return new Blob([fflate.zipSync(o)],{type:\"application\/zip\"});\r\n}\r\nconst DL_OK=\/\\.(gif|png|jpg|jpeg|webp|mp4|webm|txt|json|md|csv|ttf|html|svg|pdf|zip)$\/i;\r\nlet DLC; async function dlCap(){ if(DLC!==undefined) return DLC; try{ DLC=window.claude&&window.claude.use?await Promise.race([window.claude.use(\"downloads\"),sleep(5000).then(()=>null)]):null }catch(e){ DLC=null } return DLC }\r\nasync function saveFile(blob,name){\r\n  const d=await dlCap();\r\n  if(d){ let data=blob, fn=name; if(!DL_OK.test(name)){ data=await zipBlob([{name,blob}]); fn=name+\".zip\" }\r\n    try{ await d.save({filename:fn,data}); return }catch(e){ const c=e&&e.code; if(c===\"declined\"||c===\"rate_limited\") return; if(c===\"too_large\"){ toast(\"Fichier trop lourd pour \u00eatre t\u00e9l\u00e9charg\u00e9 ici\"); return } } }\r\n  const u=URL.createObjectURL(blob), a=document.createElement(\"a\"); a.href=u; a.download=name; a.rel=\"noopener\"; a.style.display=\"none\"; $(\"#okk\").appendChild(a); a.click(); a.remove(); setTimeout(()=>URL.revokeObjectURL(u),60000);\r\n}\r\n\r\n\/* =====================================================================\r\n   CONVERTERS\r\n   ===================================================================== *\/\r\nconst outName=(it,f)=>`${it.base}.${OUTX[f]||f}`;\r\nconst CONV={\r\n  async img(it,f,prog){ prog(.2,\"D\u00e9codage\u2026\"); let c=await decodeImage(it); await nextFrame(); c=resize(c,S.size); prog(.55,f===\"svg\"?\"Vectorisation\u2026\":\"Encodage\u2026\"); await nextFrame(); return {blob:await encodeRaster(c,f),name:outName(it,f)} },\r\n  async vec(it,f,prog){ const info=await svgInfo(it);\r\n    if(f===\"svg\") return {blob:new Blob([optimizeSVG(info)],{type:\"image\/svg+xml\"}),name:it.base+\".min.svg\"};\r\n    if(f===\"pdf\"){ prog(.3,\"Mise en page vectorielle\u2026\"); await need(\"jspdf\",\"svg2pdf\"); const {jsPDF}=window.jspdf, w=info.w*.75, h=info.h*.75, doc=new jsPDF({orientation:w>h?\"l\":\"p\",unit:\"pt\",format:[w,h]});\r\n      const hold=document.createElement(\"div\"); hold.style.cssText=\"position:fixed;left:-99999px;top:0;width:10px;height:10px;overflow:hidden\"; hold.appendChild(document.importNode(info.doc.documentElement,true)); $(\"#okk\").appendChild(hold);\r\n      try{ await doc.svg(hold.firstChild,{x:0,y:0,width:w,height:h}) } finally{ hold.remove() } return {blob:doc.output(\"blob\"),name:outName(it,f)} }\r\n    prog(.3,\"Rendu\u2026\"); const c=await rasterSVG(it,S.size); prog(.6,\"Encodage\u2026\"); return {blob:await encodeRaster(c,f),name:outName(it,f)} },\r\n  async pdf(it,f,prog){ const pdf=await pdfDoc(it), n=S.pages===\"all\"?pdf.numPages:1, out=[];\r\n    for(let i=1;i<=n;i++){ prog((i-.5)\/n,`Page ${i} \/ ${n}`); const c=await renderPage(pdf,i,S.dpi,S.pdfAlpha&&f!==\"jpg\"); out.push({name:n>1?`${it.base}-p${String(i).padStart(2,\"0\")}.${f}`:outName(it,f),blob:await encodeRaster(c,f)}) }\r\n    return n>1?{blob:await zipBlob(out),name:`${it.base}-${f}.zip`,count:n}:out[0] },\r\n  async m3d(it,f,prog){ prog(.05,\"Lecture du mod\u00e8le\u2026\"); const blob=await export3D(it,f,prog); return {blob,name:blob.type===\"application\/zip\"?`${it.base}-dae.zip`:outName(it,f)} },\r\n  async vid(it,f,prog){ if(f===\"gif\") return {blob:await videoGIF(it,prog),name:outName(it,f)};\r\n    if(f===\"png\"||f===\"jpg\"){ const c=await videoFrame(it); return {blob:await encodeRaster(c,f),name:`${it.base}-image.${f}`} }\r\n    return {blob:await videoRecord(it,f,prog),name:outName(it,f)} },\r\n  async font(it,f){ const F=await fontParse(it); await need(\"fflate\"); const woff=()=>new Blob([sfntToWoff(F.sfnt)],{type:\"font\/woff\"}), raw=()=>new Blob([F.sfnt],{type:\"font\/\"+F.kind});\r\n    if(f===\"woff\") return {blob:woff(),name:it.base+\".woff\"};\r\n    if(f===\"ttf\"||f===\"otf\") return {blob:raw(),name:it.base+\".\"+F.kind};\r\n    const files=[`${it.base}.woff`,`${it.base}.${F.kind}`];\r\n    return {blob:await zipBlob([{name:files[0],blob:woff()},{name:files[1],blob:raw()},{name:it.base+\".css\",blob:new Blob([fontCSS(it,files)],{type:\"text\/css\"})}]),name:`${it.base}-kit-web.zip`} }\r\n};\r\n\r\n\/* =====================================================================\r\n   ANALYSIS (on drop: thumbnail + details)\r\n   ===================================================================== *\/\r\nfunction thumbOf(c){ const s=116, k=Math.min(1,s\/Math.max(c.width,c.height)), t=scaleTo(c,Math.max(1,Math.round(c.width*k)),Math.max(1,Math.round(c.height*k))); return t.toDataURL(\"image\/png\") }\r\nconst NATIVE=[\"jpg\",\"jpeg\",\"png\",\"webp\",\"avif\",\"gif\",\"bmp\",\"ico\",\"svg\"];\r\nconst ANA={\r\n  async img(it){ const c=await decodeImage(it); it.dims=[c.width,c.height]; it.meta=`${c.width} \u00d7 ${c.height}`; it.thumb=thumbOf(c);\r\n    if(!NATIVE.includes(it.ext)){ const p=resize(c,\"1600\"); it.pv=URL.createObjectURL(await blobOf(p,alphaIn(pixels(p))?\"image\/png\":\"image\/jpeg\",.9)) } else it.pv=URL.createObjectURL(it.file);\r\n    if(it.ext===\"hdr\"||it.ext===\"exr\") it.meta+=\" \u00b7 HDR\"; },\r\n  async vec(it){ const info=await svgInfo(it); it.meta=`${info.w} \u00d7 ${info.h}`; it.thumb=thumbOf(await rasterSVG(it,\"240\")); it.pv=URL.createObjectURL(new Blob([new XMLSerializer().serializeToString(info.doc)],{type:\"image\/svg+xml\"})) },\r\n  async pdf(it){ const pdf=await pdfDoc(it); it.pages=pdf.numPages; const p=await pdf.getPage(1), vp=p.getViewport({scale:1}); it.meta=`${it.pages} page${it.pages>1?\"s\":\"\"} \u00b7 ${Math.round(vp.width\/72*25.4)} \u00d7 ${Math.round(vp.height\/72*25.4)} mm`;\r\n    const c=await renderPage(pdf,1,Math.min(110,72*900\/Math.max(vp.width,vp.height)),false); it.thumb=thumbOf(c); it.pv=URL.createObjectURL(await blobOf(c,\"image\/png\")) },\r\n  async m3d(it){ await load3D(it); const s=stats3D(it); it.meta=`${s.tris.toLocaleString(\"fr-FR\")} triangles${s.anims?` \u00b7 ${s.anims} anim.`:\"\"}${it.aux.length?` \u00b7 +${it.aux.length} fichier${it.aux.length>1?\"s\":\"\"} li\u00e9${it.aux.length>1?\"s\":\"\"}`:\"\"}`; try{ it.thumb=await thumb3D(it) }catch(e){} },\r\n  async vid(it){ const v=await loadVideo(it.file); try{ it.dur=v.duration; it.vw=v.videoWidth; it.vh=v.videoHeight; it.meta=`${v.videoWidth} \u00d7 ${v.videoHeight} \u00b7 ${isFinite(v.duration)?v.duration.toFixed(1).replace(\".\",\",\")+\" s\":\"dur\u00e9e ?\"}`;\r\n    await seek(v,Math.min(.5,(v.duration||1)\/2)); const c=mkCanvas(v.videoWidth,v.videoHeight); c.getContext(\"2d\").drawImage(v,0,0); it.thumb=thumbOf(c) } finally{ URL.revokeObjectURL(v.src) } it.pv=URL.createObjectURL(it.file) },\r\n  async font(it){ const F=await fontParse(it); it.meta=[F.names.family,F.names.style].filter(Boolean).join(\" \u00b7 \")+\" \u00b7 \"+F.kind.toUpperCase();\r\n    if(it.ext===\"woff\"&&!outsFor(it).includes(it.target)) it.target=F.kind;\r\n    const c=mkCanvas(116,116), x=c.getContext(\"2d\"); x.fillStyle=\"#E9EEF6\"; x.font=`64px \"${F.fam}\"`; x.textAlign=\"center\"; x.textBaseline=\"middle\"; x.fillText(\"Aa\",58,62); it.thumb=c.toDataURL() }\r\n};\r\n\r\n\/* =====================================================================\r\n   STATE + UI\r\n   ===================================================================== *\/\r\nconst ITEMS=[]; let SEL=null, UID=0, AQ=Promise.resolve(), RUNNING=false;\r\nconst byId=id=>ITEMS.find(i=>i.id===id);\r\nconst rowEl=it=>$(`#queue .row[data-id=\"${it.id}\"]`);\r\nasync function linkedNames(files){ \/\/ texture names referenced by the 3D files of a drop (OBJ\/MTL, glTF, FBX, DAE\u2026)\r\n  const src=files.filter(f=>[\"obj\",\"mtl\",\"gltf\",\"fbx\",\"dae\",\"3ds\"].includes(extOf(f.name))&&f.size<80e6); let txt=\"\";\r\n  for(const f of src){ const u=new Uint8Array(await f.arrayBuffer()); for(let i=0;i<u.length;i+=65536) txt+=String.fromCharCode.apply(null,u.subarray(i,i+65536)) }\r\n  return txt.toLowerCase();\r\n}\r\nasync function addFiles(list){\r\n  const files=[...(list||[])].filter(f=>f&&f.size>0); if(!files.length) return;\r\n  const has3d=files.some(f=>catOf(extOf(f.name))===\"m3d\"), aux=[], added=[], refs=has3d?await linkedNames(files):\"\";\r\n  for(const f of files){ const e=extOf(f.name), c=catOf(e);\r\n    if([\"bin\",\"mtl\"].includes(e)||(has3d&&[\"png\",\"jpg\",\"jpeg\",\"webp\",\"tga\",\"bmp\",\"tif\",\"tiff\"].includes(e)&&refs.includes(f.name.toLowerCase()))){ aux.push(f); continue }\r\n    if(!c){ toast(`Format .${e||\"?\"} non pris en charge (${f.name})`); continue }\r\n    if(f.size>CONFIG.maxFileMB*1048576){ toast(`${f.name} est trop lourd (plus de ${CONFIG.maxFileMB} Mo)`); continue }\r\n    const it={id:++UID,file:f,name:f.name,ext:e,base:baseOf(f.name),cat:c,status:\"wait\",prog:0,meta:\"\",aux:[]}; it.target=defTarget(it); ITEMS.push(it); added.push(it) }\r\n  if(aux.length){ const t=added.filter(i=>i.cat===\"m3d\"); if(t.length){ t.forEach(i=>i.aux=aux); toast(`${aux.length} fichier${aux.length>1?\"s\":\"\"} associ\u00e9${aux.length>1?\"s\":\"\"} au mod\u00e8le 3D (textures, mat\u00e9riaux)`) } }\r\n  if(!added.length) return;\r\n  if(!SEL||!byId(SEL)) SEL=added[0].id;\r\n  $(\"#work\").hidden=false; $(\"#drop\").classList.add(\"mini\");\r\n  renderQueue(); renderSettings(); renderPreview();\r\n  added.forEach(it=>{ it.ready=AQ=AQ.then(()=>analyze(it)) });\r\n  if(added.length) setTimeout(()=>{ const w=$(\"#work\").getBoundingClientRect(); if(w.top>window.innerHeight*.7) window.scrollBy({top:w.top-90,behavior:\"smooth\"}) },80);\r\n}\r\nasync function analyze(it){\r\n  if(!ITEMS.includes(it)) return; it.status=\"ana\"; renderRow(it);\r\n  try{ await ANA[it.cat](it); it.status=\"idle\" }catch(e){ console.error(e); it.status=\"err\"; it.err=msg(e); it.bad=true }\r\n  if(!ITEMS.includes(it)) return; renderRow(it); renderSettings(); if(SEL===it.id) renderPreview();\r\n}\r\nfunction rowHTML(it){\r\n  const C=CATS[it.cat], st=it.status, busy=st===\"busy\"||st===\"ana\"||st===\"wait\";\r\n  const th=it.thumb?`<img decoding=\"async\" src=\"${it.thumb}\" alt=\"\">`:`<span class=\"gl\">${esc(it.ext.toUpperCase())}<\/span>`;\r\n  let info=`<span class=\"tag\">${esc(it.ext.toUpperCase())}<\/span><span>${fmtB(it.file.size)}<\/span>${it.meta?`<span>${esc(it.meta)}<\/span>`:\"\"}`;\r\n  if(st===\"wait\"||st===\"ana\") info+=`<span>Analyse\u2026<\/span>`;\r\n  if(st===\"busy\") info=`<span class=\"tag\">${esc(it.ext.toUpperCase())}<\/span><span class=\"stg\">${esc(it.stage||\"Conversion\u2026\")}<\/span>`;\r\n  if(st===\"done\"){ const r=it.out.blob.size\/it.file.size, p=Math.round((r-1)*100); info=`<span class=\"tag\">${esc(it.ext.toUpperCase())}<\/span><span>${fmtB(it.file.size)}<\/span><span class=\"res${p>0?\" big\":\"\"}\">\u2192 ${esc((OUTX[it.target]||it.target).toUpperCase())} \u00b7 ${fmtB(it.out.blob.size)}${it.out.count?` \u00b7 ${it.out.count} fichiers`:\"\"}${Math.abs(p)>=1?` \u00b7 ${p>0?\"+\":\"\u2212\"}${Math.abs(p)} %`:\"\"}<\/span>` }\r\n  if(st===\"err\") info=`<span class=\"tag\">${esc(it.ext.toUpperCase())}<\/span><span class=\"err\">${esc(it.err)}<\/span>`;\r\n  const opts=outsFor(it).map(f=>`<option value=\"${f}\"${f===it.target?\" selected\":\"\"}>${esc(outLabel(it,f))}<\/option>`).join(\"\");\r\n  const act=st===\"done\"?`<button class=\"ib dl\" data-act=\"dl\" type=\"button\" title=\"T\u00e9l\u00e9charger\" aria-label=\"T\u00e9l\u00e9charger\">${ICON.dl}<\/button>`\r\n    :busy?\"\":`<button class=\"ib go\" data-act=\"go\" type=\"button\" title=\"${st===\"err\"?\"R\u00e9essayer\":\"Convertir\"}\" aria-label=\"Convertir\">${st===\"err\"?ICON.retry:ICON.go}<\/button>`;\r\n  return `<div class=\"row${SEL===it.id?\" sel\":\"\"}\" data-id=\"${it.id}\" style=\"--c:${C.c}\"><div class=\"th${it.cat===\"img\"||it.cat===\"vec\"?\" chk\":\"\"}\">${th}${busy?'<div class=\"spin\"><i><\/i><\/div>':\"\"}<\/div>`+\r\n    `<div class=\"mid\"><div class=\"nm\" title=\"${esc(it.name)}\">${esc(it.name)}<\/div><div class=\"mt\">${info}<\/div><\/div>`+\r\n    `<div class=\"ctl\"><span class=\"arr\">\u2192<\/span><select class=\"fsel\" data-act=\"fmt\" aria-label=\"Format de sortie pour ${esc(it.name)}\"${st===\"busy\"?\" disabled\":\"\"}>${opts}<\/select>${act}<button class=\"ib\" data-act=\"rm\" type=\"button\" title=\"Retirer\" aria-label=\"Retirer\"${st===\"busy\"?\" disabled\":\"\"}>${ICON.rm}<\/button><\/div>`+\r\n    `<div class=\"pb\" style=\"width:${st===\"busy\"?Math.round(it.prog*100):0}%\"><\/div><\/div>`;\r\n}\r\nfunction renderQueue(){ $(\"#queue\").innerHTML=ITEMS.map(rowHTML).join(\"\"); updFoot() }\r\nfunction renderRow(it){ const el=rowEl(it); if(!el){ renderQueue(); return } const t=document.createElement(\"div\"); t.innerHTML=rowHTML(it); const n=t.firstChild; n.style.animation=\"none\"; el.replaceWith(n); updFoot() }\r\nfunction updFoot(){\r\n  const n=ITEMS.length, done=ITEMS.filter(i=>i.status===\"done\").length, todo=ITEMS.filter(i=>i.status!==\"done\"&&!i.bad).length;\r\n  $(\"#qcount\").textContent=`${n} fichier${n>1?\"s\":\"\"}${done?` \u00b7 ${done} pr\u00eat${done>1?\"s\":\"\"}`:\"\"}`;\r\n  const r=$(\"#runall\"); if(!RUNNING){ r.disabled=!todo; r.textContent=todo?`Tout convertir (${todo})`:\"Tout est converti\" }\r\n  const z=$(\"#zipall\"); z.disabled=done<1; z.textContent=done>1?`Tout t\u00e9l\u00e9charger (.zip)`:\"T\u00e9l\u00e9charger\";\r\n}\r\nfunction setProg(it,p,stage){ it.prog=clamp(p,0,1); if(stage) it.stage=stage; const el=rowEl(it); if(!el) return; el.querySelector(\".pb\").style.width=Math.round(it.prog*100)+\"%\"; if(stage){ const s=el.querySelector(\".stg\"); if(s) s.textContent=stage } }\r\nasync function convert(it){\r\n  if(!it||it.status===\"busy\") return; if(it.ready) await it.ready; if(!ITEMS.includes(it)) return;\r\n  it.status=\"busy\"; it.prog=0; it.stage=\"Pr\u00e9paration\u2026\"; it.err=\"\"; renderRow(it); await nextFrame();\r\n  try{ const out=await CONV[it.cat](it,it.target,(p,s)=>setProg(it,p,s)); if(!out||!out.blob||!out.blob.size) throw new Error(\"R\u00e9sultat vide\"); if(it.outUrl){ URL.revokeObjectURL(it.outUrl); it.outUrl=null } it.out=out; it.status=\"done\"; it.bad=false }\r\n  catch(e){ console.error(e); it.status=\"err\"; it.err=msg(e) }\r\n  if(!ITEMS.includes(it)) return; renderRow(it); if(SEL===it.id) renderPreview();\r\n}\r\nfunction invalidate(pred){ ITEMS.forEach(i=>{ if(i.status===\"done\"&&pred(i)){ i.status=\"idle\"; i.out=null; if(i.outUrl){ URL.revokeObjectURL(i.outUrl); i.outUrl=null } } }); renderQueue() }\r\nfunction removeItem(it){\r\n  const k=ITEMS.indexOf(it); if(k<0) return; ITEMS.splice(k,1);\r\n  [it.pv,it.outUrl,...(it.urls||[])].forEach(u=>u&&URL.revokeObjectURL(u));\r\n  if(R3&&R3.it===it){ R3.sc.remove(it.holder); R3.cur=null; R3.it=null }\r\n  if(it.pdf) try{ it.pdf.destroy() }catch(e){}\r\n  if(SEL===it.id) SEL=ITEMS.length?ITEMS[Math.min(k,ITEMS.length-1)].id:null;\r\n  if(!ITEMS.length){ $(\"#work\").hidden=true; $(\"#drop\").classList.remove(\"mini\") }\r\n  renderQueue(); renderSettings(); renderPreview();\r\n}\r\n\r\n\/* ---------- preview ---------- *\/\r\nasync function renderPreview(){\r\n  const it=byId(SEL), box=$(\"#pvbox\"), st=$(\"#pvstats\"), x=$(\"#pvx\");\r\n  if(R3&&R3.raf){ cancelAnimationFrame(R3.raf); R3.raf=0 }\r\n  box.className=\"pvbox\"; x.innerHTML=\"\"; st.innerHTML=\"\"; $(\"#pvinfo\").textContent=it?it.name:\"\";\r\n  const chip=(k,v)=>`<span>${k} <b>${esc(v)}<\/b><\/span>`;\r\n  if(!it){ box.innerHTML='<div class=\"ph\">S\u00e9lectionne un fichier<\/div>'; return }\r\n  if(it.status===\"wait\"||it.status===\"ana\"){ box.innerHTML='<div class=\"ph\"><span class=\"busyi\"><\/span><br><br>Analyse du fichier\u2026<\/div>'; return }\r\n  if(it.bad){ box.innerHTML=`<div class=\"ph\">${esc(it.err||\"Fichier illisible\")}<\/div>`; return }\r\n  const done=it.status===\"done\", outIsImg=done&&[\"png\",\"jpg\",\"webp\",\"avif\",\"gif\",\"bmp\",\"ico\",\"svg\"].includes(it.target)&&!it.out.count&&it.out.blob.type!==\"application\/zip\";\r\n  if(done&&!it.outUrl) it.outUrl=URL.createObjectURL(it.out.blob);\r\n  switch(it.cat){\r\n    case \"img\": case \"vec\": case \"pdf\": {\r\n      box.classList.add(\"chk\");\r\n      if(outIsImg){ box.innerHTML=`<div class=\"cmp\" style=\"--x:50%\"><img decoding=\"async\" src=\"${it.pv}\" alt=\"Original\"><img decoding=\"async\" class=\"after\" src=\"${it.outUrl}\" alt=\"Converti\"><div class=\"ln\"><\/div><span class=\"cap l\">${esc(it.ext.toUpperCase())}<\/span><span class=\"cap r\">${esc((OUTX[it.target]||it.target).toUpperCase())}<\/span><input type=\"range\" min=\"0\" max=\"100\" value=\"50\" aria-label=\"Comparer avant et apr\u00e8s\"><\/div>`;\r\n        const cmp=box.querySelector(\".cmp\"); cmp.querySelector(\"input\").oninput=e=>cmp.style.setProperty(\"--x\",e.target.value+\"%\") }\r\n      else box.innerHTML=`<img decoding=\"async\" src=\"${it.pv}\" alt=\"\">`;\r\n      if(it.meta) st.innerHTML+=chip(it.cat===\"pdf\"?\"Document\":\"Taille\",it.meta);\r\n      st.innerHTML+=chip(\"Poids\",fmtB(it.file.size));\r\n      if(done) st.innerHTML+=chip(\"R\u00e9sultat\",fmtB(it.out.blob.size));\r\n      if(it.cat!==\"pdf\"&&it.dims&&S.size!==\"orig\"){ const [w,h]=sizeTarget(it.dims[0],it.dims[1],S.size); st.innerHTML+=chip(\"Sortie\",`${w} \u00d7 ${h}`) }\r\n      break }\r\n    case \"m3d\": {\r\n      try{ const R=await ensureR3(); if(SEL!==it.id) return; box.innerHTML=\"\"; box.appendChild(R.r.domElement); put3D(it); loop3D();\r\n        const s=stats3D(it), u=v=>v>=1?v.toFixed(2).replace(\".\",\",\")+\" m\":(v*100).toFixed(1).replace(\".\",\",\")+\" cm\";\r\n        st.innerHTML=chip(\"Triangles\",s.tris.toLocaleString(\"fr-FR\"))+chip(\"Maillages\",s.meshes)+chip(\"Mat\u00e9riaux\",s.mats)+(s.tex?chip(\"Textures\",s.tex):\"\")+(s.anims?chip(\"Animations\",s.anims):\"\")+chip(\"Dimensions\",`${u(s.size.x)} \u00d7 ${u(s.size.y)} \u00d7 ${u(s.size.z)}`);\r\n        x.innerHTML='<p class=\"note\" style=\"margin-top:10px\">Fais tourner le mod\u00e8le \u00e0 la souris ou au doigt. Couch\u00e9 ou trop petit ? Ajuste l\u2019axe et l\u2019\u00e9chelle dans les r\u00e9glages.<\/p>' }\r\n      catch(e){ box.innerHTML=`<div class=\"ph\">${esc(msg(e))}<\/div>` }\r\n      break }\r\n    case \"vid\": {\r\n      if(done&&it.target===\"gif\") box.innerHTML=`<img decoding=\"async\" src=\"${it.outUrl}\" alt=\"GIF\">`;\r\n      else if(done&&(it.target===\"png\"||it.target===\"jpg\")) box.innerHTML=`<img decoding=\"async\" src=\"${it.outUrl}\" alt=\"\">`;\r\n      else box.innerHTML=`<video src=\"${done?it.outUrl:it.pv}\" muted loop autoplay playsinline controls><\/video>`;\r\n      st.innerHTML=chip(\"Vid\u00e9o\",it.meta)+chip(\"Poids\",fmtB(it.file.size))+(done?chip(\"R\u00e9sultat\",fmtB(it.out.blob.size)):\"\");\r\n      break }\r\n    case \"font\": {\r\n      const F=it.font; box.innerHTML=`<div class=\"ftest\" style=\"font-family:'${F.fam}',system-ui\"><div class=\"aa\">Aa Gg 3D<\/div><div class=\"pg\" contenteditable=\"true\" spellcheck=\"false\">Portez ce vieux whisky au juge blond qui fume.<\/div><div class=\"ab\">ABCDEFGHIJKLMNOPQRSTUVWXYZ \u00b7 abcdefghijklmnopqrstuvwxyz \u00b7 0123456789 \u00e9\u00e8\u00e0\u00e7\u0153&!?<\/div><\/div>`;\r\n      st.innerHTML=chip(\"Famille\",F.names.family||it.base)+chip(\"Style\",F.names.style||\"Regular\")+chip(\"Contours\",F.kind===\"otf\"?\"PostScript (OTF)\":\"TrueType (TTF)\");\r\n      const css=fontCSS(it,[`${it.base}.woff`,`${it.base}.${F.kind}`]); x.innerHTML=`<div class=\"code\"><pre>${esc(css)}<\/pre><button class=\"btn gho sm\" type=\"button\" id=\"copycss\">Copier<\/button><\/div>`;\r\n      $(\"#copycss\").onclick=()=>{ (navigator.clipboard?navigator.clipboard.writeText(css):Promise.reject()).then(()=>toast(\"CSS copi\u00e9\")).catch(()=>toast(\"Copie impossible ici\")) };\r\n      break }\r\n  }\r\n}\r\n\r\n\/* ---------- settings ---------- *\/\r\nconst fld=(label,ctl)=>`<div class=\"fld\"><span>${label}<\/span><div class=\"ctl2\">${ctl}<\/div><\/div>`;\r\nconst chips=(k,opts,cur)=>`<div class=\"chips\">${opts.map(([v,l])=>`<button class=\"chip${String(cur)===String(v)?\" on\":\"\"}\" type=\"button\" data-k=\"${k}\" data-v=\"${v}\">${l}<\/button>`).join(\"\")}<\/div>`;\r\nconst sel=(k,opts,cur)=>`<select class=\"fsel\" data-k=\"${k}\">${opts.map(([v,l])=>`<option value=\"${v}\"${String(cur)===String(v)?\" selected\":\"\"}>${l}<\/option>`).join(\"\")}<\/select>`;\r\nconst rng=(k,min,max,step,cur,fmt)=>`<input type=\"range\" data-k=\"${k}\" min=\"${min}\" max=\"${max}\" step=\"${step}\" value=\"${cur}\" style=\"--p:${(cur-min)\/(max-min)*100}%\"><span class=\"val\" data-val=\"${k}\">${fmt(cur)}<\/span>`;\r\nconst FMTV={ q:v=>v+\" %\", colors:v=>v, exposure:v=>(v>0?\"+\":\"\")+(+v).toFixed(1).replace(\".\",\",\")+\" EV\", start:v=>(+v).toFixed(1).replace(\".\",\",\")+\" s\" };\r\nconst SIZES=[[\"orig\",\"Taille d'origine\"],[\"x2\",\"Agrandir \u00d72\"],[\"x4\",\"Agrandir \u00d74\"],[\"4096\",\"4096 px max\"],[\"2048\",\"2048 px max\"],[\"1920\",\"1920 px max\"],[\"1080\",\"1080 px max\"],[\"512\",\"512 px max\"],[\"half\",\"Moiti\u00e9 (50 %)\"]];\r\nfunction renderSettings(){\r\n  const box=$(\"#set\"); if(!ITEMS.length){ box.innerHTML=\"\"; return }\r\n  const cur0=byId(SEL)||ITEMS[0], cats=[cur0.cat]; let h=\"\";\r\n  for(const k of cats){ const C=CATS[k], its=ITEMS.filter(i=>i.cat===k), tg=new Set(its.map(i=>i.target)), cur=tg.size===1?[...tg][0]:null;\r\n    const outs=[...new Set(its.flatMap(outsFor))];\r\n    h+=`<div class=\"sec\" style=\"--c:${C.c}\"><h4><i><\/i>${C.name}<span style=\"font:500 11px var(--f-mono);color:var(--faint);margin-left:auto\">${its.length} fichier${its.length>1?\"s\":\"\"}<\/span><\/h4>`;\r\n    h+=`<div><div class=\"lbl\" style=\"margin-bottom:8px\">Tout convertir en<\/div><div class=\"chips\">${outs.map(f=>`<button class=\"chip${f===cur?\" on\":\"\"}\" type=\"button\" data-all=\"${k}\" data-f=\"${f}\">${esc(outLabel(its[0],f))}<\/button>`).join(\"\")}<\/div><\/div>`;\r\n    const has=f=>tg.has(f);\r\n    if(k===\"img\"||k===\"vec\"){\r\n      if([...tg].some(f=>![\"svg\",\"pdf\"].includes(f)||k===\"img\")&&!(k===\"vec\"&&tg.size===1&&(has(\"svg\")||has(\"pdf\")))) h+=fld(k===\"vec\"?\"Taille de rendu\":\"Dimensions\",sel(\"size\",SIZES,S.size));\r\n      if(has(\"jpg\")||has(\"webp\")||has(\"avif\")||(k===\"img\"&&has(\"pdf\"))) h+=fld(\"Qualit\u00e9\",rng(\"q\",30,100,1,S.q,FMTV.q));\r\n      if(has(\"jpg\")||has(\"bmp\")||(k===\"img\"&&has(\"pdf\"))) h+=fld(\"Fond (transparence)\",`<div class=\"sw\">${[\"#FFFFFF\",\"#000000\",\"#F4F1EA\"].map(c=>`<button type=\"button\" data-k=\"bg\" data-v=\"${c}\" class=\"${S.bg===c?\"on\":\"\"}\" style=\"background:${c}\" aria-label=\"Fond ${c}\"><\/button>`).join(\"\")}<input type=\"color\" data-k=\"bg\" value=\"${S.bg}\" aria-label=\"Couleur de fond\"><\/div>`);\r\n      if(has(\"ico\")) h+=fld(\"Tailles d'ic\u00f4ne\",`<div class=\"chips\">${[16,24,32,48,64,128,256].map(s=>`<button class=\"chip${S.ico.includes(s)?\" on\":\"\"}\" type=\"button\" data-ico=\"${s}\">${s}<\/button>`).join(\"\")}<\/div>`);\r\n      if(k===\"img\"&&has(\"svg\")) h+=fld(\"Couleurs\",rng(\"colors\",2,64,1,S.colors,FMTV.colors));\r\n      if(k===\"img\"&&its.some(i=>i.ext===\"hdr\"||i.ext===\"exr\")) h+=fld(\"Exposition HDR\",rng(\"exposure\",-4,4,.1,S.exposure,FMTV.exposure));\r\n      if(k===\"vec\"&&has(\"svg\")) h+=`<p class=\"note\">SVG optimis\u00e9 : m\u00e9tadonn\u00e9es d'Illustrator, Inkscape ou Figma retir\u00e9es, d\u00e9cimales r\u00e9duites, rendu identique.<\/p>`;\r\n      if(k===\"img\"&&has(\"svg\")) h+=`<p class=\"note\">Vectorisation automatique : id\u00e9ale pour logos, pictos et aplats. Moins adapt\u00e9e aux photos.<\/p>`;\r\n    }\r\n    if(k===\"pdf\"){ h+=fld(\"R\u00e9solution\",chips(\"dpi\",[[72,\"72 dpi\"],[150,\"150\"],[300,\"300\"],[600,\"600\"]],S.dpi));\r\n      if(its.some(i=>i.pages>1)) h+=fld(\"Pages\",chips(\"pages\",[[\"all\",\"Toutes\"],[\"first\",\"La premi\u00e8re\"]],S.pages));\r\n      if(has(\"png\")||has(\"webp\")) h+=`<label class=\"tg\"><input type=\"checkbox\" data-k=\"pdfAlpha\"${S.pdfAlpha?\" checked\":\"\"}><i><\/i>Fond transparent (logos, fichiers .ai)<\/label>`;\r\n      if(has(\"jpg\")||has(\"webp\")) h+=fld(\"Qualit\u00e9\",rng(\"q\",30,100,1,S.q,FMTV.q)) }\r\n    if(k===\"m3d\"){ h+=fld(\"\u00c9chelle\",sel(\"scale\",[[1,\"Aucune (\u00d71)\"],[.01,\"cm \u2192 m (\u00d70,01)\"],[100,\"m \u2192 cm (\u00d7100)\"],[.001,\"mm \u2192 m (\u00d70,001)\"],[1000,\"m \u2192 mm (\u00d71000)\"],[.0254,\"pouces \u2192 m\"],[10,\"\u00d710\"],[.1,\"\u00d70,1\"]],S.scale));\r\n      h+=fld(\"Axe vertical\",chips(\"up\",[[\"keep\",\"Conserver\"],[\"zy\",\"Z \u2192 Y\"],[\"yz\",\"Y \u2192 Z\"]],S.up));\r\n      h+=`<label class=\"tg\"><input type=\"checkbox\" data-k=\"ground\"${S.ground?\" checked\":\"\"}><i><\/i>Centrer et poser au sol<\/label>`;\r\n      if((has(\"glb\")||has(\"gltf\"))&&its.some(i=>i.obj&&i.obj.animations.length)) h+=`<label class=\"tg\"><input type=\"checkbox\" data-k=\"anim\"${S.anim?\" checked\":\"\"}><i><\/i>Inclure les animations<\/label>`;\r\n      if(has(\"fbx\")) h+=`<p class=\"note\">FBX binaire 7.4 avec mat\u00e9riaux et textures int\u00e9gr\u00e9s, lisible par Blender, Maya, 3ds Max, Cinema 4D, Unity et Unreal. Les animations ne sont pas export\u00e9es en FBX.<\/p>`;\r\n      if(has(\"stl\")||has(\"obj\")||has(\"ply\")) h+=`<p class=\"note\">STL, OBJ et PLY ne gardent que la g\u00e9om\u00e9trie (et les couleurs de sommets en PLY).<\/p>`;\r\n      if(has(\"usdz\")) h+=`<p class=\"note\">USDZ : le format de la r\u00e9alit\u00e9 augment\u00e9e sur iPhone et iPad (Quick Look).<\/p>` }\r\n    if(k===\"vid\"){ const maxD=Math.max(1,...its.map(i=>isFinite(i.dur)?i.dur:1));\r\n      h+=fld(\"D\u00e9but\",rng(\"start\",0,Math.max(.1,Math.floor(maxD*10)\/10),.1,Math.min(S.start,maxD),FMTV.start));\r\n      h+=fld(\"Dur\u00e9e\",chips(\"dur\",[[3,\"3 s\"],[5,\"5 s\"],[10,\"10 s\"],[20,\"20 s\"],[\"all\",\"Tout\"]],S.dur));\r\n      if(has(\"gif\")||has(\"mp4\")||has(\"webm\")) h+=fld(\"Largeur\",chips(\"gw\",[[320,\"320\"],[480,\"480\"],[720,\"720\"],[1080,\"1080\"],[\"orig\",\"Origine\"]],S.gw));\r\n      if(has(\"gif\")) h+=fld(\"Images \/ s\",chips(\"fps\",[[10,\"10\"],[15,\"15\"],[20,\"20\"],[25,\"25\"]],S.fps));\r\n      if(has(\"mp4\")||has(\"webm\")) h+=`<p class=\"note\">MP4 et WebM sont r\u00e9encod\u00e9s en temps r\u00e9el : compte la dur\u00e9e du clip.<\/p>` }\r\n    if(k===\"font\") h+=`<p class=\"note\">WOFF pour ton site, TTF ou OTF pour installer la police, Kit web = WOFF + police + CSS pr\u00eat \u00e0 coller.<\/p>`;\r\n    h+=`<\/div>` }\r\n  box.innerHTML=h;\r\n}\r\nfunction setOpt(k,v){\r\n  const num=[\"q\",\"colors\",\"exposure\",\"dpi\",\"scale\",\"start\"].includes(k)||([\"dur\",\"gw\",\"fps\"].includes(k)&&v!==\"all\"&&v!==\"orig\");\r\n  S[k]=typeof v===\"boolean\"?v:num?+v:v;\r\n  const aff={q:[\"img\",\"vec\",\"pdf\"],size:[\"img\",\"vec\"],bg:[\"img\",\"vec\"],colors:[\"img\"],exposure:[\"img\"],dpi:[\"pdf\"],pages:[\"pdf\"],pdfAlpha:[\"pdf\"],scale:[\"m3d\"],up:[\"m3d\"],ground:[\"m3d\"],anim:[\"m3d\"],fps:[\"vid\"],gw:[\"vid\"],start:[\"vid\"],dur:[\"vid\"]}[k]||[];\r\n  if([\"scale\",\"up\",\"ground\"].includes(k)){ ITEMS.forEach(i=>i.cat===\"m3d\"&&apply3D(i)); if(R3&&R3.cur) frame3D() }\r\n  if(k===\"exposure\") ITEMS.forEach(i=>{ if(i.hdr){ const c=toneMap(i.hdr,S.exposure); i.thumb=thumbOf(c); blobOf(resize(c,\"1600\"),\"image\/jpeg\",.9).then(b=>{ if(i.pv) URL.revokeObjectURL(i.pv); i.pv=URL.createObjectURL(b); if(SEL===i.id) renderPreview() }) } });\r\n  invalidate(i=>aff.includes(i.cat)); renderSettings(); renderPreview();\r\n}\r\n\r\n\/* ---------- format matrix + marquee ---------- *\/\r\nfunction renderCats(){\r\n  const fl=(a,o)=>`<div class=\"fl${o?\" o\":\"\"}\">${a.filter(e=>!ALIAS.includes(e)).map(e=>`<b>${esc(e===\"zip\"?\"KIT WEB\":e.toUpperCase())}<\/b>`).join(\"\")}<\/div>`;\r\n  $(\"#cgrid\").innerHTML=Object.entries(CATS).map(([k,C])=>`<button class=\"glass cat\" type=\"button\" data-cat=\"${k}\" style=\"--c:${C.c}\"><div class=\"ct\"><span class=\"ci\">${C.icon}<\/span><div><h3>${C.name}<\/h3><small>${C.sub}<\/small><\/div><\/div>`+\r\n    `<div class=\"io\"><span>ENTR\u00c9E<\/span>${fl(C.ins)}<\/div><div class=\"io\"><span>SORTIE<\/span>${fl(C.outs.filter(okOut),1)}<\/div><\/button>`).join(\"\");\r\n  const all=[]; Object.values(CATS).forEach(C=>[...C.ins,...C.outs.filter(okOut)].forEach(e=>{ if(!ALIAS.includes(e)&&e!==\"zip\"&&!all.find(a=>a[0]===e)) all.push([e,C.c]) }));\r\n  $(\"#kfmt\").textContent=all.length;\r\n  const shuffled=s=>{ const a=all.slice(); let r=s; for(let i=a.length-1;i>0;i--){ r=(r*9301+49297)%233280; const j=Math.floor(r\/233280*(i+1)); [a[i],a[j]]=[a[j],a[i]] } return a };\r\n  [\"#m1\",\"#m2\",\"#m3\"].forEach((id,n)=>{ const row=shuffled(7+n*13).map(([e,c])=>`<span class=\"fb\" style=\"--c:${c}\"><i><\/i>${e.toUpperCase()}<\/span>`).join(\"\"); $(id).innerHTML=row+row });\r\n}\r\n\r\n\/* ---------- capability probe ---------- *\/\r\nasync function probe(){\r\n  const c=mkCanvas(2,2); for(const [k,m] of [[\"webp\",\"image\/webp\"],[\"avif\",\"image\/avif\"]]){ const b=await new Promise(r=>{ try{ c.toBlob(r,m,.8) }catch(e){ r(null) } }); CAN[k]=!!b&&b.type===m }\r\n  const MR=window.MediaRecorder, cap=!!HTMLCanvasElement.prototype.captureStream;\r\n  CAN.mp4=cap&&!!MR&&[\"video\/mp4;codecs=avc1.42E01E\",\"video\/mp4;codecs=avc1\"].some(t=>MR.isTypeSupported(t));\r\n  CAN.webm=cap&&!!MR&&[\"video\/webm;codecs=vp9\",\"video\/webm\"].some(t=>MR.isTypeSupported(t));\r\n}\r\n\r\n\/* ---------- events ---------- *\/\r\nfunction bind(){\r\n  const input=$(\"#file\"), drop=$(\"#drop\"), okk=$(\"#okk\");\r\n  $(\"#ctalink\").href=CONFIG.contactUrl;\r\n  const pick=accept=>{ input.accept=accept||\"\"; input.click() };\r\n  input.onchange=()=>{ addFiles(input.files); input.value=\"\"; input.accept=\"\" };\r\n  drop.addEventListener(\"click\",e=>{ if(e.target.closest(\"input\")) return; pick() });\r\n  drop.addEventListener(\"keydown\",e=>{ if(e.key===\"Enter\"||e.key===\" \"){ e.preventDefault(); pick() } });\r\n  let depth=0;\r\n  okk.addEventListener(\"dragenter\",e=>{ if(![...(e.dataTransfer?e.dataTransfer.types:[])].includes(\"Files\")) return; e.preventDefault(); depth++; drop.classList.add(\"over\") });\r\n  okk.addEventListener(\"dragover\",e=>{ if([...(e.dataTransfer?e.dataTransfer.types:[])].includes(\"Files\")){ e.preventDefault(); e.dataTransfer.dropEffect=\"copy\" } });\r\n  okk.addEventListener(\"dragleave\",()=>{ if(--depth<=0){ depth=0; drop.classList.remove(\"over\") } });\r\n  okk.addEventListener(\"drop\",e=>{ if(!e.dataTransfer||!e.dataTransfer.files.length) return; e.preventDefault(); depth=0; drop.classList.remove(\"over\"); addFiles(e.dataTransfer.files) });\r\n  document.addEventListener(\"paste\",e=>{ const a=document.activeElement; if(a&&(a.isContentEditable||\/^(input|textarea|select)$\/i.test(a.tagName))) return; if(!okk.isConnected) return;\r\n    const fs=[...(e.clipboardData?e.clipboardData.files:[])]; if(!fs.length) return; e.preventDefault();\r\n    addFiles(fs.map((f,i)=>f.name&&f.name!==\"image.png\"?f:new File([f],`image-collee-${Date.now()%100000}${i?\"-\"+i:\"\"}.${(f.type.split(\"\/\")[1]||\"png\").replace(\"jpeg\",\"jpg\")}`,{type:f.type}))) });\r\n  $(\"#cgrid\").addEventListener(\"click\",e=>{ const b=e.target.closest(\"[data-cat]\"); if(!b) return; const C=CATS[b.dataset.cat]; pick(C.ins.map(x=>\".\"+x).join(\",\")+(b.dataset.cat===\"m3d\"?\",.bin,.mtl,.png,.jpg,.jpeg\":\"\")) });\r\n  const Q=$(\"#queue\");\r\n  Q.addEventListener(\"click\",e=>{ const row=e.target.closest(\".row\"); if(!row) return; const it=byId(+row.dataset.id); if(!it) return; const a=e.target.closest(\"[data-act]\");\r\n    const act=a&&a.dataset.act;\r\n    if(act===\"fmt\") return;\r\n    if(act===\"rm\"){ removeItem(it); return }\r\n    if(act===\"go\"){ convert(it); }\r\n    if(act===\"dl\"&&it.out){ saveFile(it.out.blob,it.out.name); }\r\n    if(SEL!==it.id){ const oc=(byId(SEL)||{}).cat; SEL=it.id; Q.querySelectorAll(\".row\").forEach(r=>r.classList.toggle(\"sel\",+r.dataset.id===SEL)); renderPreview(); if(oc!==it.cat) renderSettings() } });\r\n  Q.addEventListener(\"change\",e=>{ const s=e.target.closest('[data-act=\"fmt\"]'); if(!s) return; const it=byId(+s.closest(\".row\").dataset.id); if(!it) return;\r\n    it.target=s.value; if(it.status===\"done\"||it.status===\"err\"){ it.status=it.bad?\"err\":\"idle\"; it.out=null; if(it.outUrl){ URL.revokeObjectURL(it.outUrl); it.outUrl=null } } SEL=it.id; renderQueue(); renderSettings(); renderPreview() });\r\n  $(\"#clear\").onclick=()=>{ [...ITEMS].forEach(i=>{ if(i.status!==\"busy\") removeItem(i) }) };\r\n  $(\"#runall\").onclick=async()=>{ if(RUNNING) return; RUNNING=true; const b=$(\"#runall\"); b.disabled=true; const list=ITEMS.filter(i=>i.status!==\"done\"&&!i.bad);\r\n    for(let k=0;k<list.length;k++){ b.textContent=`Conversion ${k+1} \/ ${list.length}\u2026`; await convert(list[k]) }\r\n    RUNNING=false; updFoot(); const ok=list.filter(i=>i.status===\"done\").length; toast(ok===list.length?`${ok} fichier${ok>1?\"s\":\"\"} pr\u00eat${ok>1?\"s\":\"\"} \u00e0 t\u00e9l\u00e9charger`:`${ok} sur ${list.length} convertis`) };\r\n  $(\"#zipall\").onclick=async()=>{ const d=ITEMS.filter(i=>i.status===\"done\"); if(!d.length) return; if(d.length===1){ saveFile(d[0].out.blob,d[0].out.name); return }\r\n    const b=$(\"#zipall\"); b.disabled=true; b.textContent=\"Pr\u00e9paration du .zip\u2026\"; try{ saveFile(await zipBlob(d.map(i=>i.out)),\"format-lab-ok3d.zip\") }catch(e){ toast(msg(e)) } updFoot() };\r\n  const set=$(\"#set\");\r\n  set.addEventListener(\"click\",e=>{ const all=e.target.closest(\"[data-all]\"); if(all){ ITEMS.filter(i=>i.cat===all.dataset.all&&i.status!==\"busy\").forEach(i=>{ if(outsFor(i).includes(all.dataset.f)){ i.target=all.dataset.f; if(i.status===\"done\"){ i.status=\"idle\"; i.out=null; if(i.outUrl){ URL.revokeObjectURL(i.outUrl); i.outUrl=null } } } }); renderQueue(); renderSettings(); renderPreview(); return }\r\n    const ic=e.target.closest(\"[data-ico]\"); if(ic){ const s=+ic.dataset.ico; S.ico=S.ico.includes(s)?S.ico.filter(x=>x!==s):[...S.ico,s]; if(!S.ico.length) S.ico=[s]; setOpt(\"ico\",S.ico); return }\r\n    const c=e.target.closest(\"button[data-k]\"); if(c) setOpt(c.dataset.k,c.dataset.v) });\r\n  set.addEventListener(\"input\",e=>{ const r=e.target; if(r.type!==\"range\") return; const k=r.dataset.k; r.style.setProperty(\"--p\",(r.value-r.min)\/(r.max-r.min)*100+\"%\"); const v=set.querySelector(`[data-val=\"${k}\"]`); if(v) v.textContent=FMTV[k](r.value) });\r\n  set.addEventListener(\"change\",e=>{ const t=e.target, k=t.dataset.k; if(!k) return; setOpt(k,t.type===\"checkbox\"?t.checked:t.value) });\r\n}\r\n\r\n\/* ---------- init ---------- *\/\r\n(async()=>{ await probe(); renderCats(); bind(); dlCap() })();\r\n\r\n})();\r\n<\/script>\r\n<script>\r\n(() => {\r\n  const root = document.querySelector('.okk-scope[data-okk=\"format\"]');\r\n  if (!root || root.dataset.formatLayoutReady) return;\r\n  root.dataset.formatLayoutReady = 'true';\r\n  const doc = root.ownerDocument;\r\n  const win = doc.defaultView;\r\n  let pending = 0;\r\n  const setVar = (name, value) => {\r\n    if (root.style.getPropertyValue(name) !== value) root.style.setProperty(name, value);\r\n  };\r\n  const measure = () => {\r\n    pending = 0;\r\n    if (!root.isConnected) { observer?.disconnect(); return; }\r\n    const shift = parseFloat(root.style.getPropertyValue('--okk-shift')) || 0;\r\n    const left = root.getBoundingClientRect().left;\r\n    setVar('--okk-width', doc.documentElement.clientWidth + 'px');\r\n    setVar('--okk-shift', Math.round((shift - left) * 100) \/ 100 + 'px');\r\n  };\r\n  const schedule = () => { if (!pending) pending = win.requestAnimationFrame(measure); };\r\n  const observer = typeof win.ResizeObserver === 'function' ? new win.ResizeObserver(schedule) : null;\r\n  observer?.observe(doc.documentElement);\r\n  if (root.parentElement) observer?.observe(root.parentElement);\r\n  win.addEventListener('resize', schedule, { passive: true });\r\n  win.addEventListener('orientationchange', schedule);\r\n  win.addEventListener('load', schedule, { once: true });\r\n  doc.fonts?.ready.then(schedule);\r\n  measure();\r\n})();\r\n\r\n<\/script>\r\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>OK3D Lab \u00b7 Convertisseur Tous tes fichiers.Le bon format. Images, vectoriel, PDF, vid\u00e9os, polices et mod\u00e8les 3D : d\u00e9pose, choisis, t\u00e9l\u00e9charge. Tout se passe dans ton navigateur, tes fichiers ne quittent jamais ton appareil. 40+formats100 %local0inscription D\u00e9pose tes fichiers ici Plusieurs fichiers \u00e0 la fois, de tous types \u00b7 ou colle une image avec Ctrl+V [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-15727","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15727","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/comments?post=15727"}],"version-history":[{"count":4,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15727\/revisions"}],"predecessor-version":[{"id":15732,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15727\/revisions\/15732"}],"wp:attachment":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/media?parent=15727"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}