{"id":15668,"date":"2026-10-16T19:19:04","date_gmt":"2026-10-16T17:19:04","guid":{"rendered":"https:\/\/www.ok3d.fr\/?page_id=15668"},"modified":"2026-10-16T20:44:03","modified_gmt":"2026-10-16T18:44:03","slug":"keystroke","status":"publish","type":"page","link":"https:\/\/www.ok3d.fr\/en\/keystroke\/","title":{"rendered":"Keystroke \u2013 Keyboard Shortcut Quiz"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"15668\" class=\"elementor elementor-15668\" 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-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<!-- Keystroke OK3D v2 \u2014 coller tout ce fichier dans un widget HTML Elementor. -->\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\n<link rel=\"preconnect\" href=\"https:\/\/fonts.gstatic.com\" crossorigin>\n<link rel=\"stylesheet\" href=\"https:\/\/fonts.googleapis.com\/css2?family=Unbounded:wght@500;700&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@500;600&display=swap\">\n<style>\n\n\/* Layout: dark studio \"viewport\" \u2014 a centred 1240px column; quiz splits into stage | question on wide screens, stacks on phones.\n   Brand: OK3D blue on deep night, ice-cyan glints. Couleurs modifiables ici. *\/\n.okk{\n  color-scheme:dark;\n  --bg:#06080C; --bg2:#0A0E15; --ink:#EEF2F8; --muted:#A1ADBF; --faint:#8592A8; --line:#1C2433; --line2:#2A3549;\n  --brand:#2E6BFF; --brand-hi:#4F86FF; --brand2:#7CC4FF; --glow:rgba(46,107,255,.45);\n  --good:#35E0A1; --bad:#FF5A6E; --gold:#FFCB6B;\n  \/* stage *\/\n  --stage:#080B12; --dot:#141B29; --surface:#101623; --shape:#273043; --accent-ink:#FFFFFF;\n  --s1:#3D7BFF; --s2:#FFB454; --s3:#2FD3C0; --s4:#FF6F9F;\n  --bt:#3E4A61; --bm:#323C50; --bl:#262E3F; --br:#1A2030; --be:#6F86B4;\n  --f-display:\"Unbounded\",\"Arial Black\",system-ui,sans-serif;\n  --f-body:\"Geist\",system-ui,-apple-system,\"Segoe UI\",sans-serif;\n  --f-mono:\"Geist Mono\",ui-monospace,Menlo,Consolas,monospace;\n}\n.okk, .okk{background:var(--bg);color:var(--ink)}\n.okk{font-family:var(--f-body);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}\n.okk, .okk *{box-sizing:border-box}\n.okk button{font:inherit;color:inherit;cursor:pointer;background:none;border:0;padding:0}\n.okk a{color:inherit} .okk a.btn{text-decoration:none}\n.okk :focus-visible{outline:2px solid var(--brand2);outline-offset:3px;border-radius:6px}\n\n.okk{position:relative;overflow:hidden;min-height:100%;\n  padding-left:max(16px,env(safe-area-inset-left,0px));padding-right:max(16px,env(safe-area-inset-right,0px));padding-block:0 28px;\n  background:\n    radial-gradient(1100px 560px at 12% -8%, rgba(46,107,255,.20), transparent 62%),\n    radial-gradient(800px 480px at 105% 4%, rgba(124,196,255,.08), transparent 60%),\n    var(--bg)}\n.okk::before{content:\"\";position:absolute;inset:0;pointer-events:none;opacity:.5;\n  background-image:linear-gradient(rgba(124,196,255,.035) 1px,transparent 1px),linear-gradient(90deg,rgba(124,196,255,.035) 1px,transparent 1px);\n  background-size:56px 56px;mask-image:linear-gradient(180deg,#000,transparent 70%);-webkit-mask-image:linear-gradient(180deg,#000,transparent 70%)}\n.okk .wrap{position:relative;max-width:1240px;margin:0 auto}\n.okk #fxl{position:absolute;inset:0;pointer-events:none;z-index:50;overflow:hidden}\n.okk #fxl i{position:absolute;border-radius:50%;display:block}\n.okk #fxl b{position:absolute;font:700 18px var(--f-display);color:var(--good);text-shadow:0 0 18px rgba(53,224,161,.6);white-space:nowrap;transform:translate(-50%,-50%)}\n\n\/* header *\/\n.okk .bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-block:18px;flex-wrap:wrap}\n.okk .brand{display:flex;align-items:center;gap:12px;text-decoration:none}\n.okk .brand .mk{font:700 17px var(--f-display);letter-spacing:-.02em;display:flex;align-items:center;gap:8px}\n.okk .brand .mk i{width:26px;height:26px;border-radius:9px;display:grid;place-items:center;font-style:normal;font-size:9px;letter-spacing:0;\n  background:linear-gradient(150deg,var(--brand-hi),#1C47C9);box-shadow:0 0 0 1px rgba(255,255,255,.12) inset,0 6px 20px -6px var(--glow)}\n.okk .brand img{height:26px;width:auto;display:block}\n.okk .brand .pr{font:500 13px var(--f-body);color:var(--muted);padding-left:12px;border-left:1px solid var(--line2)}\n.okk .tools{display:flex;align-items:center;gap:8px}\n.okk .lvl{display:flex;align-items:center;gap:10px;padding:5px 12px 5px 6px;border:1px solid var(--line);border-radius:999px;background:rgba(255,255,255,.02)}\n.okk .lvl .ring{width:28px;height:28px;border-radius:50%;display:grid;place-items:center;font:700 11px var(--f-display);\n  background:conic-gradient(var(--brand2) calc(var(--p)*1%),var(--line2) 0);position:relative}\n.okk .lvl .ring::after{content:\"\";position:absolute;inset:3px;border-radius:50%;background:var(--bg2)}\n.okk .lvl .ring span{position:relative;z-index:1}\n.okk .lvl small{font:500 12px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}\n.okk .icon-btn{width:36px;height:36px;border-radius:50%;border:1px solid var(--line);display:grid;place-items:center;color:var(--muted);transition:color .2s,border-color .2s}\n.okk .icon-btn:hover, .okk .icon-btn[aria-pressed=\"true\"]{color:var(--ink);border-color:var(--line2)}\n.okk .icon-btn svg{width:16px;height:16px}\n.okk .seg{display:flex;border:1px solid var(--line);border-radius:999px;padding:3px;background:rgba(255,255,255,.02)}\n.okk .seg button{padding:5px 11px;border-radius:999px;font:600 11.5px var(--f-body);color:var(--muted);letter-spacing:.02em}\n.okk .seg button[aria-pressed=\"true\"]{background:var(--ink);color:var(--bg)}\n\n\/* type *\/\n.okk .eyebrow{font:600 11px var(--f-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--brand2);display:inline-flex;align-items:center;gap:8px}\n.okk .eyebrow::before{content:\"\";width:6px;height:6px;border-radius:2px;background:var(--brand2);box-shadow:0 0 10px var(--brand2)}\n.okk h1, .okk h2, .okk h3{margin:0;font-family:var(--f-display);letter-spacing:-.03em;text-wrap:balance}\n.okk .grad{background:linear-gradient(100deg,var(--brand-hi) 0%,var(--brand2) 60%,#E9F5FF 100%);-webkit-background-clip:text;background-clip:text;color:transparent}\n.okk .muted{color:var(--muted)}\n\n\/* buttons *\/\n.okk .btn{display:inline-flex;align-items:center;justify-content:center;gap:10px;height:48px;padding:0 24px;border-radius:999px;font-weight:600;font-size:15px;transition:transform .15s,box-shadow .2s,background-color .2s;white-space:nowrap}\n.okk .btn.pri{background:linear-gradient(180deg,var(--brand-hi),#2152E0);color:#fff;box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 10px 30px -10px var(--glow),0 0 0 1px rgba(46,107,255,.6)}\n.okk .btn.pri:hover{transform:translateY(-1px);box-shadow:inset 0 1px 0 rgba(255,255,255,.28),0 16px 40px -10px var(--glow),0 0 0 1px rgba(124,196,255,.7)}\n.okk .btn.gho{border:1px solid var(--line2);background:rgba(255,255,255,.02)}\n.okk .btn.gho:hover{border-color:var(--muted)}\n.okk .btn kbd{height:20px;min-width:0;font-size:10.5px;padding:0 6px;border-bottom-width:1px;box-shadow:none;background:rgba(255,255,255,.08);border-color:rgba(255,255,255,.18);color:inherit}\n\n\/* glass *\/\n.okk .glass{background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.012));border:1px solid var(--line);border-radius:24px;box-shadow:inset 0 1px 0 rgba(255,255,255,.045)}\n\n\/* keycaps *\/\n.okk kbd{font:600 13px var(--f-mono);min-width:30px;height:30px;padding:0 8px;display:inline-grid;place-items:center;color:var(--ink);\n  background:linear-gradient(180deg,#1B2333,#0E131D);border:1px solid #29344A;border-bottom:3px solid #04060A;border-radius:8px;\n  box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 4px 12px rgba(0,0,0,.35);transition:transform .08s,border-bottom-width .08s,color .15s,box-shadow .15s}\n.okk kbd.down{transform:translateY(2px);border-bottom-width:1px;color:var(--brand2);box-shadow:0 0 0 1px var(--brand),0 0 22px var(--glow)}\n.okk .keys{display:inline-flex;align-items:center;gap:5px;flex-wrap:wrap;justify-content:center}\n.okk .keys .plus{color:var(--faint);font-size:12px}\n\n\/* ---------- HOME ---------- *\/\n.okk .hero{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(0,.95fr);gap:40px;align-items:center;padding-block:28px 40px}\n.okk .hero h1{font-size:clamp(36px,5.4vw,70px);line-height:1.02;margin-top:18px;font-weight:700}\n.okk .hero .lede{color:var(--muted);font-size:clamp(15px,1.35vw,17px);max-width:46ch;margin:20px 0 0}\n.okk .hero .cta{display:flex;gap:12px;flex-wrap:wrap;margin-top:28px}\n.okk .stats{display:flex;gap:28px;margin-top:34px;flex-wrap:wrap}\n.okk .stats div{display:flex;flex-direction:column;gap:2px}\n.okk .stats b{font:700 26px var(--f-display);letter-spacing:-.03em}\n.okk .stats span{font:500 11px var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}\n\n.okk .hviz{position:relative;aspect-ratio:1\/0.86;max-width:100%;perspective:1100px;border-radius:28px;overflow:hidden;\n  background:radial-gradient(60% 55% at 50% 45%,rgba(46,107,255,.28),transparent 70%),linear-gradient(180deg,#0A0F18,#06080C);border:1px solid var(--line)}\n.okk .hviz .floor{position:absolute;left:-30%;right:-30%;bottom:-18%;height:80%;transform:rotateX(72deg);transform-origin:50% 100%;\n  background:linear-gradient(rgba(124,196,255,.22) 1px,transparent 1px) 0 0\/100% 34px,linear-gradient(90deg,rgba(124,196,255,.22) 1px,transparent 1px) 0 0\/34px 100%;\n  mask-image:radial-gradient(closest-side,#000,transparent);-webkit-mask-image:radial-gradient(closest-side,#000,transparent);animation:floorrun 3s linear infinite}\n@keyframes floorrun{to{background-position:0 34px,0 0}}\n.okk .hviz .plane{position:absolute;inset:0 0 18% 0;display:flex;align-items:center;justify-content:center;gap:clamp(8px,1.4vw,16px);\n  transform-style:preserve-3d;transform:rotateX(var(--rx,46deg)) rotateZ(var(--rz,-20deg));transition:transform .5s cubic-bezier(.2,.8,.2,1)}\n.okk .hk{height:clamp(54px,8.4vw,92px);min-width:clamp(54px,8.4vw,92px);padding:0 clamp(10px,1.6vw,20px);border-radius:clamp(12px,1.6vw,18px);display:grid;place-items:center;\n  font:600 clamp(15px,2vw,24px) var(--f-mono);color:var(--ink);transform-style:preserve-3d;\n  background:linear-gradient(155deg,#26314A 0%,#131A29 55%,#0B1019 100%);\n  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),inset 0 -2px 0 rgba(0,0,0,.45),0 12px 0 #04060A,0 34px 50px rgba(0,0,0,.6);transition:transform .12s,box-shadow .12s,color .12s}\n.okk .hk span{text-shadow:0 0 16px rgba(124,196,255,.3)}\n.okk .hk.down{transform:translateZ(-9px);color:var(--brand2);\n  box-shadow:inset 0 1px 0 rgba(255,255,255,.14),0 3px 0 #04060A,0 0 0 1px rgba(124,196,255,.55),0 0 46px rgba(46,107,255,.85)}\n.okk .hcap{position:absolute;left:0;right:0;bottom:18px;display:flex;flex-direction:column;align-items:center;gap:4px;text-align:center;padding:0 16px}\n.okk .hcap .a{font:700 clamp(13px,1.5vw,17px) var(--f-display);letter-spacing:-.02em}\n.okk .hcap .s{font:500 11px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}\n\n.okk .sec-h{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;margin:8px 0 18px}\n.okk .sec-h h2{font-size:clamp(22px,2.6vw,32px);font-weight:700}\n.okk .filters{display:flex;gap:6px;flex-wrap:wrap}\n.okk .filters button{padding:8px 14px;border-radius:999px;border:1px solid var(--line);font-size:13px;font-weight:500;color:var(--muted);transition:all .2s}\n.okk .filters button[aria-pressed=\"true\"]{color:var(--ink);border-color:var(--brand);background:rgba(46,107,255,.12)}\n.okk .agrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:14px}\n.okk .acard{position:relative;text-align:left;padding:20px;border-radius:22px;border:1px solid var(--line);overflow:hidden;\n  background:linear-gradient(180deg,rgba(255,255,255,.035),rgba(255,255,255,.01));display:flex;flex-direction:column;gap:16px;transition:transform .25s cubic-bezier(.2,.8,.2,1),border-color .25s}\n.okk .acard::before{content:\"\";position:absolute;inset:0;opacity:0;transition:opacity .3s;\n  background:radial-gradient(320px circle at var(--mx,50%) var(--my,0%),color-mix(in srgb,var(--hue) 22%,transparent),transparent 60%)}\n.okk .acard:hover{transform:translateY(-3px);border-color:color-mix(in srgb,var(--hue) 45%,var(--line))}\n.okk .acard:hover::before{opacity:1}\n.okk .acard>*{position:relative}\n.okk .acard .top{display:flex;align-items:center;gap:14px}\n.okk .mono{width:52px;height:52px;border-radius:15px;display:grid;place-items:center;font:700 17px var(--f-display);letter-spacing:-.02em;flex:none;\n  color:var(--hue);background:color-mix(in srgb,var(--hue) 12%,#0B0F17);box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--hue) 35%,transparent),0 10px 30px -14px var(--hue)}\n.okk .acard b{display:block;font:700 16px var(--f-display);letter-spacing:-.02em}\n.okk .acard small{color:var(--muted);font-size:13px}\n.okk .abar{height:4px;border-radius:4px;background:var(--line);overflow:hidden}\n.okk .abar i{display:block;height:100%;background:linear-gradient(90deg,var(--hue),color-mix(in srgb,var(--hue) 40%,#fff));border-radius:4px}\n.okk .ameta{display:flex;justify-content:space-between;gap:8px;font:500 11.5px var(--f-mono);color:var(--muted);font-variant-numeric:tabular-nums}\n.okk .badge{display:inline-flex;align-items:center;gap:6px;padding:4px 10px;border-radius:999px;font:600 11px var(--f-mono);letter-spacing:.06em;text-transform:uppercase;\n  color:var(--gold);background:rgba(255,203,107,.1);box-shadow:inset 0 0 0 1px rgba(255,203,107,.35)}\n.okk .acard .badge{position:absolute;top:18px;right:18px}\n\n.okk .studio{margin-top:56px;padding:clamp(22px,4vw,40px);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center}\n.okk .studio h3{font-size:clamp(20px,2.4vw,30px);font-weight:700}\n.okk .studio p{color:var(--muted);margin:10px 0 0;max-width:56ch}\n.okk .foot{margin-top:28px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font:500 12px var(--f-mono);color:var(--faint);letter-spacing:.04em}\n\n\/* ---------- APP DETAIL ---------- *\/\n.okk .crumb{display:inline-flex;align-items:center;gap:8px;color:var(--muted);font-size:14px;padding:6px 0;margin-top:6px}\n.okk .crumb:hover{color:var(--ink)}\n.okk .ahead{display:flex;align-items:center;gap:20px;margin:18px 0 26px;flex-wrap:wrap}\n.okk .ahead .mono{width:76px;height:76px;border-radius:22px;font-size:24px}\n.okk .ahead h1{font-size:clamp(30px,4.4vw,52px);font-weight:700}\n.okk .ahead p{margin:6px 0 0;color:var(--muted)}\n.okk .chips{display:flex;gap:8px;flex-wrap:wrap;margin-top:12px}\n.okk .chips span{font:500 12px var(--f-mono);color:var(--muted);padding:5px 10px;border-radius:999px;border:1px solid var(--line);font-variant-numeric:tabular-nums}\n.okk .modes{display:grid;grid-template-columns:1fr 1fr;gap:14px}\n.okk .mode{position:relative;text-align:left;padding:26px;border-radius:24px;border:1px solid var(--line);overflow:hidden;\n  background:linear-gradient(180deg,rgba(255,255,255,.04),rgba(255,255,255,.01));display:flex;flex-direction:column;gap:12px;transition:transform .25s,border-color .25s}\n.okk .mode:hover{transform:translateY(-3px);border-color:var(--brand)}\n.okk .mode::after{content:\"\";position:absolute;width:260px;height:260px;right:-90px;top:-120px;border-radius:50%;background:radial-gradient(closest-side,rgba(46,107,255,.28),transparent);pointer-events:none}\n.okk .mode.m2::after{background:radial-gradient(closest-side,rgba(255,203,107,.22),transparent)}\n.okk .mode h3{font-size:22px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:10px}\n.okk .mode p{margin:0;color:var(--muted);max-width:44ch}\n.okk .mode .pills{display:flex;gap:6px;flex-wrap:wrap}\n.okk .mode .pills span{font:500 11px var(--f-mono);letter-spacing:.05em;padding:4px 9px;border-radius:999px;background:rgba(255,255,255,.05);color:var(--ink)}\n.okk .mode .go{width:38px;height:38px;border-radius:50%;display:grid;place-items:center;background:var(--ink);color:var(--bg);font-size:16px;flex:none}\n.okk .opts-row{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-top:14px;padding:14px 18px;border-radius:16px;border:1px solid var(--line);flex-wrap:wrap}\n.okk .opts-row label{display:flex;align-items:center;gap:12px;cursor:pointer;font-size:14px}\n.okk .opts-row small{color:var(--muted);font-size:12.5px}\n.okk .sw{position:relative;width:40px;height:22px;border-radius:999px;background:var(--line2);transition:background .2s;flex:none}\n.okk .sw::after{content:\"\";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .2s}\n.okk input.swi{position:absolute;opacity:0;width:1px;height:1px}\n.okk input.swi:checked+.sw{background:var(--brand)}\n.okk input.swi:checked+.sw::after{transform:translateX(18px)}\n.okk input.swi:focus-visible+.sw{outline:2px solid var(--brand2);outline-offset:2px}\n\n.okk .lib{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(0,.9fr);gap:18px;align-items:start;margin-top:44px}\n.okk .lib .sticky{position:sticky;top:12px}\n.okk .libh{margin-bottom:14px}\n.okk .libh h2{font-size:clamp(20px,2.2vw,26px);font-weight:700}\n.okk .libh p{color:var(--muted);margin:6px 0 0;font-size:14px}\n.okk .list{display:flex;flex-direction:column;border-top:1px solid var(--line)}\n.okk .li{display:grid;grid-template-columns:22px minmax(0,1fr) auto;gap:12px;align-items:center;text-align:left;padding:12px 6px;border-bottom:1px solid var(--line);transition:background .2s}\n.okk .li:hover{background:rgba(255,255,255,.025)}\n.okk .li .pl{width:22px;height:22px;border-radius:50%;display:grid;place-items:center;border:1px solid var(--line2);font-size:8px;color:var(--muted)}\n.okk .li.on{background:rgba(46,107,255,.08)}\n.okk .li.on .pl{background:var(--brand);border-color:var(--brand);color:#fff}\n.okk .li .nm{font-size:14px;font-weight:500;min-width:0}\n.okk .li .nm small{display:block;color:var(--faint);font:500 11px var(--f-mono);margin-top:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.okk .li kbd{font-size:11.5px;min-width:24px;height:24px;padding:0 6px;border-radius:6px}\n\n\/* ---------- STAGE ---------- *\/\n.okk .scard{overflow:hidden;padding:0;position:relative}\n.okk .stage{position:relative;aspect-ratio:16\/9;max-width:100%;overflow:hidden;background-color:var(--stage);\n  background-image:radial-gradient(var(--dot) 1px,transparent 1.3px),radial-gradient(70% 70% at 50% 40%,rgba(46,107,255,.10),transparent 70%);background-size:14px 14px,100% 100%}\n.okk .stage>svg{position:absolute;inset:0;width:100%;height:100%;overflow:hidden;transition:filter .5s}\n.okk .stage svg *{transform-box:fill-box;transform-origin:center}\n.okk .stage svg .world{transform-box:view-box;transform-origin:0 0}\n.okk .stage .crn{position:absolute;width:14px;height:14px;border-color:rgba(124,196,255,.45);border-style:solid;border-width:0;z-index:3;pointer-events:none}\n.okk .stage .c-tl{top:10px;left:10px;border-top-width:1.5px;border-left-width:1.5px}.okk .stage .c-tr{top:10px;right:10px;border-top-width:1.5px;border-right-width:1.5px}\n.okk .stage .c-bl{bottom:10px;left:10px;border-bottom-width:1.5px;border-left-width:1.5px}.okk .stage .c-br{bottom:10px;right:10px;border-bottom-width:1.5px;border-right-width:1.5px}\n.okk .stage .vp{position:absolute;top:14px;left:30px;z-index:3;font:600 10px var(--f-mono);letter-spacing:.14em;color:var(--muted);text-transform:uppercase;transition:color .3s}\n.okk .stage .vp.good{color:var(--good)}.okk .stage .vp.bad{color:var(--bad)}\n.okk .stage .rep{position:absolute;top:10px;right:30px;z-index:4;padding:5px 11px;border-radius:999px;font:600 11.5px var(--f-body);border:1px solid var(--line2);background:rgba(8,11,18,.8);backdrop-filter:blur(6px)}\n.okk .stage .rep:hover{border-color:var(--brand2)}\n.okk .stage .q{position:absolute;inset:0;display:grid;place-items:center;z-index:2;font:700 clamp(44px,8vw,90px) var(--f-display);color:rgba(124,196,255,.7);text-shadow:0 0 40px var(--glow);opacity:0;transition:opacity .4s;pointer-events:none}\n.okk .stage.mystery>svg{filter:blur(9px) saturate(.6)}\n.okk .stage.mystery .q{opacity:1}\n.okk .stage::after{content:\"\";position:absolute;inset:0;pointer-events:none;z-index:2;opacity:0;\n  background:linear-gradient(105deg,transparent 30%,rgba(124,196,255,.22) 48%,rgba(255,255,255,.35) 50%,rgba(124,196,255,.22) 52%,transparent 70%);transform:translateX(-100%)}\n.okk .stage.sweep::after{animation:sweep .9s cubic-bezier(.4,0,.2,1)}\n@keyframes sweep{0%{opacity:1;transform:translateX(-100%)}100%{opacity:1;transform:translateX(100%)}}\n.okk .stage.hit{box-shadow:inset 0 0 0 2px var(--bad),inset 0 0 60px rgba(255,90,110,.35)}\n.okk .strip{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 16px;border-top:1px solid var(--line);min-height:62px}\n.okk .strip .lab{font:600 10px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}\n.okk .strip .ks{display:flex;gap:6px;align-items:center;flex-wrap:wrap;justify-content:flex-end}\n.okk .strip kbd{min-width:38px;height:38px;font-size:15px;border-radius:10px}\n.okk .strip kbd.ph{background:transparent;border-style:dashed;border-bottom-width:1px;color:var(--faint);box-shadow:none}\n.okk .strip.ok kbd:not(.ph){box-shadow:0 0 0 1px rgba(53,224,161,.6),0 0 18px rgba(53,224,161,.3)}\n.okk .timer{height:3px;background:var(--line)}\n.okk .timer i{display:block;height:100%;width:100%;background:linear-gradient(90deg,var(--brand),var(--brand2));box-shadow:0 0 12px var(--glow)}\n\n\/* stage vocabulary *\/\n.okk .c1{fill:var(--s1)} .okk .c2{fill:var(--s2)} .okk .c3{fill:var(--s3)} .okk .c4{fill:var(--s4)} .okk .cs{fill:var(--shape)} .okk .ci{fill:var(--ink)} .okk .cb{fill:var(--surface)}\n.okk .th{fill:var(--s3)} .okk .thE{fill:var(--surface);stroke:var(--line2)}\n.okk .sh{fill:var(--shape);fill-opacity:.7;stroke:var(--ink);stroke-width:1.2}\n.okk .ln{fill:none;stroke:var(--ink);stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round}\n.okk .thick2{stroke-width:2.2}\n.okk .ol{stroke:var(--ink);stroke-width:1.2}\n.okk .sel{fill:none;stroke:var(--s1);stroke-width:1}\n.okk .csel{fill:none;stroke:var(--s4);stroke-width:1.2}\n.okk .hdl{fill:var(--stage);stroke:var(--s1);stroke-width:1} .okk .hdl.c{stroke:var(--s4)}\n.okk .anc{fill:var(--stage);stroke:var(--brand2);stroke-width:1.1} .okk .anc.on{fill:var(--brand2)}\n.okk .drag{fill:none;stroke:var(--brand2);stroke-width:1.5;stroke-dasharray:3 3}\n.okk .hline{stroke:var(--brand2);stroke-width:1} .okk .hdot{fill:var(--brand2)}\n.okk .ghost{fill:none;stroke:var(--muted);stroke-width:1;stroke-dasharray:3 3}\n.okk .lb{font:500 7.5px var(--f-mono);fill:var(--muted);letter-spacing:.02em} .okk .lb.ink{fill:var(--ink)} .okk .lb.comp{fill:var(--s4)} .okk .lb.good{fill:var(--good)} .okk .lb.br{fill:var(--brand2)}\n.okk .rt{font:500 7.5px var(--f-body);fill:var(--ink)} .okk .rt.dim{fill:var(--muted)}\n.okk .tx{font-family:var(--f-display);font-weight:700;fill:var(--ink)}\n.okk .env{fill:var(--shape);fill-opacity:.6;stroke:var(--ink);stroke-width:1}\n.okk .meshl{fill:none;stroke:#fff;stroke-opacity:.45;stroke-width:.8}\n.okk .frame{fill:#E9ECF1;stroke:var(--line2);stroke-width:1}\n.okk .pill{fill:var(--s1)} .okk .pillt{font:600 7px var(--f-mono);fill:#fff}\n.okk .caret{stroke:var(--brand2);stroke-width:1.5}\n.okk .alf{fill:none;stroke:var(--s1);stroke-width:1.2}\n.okk .gapm{fill:var(--s4);fill-opacity:.3} .okk .gapt{font:600 7px var(--f-mono);fill:var(--s4)}\n.okk .btxt{font:600 10px var(--f-body);fill:#fff}\n.okk .cdia{fill:var(--s4)}\n.okk .panel{fill:var(--surface);stroke:var(--line2);stroke-width:1}\n.okk .pl{fill:var(--line2)}\n.okk .row-bg{fill:transparent} .okk .row-bg.on{fill:var(--s1);fill-opacity:.2}\n.okk .strk{stroke:var(--ink);stroke-width:2}\n.okk .thick{fill:none;stroke:var(--s1);stroke-width:14;stroke-linecap:round}\n.okk .thickol{fill:none;stroke:var(--ink);stroke-width:16.5;stroke-linecap:round}\n.okk .pin{fill:var(--s4)} .okk .pint{font:700 8px var(--f-body);fill:#fff}\n.okk .ripple{fill:var(--brand2)}\n.okk .cur path{fill:#fff;stroke:#0A0D14;stroke-width:1.1;stroke-linejoin:round}\n.okk .hdr rect{fill:var(--surface);stroke:var(--line2);stroke-width:.8}\n.okk .grid line{stroke:#1E2840;stroke-width:.7}\n.okk .axx{stroke:var(--bad);stroke-width:1;stroke-opacity:.7}\n.okk .bt{fill:var(--bt)} .okk .bm{fill:var(--bm)} .okk .bl{fill:var(--bl)} .okk .br{fill:var(--br)}\n.okk .bx polygon{stroke:var(--be);stroke-width:.6;stroke-linejoin:round}\n.okk .bx.on polygon{stroke:var(--s2);stroke-width:1.3}\n.okk .bx.ed polygon{stroke:#C9D6F2;stroke-width:.8}\n.okk .fsel{fill:var(--s2);fill-opacity:.4;stroke:var(--s2);stroke-width:1;stroke-linejoin:round}\n.okk .bedge{fill:none;stroke:var(--s2);stroke-width:1.2;stroke-linejoin:round}\n.okk .cut{fill:none;stroke:var(--s2);stroke-width:1.6;stroke-linejoin:round}\n.okk .vdot{fill:#DDE6F7} .okk .vdot.on{fill:var(--s2)}\n.okk .wire{fill:none;stroke:var(--be);stroke-width:.7}\n.okk .infl{fill:none;stroke:var(--brand2);stroke-width:.8;stroke-dasharray:2 2}\n.okk .tile{fill:var(--stage);stroke:var(--line2);stroke-width:.5} .okk .tile.on{stroke:var(--s2);stroke-width:1.4}\n.okk .shadow{fill:#000;fill-opacity:.35} .okk .shine{fill:#fff;fill-opacity:.16}\n.okk .sph{fill:var(--bl)} .okk .sphh{fill:var(--bt)} .okk .sphs{fill:none;stroke:var(--s2);stroke-width:1.3}\n.okk .cur3d{fill:none;stroke:var(--bad);stroke-width:1;stroke-dasharray:2 2}\n.okk .antsb{fill:none;stroke:#000;stroke-width:1} .okk .antsw{fill:none;stroke:#fff;stroke-width:1;stroke-dasharray:3 3;animation:ants .7s linear infinite}\n@keyframes ants{to{stroke-dashoffset:-6}}\n.okk .qm{fill:var(--s1);fill-opacity:.32;fill-rule:evenodd}\n.okk .cmask{fill:#05070A;fill-opacity:.6;fill-rule:evenodd} .okk .cropf{fill:none;stroke:#fff;stroke-width:1} .okk .cropc{fill:none;stroke:#fff;stroke-width:2.6}\n.okk .canvasp{fill:#E9ECF1;stroke:var(--line2)} .okk .paint{fill:none;stroke:var(--s1);stroke-linecap:round;stroke-linejoin:round} .okk .brushc{fill:none;stroke:var(--brand2);stroke-width:1}\n.okk .canvasp~.lb.ink{fill:#11151C}\n.okk .tri-w{fill:#fff;stroke:#0A0D14;stroke-width:.8}\n.okk .wipe{stroke:#fff;stroke-width:1.5}\n.okk .bars rect{fill:var(--muted);fill-opacity:.6}\n.okk .cgrid line{stroke:var(--line2);stroke-width:.6}\n\/* new vocabulary *\/\n.okk .wedge{stroke:var(--brand2);stroke-width:1.1} .okk .wedge.h{stroke-dasharray:3 2;stroke-opacity:.55}\n.okk .pane{fill:rgba(255,255,255,.015);stroke:var(--line2);stroke-width:.8} .okk .orth{fill:var(--bl);stroke:var(--be);stroke-width:.8} .okk .orthg{stroke:var(--line2);stroke-width:.6} .okk .orthf{fill:var(--bt);stroke:var(--s2);stroke-width:1.3}\n.okk .cell{fill:transparent;stroke:#C9D6F2;stroke-width:.5} .okk .cell.on{fill:var(--s2);fill-opacity:.55;stroke:var(--s2)}\n.okk .subd line{stroke:#C9D6F2;stroke-width:.6;stroke-opacity:.8}\n.okk .mag{fill:var(--bad)}\n.okk .oico{fill:var(--brand2)} .okk .tree{stroke:var(--muted);stroke-width:.7;fill:none} .okk .link{stroke:var(--brand2);stroke-width:1;stroke-dasharray:3 2;fill:none}\n.okk .bbox{fill:none;stroke:var(--s2);stroke-width:.9;stroke-dasharray:3 2}\n.okk .field{fill:var(--stage);stroke:var(--line2);stroke-width:.8}\n.okk .hlr{fill:var(--s1);fill-opacity:.22;stroke:var(--s1);stroke-width:.8}\n.okk .ring2{fill:none;stroke:var(--brand2);stroke-width:1.4}\n.okk .dim{fill:#04060A;fill-opacity:.62}\n.okk .track{fill:var(--line2)} .okk .fillb{fill:var(--brand2)}\n.okk .mgl{fill:none;stroke:var(--muted);stroke-width:1.2}\n.okk .comp{fill:#05070B;stroke:var(--line2);stroke-width:.8}\n.okk .tab{fill:var(--stage);stroke:var(--line2);stroke-width:.7}\n.okk .ruler line{stroke:var(--line2);stroke-width:.7}\n.okk .tbar{fill-opacity:.85}\n.okk .kf{fill:var(--s2);stroke:#0A0D14;stroke-width:.6} .okk .kf.on{fill:#fff}\n.okk .ph line{stroke:var(--brand2);stroke-width:1.2} .okk .ph rect{fill:var(--brand2)}\n.okk .swatch{fill:none;stroke:var(--muted);stroke-width:.9} .okk .swatch.on{fill:var(--s2);stroke:var(--s2)}\n.okk .readout rect{fill:var(--surface);stroke:var(--line2);stroke-width:.7}\n.okk .nul{fill:none;stroke:var(--bad);stroke-width:1.2}\n.okk .page{fill:#EEF0F4} .okk .guide{fill:none;stroke:#E04FD0;stroke-width:.6} .okk .colg{stroke:#8E6CFF;stroke-width:.6} .okk .bleed{fill:none;stroke:#FF4D4D;stroke-width:.6}\n.okk .fedge{fill:none;stroke:#3D7BFF;stroke-width:.9}\n.okk .pbar{fill:#BBC2CE} .okk .ptx{font-family:var(--f-display);font-weight:700;fill:#11151C} .okk .ptxt{font:500 6.2px var(--f-body);fill:#2A2F38}\n.okk .hid{font:600 6px var(--f-mono);fill:#2E6BFF}\n.okk .tsel{fill:#3D7BFF;fill-opacity:.28}\n.okk .pboard{fill:#020305}\n.okk .zbo{fill:none;stroke:#FF6B6B;stroke-width:1.3} .okk .zbi{fill:none;stroke:#FFD36B;stroke-width:1}\n.okk .zst{fill:none;stroke-linecap:round} .okk .zw{fill:none;stroke:#1B0703;stroke-opacity:.55;stroke-width:.6}\n.okk .zmask{fill:#05070A;fill-opacity:.62}\n.okk .spaint{fill:none;stroke:var(--s1);stroke-linecap:round;stroke-opacity:.95}\n.okk .pcell{fill:transparent;stroke:#fff;stroke-opacity:.18;stroke-width:.6} .okk .pcell.on{fill:var(--s2);fill-opacity:.9;stroke:var(--s2);stroke-opacity:1}\n.okk .uvpane{fill:#0B1019;stroke:var(--line2);stroke-width:.8} .okk .uvi{fill:var(--s1);fill-opacity:.14;stroke:var(--brand2);stroke-width:.7}\n.okk .divl{stroke:var(--line2);stroke-width:1}\n\n\/* ---------- QUIZ ---------- *\/\n.okk .qbar{display:grid;grid-template-columns:auto minmax(0,1fr) auto auto;gap:14px;align-items:center;margin:4px 0 16px}\n.okk .qx{width:40px;height:40px;border-radius:50%;border:1px solid var(--line2);display:grid;place-items:center;font-size:16px;color:var(--muted)}\n.okk .qx:hover{color:var(--ink)}\n.okk .qinfo{display:flex;align-items:center;gap:12px;min-width:0}\n.okk .qinfo .mono{width:40px;height:40px;border-radius:12px;font-size:13px}\n.okk .qinfo .t{min-width:0;flex:1}\n.okk .qinfo b{font:700 14px var(--f-display);letter-spacing:-.02em;display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.okk .prog{height:4px;background:var(--line);border-radius:4px;overflow:hidden;margin-top:7px;max-width:420px}\n.okk .prog i{display:block;height:100%;background:linear-gradient(90deg,var(--brand),var(--brand2));border-radius:4px;transition:width .5s cubic-bezier(.2,.8,.2,1)}\n.okk .qinfo small{font:500 11px var(--f-mono);color:var(--muted)}\n.okk .score{display:flex;flex-direction:column;align-items:flex-end;line-height:1}\n.okk .score b{font:700 26px var(--f-body);letter-spacing:-.02em;font-variant-numeric:tabular-nums}\n.okk .score span{font:500 10px var(--f-mono);letter-spacing:.14em;color:var(--muted);text-transform:uppercase;margin-top:4px}\n.okk .combo{min-width:66px;height:40px;border-radius:12px;border:1px solid var(--line);display:grid;place-items:center;font:700 15px var(--f-display);color:var(--faint);transition:all .3s}\n.okk .combo small{display:block;font:500 8.5px var(--f-mono);letter-spacing:.14em;text-align:center}\n.okk .combo.hot{color:var(--gold);border-color:rgba(255,203,107,.5);box-shadow:0 0 24px -4px rgba(255,203,107,.5),inset 0 0 18px -6px rgba(255,203,107,.5)}\n.okk .qgrid{display:grid;grid-template-columns:minmax(0,1.4fr) minmax(330px,1fr);gap:18px;align-items:start}\n.okk .qpanel{padding:24px;display:flex;flex-direction:column;gap:16px}\n.okk .qkind{font:600 10.5px var(--f-mono);letter-spacing:.16em;text-transform:uppercase;color:var(--brand2)}\n.okk .qpanel h2{font-size:clamp(22px,2.6vw,32px);line-height:1.08;font-weight:700}\n.okk .qpanel .qd{margin:0;color:var(--muted);font-size:14.5px}\n.okk .qpanel .qmenu{font:500 11.5px var(--f-mono);color:var(--faint)}\n.okk .qkeys{display:flex;gap:8px;flex-wrap:wrap;padding:6px 0}\n.okk .qkeys kbd{min-width:52px;height:52px;font-size:20px;border-radius:12px;border-bottom-width:4px}\n.okk .opts{display:grid;grid-template-columns:1fr 1fr;gap:10px}\n.okk .opt{position:relative;border:1px solid var(--line2);border-radius:16px;padding:12px;min-height:68px;display:flex;align-items:center;justify-content:center;text-align:center;\n  background:rgba(255,255,255,.02);transition:border-color .2s,transform .15s,background-color .3s,opacity .3s,box-shadow .3s;font-size:14px;font-weight:500;line-height:1.25}\n.okk .opt:hover:not(:disabled){border-color:var(--brand2);transform:translateY(-1px);background:rgba(46,107,255,.06)}\n.okk .opt:disabled{cursor:default}\n.okk .opt .n{position:absolute;top:7px;left:9px;font:600 9.5px var(--f-mono);color:var(--faint)}\n.okk .opt.ok{border-color:var(--good);background:rgba(53,224,161,.1);box-shadow:0 0 0 1px var(--good),0 0 30px -6px rgba(53,224,161,.55)}\n.okk .opt.ko{border-color:var(--bad);background:rgba(255,90,110,.1);box-shadow:0 0 0 1px var(--bad)}\n.okk .opt.dim{opacity:.32}\n.okk .opt kbd{font-size:12px;min-width:27px;height:27px;padding:0 7px;border-radius:7px}\n.okk .verdict{display:flex;align-items:center;justify-content:space-between;gap:12px;min-height:48px}\n.okk .verdict .msg{font-weight:600;min-width:0}\n.okk .verdict .msg small{display:block;font:500 12px var(--f-mono);color:var(--muted);margin-top:2px}\n.okk .verdict .msg.good{color:var(--good)} .okk .verdict .msg.bad{color:var(--bad)}\n.okk .hint{font-size:12.5px;color:var(--faint);min-height:48px;display:flex;align-items:center}\n\n\/* ---------- RESULTS ---------- *\/\n.okk .res{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:18px;margin-top:6px}\n.okk .rhero{padding:clamp(24px,4vw,40px);position:relative;overflow:hidden;display:flex;flex-direction:column;gap:18px}\n.okk .rhero::after{content:\"\";position:absolute;width:420px;height:420px;right:-160px;top:-180px;border-radius:50%;background:radial-gradient(closest-side,rgba(46,107,255,.3),transparent);pointer-events:none}\n.okk .rank{font:700 clamp(84px,14vw,150px) var(--f-display);line-height:.85;letter-spacing:-.06em}\n.okk .rank-row{display:flex;align-items:flex-end;justify-content:space-between;gap:16px;flex-wrap:wrap;position:relative;z-index:1}\n.okk .rscore b{display:block;font:700 clamp(36px,5vw,56px) var(--f-body);letter-spacing:-.03em;font-variant-numeric:tabular-nums;line-height:1}\n.okk .rscore span{font:500 11px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--muted)}\n.okk .rstats{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;position:relative;z-index:1}\n.okk .rstats div{padding:12px;border-radius:14px;border:1px solid var(--line);background:rgba(0,0,0,.2)}\n.okk .rstats b{display:block;font:700 20px var(--f-body);font-variant-numeric:tabular-nums}\n.okk .rstats span{font:500 10px var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--muted)}\n.okk .cert{display:flex;align-items:center;gap:16px;padding:16px;border-radius:18px;background:linear-gradient(120deg,rgba(255,203,107,.14),rgba(255,203,107,.03));box-shadow:inset 0 0 0 1px rgba(255,203,107,.35);position:relative;z-index:1}\n.okk .medal{width:54px;height:54px;border-radius:50%;flex:none;display:grid;place-items:center;font:700 16px var(--f-display);color:#2A1B00;\n  background:conic-gradient(from 200deg,#FFE5A3,#E2A93A,#FFF1C9,#C98E22,#FFE5A3);box-shadow:0 0 30px rgba(255,203,107,.5)}\n.okk .cert b{font:700 15px var(--f-display);color:var(--gold)}\n.okk .cert p{margin:2px 0 0;color:var(--muted);font-size:13px}\n.okk .rmsg{margin:0;color:var(--muted);position:relative;z-index:1}\n.okk .ract{display:flex;gap:10px;flex-wrap:wrap;position:relative;z-index:1}\n.okk .rside{display:flex;flex-direction:column;gap:18px}\n.okk .rlist{padding:20px}\n.okk .rlist h3{font-size:16px;font-weight:700;margin-bottom:10px}\n.okk .rlist .row{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 0;border-top:1px solid var(--line);font-size:14px}\n.okk .rlist .row span{min-width:0}\n.okk .rlist .ok{color:var(--good);font:600 12px var(--f-mono)}\n.okk .rcta{padding:22px;display:flex;flex-direction:column;gap:10px;background:linear-gradient(140deg,rgba(46,107,255,.16),rgba(46,107,255,.02))}\n.okk .rcta h3{font-size:18px;font-weight:700}\n.okk .rcta p{margin:0;color:var(--muted);font-size:14px}\n.okk .toast{position:absolute;left:50%;bottom:22px;transform:translateX(-50%);padding:10px 16px;border-radius:999px;background:var(--ink);color:var(--bg);font-weight:600;font-size:13px;z-index:60}\n\n\/* ---------- responsive ---------- *\/\n@media (max-width:1000px){\n  .okk .qgrid{grid-template-columns:1fr}\n  .okk .lib{grid-template-columns:1fr} .okk .lib .sticky{position:relative;top:0}\n  .okk .res{grid-template-columns:1fr}\n}\n@media (max-width:820px){\n  .okk .hero{grid-template-columns:1fr;gap:26px;padding-top:12px}\n  .okk .modes{grid-template-columns:1fr}\n  .okk .studio{grid-template-columns:1fr}\n}\n@media (max-width:560px){\n  .okk .bar .pr{display:none}\n  .okk .lvl small{display:none}\n  .okk .qbar{grid-template-columns:auto minmax(0,1fr) auto;gap:10px}\n  .okk .qbar .combo{display:none}\n  .okk .qpanel{padding:18px}\n  .okk .opt{min-height:60px;font-size:13px}\n  .okk .rstats{grid-template-columns:repeat(2,minmax(0,1fr))}\n  .okk .strip{padding:10px 12px}\n  .okk .strip kbd{min-width:32px;height:32px;font-size:13px}\n  .okk .stats{gap:20px}\n  .okk .agrid{grid-template-columns:1fr}\n}\n@media (hover:none){ .okk .kbonly{display:none!important} }\n@media (prefers-reduced-motion:reduce){ .okk, .okk *{animation:none!important;transition:none!important} }\n\n\n\/* Barre de jeu compacte sous le menu WordPress \u2014 aucun second header de site. *\/\n.okk .bar{display:flex;align-items:center;justify-content:space-between;gap:16px;\n  padding:14px 0;margin:0 0 22px;border-bottom:1px solid rgba(124,196,255,.13);flex-wrap:wrap}\n.okk .appmark{display:inline-flex;align-items:center;gap:11px;text-align:left;\n  background:none;border:0;padding:5px 0;color:var(--ink);cursor:pointer;min-width:0}\n.okk .appmark-icon{width:34px;height:34px;display:grid;place-items:center;\n  border:1px solid rgba(124,196,255,.3);border-radius:10px;color:var(--brand2);\n  background:linear-gradient(145deg,rgba(46,107,255,.22),rgba(46,107,255,.035));\n  box-shadow:inset 0 1px 0 rgba(255,255,255,.07);font-size:19px;line-height:1}\n.okk .appmark-text{display:flex;flex-direction:column;gap:1px}\n.okk .appmark-text strong{font:700 12px var(--f-display);letter-spacing:.065em;color:var(--ink)}\n.okk .appmark-text small{font:600 9px var(--f-mono);letter-spacing:.13em;color:var(--muted)}\n.okk .appmark:hover .appmark-icon{border-color:var(--brand2);color:#fff}\n.okk .tools{margin-left:auto}\n.okk, .okk #grid {scroll-margin-top:110px}\n@media(max-width:560px){\n  .okk .bar{gap:10px;padding:12px 0;margin-bottom:18px}\n  .okk .appmark{gap:8px}\n  .okk .appmark-icon{width:30px;height:30px;border-radius:9px;font-size:16px}\n  .okk .appmark-text strong{font-size:10px}\n  .okk .appmark-text small{font-size:8px}\n  .okk .tools{gap:5px}\n  .okk .lvl{padding:4px 7px 4px 4px}\n  .okk .lvl .ring{width:25px;height:25px}\n  .okk .icon-btn{width:32px;height:32px}\n  .okk .seg button{padding:5px 7px;font-size:10px}\n}\n\n\n\/* Elementor: styles limit\u00e9s au module et largeur ind\u00e9pendante du conteneur. *\/\n.okk{isolation:isolate;min-width:0;scroll-margin-top:var(--okk-header-offset,94px);--okk-content-width:1320px;--okk-header-offset:94px;padding-top:var(--okk-header-offset)}\n.okk[data-layout=\"full\"]{width:var(--okk-viewport,100%);max-width:none!important;margin-left:var(--okk-offset,0px);margin-right:0}\n.okk .wrap{width:100%;max-width:var(--okk-content-width);padding-bottom:20px}\n.okk [hidden]{display:none!important}\n.okk button,.okk input{font-family:var(--f-body);text-transform:none;letter-spacing:normal;box-shadow:none}\n.okk button{white-space:normal;line-height:1.4}\n.okk button:not(:disabled):hover,.okk button:not(:disabled):focus{color:inherit}\n.okk button:disabled{cursor:default}\n.okk .btn.pri:hover,.okk .btn.pri:focus{color:#fff}\n.okk p{margin-block:0 1em}\n.okk .sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}\n.okk .icon-btn{width:44px;height:44px;flex:none}\n.okk .seg button{min-height:36px}\n.okk .bar{padding-block:18px;margin-bottom:24px}\n.okk .hero{padding-block:24px 48px;gap:clamp(24px,4vw,64px)}\n.okk .hero .lede{max-width:49ch;line-height:1.7}\n.okk .hero .microcopy{font-size:12px;color:var(--muted);margin:16px 0 0}\n.okk .stats{gap:clamp(18px,3vw,40px)}\n.okk .discovery{display:grid;grid-template-columns:minmax(200px,1fr) auto;gap:16px;align-items:center;margin-bottom:22px}\n.okk .search{display:flex;gap:10px;align-items:center;border:1px solid var(--line2);background:#0c111b;border-radius:14px;padding:0 14px;min-height:50px}\n.okk .search input{width:100%;min-width:0;color:var(--ink);background:transparent;border:0;outline:0;padding:12px 0;font-size:16px;box-shadow:none}\n.okk .search:focus-within{border-color:var(--brand2);box-shadow:0 0 0 2px #7cc4ff22}\n.okk .search input::placeholder{color:var(--muted);opacity:1}\n.okk .resume{display:flex;justify-content:space-between;align-items:center;gap:16px;padding:18px 22px;margin-bottom:22px;border:1px solid #2e6bff55;border-radius:18px;background:#2e6bff12}\n.okk .resume b{display:block;font-size:15px}.okk .resume small{color:var(--muted)}\n.okk .result-count{font:500 12px var(--f-mono);color:var(--muted);min-height:20px;margin-bottom:12px}\n.okk .empty{padding:28px;border:1px dashed var(--line2);border-radius:18px;color:var(--muted)}\n.okk .filter-line{display:flex;gap:8px;flex-wrap:wrap}\n.okk .filter-line button{min-height:44px}\n.okk .library-search{margin:0 0 14px}\n.okk .lib .li{min-height:58px}\n.okk .lib .li .nm small{color:var(--muted)}\n.okk .library-tools{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px;color:var(--muted);font-size:12px;flex-wrap:wrap}\n.okk .library-tools button{border:1px solid var(--line2);padding:8px 12px;border-radius:999px;min-height:40px}\n.okk .library-tools button[aria-pressed=\"true\"]{border-color:var(--brand);background:#2e6bff22;color:var(--ink)}\n.okk .qtools{display:flex;justify-content:space-between;gap:12px;align-items:center;margin:0 0 18px;color:var(--muted);font-size:12px;flex-wrap:wrap}\n.okk .capture{display:flex;align-items:center;gap:10px;padding:10px 14px;border:1px solid var(--line2);border-radius:12px;min-height:44px;background:#ffffff03}\n.okk .capture[aria-pressed=\"true\"]{border-color:var(--brand2);color:var(--ink);background:#2e6bff22}\n.okk .capture-help{color:var(--muted);font-size:12px;max-width:60ch}\n.okk .timer-label{font:500 10px var(--f-mono);text-transform:uppercase;letter-spacing:.08em;color:var(--muted);padding:10px 16px 5px}\n.okk .verdict{align-items:flex-start;flex-wrap:wrap}.okk .verdict .btn{flex:none}\n.okk .bar .studio-link{font-size:12px;color:var(--muted);text-decoration:none;min-height:44px;display:inline-flex;align-items:center}\n.okk .stage .rep{min-height:36px}\n.okk .foot{gap:16px;border-top:1px solid var(--line);padding-top:20px;line-height:1.7}\n.okk .foot span{max-width:100%}\n.okk .toast{position:fixed;bottom:24px;max-width:min(90vw,640px);width:max-content;border-radius:14px;line-height:1.5;box-shadow:0 10px 50px #0008}\n.okk .qpanel{min-width:0}.okk .keys{max-width:100%}.okk .rlist .row{flex-wrap:wrap}\n.okk .btn{min-height:48px;height:auto;padding-block:12px;text-align:center}\n.okk .library-note{color:var(--muted);font-size:12px;line-height:1.7;margin-top:16px}\n@media(min-width:1500px){.okk .hero h1{font-size:76px}}\n@media(max-width:700px){.okk{--okk-header-offset:78px}.okk .discovery{grid-template-columns:1fr}.okk .resume{align-items:flex-start;flex-direction:column}.okk .studio-link{display:none!important}.okk .hero{padding-bottom:32px}.okk .lib{margin-top:28px}.okk .stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px}.okk .stats b{font-size:23px}.okk .stats span{font-size:10px;letter-spacing:.07em}.okk .opts{grid-template-columns:1fr 1fr}.okk .qtools{align-items:flex-start}.okk .ract .btn{flex:1 1 180px}}\n@media(max-width:400px){.okk .tools{gap:4px}.okk .lvl{display:none}.okk .hero h1{font-size:36px}.okk .btn{font-size:14px;padding-inline:18px}.okk .opt{padding:16px 8px}.okk .opt kbd{padding-inline:4px;min-width:22px;font-size:11px}.okk .keys{gap:3px}.okk .hk{height:54px;min-width:46px;padding-inline:10px}.okk .bar{gap:10px}.okk .rstats b{font-size:18px}}\n@media(max-width:700px){.okk .qgrid{display:flex;flex-direction:column}.okk .qgrid>.qpanel{order:0;width:100%}.okk .qgrid>.scard{order:1;width:100%}.okk .qtools{margin-bottom:12px}.okk .capture-help{font-size:11px}.okk .qpanel{gap:12px}.okk .qpanel .qd{font-size:13px}}\n@media(prefers-reduced-motion:reduce){.okk .floor{animation:none!important}}\n\n\n\/* Affichage app lorsque le plein \u00e9cran natif n'est pas disponible. *\/\n.okk[data-keystroke].is-app-screen {\n  position: fixed !important; inset: 0 !important; left: 0 !important;\n  width: 100% !important; max-width: none !important; margin: 0 !important;\n  height: 100vh; height: 100dvh; overflow: auto; overscroll-behavior: contain;\n  z-index: 2147483646;\n}\n.okk[data-keystroke]:fullscreen, .okk[data-keystroke].is-app-screen {\n  --okk-header-offset: 0px !important; --okk-top-gap: 32px !important;\n  --okk-admin-height: 0px !important;\n  left: 0 !important; width: 100% !important; max-width: none !important; margin: 0 !important;\n  height: 100vh; height: 100dvh; overflow: auto; overscroll-behavior: contain;\n  padding-top: 24px !important;\n}\n.okk[data-keystroke]:-webkit-full-screen {\n  --okk-header-offset: 0px !important; --okk-top-gap: 32px !important; --okk-admin-height: 0px !important;\n  left: 0 !important; width: 100% !important; max-width: none !important; margin: 0 !important;\n  height: 100vh; overflow: auto;\n  padding-top: 24px !important;\n}\n.okk[data-keystroke] .appbar {\n  display: flex; justify-content: flex-end; align-items: center; gap: 12px;\n  flex-wrap: wrap; padding: 0 0 18px;\n}\n.okk[data-keystroke] .screen-btn { margin-left: auto; flex: 0 0 auto; }\n.okk[data-keystroke]:fullscreen .appbar, .okk[data-keystroke].is-app-screen .appbar,\n.okk[data-keystroke]:fullscreen .bar, .okk[data-keystroke].is-app-screen .bar {\n  position: sticky; top: 0; z-index: 40; background: var(--bg, #06080c);\n  padding-block: 10px; margin-bottom: 12px;\n}\n\n\/* Garder niveau, XP et commandes visibles, m\u00eame sur les petits t\u00e9l\u00e9phones. *\/\n.okk[data-keystroke] .tools { flex-wrap: wrap; justify-content: flex-end; min-width: 0; }\n.okk[data-keystroke] .screen-btn {\n  display: inline-flex; align-items: center; justify-content: center;\n  border: 1px solid var(--line2); border-radius: 999px; padding: 8px 13px;\n  min-height: 36px; font-size: 12.5px; font-weight: 600; background: var(--bg2);\n}\n.okk[data-keystroke] .lvl { display: flex !important; flex-shrink: 0; }\n@media (max-width: 700px) {\n  .okk[data-keystroke] .tools { width: 100%; gap: 8px; }\n  .okk[data-keystroke] .tools .screen-btn { margin-left: 0; }\n}\n\n<\/style>\n<div class=\"okk\" data-keystroke data-layout=\"full\" tabindex=\"-1\" role=\"region\" aria-label=\"Keystroke \u2014 entra\u00eenement aux raccourcis clavier\">\n  <div id=\"fxl\" aria-hidden=\"true\"><\/div>\n  <div class=\"wrap\">\n    <header class=\"bar\" id=\"bar\"><\/header>\n    <div id=\"view\"><\/div><p id=\"okk-status\" class=\"sr-only\" role=\"status\" aria-live=\"polite\" aria-atomic=\"true\"><\/p>\n    <footer class=\"foot\" id=\"foot\"><\/footer>\n  <\/div>\n<\/div>\n<script>\n(() => {\n\"use strict\";\nconst script = document.currentScript;\nconst adjacent = script && script.previousElementSibling;\nconst root = adjacent && adjacent.matches('[data-keystroke]') ? adjacent : document.querySelector('[data-keystroke]:not([data-ready=\"true\"])');\nif (!root || !root.matches('[data-keystroke]') || root.dataset.ready === 'true') return;\nroot.dataset.ready = 'true';\nroot.id = 'ok3d-keystroke-' + Math.random().toString(36).slice(2, 9);\nconst $ = selector => selector === '#okk' ? root : root.querySelector(selector);\nconst $$ = selector => root.querySelectorAll(selector);\nconst announce = text => { const live = $('#okk-status'); if(live) live.textContent = text; };\nlet captureKeys = false, heroVisible = true;\nlet storageAvailable = true;\n\n\/* =====================================================================\n   CONFIG \u2014 r\u00e9glages modifiables sans toucher au reste du code\n   ===================================================================== *\/\nconst CONFIG = {\n  brand: \"OK3D\",\n  product: \"Keystroke\",\n  logoUrl: \"https:\/\/www.ok3d.fr\/wp-content\/uploads\/2026\/09\/ok3d_logo_arrondi_blanc.png\",                                  \/\/ ex. \"https:\/\/www.ok3d.fr\/wp-content\/uploads\/2026\/09\/ok3d_logo_arrondi_blanc.png\" (version WordPress)\n  siteUrl: \"https:\/\/www.ok3d.fr\",\n  contactUrl: \"https:\/\/www.ok3d.fr\/configurateur-projet\/\",\n  shareUrl: \"https:\/\/www.ok3d.fr\/keystroke\",    \/\/ adresse de la page qui h\u00e9berge l'app\n  sprintLength: 10,                             \/\/ questions par Sprint\n  timePerQuestion: 12,                          \/\/ secondes avant que le bonus de vitesse tombe \u00e0 0\n  reverseRatio: 0.35,                           \/\/ part de questions invers\u00e9es (\u00ab Que fait ce raccourci ? \u00bb)\n  soundDefault: false,\n  storageKey: \"okk-keystroke-v1\"\n};\n\n\/* =====================================================================\n   DONN\u00c9ES \u2014 [libell\u00e9 FR, menu EN, description, touches (notation Windows), animation]\n   ===================================================================== *\/\nconst DATA = {\n  ai:{name:\"Illustrator\", mono:\"Ai\", hue:\"#FF9F43\", cat:\"2d\", sub:\"Design vectoriel\", vp:\"Plan de travail 01\", mac:true, items:[\n    [\"Concepteur de formes\",\"Shape Builder Tool\",\"Fusionne les zones de formes qui se chevauchent en un gliss\u00e9.\",[\"Shift\",\"M\"],\"shapebuilder\"],\n    [\"Outil Plume\",\"Pen Tool\",\"Trace des chemins avec points d'ancrage et poign\u00e9es.\",[\"P\"],\"pen\"],\n    [\"S\u00e9lection directe\",\"Direct Selection Tool\",\"Attrape un point d'ancrage pr\u00e9cis pour le d\u00e9placer.\",[\"A\"],\"direct\"],\n    [\"Grouper\",\"Object \u203a Group\",\"R\u00e9unit les objets s\u00e9lectionn\u00e9s en un seul groupe.\",[\"Ctrl\",\"G\"],\"group\"],\n    [\"Dissocier\",\"Object \u203a Ungroup\",\"S\u00e9pare un groupe en objets ind\u00e9pendants.\",[\"Ctrl\",\"Shift\",\"G\"],\"ungroup\"],\n    [\"Cr\u00e9er un masque d'\u00e9cr\u00eatage\",\"Clipping Mask \u203a Make\",\"L'objet du dessus d\u00e9coupe ceux du dessous.\",[\"Ctrl\",\"7\"],\"clip\"],\n    [\"Mode Trac\u00e9s\",\"View \u203a Outline\",\"Affiche uniquement les contours, sans couleur.\",[\"Ctrl\",\"Y\"],\"outline\"],\n    [\"Vectoriser le texte\",\"Type \u203a Create Outlines\",\"Transforme les lettres en formes vectorielles.\",[\"Ctrl\",\"Shift\",\"O\"],\"typeoutline\"],\n    [\"Verrouiller la s\u00e9lection\",\"Object \u203a Lock\",\"Emp\u00eache toute modification accidentelle.\",[\"Ctrl\",\"2\"],\"lock\"],\n    [\"Masquer la s\u00e9lection\",\"Object \u203a Hide\",\"Cache les objets sans les supprimer.\",[\"Ctrl\",\"3\"],\"hide\"],\n    [\"Joindre\",\"Object \u203a Path \u203a Join\",\"Relie deux points d'extr\u00e9mit\u00e9 ouverts.\",[\"Ctrl\",\"J\"],\"join\"],\n    [\"Coller devant\",\"Edit \u203a Paste in Front\",\"Colle \u00e0 la m\u00eame position, au-dessus de l'original.\",[\"Ctrl\",\"F\"],\"pastefront\"],\n    [\"R\u00e9p\u00e9ter la transformation\",\"Transform Again\",\"Rejoue le dernier d\u00e9placement, rotation ou \u00e9chelle.\",[\"Ctrl\",\"D\"],\"again\"],\n    [\"Distorsion via objet du dessus\",\"Envelope \u203a Make with Top Object\",\"D\u00e9forme l'objet selon la forme pos\u00e9e au-dessus.\",[\"Ctrl\",\"Alt\",\"C\"],\"envelope\"],\n    [\"Distorsion avec un filet\",\"Envelope \u203a Make with Mesh\",\"D\u00e9forme l'objet avec une grille de points.\",[\"Ctrl\",\"Alt\",\"M\"],\"mesh\"],\n    [\"Cr\u00e9er une transition\",\"Blend \u203a Make\",\"G\u00e9n\u00e8re les \u00e9tapes interm\u00e9diaires entre deux formes.\",[\"Ctrl\",\"Alt\",\"B\"],\"blend\"],\n    [\"Premier plan\",\"Arrange \u203a Bring to Front\",\"Place l'objet au-dessus de tous les autres.\",[\"Ctrl\",\"Shift\",\"]\"],\"front\"],\n    [\"Pipette\",\"Eyedropper Tool\",\"Copie le style d'un objet vers la s\u00e9lection.\",[\"I\"],\"eyedrop\"],\n  ]},\n  ps:{name:\"Photoshop\", mono:\"Ps\", hue:\"#31A8FF\", cat:\"2d\", sub:\"Retouche d'image\", vp:\"Calque 1 \u00b7 RVB\/8\", mac:true, items:[\n    [\"Transformation manuelle\",\"Free Transform\",\"Redimensionne, pivote ou d\u00e9forme le calque.\",[\"Ctrl\",\"T\"],\"ftransform\"],\n    [\"Nouveau calque\",\"New Layer\",\"Cr\u00e9e un calque vide au-dessus du calque actif.\",[\"Ctrl\",\"Shift\",\"N\"],\"newlayer\"],\n    [\"Dupliquer le calque\",\"Layer via Copy\",\"Copie le calque, ou la s\u00e9lection, dans un nouveau calque.\",[\"Ctrl\",\"J\"],\"duplayer\"],\n    [\"Fusion tampon\",\"Stamp Visible\",\"Fusionne tout le visible dans un nouveau calque.\",[\"Ctrl\",\"Alt\",\"Shift\",\"E\"],\"stamp\"],\n    [\"Niveaux\",\"Levels\",\"R\u00e8gle noirs, blancs et tons moyens.\",[\"Ctrl\",\"L\"],\"levels\"],\n    [\"Courbes\",\"Curves\",\"Ajuste finement la luminosit\u00e9 et le contraste.\",[\"Ctrl\",\"M\"],\"curves\"],\n    [\"Teinte \/ Saturation\",\"Hue\/Saturation\",\"Modifie couleur, intensit\u00e9 et luminosit\u00e9.\",[\"Ctrl\",\"U\"],\"hue\"],\n    [\"N\u00e9gatif\",\"Invert\",\"Inverse toutes les couleurs du calque.\",[\"Ctrl\",\"I\"],\"invert\"],\n    [\"D\u00e9s\u00e9lectionner\",\"Deselect\",\"Supprime la s\u00e9lection active.\",[\"Ctrl\",\"D\"],\"deselect\"],\n    [\"Intervertir la s\u00e9lection\",\"Select Inverse\",\"S\u00e9lectionne tout ce qui ne l'\u00e9tait pas.\",[\"Ctrl\",\"Shift\",\"I\"],\"inverse\"],\n    [\"Masque d'\u00e9cr\u00eatage\",\"Create Clipping Mask\",\"Limite le calque \u00e0 la forme de celui du dessous.\",[\"Ctrl\",\"Alt\",\"G\"],\"clip\"],\n    [\"Remplir\",\"Edit \u203a Fill\",\"Ouvre le remplissage, dont l'option Contenu pris en compte.\",[\"Shift\",\"F5\"],\"cafill\"],\n    [\"Recadrage\",\"Crop Tool\",\"Rogne la zone de travail.\",[\"C\"],\"crop\"],\n    [\"Agrandir le pinceau\",\"Increase Brush Size\",\"Augmente le diam\u00e8tre de l'outil actif.\",[\"]\"],\"brushup\"],\n    [\"R\u00e9duire le pinceau\",\"Decrease Brush Size\",\"Diminue le diam\u00e8tre de l'outil actif.\",[\"[\"],\"brushdown\"],\n    [\"Couleurs par d\u00e9faut\",\"Default Colors\",\"Remet le premier plan en noir, l'arri\u00e8re-plan en blanc.\",[\"D\"],\"defcol\"],\n    [\"Permuter les couleurs\",\"Switch Colors\",\"\u00c9change couleur de premier plan et d'arri\u00e8re-plan.\",[\"X\"],\"swapcol\"],\n    [\"Taille de l'image\",\"Image Size\",\"Change les dimensions et la r\u00e9solution.\",[\"Ctrl\",\"Alt\",\"I\"],\"imgsize\"],\n  ]},\n  id:{name:\"InDesign\", mono:\"Id\", hue:\"#FF3D7F\", cat:\"2d\", sub:\"Mise en page\", vp:\"Page 1 \u00b7 A4\", mac:true, items:[\n    [\"Placer\",\"File \u203a Place\",\"Importe une image ou un texte dans la mise en page.\",[\"Ctrl\",\"D\"],\"id:place\"],\n    [\"Outil Texte\",\"Type Tool\",\"Cr\u00e9e un bloc de texte et place le curseur.\",[\"T\"],\"text\"],\n    [\"Bloc rectangulaire\",\"Rectangle Frame Tool\",\"Dessine un bloc vide, pr\u00eat \u00e0 recevoir une image.\",[\"F\"],\"id:frame\"],\n    [\"Ajuster le contenu proportionnellement\",\"Fit Content Proportionally\",\"L'image tient enti\u00e8re dans son bloc.\",[\"Ctrl\",\"Alt\",\"Shift\",\"E\"],\"id:fit\"],\n    [\"Remplir le bloc proportionnellement\",\"Fill Frame Proportionally\",\"L'image couvre tout le bloc, sans d\u00e9formation.\",[\"Ctrl\",\"Alt\",\"Shift\",\"C\"],\"id:fill\"],\n    [\"Ajuster le bloc au contenu\",\"Fit Frame to Content\",\"Le bloc \u00e9pouse exactement les dimensions de l'image.\",[\"Ctrl\",\"Alt\",\"C\"],\"id:fitframe\"],\n    [\"Mode Aper\u00e7u\",\"Screen Mode \u203a Preview\",\"Masque rep\u00e8res, marges et bords pour juger la page.\",[\"W\"],\"id:preview\"],\n    [\"Caract\u00e8res masqu\u00e9s\",\"Show Hidden Characters\",\"R\u00e9v\u00e8le espaces et fins de paragraphe.\",[\"Ctrl\",\"Alt\",\"I\"],\"id:hidden\"],\n    [\"Habillage\",\"Text Wrap\",\"Fait contourner un objet par le texte.\",[\"Ctrl\",\"Alt\",\"W\"],\"id:wrap\"],\n    [\"Num\u00e9ro de page automatique\",\"Current Page Number\",\"Ins\u00e8re un num\u00e9ro qui suit la page.\",[\"Ctrl\",\"Alt\",\"Shift\",\"N\"],\"id:pagenum\"],\n    [\"Dupliquer et d\u00e9caler\",\"Step and Repeat\",\"Cr\u00e9e des copies \u00e0 intervalle r\u00e9gulier.\",[\"Ctrl\",\"Alt\",\"U\"],\"again\"],\n    [\"Vectoriser\",\"Create Outlines\",\"Transforme le texte en trac\u00e9s.\",[\"Ctrl\",\"Shift\",\"O\"],\"typeoutline\"],\n    [\"Masquer les bords de bloc\",\"Hide Frame Edges\",\"Cache les contours bleus des blocs.\",[\"Ctrl\",\"H\"],\"id:edges\"],\n    [\"Augmenter l'approche\",\"Increase Tracking\",\"Espace les lettres du texte s\u00e9lectionn\u00e9.\",[\"Alt\",\"\u2192\"],\"id:track\"],\n    [\"Panneau Caract\u00e8re\",\"Character Panel\",\"Ouvre police, corps, interlignage et approche.\",[\"Ctrl\",\"T\"],\"panel:char\"],\n    [\"Nuancier\",\"Swatches Panel\",\"Ouvre les couleurs enregistr\u00e9es du document.\",[\"F5\"],\"panel:swatch\"],\n    [\"Exporter\",\"File \u203a Export\",\"Exporte le document en PDF ou dans un autre format.\",[\"Ctrl\",\"E\"],\"progress:pdf\"],\n    [\"Page enti\u00e8re dans la fen\u00eatre\",\"Fit Page in Window\",\"Ajuste le zoom sur la page active.\",[\"Ctrl\",\"0\"],\"zoomfit\"],\n  ]},\n  ae:{name:\"After Effects\", mono:\"Ae\", hue:\"#9999FF\", cat:\"motion\", sub:\"Motion design\", vp:\"Comp 1 \u00b7 1920\u00d71080\", mac:true, items:[\n    [\"Nouvelle composition\",\"Composition \u203a New\",\"Cr\u00e9e une nouvelle composition.\",[\"Ctrl\",\"N\"],\"tl:newcomp\"],\n    [\"Pr\u00e9composer\",\"Pre-compose\",\"Regroupe les calques s\u00e9lectionn\u00e9s dans une composition.\",[\"Ctrl\",\"Shift\",\"C\"],\"tl:precomp\"],\n    [\"Scinder le calque\",\"Split Layer\",\"Coupe le calque en deux \u00e0 l'indicateur de temps.\",[\"Ctrl\",\"Shift\",\"D\"],\"tl:split\"],\n    [\"Couper le d\u00e9but du calque\",\"Trim In Point\",\"Coupe le d\u00e9but du calque \u00e0 l'indicateur de temps.\",[\"Alt\",\"[\"],\"tl:trimin\"],\n    [\"Couper la fin du calque\",\"Trim Out Point\",\"Coupe la fin du calque \u00e0 l'indicateur de temps.\",[\"Alt\",\"]\"],\"tl:trimout\"],\n    [\"Afficher la position\",\"Position\",\"D\u00e9ploie la propri\u00e9t\u00e9 Position du calque.\",[\"P\"],\"tl:prop-P\"],\n    [\"Afficher l'\u00e9chelle\",\"Scale\",\"D\u00e9ploie la propri\u00e9t\u00e9 \u00c9chelle du calque.\",[\"S\"],\"tl:prop-S\"],\n    [\"Afficher la rotation\",\"Rotation\",\"D\u00e9ploie la propri\u00e9t\u00e9 Rotation du calque.\",[\"R\"],\"tl:prop-R\"],\n    [\"Afficher l'opacit\u00e9\",\"Opacity\",\"D\u00e9ploie la propri\u00e9t\u00e9 Opacit\u00e9 du calque.\",[\"T\"],\"tl:prop-T\"],\n    [\"Afficher les images cl\u00e9s\",\"Reveal Keyframes\",\"Montre toutes les propri\u00e9t\u00e9s anim\u00e9es du calque.\",[\"U\"],\"tl:showkeys\"],\n    [\"Lissage de vitesse\",\"Easy Ease\",\"Adoucit l'entr\u00e9e et la sortie des images cl\u00e9s.\",[\"F9\"],\"tl:ease\"],\n    [\"Nouveau solide\",\"New Solid\",\"Cr\u00e9e un calque de couleur unie.\",[\"Ctrl\",\"Y\"],\"tl:solid\"],\n    [\"Nouvel objet nul\",\"New Null Object\",\"Cr\u00e9e un calque invisible qui pilote les autres.\",[\"Ctrl\",\"Alt\",\"Shift\",\"Y\"],\"tl:null\"],\n    [\"Nouveau calque d'effets\",\"New Adjustment Layer\",\"Applique ses effets \u00e0 tous les calques du dessous.\",[\"Ctrl\",\"Alt\",\"Y\"],\"tl:adjust\"],\n    [\"Dupliquer le calque\",\"Duplicate\",\"Copie le calque s\u00e9lectionn\u00e9.\",[\"Ctrl\",\"D\"],\"tl:dup\"],\n    [\"Pr\u00e9visualiser\",\"Preview\",\"Lance la lecture de la composition.\",[\"Espace\"],\"tl:play\"],\n    [\"Image cl\u00e9 suivante\",\"Go to Next Keyframe\",\"Saute \u00e0 la prochaine image cl\u00e9 visible.\",[\"K\"],\"tl:nextkey\"],\n    [\"Cl\u00e9 de position\",\"Add Position Keyframe\",\"Pose une image cl\u00e9 de position \u00e0 l'instant courant.\",[\"Alt\",\"Shift\",\"P\"],\"tl:addkey\"],\n    [\"Ajouter \u00e0 la file de rendu\",\"Add to Render Queue\",\"Envoie la composition au rendu.\",[\"Ctrl\",\"M\"],\"progress:rq\"],\n  ]},\n  fg:{name:\"Figma\", mono:\"Fg\", hue:\"#A259FF\", cat:\"motion\", sub:\"Design d'interface\", vp:\"Frame \u00b7 Desktop\", mac:true, items:[\n    [\"Cadre\",\"Frame Tool\",\"Dessine un cadre, base de tout \u00e9cran.\",[\"F\"],\"frame\"],\n    [\"Ajouter l'auto layout\",\"Add Auto Layout\",\"Aligne les \u00e9l\u00e9ments avec un espacement automatique.\",[\"Shift\",\"A\"],\"autolayout\"],\n    [\"Cr\u00e9er un composant\",\"Create Component\",\"Transforme la s\u00e9lection en composant r\u00e9utilisable.\",[\"Ctrl\",\"Alt\",\"K\"],\"component\"],\n    [\"Grouper\",\"Group Selection\",\"R\u00e9unit les calques s\u00e9lectionn\u00e9s.\",[\"Ctrl\",\"G\"],\"group\"],\n    [\"Encadrer la s\u00e9lection\",\"Frame Selection\",\"Place la s\u00e9lection dans un nouveau cadre.\",[\"Ctrl\",\"Alt\",\"G\"],\"framesel\"],\n    [\"Masquer l'interface\",\"Show\/Hide UI\",\"Cache les panneaux pour ne voir que le canevas.\",[\"Ctrl\",\"\\\\\"],\"hideui\"],\n    [\"Plume\",\"Pen Tool\",\"Dessine des trac\u00e9s vectoriels.\",[\"P\"],\"pen\"],\n    [\"Texte\",\"Text Tool\",\"Cr\u00e9e un bloc de texte.\",[\"T\"],\"text\"],\n    [\"Rectangle\",\"Rectangle Tool\",\"Dessine un rectangle.\",[\"R\"],\"rect\"],\n    [\"Copier les propri\u00e9t\u00e9s\",\"Copy Properties\",\"Copie remplissage, contour et effets.\",[\"Ctrl\",\"Alt\",\"C\"],\"copyprops\"],\n    [\"Coller les propri\u00e9t\u00e9s\",\"Paste Properties\",\"Applique les propri\u00e9t\u00e9s copi\u00e9es.\",[\"Ctrl\",\"Alt\",\"V\"],\"pasteprops\"],\n    [\"D\u00e9tacher l'instance\",\"Detach Instance\",\"Coupe le lien entre l'instance et son composant.\",[\"Ctrl\",\"Alt\",\"B\"],\"detach\"],\n    [\"Tout afficher\",\"Zoom to Fit\",\"Ajuste le zoom pour voir tout le canevas.\",[\"Shift\",\"1\"],\"zoomfit\"],\n    [\"Zoom sur la s\u00e9lection\",\"Zoom to Selection\",\"Cadre la vue sur la s\u00e9lection.\",[\"Shift\",\"2\"],\"zoomsel\"],\n    [\"Aplatir\",\"Flatten\",\"Fusionne les formes en un seul vecteur.\",[\"Ctrl\",\"E\"],\"flatten\"],\n    [\"Utiliser comme masque\",\"Use as Mask\",\"Le calque du bas masque ceux du dessus.\",[\"Ctrl\",\"Alt\",\"M\"],\"clip\"],\n    [\"Vectoriser le contour\",\"Outline Stroke\",\"Convertit le contour en forme pleine.\",[\"Ctrl\",\"Alt\",\"O\"],\"outstroke\"],\n    [\"Commenter\",\"Comment\",\"Passe en mode commentaire.\",[\"C\"],\"comment\"],\n  ]},\n  bl:{name:\"Blender\", mono:\"Bl\", hue:\"#F5932A\", cat:\"3d\", sub:\"3D open source\", vp:\"Vue utilisateur \u00b7 Perspective\", mac:false, items:[\n    [\"D\u00e9placer\",\"Grab\",\"Translate l'objet ou les \u00e9l\u00e9ments s\u00e9lectionn\u00e9s.\",[\"G\"],\"grab\"],\n    [\"Pivoter\",\"Rotate\",\"Fait tourner la s\u00e9lection autour du pivot.\",[\"R\"],\"rotate\"],\n    [\"Mettre \u00e0 l'\u00e9chelle\",\"Scale\",\"Agrandit ou r\u00e9duit la s\u00e9lection.\",[\"S\"],\"scale\"],\n    [\"Extruder\",\"Extrude\",\"Pousse des faces, ar\u00eates ou sommets vers l'ext\u00e9rieur.\",[\"E\"],\"extrude\"],\n    [\"Incruster des faces\",\"Inset Faces\",\"Cr\u00e9e une face plus petite \u00e0 l'int\u00e9rieur de chaque face.\",[\"I\"],\"inset\"],\n    [\"Coupe en boucle\",\"Loop Cut and Slide\",\"Ajoute une boucle d'ar\u00eates autour du maillage.\",[\"Ctrl\",\"R\"],\"loopcut\"],\n    [\"Biseauter\",\"Bevel\",\"Chanfreine les ar\u00eates s\u00e9lectionn\u00e9es.\",[\"Ctrl\",\"B\"],\"bevel\"],\n    [\"Menu Ajouter\",\"Add Menu\",\"Ajoute un maillage, une cam\u00e9ra, une lumi\u00e8re\u2026\",[\"Shift\",\"A\"],\"addmenu\"],\n    [\"Dupliquer\",\"Duplicate\",\"Copie la s\u00e9lection et la place en d\u00e9placement.\",[\"Shift\",\"D\"],\"dup3d\"],\n    [\"Mode \u00c9dition\",\"Toggle Edit Mode\",\"Bascule entre mode Objet et mode \u00c9dition.\",[\"Tab\"],\"editmode\"],\n    [\"Appliquer les transformations\",\"Apply \u203a Transform\",\"Fige rotation et \u00e9chelle dans le maillage.\",[\"Ctrl\",\"A\"],\"apply\"],\n    [\"Joindre des objets\",\"Join\",\"Fusionne plusieurs objets en un seul.\",[\"Ctrl\",\"J\"],\"join3d\"],\n    [\"S\u00e9parer\",\"Separate\",\"D\u00e9tache la s\u00e9lection dans un nouvel objet.\",[\"P\"],\"separate\"],\n    [\"Couteau\",\"Knife Tool\",\"D\u00e9coupe librement \u00e0 travers les faces.\",[\"K\"],\"knife\"],\n    [\"Cr\u00e9er une face\",\"Fill \/ New Face\",\"Remplit l'espace entre les sommets s\u00e9lectionn\u00e9s.\",[\"F\"],\"fill3d\"],\n    [\"Fusionner\",\"Merge\",\"R\u00e9unit les sommets s\u00e9lectionn\u00e9s en un point.\",[\"M\"],\"merge3d\"],\n    [\"\u00c9dition proportionnelle\",\"Proportional Editing\",\"Entra\u00eene aussi les sommets voisins, en douceur.\",[\"O\"],\"prop\"],\n    [\"Lancer le rendu\",\"Render Image\",\"Calcule l'image depuis la cam\u00e9ra active.\",[\"F12\"],\"render\"],\n    [\"Cadrer la s\u00e9lection\",\"View Selected\",\"Centre la vue sur ce qui est s\u00e9lectionn\u00e9.\",[\"Num .\"],\"frame3d\"],\n    [\"Vue locale\",\"Local View\",\"Isole la s\u00e9lection dans la vue 3D.\",[\"Num \/\"],\"local\"],\n  ]},\n  c4:{name:\"Cinema 4D\", mono:\"C4\", hue:\"#4B8BFF\", cat:\"3d\", sub:\"3D & motion\", vp:\"Perspective \u00b7 Cam\u00e9ra par d\u00e9faut\", mac:true, items:[\n    [\"D\u00e9placer\",\"Move\",\"Translate l'objet s\u00e9lectionn\u00e9.\",[\"E\"],\"grab\"],\n    [\"Pivoter\",\"Rotate\",\"Fait tourner l'objet s\u00e9lectionn\u00e9.\",[\"R\"],\"rotate\"],\n    [\"Mettre \u00e0 l'\u00e9chelle\",\"Scale\",\"Agrandit ou r\u00e9duit l'objet.\",[\"T\"],\"scale\"],\n    [\"S\u00e9lection directe\",\"Live Selection\",\"Peint la s\u00e9lection directement sur les polygones.\",[\"9\"],\"liveselect\"],\n    [\"Rendre \u00e9ditable\",\"Make Editable\",\"Convertit un objet param\u00e9trique en polygones.\",[\"C\"],\"editable\"],\n    [\"Grouper les objets\",\"Group Objects\",\"Range la s\u00e9lection sous un nouveau Null.\",[\"Alt\",\"G\"],\"outliner:group\"],\n    [\"Rendu dans la vue\",\"Render View\",\"Calcule le rendu directement dans la vue active.\",[\"Ctrl\",\"R\"],\"render\"],\n    [\"Rendu dans la visionneuse\",\"Render to Picture Viewer\",\"Lance le rendu final dans la visionneuse d'images.\",[\"Shift\",\"R\"],\"progress:pv\"],\n    [\"Cadrer la s\u00e9lection\",\"Frame Selected Elements\",\"Centre la vue sur les \u00e9l\u00e9ments s\u00e9lectionn\u00e9s.\",[\"S\"],\"frame3d\"],\n    [\"Cadrer la sc\u00e8ne\",\"Frame Geometry\",\"Recadre la vue sur toute la sc\u00e8ne.\",[\"H\"],\"frameall\"],\n    [\"Extruder\",\"Extrude\",\"Pousse les polygones s\u00e9lectionn\u00e9s.\",[\"D\"],\"extrude\"],\n    [\"Extrusion interne\",\"Inner Extrude\",\"Cr\u00e9e un polygone en retrait dans chaque face.\",[\"I\"],\"inset\"],\n    [\"Lecture\",\"Play Forwards\",\"Lance la lecture de l'animation.\",[\"F8\"],\"tl:3dplay\"],\n    [\"Enregistrer les objets actifs\",\"Record Active Objects\",\"Pose une cl\u00e9 sur position, rotation et \u00e9chelle.\",[\"F9\"],\"tl:3dkey\"],\n    [\"Les 4 vues\",\"All Views\",\"Affiche dessus, face, droite et perspective.\",[\"F5\"],\"quad:split\"],\n    [\"Gestionnaire d'objets\",\"Object Manager\",\"Active la liste hi\u00e9rarchique des objets.\",[\"Shift\",\"F1\"],\"panel:objects\"],\n    [\"R\u00e9glages du rendu\",\"Render Settings\",\"Ouvre r\u00e9solution, format et qualit\u00e9 de rendu.\",[\"Ctrl\",\"B\"],\"panel:render\"],\n    [\"Commander\",\"Commander\",\"Cherche et lance n'importe quelle commande au clavier.\",[\"Shift\",\"C\"],\"commander\"],\n  ]},\n  my:{name:\"Maya\", mono:\"My\", hue:\"#37C6C0\", cat:\"3d\", sub:\"3D & animation\", vp:\"persp \u00b7 Vue perspective\", mac:false, items:[\n    [\"D\u00e9placer\",\"Move Tool\",\"Translate l'objet s\u00e9lectionn\u00e9.\",[\"W\"],\"grab\"],\n    [\"Pivoter\",\"Rotate Tool\",\"Fait tourner l'objet s\u00e9lectionn\u00e9.\",[\"E\"],\"rotate\"],\n    [\"Mettre \u00e0 l'\u00e9chelle\",\"Scale Tool\",\"Agrandit ou r\u00e9duit l'objet.\",[\"R\"],\"scale\"],\n    [\"Cadrer la s\u00e9lection\",\"Frame Selection\",\"Centre la vue sur la s\u00e9lection.\",[\"F\"],\"frame3d\"],\n    [\"Tout cadrer\",\"Frame All\",\"Recadre la vue sur toute la sc\u00e8ne.\",[\"A\"],\"frameall\"],\n    [\"Aper\u00e7u liss\u00e9\",\"Smooth Mesh Preview\",\"Affiche le maillage liss\u00e9 sans le modifier.\",[\"3\"],\"smooth\"],\n    [\"Filaire\",\"Wireframe\",\"Affiche uniquement les ar\u00eates.\",[\"4\"],\"wire:wire\"],\n    [\"Ombr\u00e9\",\"Smooth Shade All\",\"Revient \u00e0 l'affichage ombr\u00e9.\",[\"5\"],\"wire:shade\"],\n    [\"Grouper\",\"Group\",\"Range la s\u00e9lection dans un nouveau groupe.\",[\"Ctrl\",\"G\"],\"outliner:group\"],\n    [\"Parenter\",\"Parent\",\"Rend le premier objet s\u00e9lectionn\u00e9 enfant du dernier.\",[\"P\"],\"outliner:parent\"],\n    [\"Annuler le parentage\",\"Unparent\",\"Sort l'objet de la hi\u00e9rarchie de son parent.\",[\"Shift\",\"P\"],\"outliner:unparent\"],\n    [\"Dupliquer\",\"Duplicate\",\"Copie l'objet s\u00e9lectionn\u00e9.\",[\"Ctrl\",\"D\"],\"dup3d\"],\n    [\"Extruder\",\"Extrude\",\"Pousse les faces s\u00e9lectionn\u00e9es.\",[\"Ctrl\",\"E\"],\"extrude\"],\n    [\"S\u00e9lection douce\",\"Soft Select\",\"\u00c9tend l'influence aux sommets voisins.\",[\"B\"],\"prop\"],\n    [\"Poser une cl\u00e9\",\"Set Key\",\"Enregistre une image cl\u00e9 sur l'objet.\",[\"S\"],\"tl:3dkey\"],\n    [\"Masquer la s\u00e9lection\",\"Hide Selection\",\"Cache les objets s\u00e9lectionn\u00e9s.\",[\"Ctrl\",\"H\"],\"hide3d\"],\n    [\"Une vue \/ quatre vues\",\"Toggle Single\/Four View\",\"Bascule entre une vue et quatre vues.\",[\"Espace\"],\"quad:split\"],\n    [\"Mode objet \/ composant\",\"Object\/Component Mode\",\"Bascule entre l'objet et ses sommets, ar\u00eates, faces.\",[\"F8\"],\"editmode\"],\n  ]},\n  mx:{name:\"3ds Max\", mono:\"Mx\", hue:\"#2BB3E8\", cat:\"3d\", sub:\"3D & visualisation\", vp:\"[+] [Perspective] [Standard]\", mac:false, items:[\n    [\"S\u00e9lectionner et d\u00e9placer\",\"Select and Move\",\"Translate l'objet s\u00e9lectionn\u00e9.\",[\"W\"],\"grab\"],\n    [\"S\u00e9lectionner et pivoter\",\"Select and Rotate\",\"Fait tourner l'objet s\u00e9lectionn\u00e9.\",[\"E\"],\"rotate\"],\n    [\"S\u00e9lectionner et mettre \u00e0 l'\u00e9chelle\",\"Select and Scale\",\"Agrandit ou r\u00e9duit l'objet.\",[\"R\"],\"scale\"],\n    [\"Zoom \u00e9tendu sur la s\u00e9lection\",\"Zoom Extents Selected\",\"Centre la vue sur la s\u00e9lection.\",[\"Z\"],\"frame3d\"],\n    [\"Isoler la s\u00e9lection\",\"Isolate Selection\",\"N'affiche plus que les objets s\u00e9lectionn\u00e9s.\",[\"Alt\",\"Q\"],\"local\"],\n    [\"Filaire \/ Ombr\u00e9\",\"Wireframe Toggle\",\"Bascule la vue entre filaire et ombr\u00e9.\",[\"F3\"],\"wire:wire\"],\n    [\"Faces + ar\u00eates\",\"Edged Faces\",\"Superpose les ar\u00eates au rendu ombr\u00e9.\",[\"F4\"],\"wire:edged\"],\n    [\"Afficher la grille\",\"Toggle Grid\",\"Masque ou affiche la grille de construction.\",[\"G\"],\"gridt\"],\n    [\"Accrochages\",\"Snaps Toggle\",\"Active l'accrochage \u00e0 la grille.\",[\"S\"],\"snap\"],\n    [\"\u00c9diteur de mat\u00e9riaux\",\"Material Editor\",\"Ouvre les mat\u00e9riaux de la sc\u00e8ne.\",[\"M\"],\"panel:mat\"],\n    [\"Rendu rapide\",\"Quick Render\",\"Lance le rendu de la vue active.\",[\"Shift\",\"Q\"],\"render\"],\n    [\"Configuration du rendu\",\"Render Setup\",\"Ouvre r\u00e9solution, moteur et sortie.\",[\"F10\"],\"panel:render\"],\n    [\"S\u00e9lectionner d'apr\u00e8s le nom\",\"Select by Name\",\"Liste les objets pour les choisir par leur nom.\",[\"H\"],\"panel:names\"],\n    [\"Agrandir la fen\u00eatre\",\"Maximize Viewport\",\"Passe de quatre vues \u00e0 une seule, plein cadre.\",[\"Alt\",\"W\"],\"quad:max\"],\n    [\"Vue de dessus\",\"Top View\",\"Affiche la sc\u00e8ne vue de dessus.\",[\"T\"],\"viewsw:top\"],\n    [\"Vue perspective\",\"Perspective View\",\"Revient en vue perspective.\",[\"P\"],\"viewsw:persp\"],\n    [\"Transparence\",\"See-Through\",\"Rend l'objet semi-transparent.\",[\"Alt\",\"X\"],\"wire:xray\"],\n    [\"Niveau Sommet\",\"Vertex Sub-object Level\",\"Passe en \u00e9dition des sommets.\",[\"1\"],\"editmode\"],\n  ]},\n  zb:{name:\"ZBrush\", mono:\"Zb\", hue:\"#E8743B\", cat:\"3d\", sub:\"Sculpture num\u00e9rique\", vp:\"Canvas \u00b7 SDiv 1\", mac:true, items:[\n    [\"Taille du pinceau\",\"Draw Size\",\"R\u00e8gle le diam\u00e8tre du pinceau.\",[\"S\"],\"zb:size\"],\n    [\"Att\u00e9nuation\",\"Focal Shift\",\"R\u00e8gle la douceur du bord du pinceau.\",[\"O\"],\"zb:focal\"],\n    [\"Intensit\u00e9 Z\",\"Z Intensity\",\"R\u00e8gle la profondeur du relief sculpt\u00e9.\",[\"U\"],\"zb:zint\"],\n    [\"Intensit\u00e9 RVB\",\"RGB Intensity\",\"R\u00e8gle la force de la peinture.\",[\"I\"],\"zb:rgb\"],\n    [\"Palette de pinceaux\",\"Brush Palette\",\"Ouvre la liste des pinceaux sous le curseur.\",[\"B\"],\"panel:brush\"],\n    [\"Diviser\",\"Divide\",\"Ajoute un niveau de subdivision au maillage.\",[\"Ctrl\",\"D\"],\"zb:divide\"],\n    [\"Subdivision sup\u00e9rieure\",\"Higher Subdivision\",\"Monte au niveau de d\u00e9tail suivant.\",[\"D\"],\"zb:subup\"],\n    [\"Subdivision inf\u00e9rieure\",\"Lower Subdivision\",\"Redescend au niveau de d\u00e9tail pr\u00e9c\u00e9dent.\",[\"Shift\",\"D\"],\"zb:subdown\"],\n    [\"PolyFrame\",\"Draw PolyFrame\",\"Affiche le maillage et les polygroupes.\",[\"Shift\",\"F\"],\"zb:polyframe\"],\n    [\"Sym\u00e9trie\",\"Activate Symmetry\",\"Sculpte des deux c\u00f4t\u00e9s \u00e0 la fois.\",[\"X\"],\"zb:sym\"],\n    [\"Mode \u00c9dition\",\"Edit Mode\",\"Passe l'outil en mode sculpture 3D.\",[\"T\"],\"zb:edit\"],\n    [\"Inverser le masque\",\"Inverse Mask\",\"La zone prot\u00e9g\u00e9e devient libre, et inversement.\",[\"Ctrl\",\"I\"],\"zb:invmask\"],\n    [\"Grouper le masqu\u00e9\",\"Group Masked & Clear Mask\",\"Cr\u00e9e un polygroupe avec la zone masqu\u00e9e, puis efface le masque.\",[\"Ctrl\",\"W\"],\"zb:groupmask\"],\n    [\"Cadrer le maillage\",\"Frame\",\"Recentre l'objet dans le canevas.\",[\"F\"],\"zb:frame\"],\n    [\"D\u00e9placer (gizmo)\",\"Move\",\"Active le gizmo de d\u00e9placement.\",[\"W\"],\"zb:move\"],\n    [\"Vider le calque\",\"Clear Layer\",\"Efface le canevas 2,5D.\",[\"Ctrl\",\"N\"],\"zb:clear\"],\n  ]},\n  sp:{name:\"Substance 3D Painter\", mono:\"Sp\", hue:\"#E8505B\", cat:\"3d\", sub:\"Texturing 3D\", vp:\"Viewport 3D \u00b7 Material\", mac:true, items:[\n    [\"Pinceau\",\"Paint\",\"Peint directement sur le mod\u00e8le.\",[\"1\"],\"sp:paint\"],\n    [\"Gomme\",\"Eraser\",\"Efface la peinture du calque actif.\",[\"2\"],\"sp:erase\"],\n    [\"Projection\",\"Projection\",\"Projette une image depuis la vue.\",[\"3\"],\"sp:project\"],\n    [\"Remplissage polygone\",\"Polygon Fill\",\"Remplit des polygones d'un clic.\",[\"4\"],\"sp:polyfill\"],\n    [\"Estompe\",\"Smudge\",\"\u00c9tale la peinture existante.\",[\"5\"],\"sp:smudge\"],\n    [\"Clonage\",\"Clone\",\"Copie une zone peinte vers une autre.\",[\"6\"],\"sp:clone\"],\n    [\"Pipette de mat\u00e9riau\",\"Material Picker\",\"R\u00e9cup\u00e8re le mat\u00e9riau sous le curseur.\",[\"P\"],\"sp:picker\"],\n    [\"Vues 3D et 2D\",\"3D\/2D Views\",\"Affiche le mod\u00e8le et sa carte UV c\u00f4te \u00e0 c\u00f4te.\",[\"F1\"],\"sp:split\"],\n    [\"Vue 3D seule\",\"3D View Only\",\"N'affiche que le mod\u00e8le.\",[\"F2\"],\"sp:3d\"],\n    [\"Vue 2D seule\",\"2D View Only\",\"N'affiche que la carte UV.\",[\"F3\"],\"sp:2d\"],\n    [\"Canal suivant\",\"Next Channel\",\"Affiche un canal du mat\u00e9riau en solo.\",[\"C\"],\"sp:channel\"],\n    [\"Vue mat\u00e9riau\",\"Material Mode\",\"Revient au rendu complet du mat\u00e9riau.\",[\"M\"],\"sp:material\"],\n    [\"Carte de maillage suivante\",\"Next Mesh Map\",\"Affiche une carte cuite en solo.\",[\"B\"],\"sp:meshmap\"],\n    [\"Exporter les textures\",\"Export Textures\",\"Ouvre la fen\u00eatre d'export des textures.\",[\"Ctrl\",\"Shift\",\"E\"],\"progress:tex\"],\n    [\"Agrandir le pinceau\",\"Increase Brush Size\",\"Augmente le diam\u00e8tre du pinceau.\",[\"]\"],\"brushup\"],\n    [\"R\u00e9duire le pinceau\",\"Decrease Brush Size\",\"Diminue le diam\u00e8tre du pinceau.\",[\"[\"],\"brushdown\"],\n    [\"Masquer l'interface\",\"Hide UI\",\"Cache les panneaux pour ne voir que la vue.\",[\"Tab\"],\"hideui\"],\n    [\"Mode Cuisson\",\"Baking Mode\",\"Passe dans l'espace de cuisson des cartes.\",[\"F8\"],\"sp:bake\"],\n  ]},\n};\nconst CATS = [[\"all\",\"Tous\"],[\"2d\",\"Design graphique\"],[\"3d\",\"3D\"],[\"motion\",\"Motion & UI\"]];\n\n\/* ============ animation toolkit ============ *\/\nconst NS=\"http:\/\/www.w3.org\/2000\/svg\";\nconst RM = matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\nconst EASE=\"cubic-bezier(.2,.8,.2,1)\", BOUNCE=\"cubic-bezier(.3,1.6,.5,1)\";\nlet GEN=0, UID=0;\nconst uid=()=>root.id+\"-k\"+(++UID);\nfunction S(tag,a={},p){const e=document.createElementNS(NS,tag);for(const k in a)e.setAttribute(k,a[k]);if(p)p.appendChild(e);return e}\nfunction A(el,kf,o={}){ if(typeof o===\"number\")o={delay:o};\n  return el.animate(kf,{duration:RM?1:(o.duration??500),delay:RM?0:(o.delay||0),easing:o.easing||EASE,fill:o.fill||\"both\"}) }\nconst ez={io:t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)\/2, out:t=>1-Math.pow(1-t,3), lin:t=>t};\nfunction T(dur,fn,delay=0,e=ez.io){ const g=GEN; return new Promise(res=>{ setTimeout(()=>{ if(g!==GEN)return res(); const t0=performance.now();\n  const step=n=>{ if(g!==GEN)return res(); const t=RM?1:Math.min(1,(n-t0)\/dur); fn(e(t)); t<1?requestAnimationFrame(step):res() }; requestAnimationFrame(step) }, RM?0:delay) }) }\nconst wait=ms=>new Promise(r=>setTimeout(r,RM?0:ms));\nconst lerp=(a,b,t)=>a+(b-a)*t;\nconst P=a=>a.map(p=>p[0].toFixed(1)+\",\"+p[1].toFixed(1)).join(\" \");\nconst hide=el=>{el.style.opacity=0;return el};\nconst fadeIn=(el,d=300,dl=0)=>A(el,[{opacity:0},{opacity:1}],{duration:d,delay:dl});\nconst fadeOut=(el,d=300,dl=0)=>A(el,[{opacity:1},{opacity:0}],{duration:d,delay:dl});\nconst pop=(el,dl=0)=>A(el,[{opacity:0,transform:\"scale(.6)\"},{opacity:1,transform:\"scale(1)\"}],{duration:420,delay:dl,easing:BOUNCE});\nconst bump=(el,k=1.05,d=380)=>A(el,[{transform:\"scale(1)\"},{transform:`scale(${k})`},{transform:\"scale(1)\"}],{duration:d});\nconst slide=(el,dx,dy,d=500,dl=0)=>A(el,[{transform:\"translate(0,0)\"},{transform:`translate(${dx}px,${dy}px)`}],{duration:d,delay:dl});\nconst css=n=>getComputedStyle(root).getPropertyValue(n).trim();\nfunction mix(a,b,t){const h=s=>[1,3,5].map(i=>parseInt(s.slice(i,i+2),16));const x=h(a),y=h(b);return \"#\"+x.map((v,i)=>Math.round(lerp(v,y[i],t)).toString(16).padStart(2,\"0\")).join(\"\")}\n\nfunction cursor(p,x,y){ const g=S(\"g\",{class:\"cur\"},p); S(\"path\",{d:\"M0 0 L0 13 L3.5 10 L6 15.5 L8 14.6 L5.6 9.3 L10 9.3 Z\"},g); setCur(g,x,y); return g }\nfunction setCur(c,x,y){ c._x=x; c._y=y; c.style.transform=`translate(${x}px,${y}px)` }\nfunction go(c,x,y,d=500){ const a=A(c,[{transform:`translate(${c._x}px,${c._y}px)`},{transform:`translate(${x}px,${y}px)`}],{duration:d}); c._x=x; c._y=y;\n  return a.finished.then(()=>{ c.style.transform=`translate(${x}px,${y}px)`; a.cancel() }).catch(()=>{}) }\nfunction click(p,x,y){ const c=S(\"circle\",{cx:x,cy:y,r:5,class:\"ripple\"},p); A(c,[{opacity:.55,transform:\"scale(.4)\"},{opacity:0,transform:\"scale(2.6)\"}],{duration:480}) }\nfunction anc(p,x,y){ return S(\"rect\",{x:x-2.5,y:y-2.5,width:5,height:5,class:\"anc\"},p) }\nfunction sel(p,x,y,w,h,cls=\"sel\",eight=false){ const g=S(\"g\",{},p); S(\"rect\",{x,y,width:w,height:h,class:cls},g);\n  const pts=[[x,y],[x+w,y],[x,y+h],[x+w,y+h]]; if(eight)pts.push([x+w\/2,y],[x+w\/2,y+h],[x,y+h\/2],[x+w,y+h\/2]);\n  pts.forEach(([a,b])=>S(\"rect\",{x:a-2.5,y:b-2.5,width:5,height:5,class:\"hdl\"+(cls===\"csel\"?\" c\":\"\")},g)); return g }\nfunction label(p,x,y,t,cls=\"lb\"){ const e=S(\"text\",{x,y,\"text-anchor\":\"middle\",class:cls},p); e.textContent=t; return e }\nfunction hdr(s,t){ const g=S(\"g\",{class:\"hdr\"},s); const r=S(\"rect\",{x:8,y:150,width:20,height:14,rx:3},g); const tx=S(\"text\",{x:14,y:159.6,class:\"lb ink\"},g);\n  const set=v=>{ tx.textContent=v; try{ r.setAttribute(\"width\",tx.getComputedTextLength()+12) }catch(e){} }; set(t); return set }\nfunction tpill(s,t){ const g=S(\"g\",{class:\"hdr\"},s); const r=S(\"rect\",{x:140,y:7,width:40,height:14,rx:7},g); const tx=S(\"text\",{x:160,y:16.6,\"text-anchor\":\"middle\",class:\"lb ink\"},g);\n  const set=v=>{ tx.textContent=v; try{ const w=tx.getComputedTextLength()+16; r.setAttribute(\"width\",w); r.setAttribute(\"x\",160-w\/2) }catch(e){} }; set(t); g.set=set; return g }\nfunction panel(p,x,y,w,rows,title=\"Calques\"){\n  const g=S(\"g\",{},p); S(\"rect\",{x,y,width:w,height:118,rx:6,class:\"panel\"},g);\n  const tt=S(\"text\",{x:x+8,y:y+13,class:\"lb\"},g); tt.textContent=title;\n  const list=[];\n  const mk=(r,i)=>{ const rg=S(\"g\",{},g); const ry=y+20+i*19; const bg=S(\"rect\",{x:x+4,y:ry,width:w-8,height:17,rx:3,class:\"row-bg\"+(r.on?\" on\":\"\")},rg);\n    S(\"rect\",{x:x+8,y:ry+3.5,width:13,height:10,rx:1.5,class:r.c},rg); const t=S(\"text\",{x:x+26,y:ry+11.5,class:\"rt\"},rg); t.textContent=r.t; rg._bg=bg; return rg };\n  rows.forEach((r,i)=>list.push(mk(r,i)));\n  return { g, list, async add(r){ list.forEach(rg=>{ rg._bg.classList.remove(\"on\"); slide(rg,0,19,350) });\n    const n=mk({...r,on:true},0); list.unshift(n); n.style.opacity=0;\n    await A(n,[{opacity:0,transform:\"translateX(-8px)\"},{opacity:1,transform:\"translateX(0)\"}],{duration:380,delay:200}).finished } };\n}\nfunction padlock(p,x,y){ const g=S(\"g\",{},p); S(\"path\",{d:`M${x+2.5} ${y+6} V${y+3.5} a3.5 3.5 0 0 1 7 0 V${y+6}`,class:\"ln\"},g); S(\"rect\",{x,y:y+6,width:12,height:9,rx:2,class:\"ci\"},g); return g }\nfunction ants(p,tag,a){ const g=S(\"g\",{},p); S(tag,{...a,class:\"antsb\"},g); S(tag,{...a,class:\"antsw\"},g); return g }\n\n\/* iso 3D *\/\nconst ISO=(x,y,z)=>[160+(x-y)*.866, 116+(x+y)*.5-z];\nconst xy=(a,b)=>({x1:a[0],y1:a[1],x2:b[0],y2:b[1]});\nconst CUBE=()=>({x:-25,y:-25,z:0,w:50,d:50,h:50});\nfunction floor(p){ const g=S(\"g\",{class:\"grid\"},p); for(let i=-80;i<=80;i+=20){ S(\"line\",xy(ISO(i,-80,0),ISO(i,80,0)),g); S(\"line\",xy(ISO(-80,i,0),ISO(80,i,0)),g) } return g }\nfunction box(p,o,cls=\"\"){\n  const g=S(\"g\",{class:\"bx \"+cls},p); const sides=[0,1,2,3].map(()=>S(\"polygon\",{},g)); const top=S(\"polygon\",{class:\"bt\"},g);\n  const set=(o2={})=>{ Object.assign(o,o2); const {x,y,z=0,w,d,h,r=0}=o; const cx=x+w\/2, cy=y+d\/2, c=Math.cos(r), sn=Math.sin(r);\n    const b=[[x,y],[x+w,y],[x+w,y+d],[x,y+d]].map(([px,py])=>[cx+(px-cx)*c-(py-cy)*sn, cy+(px-cx)*sn+(py-cy)*c]);\n    for(let i=0;i<4;i++){ const a=b[i], e=b[(i+1)%4]; const nx=e[1]-a[1], ny=-(e[0]-a[0]); const vis=nx+ny>.001;\n      sides[i].setAttribute(\"points\", vis&&h>0 ? P([ISO(a[0],a[1],z),ISO(e[0],e[1],z),ISO(e[0],e[1],z+h),ISO(a[0],a[1],z+h)]) : \"\");\n      sides[i].setAttribute(\"class\", ny>nx?\"bl\":\"br\"); }\n    o.top=b.map(q=>ISO(q[0],q[1],z+h)); top.setAttribute(\"points\",P(o.top)); };\n  set(); return {g,set,o};\n}\nfunction scaleBox(b,k,base){ b.set({x:base.x*k,y:base.y*k,w:base.w*k,d:base.d*k,h:base.h*k}) }\n\n\/* photo layer (HTML-level svg so CSS filters work everywhere) *\/\nfunction photo(c,x,y,w,h,o={}){\n  const L=c.layer(); const id=uid();\n  const v=S(\"svg\",{x,y,width:w,height:h,viewBox:\"0 0 240 150\",preserveAspectRatio:\"xMidYMid slice\"},L);\n  const gd=S(\"linearGradient\",{id,x1:0,y1:0,x2:0,y2:1},S(\"defs\",{},v)); S(\"stop\",{offset:0,\"stop-color\":\"#8FB8DE\"},gd); S(\"stop\",{offset:1,\"stop-color\":\"#F6D2A2\"},gd);\n  S(\"rect\",{width:240,height:150,fill:`url(#${id})`},v);\n  S(\"circle\",{cx:172,cy:54,r:17,fill:\"#FFC266\"},v);\n  S(\"polygon\",{points:\"0,112 48,72 92,100 138,56 188,96 240,74 240,150 0,150\",fill:\"#62819F\"},v);\n  S(\"polygon\",{points:\"0,126 70,104 130,128 198,110 240,122 240,150 0,150\",fill:\"#2F4B5F\"},v);\n  let obj=null; if(o.pole){ obj=S(\"g\",{},v); S(\"rect\",{x:60,y:38,width:4,height:92,fill:\"#1C2630\"},obj); S(\"rect\",{x:48,y:44,width:28,height:3,fill:\"#1C2630\"},obj) }\n  L.style.transformOrigin=`${(x+w\/2)\/3.2}% ${(y+h\/2)\/1.8}%`;\n  return {L,obj};\n}\n\n\/* ============ effects: each builds the \"before\" state and returns play() ============ *\/\nconst FX={};\n\nFX.shapebuilder=({s})=>{\n  const a=S(\"circle\",{cx:134,cy:84,r:38,class:\"sh\"},s), b=S(\"circle\",{cx:186,cy:84,r:38,class:\"sh\"},s);\n  const u=hide(S(\"g\",{},s)); S(\"circle\",{cx:134,cy:84,r:38,class:\"c1\"},u); S(\"circle\",{cx:186,cy:84,r:38,class:\"c1\"},u);\n  const ln=S(\"line\",{x1:100,y1:86,x2:100,y2:86,class:\"drag\"},s);\n  const cur=cursor(s,70,124);\n  return async()=>{\n    await go(cur,100,86,400);\n    await Promise.all([T(650,t=>ln.setAttribute(\"x2\",lerp(100,222,t))), go(cur,222,86,650)]);\n    fadeOut(ln,200); fadeOut(a,300); fadeOut(b,300); await fadeIn(u,300).finished; await bump(u).finished;\n  };\n};\n\nFX.pen=({s})=>{\n  const mk=d=>S(\"path\",{d,class:\"ln\",pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},s);\n  const p1=mk(\"M58 122 C 92 36, 150 36, 165 88\"), p2=mk(\"M165 88 C 180 140, 236 140, 262 52\");\n  const hl=hide(S(\"g\",{},s)); S(\"line\",{x1:150,y1:36,x2:180,y2:140,class:\"hline\"},hl); S(\"circle\",{cx:150,cy:36,r:2.5,class:\"hdot\"},hl); S(\"circle\",{cx:180,cy:140,r:2.5,class:\"hdot\"},hl);\n  const an=[[58,122],[165,88],[262,52]].map(([x,y])=>hide(anc(s,x,y)));\n  const cur=cursor(s,36,140);\n  return async()=>{\n    await go(cur,58,122,350); click(s,58,122); pop(an[0]);\n    await Promise.all([go(cur,165,88,600), T(600,t=>p1.setAttribute(\"stroke-dashoffset\",1-t))]);\n    pop(an[1]); fadeIn(hl,200); click(s,165,88); await wait(280);\n    await Promise.all([go(cur,262,52,600), T(600,t=>p2.setAttribute(\"stroke-dashoffset\",1-t))]);\n    pop(an[2]); click(s,262,52); fadeOut(hl,300,300);\n  };\n};\n\nFX.direct=({s})=>{\n  const pts=[[110,40],[210,40],[210,124],[110,124]];\n  const path=S(\"path\",{class:\"sh\"},s); const draw=()=>path.setAttribute(\"d\",\"M\"+pts.map(p=>p.join(\" \")).join(\" L\")+\" Z\"); draw();\n  const an=pts.map(([x,y])=>anc(s,x,y)); const cur=cursor(s,260,140);\n  return async()=>{\n    await go(cur,211,41,500); click(s,210,40); an[1].classList.add(\"on\"); await wait(200);\n    await T(650,t=>{ pts[1]=[lerp(210,244,t),lerp(40,18,t)]; draw(); an[1].setAttribute(\"x\",pts[1][0]-2.5); an[1].setAttribute(\"y\",pts[1][1]-2.5); setCur(cur,pts[1][0]+1,pts[1][1]+1) });\n  };\n};\n\nfunction twoShapes(s){ const r=S(\"rect\",{x:84,y:52,width:62,height:62,rx:4,class:\"c1\"},s); const c=S(\"circle\",{cx:206,cy:83,r:32,class:\"c2\"},s); return {r,c} }\nFX.group=({s})=>{\n  const g=S(\"g\",{},s); twoShapes(g);\n  const s1=sel(s,84,52,62,62), s2=sel(s,174,51,64,64); const all=hide(sel(s,80,47,162,72));\n  return async()=>{ fadeOut(s1,200); fadeOut(s2,200); await pop(all).finished; await wait(120);\n    const mv=[{transform:\"translateX(0)\"},{transform:\"translateX(18px)\"},{transform:\"translateX(0)\"}];\n    A(g,mv,{duration:700}); await A(all,mv,{duration:700}).finished };\n};\nFX.ungroup=({s})=>{\n  const A1=S(\"g\",{},s), B1=S(\"g\",{},s);\n  S(\"rect\",{x:84,y:52,width:62,height:62,rx:4,class:\"c1\"},A1); S(\"circle\",{cx:206,cy:83,r:32,class:\"c2\"},B1);\n  const all=sel(s,80,47,162,72); const s1=hide(sel(A1,84,52,62,62)), s2=hide(sel(B1,174,51,64,64));\n  return async()=>{ fadeOut(all,200); pop(s1); await pop(s2,80).finished; slide(A1,-16,0,500); await slide(B1,16,0,500).finished };\n};\n\nFX.clip=({s})=>{\n  const id=uid(); const cp=S(\"clipPath\",{id},S(\"defs\",{},s)); const cc=S(\"circle\",{cx:160,cy:82,r:170},cp);\n  const img=S(\"g\",{\"clip-path\":`url(#${id})`},s);\n  [\"c1\",\"c2\",\"c3\",\"c4\",\"c1\",\"c2\",\"c3\",\"c4\"].forEach((k,i)=>S(\"rect\",{x:60+i*25,y:24,width:25,height:118,class:k},img));\n  const ring=S(\"circle\",{cx:160,cy:82,r:46,class:\"ln\"},s); const sb=hide(sel(s,114,36,92,92));\n  return async()=>{ await T(850,t=>cc.setAttribute(\"r\",lerp(170,46,t))); fadeOut(ring,250); pop(sb) };\n};\n\nFX.outline=({s})=>{\n  const g=S(\"g\",{\"stroke-opacity\":0},s);\n  S(\"rect\",{x:60,y:46,width:72,height:72,class:\"c1 ol\"},g); S(\"circle\",{cx:178,cy:82,r:36,class:\"c2 ol\"},g); S(\"polygon\",{points:\"222,118 256,48 290,118\",class:\"c3 ol\"},g);\n  const lb=hide(label(s,160,30,\"MODE TRAC\u00c9S\"));\n  return async()=>{ fadeIn(lb,250); await T(750,t=>{ g.setAttribute(\"fill-opacity\",1-t); g.setAttribute(\"stroke-opacity\",t) }) };\n};\n\nFX.typeoutline=({s})=>{\n  const tx=S(\"text\",{x:160,y:104,\"text-anchor\":\"middle\",class:\"tx\",\"font-size\":64,\"stroke-opacity\":0},s); tx.textContent=\"Type\";\n  tx.style.stroke=\"var(--s1)\"; tx.style.strokeWidth=\"1\";\n  let bx=110,bw=100; try{ const bb=tx.getBBox(); bx=bb.x; bw=bb.width }catch(e){}\n  const base=S(\"line\",{x1:bx,y1:104,x2:bx+bw,y2:104,class:\"sel\"},s); S(\"rect\",{x:bx-2.5,y:101.5,width:5,height:5,class:\"hdl\"},s);\n  return async()=>{ fadeOut(base,200); await T(650,t=>{ tx.setAttribute(\"fill-opacity\",1-t*.82); tx.setAttribute(\"stroke-opacity\",t) });\n    const n=tx.getNumberOfChars(); for(let i=0;i<n;i++){ try{ const e=tx.getExtentOfChar(i);\n      [[e.x+4,e.y+e.height*.3],[e.x+e.width-4,e.y+e.height*.3],[e.x+4,104],[e.x+e.width-4,104]].forEach((p,j)=>pop(anc(s,...p),i*70+j*25)) }catch(err){} } };\n};\n\nFX.lock=({s})=>{\n  const r=S(\"rect\",{x:120,y:48,width:80,height:70,rx:4,class:\"c1\"},s); const sb=sel(s,120,48,80,70); const pl=hide(padlock(s,204,30));\n  const cur=cursor(s,250,140);\n  return async()=>{ fadeOut(sb,200); await pop(pl).finished; await go(cur,165,86,500); click(s,165,86); go(cur,195,86,400);\n    await A(r,[{transform:\"translateX(0)\"},{transform:\"translateX(3px)\"},{transform:\"translateX(-3px)\"},{transform:\"translateX(2px)\"},{transform:\"translateX(0)\"}],{duration:420}).finished;\n    await bump(pl,1.25,300).finished };\n};\n\nFX.hide=({s})=>{\n  S(\"rect\",{x:78,y:50,width:64,height:64,rx:4,class:\"c1\"},s);\n  const gh=hide(S(\"circle\",{cx:200,cy:82,r:34,class:\"ghost\"},s)); const c=S(\"circle\",{cx:200,cy:82,r:34,class:\"c2\"},s);\n  const sb=sel(s,166,48,68,68); const lb=hide(label(s,200,136,\"masqu\u00e9\"));\n  return async()=>{ fadeOut(sb,300); await A(c,[{opacity:1,transform:\"scale(1)\"},{opacity:0,transform:\"scale(.85)\"}],{duration:450}).finished; fadeIn(gh,300); fadeIn(lb,300) };\n};\n\nFX.join=({s})=>{\n  const f=hide(S(\"polygon\",{points:\"105,124 160,42 215,124\",class:\"c3\"},s));\n  S(\"path\",{d:\"M105 124 L160 42 L215 124\",class:\"ln thick2\"},s);\n  const ln=S(\"line\",{x1:105,y1:124,x2:105,y2:124,class:\"ln thick2\"},s);\n  const a=anc(s,105,124), b=anc(s,215,124); anc(s,160,42);\n  return async()=>{ a.classList.add(\"on\"); b.classList.add(\"on\"); await wait(250);\n    await T(500,t=>ln.setAttribute(\"x2\",lerp(105,215,t))); await fadeIn(f,400).finished };\n};\n\nFX.pastefront=({s})=>{\n  S(\"rect\",{x:66,y:46,width:84,height:74,rx:4,class:\"c1\"},s);\n  const cp=hide(S(\"g\",{},s)); S(\"rect\",{x:66,y:46,width:84,height:74,rx:4,class:\"c1\"},cp); sel(cp,66,46,84,74);\n  const pn=panel(s,206,20,100,[{t:\"Rectangle\",c:\"c1\",on:true}]);\n  return async()=>{ pop(cp); await pn.add({t:\"Rectangle\",c:\"c1\"}) };\n};\n\nFX.again=({s})=>{\n  const xs=[30,82,134,186,238]; const rs=xs.map((x,i)=>{ const r=S(\"rect\",{x,y:68,width:34,height:34,rx:3,class:\"c1\"},s); if(i>1)hide(r); return r });\n  S(\"line\",{x1:66,y1:58,x2:80,y2:58,class:\"drag\"},s); label(s,73,52,\"+52 px\");\n  const sb=sel(s,82,68,34,34);\n  return async()=>{ for(let i=2;i<5;i++){ pop(rs[i]); A(sb,[{transform:`translateX(${(i-2)*52}px)`},{transform:`translateX(${(i-1)*52}px)`}],{duration:260}); await wait(340) } };\n};\n\nFX.envelope=({s})=>{\n  const id=uid(); S(\"path\",{id,d:\"M76 114 Q160 40 244 114\"},S(\"defs\",{},s));\n  const top=S(\"path\",{d:\"M66 116 Q160 22 254 116 L254 134 Q160 60 66 134 Z\",class:\"env\"},s);\n  const flat=S(\"text\",{x:160,y:92,\"text-anchor\":\"middle\",class:\"tx\",\"font-size\":36},s); flat.textContent=\"WARP\";\n  const arc=hide(S(\"text\",{class:\"tx\",\"font-size\":38,\"letter-spacing\":4,\"text-anchor\":\"middle\"},s));\n  const tp=S(\"textPath\",{href:\"#\"+id,startOffset:\"50%\"},arc); tp.textContent=\"WARP\";\n  const sb=hide(sel(s,66,30,188,104));\n  return async()=>{ await A(top,[{opacity:1},{opacity:.35},{opacity:1}],{duration:420}).finished;\n    A(flat,[{opacity:1,transform:\"scaleY(1)\"},{opacity:0,transform:\"scaleY(.6)\"}],{duration:350}); fadeOut(top,350); await fadeIn(arc,450,150).finished; pop(sb) };\n};\n\nFX.mesh=({s})=>{\n  const C=6,R=5,x0=80,y0=38,W=160,H=88; const base=[];\n  for(let j=0;j<R;j++)for(let i=0;i<C;i++)base.push([x0+i*W\/(C-1),y0+j*H\/(R-1)]);\n  const fill=S(\"polygon\",{class:\"c1\"},s); const lines=S(\"path\",{class:\"meshl\"},s); const pt=anc(s,0,0); pt.classList.add(\"on\");\n  const ci=2,cj=2, cp=base[cj*C+ci];\n  const draw=k=>{ const Q=base.map(([x,y])=>{const dx=x-cp[0],dy=y-cp[1];const f=Math.exp(-(dx*dx+dy*dy)\/2400)*k;return [x+f*28,y-f*30]});\n    const g=(i,j)=>Q[j*C+i]; const o=[];\n    for(let i=0;i<C;i++)o.push(g(i,0)); for(let j=1;j<R;j++)o.push(g(C-1,j)); for(let i=C-2;i>=0;i--)o.push(g(i,R-1)); for(let j=R-2;j>0;j--)o.push(g(0,j));\n    fill.setAttribute(\"points\",P(o)); let d=\"\";\n    for(let j=1;j<R-1;j++)d+=\"M\"+[...Array(C)].map((_,i)=>g(i,j).map(n=>n.toFixed(1)).join(\" \")).join(\" L\");\n    for(let i=1;i<C-1;i++)d+=\"M\"+[...Array(R)].map((_,j)=>g(i,j).map(n=>n.toFixed(1)).join(\" \")).join(\" L\");\n    lines.setAttribute(\"d\",d); const c=g(ci,cj); pt.setAttribute(\"x\",c[0]-2.5); pt.setAttribute(\"y\",c[1]-2.5); return c };\n  draw(0); const cur=cursor(s,250,140);\n  return async()=>{ await go(cur,cp[0]+1,cp[1]+1,500); click(s,...cp); await wait(150); await T(850,t=>{ const c=draw(t); setCur(cur,c[0]+1,c[1]+1) }) };\n};\n\nFX.blend=({s})=>{\n  const c1=css(\"--s1\"), c2=css(\"--s2\");\n  const spine=hide(S(\"line\",{x1:62,y1:84,x2:258,y2:84,class:\"ghost\"},s));\n  S(\"circle\",{cx:62,cy:84,r:24,class:\"c1\"},s); S(\"circle\",{cx:258,cy:84,r:14,class:\"c2\"},s);\n  const mids=[1,2,3,4,5].map(i=>{const t=i\/6; const e=S(\"circle\",{cx:lerp(62,258,t),cy:84,r:lerp(24,14,t)},s); e.style.fill=mix(c1,c2,t); return hide(e)});\n  return async()=>{ fadeIn(spine,200); for(let i=0;i<mids.length;i++){ pop(mids[i]); await wait(90) } };\n};\n\nFX.front=({s})=>{\n  const back=S(\"rect\",{x:92,y:38,width:92,height:68,rx:4,class:\"c1\"},s);\n  S(\"rect\",{x:122,y:54,width:92,height:68,rx:4,class:\"c2\"},s); S(\"rect\",{x:152,y:70,width:92,height:68,rx:4,class:\"c3\"},s);\n  const sb=sel(s,92,38,92,68);\n  return async()=>{ await A(back,[{transform:\"translate(0,0) scale(1)\"},{transform:\"translate(-5px,-5px) scale(1.04)\"}],{duration:260}).finished;\n    back.getAnimations().forEach(a=>a.cancel()); back.style.transform=\"translate(-5px,-5px) scale(1.04)\";\n    s.appendChild(back); s.appendChild(sb);\n    await A(back,[{transform:\"translate(-5px,-5px) scale(1.04)\"},{transform:\"translate(0,0) scale(1)\"}],{duration:320}).finished };\n};\n\nFX.eyedrop=({s})=>{\n  S(\"circle\",{cx:92,cy:82,r:32,class:\"c4\"},s);\n  S(\"rect\",{x:170,y:50,width:80,height:64,rx:6,class:\"cs\"},s);\n  const ov=hide(S(\"rect\",{x:170,y:50,width:80,height:64,rx:6,class:\"c4\"},s)); sel(s,170,50,80,64);\n  const cur=cursor(s,240,140);\n  return async()=>{ await go(cur,92,82,600); click(s,92,82); await wait(220); await fadeIn(ov,420).finished; bump(ov) };\n};\n\n\/* ---- Figma ---- *\/\nfunction drawRect(kind){ return ({s})=>{\n  const r=S(\"rect\",{x:80,y:36,width:0,height:0,class:kind===\"frame\"?\"frame\":\"cs\"},s);\n  const nm=hide(S(\"text\",{x:80,y:31,class:\"lb\"},s)); nm.textContent=kind===\"frame\"?\"Frame 1\":\"Rectangle\";\n  const pill=hide(S(\"g\",{},s)); const pr=S(\"rect\",{x:135,y:0,width:50,height:12,rx:3,class:\"pill\"},pill); const pt=S(\"text\",{x:160,y:0,\"text-anchor\":\"middle\",class:\"pillt\"},pill);\n  const cur=cursor(s,62,24);\n  return async()=>{ await go(cur,80,36,350); fadeIn(pill,150);\n    await T(800,t=>{ const w=160*t,h=84*t; r.setAttribute(\"width\",w); r.setAttribute(\"height\",h); setCur(cur,80+w,36+h);\n      pt.textContent=`${Math.round(w*2)} \u00d7 ${Math.round(h*2)}`; pr.setAttribute(\"x\",80+w\/2-25); pt.setAttribute(\"x\",80+w\/2); pr.setAttribute(\"y\",36+h+5); pt.setAttribute(\"y\",36+h+13.5) });\n    fadeIn(nm,250); pop(sel(s,80,36,160,84)) };\n}}\nFX.frame=drawRect(\"frame\"); FX.rect=drawRect(\"rect\");\n\nFX.text=({s})=>{\n  const tx=S(\"text\",{x:100,y:92,class:\"tx\",\"font-size\":30},s);\n  const caret=hide(S(\"line\",{x1:100,y1:66,x2:100,y2:98,class:\"caret\"},s)); const cur=cursor(s,240,136);\n  return async()=>{ await go(cur,100,84,500); click(s,100,84); fadeIn(caret,100); fadeOut(cur,150);\n    const word=\"Bonjour\"; for(let i=1;i<=word.length;i++){ tx.textContent=word.slice(0,i); const w=tx.getComputedTextLength(); caret.setAttribute(\"x1\",102+w); caret.setAttribute(\"x2\",102+w); await wait(85) }\n    await wait(300); fadeOut(caret,150); pop(sel(s,96,64,tx.getComputedTextLength()+8,36)) };\n};\n\nFX.autolayout=({s})=>{\n  const init=[[52,34],[148,92],[226,46]], tgt=[[86,56],[140,56],[194,56]];\n  const bs=init.map(([x,y],i)=>S(\"rect\",{x,y,width:40,height:40,rx:4,class:[\"c1\",\"c2\",\"c3\"][i]},s));\n  const fr=hide(S(\"rect\",{x:74,y:44,width:172,height:64,rx:6,class:\"alf\"},s));\n  const gaps=hide(S(\"g\",{},s)); [126,180].forEach(x=>{ S(\"rect\",{x,y:56,width:14,height:40,class:\"gapm\"},gaps); const t=S(\"text\",{x:x+7,y:120,\"text-anchor\":\"middle\",class:\"gapt\"},gaps); t.textContent=\"14\" });\n  return async()=>{ await Promise.all(bs.map((b,i)=>slide(b,tgt[i][0]-init[i][0],tgt[i][1]-init[i][1],650,i*60).finished)); pop(fr); await fadeIn(gaps,300,150).finished };\n};\n\nfunction button(s){ const g=S(\"g\",{},s); S(\"rect\",{x:110,y:62,width:100,height:36,rx:8,class:\"c1\"},g); label(g,160,83.5,\"Bouton\",\"btxt\"); return g }\nfunction compLabel(s){ const g=S(\"g\",{},s); S(\"path\",{d:\"M110 50 l3.5 -3.5 l3.5 3.5 l-3.5 3.5 z\",class:\"cdia\"},g); const t=S(\"text\",{x:120,y:53,class:\"lb comp\"},g); t.textContent=\"Bouton\"; return g }\nFX.component=({s})=>{ button(s); const sb=sel(s,110,62,100,36); const cs=hide(sel(s,110,62,100,36,\"csel\")); const cl=hide(compLabel(s));\n  return async()=>{ fadeOut(sb,200); pop(cs); await pop(cl,120).finished; bump(cs,1.04) } };\nFX.detach=({s})=>{ button(s); const cs=sel(s,110,62,100,36,\"csel\"); const cl=compLabel(s);\n  const sb=hide(sel(s,110,62,100,36)); const fl=hide(S(\"text\",{x:110,y:53,class:\"lb ink\"},s)); fl.textContent=\"# Bouton\";\n  return async()=>{ fadeOut(cs,250); fadeOut(cl,250); await wait(150); pop(sb); await fadeIn(fl,300).finished } };\n\nFX.framesel=({s})=>{\n  const fr=hide(S(\"rect\",{x:96,y:38,width:146,height:86,class:\"frame\"},s));\n  S(\"circle\",{cx:132,cy:82,r:22,class:\"c2\"},s); S(\"rect\",{x:168,y:58,width:52,height:46,rx:3,class:\"c3\"},s);\n  const s1=sel(s,110,60,44,44), s2=sel(s,168,58,52,46);\n  const nm=hide(S(\"text\",{x:96,y:33,class:\"lb\"},s)); nm.textContent=\"Frame 1\"; const sb=hide(sel(s,96,38,146,86));\n  return async()=>{ fadeOut(s1,200); fadeOut(s2,200); fadeIn(fr,350); fadeIn(nm,300,150); await pop(sb,150).finished };\n};\n\nFX.hideui=({s})=>{\n  const cv=S(\"g\",{},s); S(\"rect\",{x:126,y:54,width:56,height:52,rx:6,class:\"c1\"},cv); S(\"circle\",{cx:194,cy:66,r:12,class:\"c2\"},cv);\n  const mk=x=>{ const g=S(\"g\",{},s); S(\"rect\",{x,y:-1,width:62,height:182,class:\"panel\"},g); for(let i=0;i<8;i++)S(\"rect\",{x:x+8,y:14+i*14,width:i%2?28:40,height:5,rx:2,class:\"pl\"},g); return g };\n  const L=mk(0), R=mk(258);\n  return async()=>{ slide(L,-66,0,500); slide(R,66,0,500); await A(cv,[{transform:\"scale(1)\"},{transform:\"scale(1.3)\"}],{duration:600,delay:150}).finished };\n};\n\nfunction propsScene(s){ const a=S(\"rect\",{x:62,y:48,width:72,height:66,rx:18,class:\"c2 strk\"},s); const b=S(\"rect\",{x:186,y:48,width:72,height:66,rx:2,class:\"cs\"},s); return {a,b} }\nfunction toast(s){ const g=S(\"g\",{},s); S(\"rect\",{x:98,y:10,width:124,height:20,rx:10,class:\"panel\"},g); S(\"circle\",{cx:111,cy:20,r:5,class:\"c2\"},g); const t=S(\"text\",{x:121,y:22.6,class:\"lb ink\"},g); t.textContent=\"Propri\u00e9t\u00e9s copi\u00e9es\"; return g }\nFX.copyprops=({s})=>{ propsScene(s); const ts=hide(toast(s)); const dot=hide(S(\"circle\",{cx:98,cy:81,r:6,class:\"c2\"},s)); const cur=cursor(s,240,140);\n  return async()=>{ await go(cur,100,84,500); click(s,98,81); pop(dot); await wait(200);\n    await A(dot,[{transform:\"translate(0,0) scale(1)\"},{transform:\"translate(13px,-61px) scale(.8)\"}],{duration:500}).finished; pop(ts) } };\nFX.pasteprops=({s})=>{ const {b}=propsScene(s); toast(s); const ov=S(\"rect\",{x:186,y:48,width:72,height:66,rx:2,class:\"c2 strk\",opacity:0},s); sel(s,186,48,72,66);\n  return async()=>{ await wait(150); await T(650,t=>{ ov.setAttribute(\"opacity\",t); ov.setAttribute(\"rx\",lerp(2,18,t)); b.setAttribute(\"rx\",lerp(2,18,t)) }); bump(ov) } };\n\nfunction world(s){ return S(\"g\",{class:\"world\"},s) }\nconst setW=(k,fx,fy,cy=80,cx=160)=>`translate(${(cx-k*fx).toFixed(1)}px,${(cy-k*fy).toFixed(1)}px) scale(${k})`;\nfunction zoomScene(w){ S(\"rect\",{x:-30,y:40,width:70,height:56,rx:4,class:\"c1\"},w); S(\"circle\",{cx:170,cy:84,r:28,class:\"c2\"},w);\n  S(\"rect\",{x:300,y:36,width:80,height:52,rx:4,class:\"c3\"},w); S(\"polygon\",{points:\"100,150 130,110 160,150\",class:\"c4\"},w) }\nFX.zoomfit=({s})=>{ const w=world(s); zoomScene(w); const a=setW(1.5,175,93), b=setW(.72,175,93); w.style.transform=a; const p=tpill(s,\"150 %\");\n  return async()=>{ A(w,[{transform:a},{transform:b}],{duration:800}); await T(800,t=>p.set(Math.round(lerp(150,72,t))+\" %\")) } };\nFX.zoomsel=({s})=>{ const w=world(s); zoomScene(w); sel(w,140,54,60,60); const a=setW(.72,175,93), b=setW(2,170,84); w.style.transform=a; const p=tpill(s,\"72 %\");\n  return async()=>{ A(w,[{transform:a},{transform:b}],{duration:800}); await T(800,t=>p.set(Math.round(lerp(72,200,t))+\" %\")) } };\n\nFX.flatten=({s})=>{\n  const a=S(\"rect\",{x:100,y:46,width:74,height:72,rx:3,class:\"c1\"},s), b=S(\"circle\",{cx:196,cy:82,r:36,class:\"c3\"},s);\n  const s1=sel(s,100,46,74,72), s2=sel(s,160,46,72,72);\n  const u=hide(S(\"g\",{},s)); S(\"rect\",{x:100,y:46,width:74,height:72,rx:3,class:\"c1\"},u); S(\"circle\",{cx:196,cy:82,r:36,class:\"c1\"},u);\n  const sb=hide(sel(s,100,46,132,72)); const lb=hide(S(\"text\",{x:100,y:40,class:\"lb\"},s)); lb.textContent=\"Vecteur\";\n  return async()=>{ fadeOut(s1,200); fadeOut(s2,200); fadeOut(b,350); fadeOut(a,350); await fadeIn(u,350).finished; pop(sb); fadeIn(lb,300); bump(u) };\n};\n\nFX.outstroke=({s})=>{\n  const d=\"M64 98 C 104 30, 150 144, 196 80 S 244 38, 258 64\";\n  const og=hide(S(\"g\",{},s)); S(\"path\",{d,class:\"thickol\"},og); S(\"path\",{d,class:\"thick\"},og);\n  const orig=S(\"path\",{d,class:\"thick\"},s); const cl=S(\"g\",{},s); S(\"path\",{d,class:\"sel\"},cl); [[64,98],[196,80],[258,64]].forEach(p=>anc(cl,...p));\n  return async()=>{ fadeOut(cl,250); fadeIn(og,300); fadeOut(orig,300); await wait(250);\n    const L=orig.getTotalLength(); for(let k=0;k<=6;k++){ const l=L*k\/6, p=orig.getPointAtLength(l), q=orig.getPointAtLength(Math.min(L,l+.5)), p0=orig.getPointAtLength(Math.max(0,l-.5));\n      let nx=-(q.y-p0.y), ny=q.x-p0.x; const m=Math.hypot(nx,ny)||1; nx\/=m; ny\/=m; [1,-1].forEach((sg,j)=>pop(anc(s,p.x+nx*8*sg,p.y+ny*8*sg),k*50+j*20)) } };\n};\n\nFX.comment=({s})=>{\n  S(\"rect\",{x:60,y:34,width:116,height:96,rx:6,class:\"frame\"},s); S(\"rect\",{x:72,y:46,width:60,height:8,rx:2,class:\"cs\"},s);\n  S(\"rect\",{x:72,y:60,width:92,height:40,rx:4,class:\"c3\"},s); S(\"rect\",{x:72,y:108,width:40,height:10,rx:5,class:\"c1\"},s);\n  const pin=hide(S(\"g\",{},s)); S(\"path\",{d:\"M186 76 L186 66 A10 10 0 1 1 196 76 Z\",class:\"pin\"},pin); label(pin,196,69,\"A\",\"pint\");\n  const bub=hide(S(\"g\",{},s)); S(\"rect\",{x:208,y:46,width:92,height:28,rx:6,class:\"panel\"},bub); const t=S(\"text\",{x:216,y:63,class:\"rt\"},bub); t.textContent=\"Belle grille !\";\n  const cur=cursor(s,250,140);\n  return async()=>{ await go(cur,188,78,500); click(s,188,76); await pop(pin).finished; pop(bub) };\n};\n\n\/* ---- Blender ---- *\/\nFX.grab=({s})=>{ floor(s); S(\"line\",{...xy(ISO(-120,0,0),ISO(120,0,0)),class:\"axx\"},s); const b=box(s,CUBE(),\"on\"); const H=hdr(s,\"D 0.00 m\");\n  return async()=>{ await T(850,t=>{ b.set({x:-25+38*t}); H(`D ${(t*1.52).toFixed(2)} m`) }) } };\nFX.rotate=({s})=>{ floor(s); const b=box(s,CUBE(),\"on\"); const H=hdr(s,\"R 0\u00b0\");\n  return async()=>{ await T(900,t=>{ b.set({r:t*Math.PI\/4}); H(`R ${Math.round(t*45)}\u00b0`) }) } };\nFX.scale=({s})=>{ floor(s); const base=CUBE(); const b=box(s,{...base},\"on\"); const H=hdr(s,\"S 1.00\");\n  return async()=>{ await T(800,t=>{ const k=lerp(1,1.28,t); scaleBox(b,k,base); H(`S ${k.toFixed(2)}`) }) } };\nFX.extrude=({s})=>{ floor(s); const b=box(s,{...CUBE(),h:42},\"ed\"); const f=S(\"polygon\",{class:\"fsel\",points:P(b.o.top)},s);\n  const ring=hide(S(\"polyline\",{class:\"bedge\",points:P([ISO(-25,25,42),ISO(25,25,42),ISO(25,-25,42)])},s)); const H=hdr(s,\"E 0.00 m\");\n  return async()=>{ fadeIn(ring,200); await T(800,t=>{ b.set({h:lerp(42,70,t)}); f.setAttribute(\"points\",P(b.o.top)); H(`E ${(t*1.12).toFixed(2)} m`) }) } };\nFX.inset=({s})=>{ floor(s); box(s,CUBE(),\"ed\"); const f=S(\"polygon\",{class:\"fsel\"},s); const ln=S(\"path\",{class:\"bedge\"},s);\n  const C=[[-25,-25],[25,-25],[25,25],[-25,25]], sg=[[1,1],[-1,1],[-1,-1],[1,-1]];\n  const draw=k=>{ const inn=C.map(([x,y],i)=>ISO(x+sg[i][0]*k,y+sg[i][1]*k,50)); f.setAttribute(\"points\",P(inn));\n    ln.setAttribute(\"d\",k<.5?\"\":C.map(([x,y],i)=>{const o=ISO(x,y,50);return `M${o[0]} ${o[1]} L${inn[i][0]} ${inn[i][1]}`}).join(\" \")) };\n  draw(0); const H=hdr(s,\"Incrustation 0.00\");\n  return async()=>{ await T(800,t=>{ draw(t*13); H(`Incrustation ${(t*.26).toFixed(2)}`) }) } };\nFX.loopcut=({s})=>{ floor(s); box(s,CUBE(),\"ed\"); const lp=S(\"polyline\",{class:\"cut\",pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},s);\n  const at=z=>lp.setAttribute(\"points\",P([ISO(-25,25,z),ISO(25,25,z),ISO(25,-25,z)])); at(25);\n  return async()=>{ await T(450,t=>lp.setAttribute(\"stroke-dashoffset\",1-t)); await wait(150); await T(600,t=>at(lerp(25,14,t))) } };\nFX.bevel=({s})=>{ floor(s); const b=box(s,CUBE(),\"ed\");\n  const g=S(\"g\",{class:\"bx ed\"},s); const strips=[0,1,2,3].map(i=>S(\"polygon\",{class:i<2?\"bt\":\"bm\"},g)); const top=S(\"polygon\",{class:\"bt\"},g);\n  const edge=S(\"polyline\",{class:\"bedge\",points:P([ISO(-25,25,50),ISO(25,25,50),ISO(25,-25,50)])},s);\n  const draw=k=>{ b.set({h:50-k}); const z=50-k, h=50;\n    strips[0].setAttribute(\"points\",P([ISO(-25,-25,z),ISO(25,-25,z),ISO(25-k,-25+k,h),ISO(-25+k,-25+k,h)]));\n    strips[1].setAttribute(\"points\",P([ISO(-25,-25,z),ISO(-25,25,z),ISO(-25+k,25-k,h),ISO(-25+k,-25+k,h)]));\n    strips[2].setAttribute(\"points\",P([ISO(-25,25,z),ISO(25,25,z),ISO(25-k,25-k,h),ISO(-25+k,25-k,h)]));\n    strips[3].setAttribute(\"points\",P([ISO(25,25,z),ISO(25,-25,z),ISO(25-k,-25+k,h),ISO(25-k,25-k,h)]));\n    top.setAttribute(\"points\",P([ISO(-25+k,-25+k,h),ISO(25-k,-25+k,h),ISO(25-k,25-k,h),ISO(-25+k,25-k,h)])) };\n  draw(0); const H=hdr(s,\"Biseau 0.00 m\");\n  return async()=>{ await wait(100); fadeOut(edge,300,500); await T(800,t=>{ draw(t*9); H(`Biseau ${(t*.18).toFixed(2)} m`) }) } };\nFX.addmenu=({s})=>{ floor(s); const c0=ISO(0,0,0); S(\"circle\",{cx:c0[0],cy:c0[1],r:6,class:\"cur3d\"},s);\n  const base=CUBE(); const b=box(s,{...base}); scaleBox(b,0,base); hide(b.g);\n  const m=hide(S(\"g\",{},s)); S(\"rect\",{x:178,y:24,width:92,height:76,rx:6,class:\"panel\"},m); const tt=S(\"text\",{x:186,y:36,class:\"lb\"},m); tt.textContent=\"Ajouter\";\n  const hl=hide(S(\"rect\",{x:182,y:42,width:84,height:13,rx:3,class:\"row-bg on\"},m));\n  [\"Maillage \u203a\",\"Courbe \u203a\",\"Lumi\u00e8re \u203a\",\"Cam\u00e9ra\"].forEach((t,i)=>{ const e=S(\"text\",{x:190,y:51.5+i*13,class:\"rt\"},m); e.textContent=t });\n  const cur=cursor(s,172,64);\n  return async()=>{ await pop(m).finished; await go(cur,214,48,350); fadeIn(hl,120); click(s,214,48); await wait(350); fadeOut(m,200); fadeOut(cur,200);\n    fadeIn(b.g,100); await T(550,t=>scaleBox(b,t,base),0,ez.out); b.g.classList.add(\"on\") } };\nFX.dup3d=({s})=>{ floor(s); const o={...CUBE(),x:-47,y:-3}; const a=box(s,{...o},\"on\"); const c=box(s,{...o},\"on\"); hide(c.g); const H=hdr(s,\"D 0.00 m\");\n  return async()=>{ a.g.classList.remove(\"on\"); c.g.style.opacity=1; await T(800,t=>{ c.set({x:-47+44*t,y:-3-44*t}); H(`D ${(t*1.76).toFixed(2)} m`) }) } };\nFX.editmode=({s})=>{ floor(s); const b=box(s,CUBE(),\"on\"); const H=hdr(s,\"Mode Objet\");\n  const vs=[[-25,-25,50],[25,-25,50],[25,25,50],[-25,25,50],[25,-25,0],[25,25,0],[-25,25,0]].map(p=>{const q=ISO(...p); return hide(S(\"circle\",{cx:q[0],cy:q[1],r:2,class:\"vdot\"},s))});\n  return async()=>{ b.g.classList.remove(\"on\"); b.g.classList.add(\"ed\"); H(\"Mode \u00c9dition\"); vs.forEach((v,i)=>pop(v,i*45)); await wait(400) } };\nFX.apply=({s})=>{ floor(s); const b=box(s,{x:-44,y:-4,w:58,d:58,h:58,r:.52},\"on\");\n  const g=S(\"g\",{},s); S(\"rect\",{x:224,y:26,width:86,height:62,rx:6,class:\"panel\"},g); const t0=S(\"text\",{x:232,y:39,class:\"lb\"},g); t0.textContent=\"Transformation\";\n  const r1=S(\"text\",{x:232,y:56,class:\"rt\"},g); const r2=S(\"text\",{x:232,y:74,class:\"rt\"},g);\n  const set=t=>{ r1.textContent=`Rotation Z   ${Math.round(lerp(30,0,t))}\u00b0`; r2.textContent=`\u00c9chelle   ${lerp(1.2,1,t).toFixed(2)}` }; set(0);\n  return async()=>{ await wait(150); bump(g,1.04); await T(700,set) } };\nFX.join3d=({s})=>{ floor(s); const A1=box(s,{x:-43,y:9,w:34,d:34,h:34}), B1=box(s,{x:9,y:-43,w:34,d:34,h:34});\n  const la=label(s,115,60,\"Cube\"), lbb=label(s,205,60,\"Cube.001\"); const lj=hide(label(s,160,46,\"Cube\",\"lb ink\"));\n  return async()=>{ A1.g.classList.add(\"on\"); await wait(250); B1.g.classList.add(\"on\"); await wait(350); fadeOut(la,250); fadeOut(lbb,250); await pop(lj,150).finished } };\nFX.separate=({s})=>{ floor(s); box(s,{x:-25,y:-25,z:0,w:50,d:50,h:28},\"ed\"); const up=box(s,{x:-25,y:-25,z:28,w:50,d:50,h:24},\"ed\");\n  const f=S(\"polygon\",{class:\"fsel\",points:P(up.o.top)},s); const lb=hide(label(s,210,30,\"Cube.001\"));\n  return async()=>{ await T(750,t=>{ up.set({z:28+16*t,x:-25+14*t,y:-25-14*t}); f.setAttribute(\"points\",P(up.o.top)) }); up.g.classList.remove(\"ed\"); up.g.classList.add(\"on\"); fadeOut(f,250); fadeIn(lb,300) } };\nFX.knife=({s})=>{ floor(s); box(s,CUBE(),\"ed\"); const a=ISO(-25,0,50), b=ISO(25,0,50), c=ISO(25,0,0);\n  const ln=S(\"polyline\",{class:\"cut\"},s); const d1=hide(S(\"circle\",{cx:a[0],cy:a[1],r:2.2,class:\"vdot on\"},s)), d2=hide(S(\"circle\",{cx:b[0],cy:b[1],r:2.2,class:\"vdot on\"},s)), d3=hide(S(\"circle\",{cx:c[0],cy:c[1],r:2.2,class:\"vdot on\"},s));\n  const cur=cursor(s,a[0]-30,a[1]-10);\n  return async()=>{ await go(cur,a[0],a[1],400); pop(d1);\n    await T(500,t=>{ const p=[lerp(a[0],b[0],t),lerp(a[1],b[1],t)]; ln.setAttribute(\"points\",P([a,p])); setCur(cur,...p) }); pop(d2);\n    await T(400,t=>{ const p=[lerp(b[0],c[0],t),lerp(b[1],c[1],t)]; ln.setAttribute(\"points\",P([a,b,p])); setCur(cur,...p) }); pop(d3) } };\nFX.fill3d=({s})=>{ floor(s); const Q=[[-32,-32],[32,-32],[32,32],[-32,32]].map(([x,y])=>ISO(x,y,14));\n  const f=hide(S(\"polygon\",{class:\"fsel\",points:P(Q)},s)); S(\"polyline\",{class:\"wire\",points:P([Q[0],Q[1],Q[2],Q[3]])},s);\n  const ds=Q.map(q=>S(\"circle\",{cx:q[0],cy:q[1],r:2.4,class:\"vdot\"},s));\n  return async()=>{ for(const d of ds){ d.classList.add(\"on\"); bump(d,1.6,250); await wait(110) } await fadeIn(f,450).finished } };\nFX.merge3d=({s})=>{ floor(s); const C=[[-32,-32],[32,-32],[32,32],[-32,32]]; const c=ISO(0,0,14);\n  const ln=S(\"polygon\",{class:\"wire\"},s); const ds=C.map(()=>S(\"circle\",{r:2.4,class:\"vdot on\"},s)); const one=hide(S(\"circle\",{cx:c[0],cy:c[1],r:3.4,class:\"vdot on\"},s)); const H=hdr(s,\"4 sommets\");\n  const draw=t=>{ const Q=C.map(([x,y])=>ISO(x*(1-t),y*(1-t),14)); ln.setAttribute(\"points\",P(Q)); ds.forEach((d,i)=>{d.setAttribute(\"cx\",Q[i][0]);d.setAttribute(\"cy\",Q[i][1])}) }; draw(0);\n  return async()=>{ await T(650,draw); ds.forEach(d=>d.style.opacity=0); await pop(one).finished; H(\"1 sommet\") } };\nFX.prop=({s})=>{ floor(s); const N=7, st=12, o=-36; const rows=[...Array(N)].map(()=>S(\"polyline\",{class:\"wire\"},s)), cols=[...Array(N)].map(()=>S(\"polyline\",{class:\"wire\"},s));\n  const ring=hide(S(\"polyline\",{class:\"infl\"},s)); ring.setAttribute(\"points\",P([...Array(41)].map((_,i)=>ISO(Math.cos(i\/40*6.283)*30,Math.sin(i\/40*6.283)*30,0))));\n  const ctr=S(\"circle\",{r:2.6,class:\"vdot on\"},s);\n  const draw=k=>{ const z=(x,y)=>{const d=Math.hypot(x,y)\/34; return d<1?40*k*Math.pow(1-d*d,2):0};\n    for(let j=0;j<N;j++){ rows[j].setAttribute(\"points\",P([...Array(N)].map((_,i)=>{const x=o+i*st,y=o+j*st;return ISO(x,y,z(x,y))})));\n      cols[j].setAttribute(\"points\",P([...Array(N)].map((_,i)=>{const x=o+j*st,y=o+i*st;return ISO(x,y,z(x,y))}))) }\n    const c=ISO(0,0,40*k); ctr.setAttribute(\"cx\",c[0]); ctr.setAttribute(\"cy\",c[1]) };\n  draw(0); const H=hdr(s,\"Influence 0.30 m\");\n  return async()=>{ await fadeIn(ring,300).finished; await T(900,draw) } };\nFX.render=({s})=>{ floor(s); const sh=hide(S(\"ellipse\",{cx:ISO(14,14,0)[0],cy:ISO(14,14,0)[1]+4,rx:48,ry:16,class:\"shadow\"},s)); const b=box(s,CUBE());\n  const shine=hide(S(\"polygon\",{class:\"shine\",points:P(b.o.top)},s)); const H=hdr(s,\"Vue en solide\");\n  const tiles=[]; for(let j=0;j<2;j++)for(let i=0;i<4;i++)tiles.push(hide(S(\"rect\",{x:40+i*60,y:14+j*64,width:60,height:64,class:\"tile\"},s)));\n  return async()=>{ H(\"Rendu 0 %\"); tiles.forEach(t=>fadeIn(t,120)); await wait(180); sh.style.opacity=1; shine.style.opacity=1;\n    for(let i=0;i<tiles.length;i++){ tiles[i].classList.add(\"on\"); await wait(120); fadeOut(tiles[i],180); H(`Rendu ${Math.round((i+1)\/tiles.length*100)} %`) } } };\nFX.frame3d=({s})=>{ const w=world(s); floor(w); box(w,{x:-60,y:6,w:40,d:40,h:40}); box(w,{x:30,y:-50,w:20,d:20,h:20},\"on\");\n  const c=ISO(40,-40,10); const a=setW(1,160,80), b=setW(2.2,c[0],c[1]); w.style.transform=a;\n  return async()=>{ await A(w,[{transform:a},{transform:b}],{duration:850,easing:\"cubic-bezier(.6,0,.2,1)\"}).finished } };\nFX.local=({s})=>{ const w=world(s); floor(w); const A1=box(w,{x:-66,y:20,w:30,d:30,h:30}), C1=box(w,{x:40,y:-66,w:26,d:26,h:26});\n  const sp=ISO(0,0,14); const sg=S(\"g\",{},w); S(\"circle\",{cx:sp[0],cy:sp[1],r:15,class:\"sph\"},sg); S(\"circle\",{cx:sp[0]-4,cy:sp[1]-4,r:8,class:\"sphh\"},sg); S(\"circle\",{cx:sp[0],cy:sp[1],r:15,class:\"sphs\"},sg);\n  const a=setW(1,160,80), b=setW(1.9,sp[0],sp[1]); w.style.transform=a; const H=hdr(s,\"Vue globale\");\n  return async()=>{ fadeOut(A1.g,350); fadeOut(C1.g,350); await wait(200); H(\"Vue locale\"); await A(w,[{transform:a},{transform:b}],{duration:800}).finished } };\n\n\/* ---- Photoshop ---- *\/\nFX.ftransform=c=>{ const {L}=photo(c,70,18,180,110); const s=c.s; const b=hide(sel(s,70,18,180,110,\"sel\",true)); const cur=cursor(s,280,140);\n  return async()=>{ await fadeIn(b,200).finished; await go(cur,251,129,400);\n    const k=[{transform:\"scale(1)\"},{transform:\"scale(1.1)\"}]; A(L,k,{duration:600}); go(cur,260,134,600); await A(b,k,{duration:600}).finished;\n    const r=[{transform:\"scale(1.1) rotate(0deg)\"},{transform:\"scale(1.1) rotate(-6deg)\"}]; fadeOut(cur,200); A(L,r,{duration:600}); await A(b,r,{duration:600}).finished } };\nFX.newlayer=c=>{ photo(c,16,20,178,118); const pn=panel(c.s,206,20,100,[{t:\"Arri\u00e8re-plan\",c:\"th\",on:true}]); return async()=>{ await pn.add({t:\"Calque 1\",c:\"thE\"}) } };\nFX.duplayer=c=>{ photo(c,16,20,178,118); const pn=panel(c.s,206,20,100,[{t:\"Arri\u00e8re-plan\",c:\"th\",on:true}]); return async()=>{ await pn.add({t:\"Arri\u00e8re-plan copie\",c:\"th\"}) } };\nFX.stamp=c=>{ photo(c,16,20,178,118); const pn=panel(c.s,206,20,100,[{t:\"Couleur\",c:\"c2\"},{t:\"Titre\",c:\"ci\"},{t:\"Arri\u00e8re-plan\",c:\"th\",on:true}]);\n  return async()=>{ pn.list.forEach((r,i)=>bump(r,1.04,300)); await wait(250); await pn.add({t:\"Calque fusionn\u00e9\",c:\"th\"}) } };\nconst FLAT=\"contrast(.7) saturate(.75) brightness(1.08)\", PUNCH=\"contrast(1.18) saturate(1.2) brightness(1)\";\nFX.levels=c=>{ const {L}=photo(c,16,20,178,118); L.style.filter=FLAT; const s=c.s;\n  S(\"rect\",{x:206,y:20,width:100,height:118,rx:6,class:\"panel\"},s); const tt=S(\"text\",{x:214,y:33,class:\"lb\"},s); tt.textContent=\"Niveaux\";\n  const bars=S(\"g\",{class:\"bars\"},s); for(let i=0;i<24;i++){ const h=6+42*Math.abs(Math.sin(i*.42)*.8+Math.sin(i*1.3)*.25)*(i>3&&i<21?1:.25); S(\"rect\",{x:214+i*3.6,y:104-h,width:2.6,height:h},bars) }\n  S(\"line\",{x1:214,y1:108,x2:300,y2:108,class:\"ln\"},s).style.strokeWidth=\".6\";\n  const tri=(x,cls)=>S(\"polygon\",{points:`${x-4},118 ${x},111 ${x+4},118`,class:cls},s);\n  const t1=tri(214,\"ci\"), t2=tri(257,\"cs\"), t3=tri(300,\"tri-w\");\n  return async()=>{ A(L,[{filter:FLAT},{filter:PUNCH}],{duration:850}); slide(t1,18,0,800); slide(t2,-4,0,800); await slide(t3,-14,0,800).finished } };\nFX.curves=c=>{ const {L}=photo(c,16,20,178,118); L.style.filter=FLAT; const s=c.s;\n  S(\"rect\",{x:206,y:20,width:100,height:118,rx:6,class:\"panel\"},s); const tt=S(\"text\",{x:214,y:33,class:\"lb\"},s); tt.textContent=\"Courbes\";\n  const g=S(\"g\",{class:\"cgrid\"},s); for(let i=0;i<=4;i++){ S(\"line\",{x1:214+i*21,y1:40,x2:214+i*21,y2:124},g); S(\"line\",{x1:214,y1:40+i*21,x2:298,y2:40+i*21},g) }\n  const cv=S(\"path\",{class:\"ln\"},s); const p1=S(\"circle\",{r:2.4,class:\"ci\"},s), p2=S(\"circle\",{r:2.4,class:\"ci\"},s);\n  const draw=t=>{ const a=[lerp(242,252,t),lerp(96,114,t)], b=[lerp(270,260,t),lerp(68,50,t)]; cv.setAttribute(\"d\",`M214 124 C ${a} ${b} 298 40`);\n    p1.setAttribute(\"cx\",lerp(235,236,t)); p1.setAttribute(\"cy\",lerp(103,111,t)); p2.setAttribute(\"cx\",lerp(277,276,t)); p2.setAttribute(\"cy\",lerp(61,53,t)) };\n  draw(0); return async()=>{ A(L,[{filter:FLAT},{filter:PUNCH}],{duration:850}); await T(850,draw) } };\nFX.hue=c=>{ const {L}=photo(c,16,20,178,118); const s=c.s; const id=uid();\n  const gd=S(\"linearGradient\",{id},S(\"defs\",{},s)); [\"#ff4d4d\",\"#ffd24d\",\"#4dff88\",\"#4dd2ff\",\"#7a4dff\",\"#ff4dd2\",\"#ff4d4d\"].forEach((col,i)=>S(\"stop\",{offset:i\/6,\"stop-color\":col},gd));\n  S(\"rect\",{x:206,y:20,width:100,height:118,rx:6,class:\"panel\"},s); const tt=S(\"text\",{x:214,y:33,class:\"lb\"},s); tt.textContent=\"Teinte\";\n  S(\"rect\",{x:214,y:56,width:84,height:8,rx:4,fill:`url(#${id})`},s); const kn=S(\"circle\",{cx:256,cy:60,r:5.5,class:\"cb\"},s); kn.style.stroke=\"var(--ink)\";\n  const v=S(\"text\",{x:214,y:84,class:\"rt\"},s); v.textContent=\"0\u00b0\";\n  return async()=>{ A(L,[{filter:\"hue-rotate(0deg)\"},{filter:\"hue-rotate(130deg)\"}],{duration:900}); slide(kn,32,0,900); await T(900,t=>v.textContent=`+${Math.round(t*130)}\u00b0`) } };\nFX.invert=c=>{ photo(c,50,14,220,124); const {L:L2}=photo(c,50,14,220,124); L2.style.filter=\"invert(1)\";\n  const a=\"inset(7.8% 84.4% 23.3% 15.6%)\", b=\"inset(7.8% 15.6% 23.3% 15.6%)\"; L2.style.clipPath=a; const w=hide(S(\"line\",{x1:50,y1:14,x2:50,y2:138,class:\"wipe\"},c.s));\n  return async()=>{ w.style.opacity=1; A(L2,[{clipPath:a},{clipPath:b}],{duration:800,easing:\"cubic-bezier(.6,0,.3,1)\"}); await A(w,[{transform:\"translateX(0)\"},{transform:\"translateX(220px)\"}],{duration:800,easing:\"cubic-bezier(.6,0,.3,1)\"}).finished; fadeOut(w,200) } };\nFX.deselect=c=>{ photo(c,50,14,220,124); const a=ants(c.s,\"rect\",{x:150,y:30,width:90,height:62}); const lb=hide(tpill(c.s,\"Aucune s\u00e9lection\"));\n  return async()=>{ await wait(200); await fadeOut(a,250).finished; fadeIn(lb,250) } };\nFX.inverse=c=>{ photo(c,50,14,220,124); const s=c.s; const e={cx:210,cy:56,rx:34,ry:28};\n  const ell=`M${e.cx-e.rx} ${e.cy} a${e.rx} ${e.ry} 0 1 0 ${2*e.rx} 0 a${e.rx} ${e.ry} 0 1 0 ${-2*e.rx} 0 Z`;\n  const inside=S(\"path\",{d:ell,class:\"qm\"},s); const outside=hide(S(\"path\",{d:\"M50 14 H270 V138 H50 Z \"+ell,class:\"qm\"},s));\n  ants(s,\"ellipse\",e); const border=hide(ants(s,\"rect\",{x:51,y:15,width:218,height:122}));\n  return async()=>{ await wait(150); fadeOut(inside,350); fadeIn(outside,350); await fadeIn(border,250,150).finished } };\nFX.cafill=c=>{ const {obj}=photo(c,50,14,220,124,{pole:true}); const a=ants(c.s,\"rect\",{x:88,y:36,width:38,height:92}); const p=hide(tpill(c.s,\"Contenu pris en compte\"));\n  return async()=>{ await pop(p).finished; await wait(200); await A(obj,[{opacity:1},{opacity:0}],{duration:700}).finished; fadeOut(a,250); fadeOut(p,300,300) } };\nFX.crop=c=>{ const {L}=photo(c,50,14,220,124); const s=c.s;\n  const mask=S(\"path\",{class:\"cmask\"},s); const fr=S(\"rect\",{class:\"cropf\"},s); const cr=S(\"path\",{class:\"cropc\"},s);\n  const draw=(x,y,w,h)=>{ mask.setAttribute(\"d\",`M50 14 H270 V138 H50 Z M${x} ${y} H${x+w} V${y+h} H${x} Z`); fr.setAttribute(\"x\",x); fr.setAttribute(\"y\",y); fr.setAttribute(\"width\",w); fr.setAttribute(\"height\",h);\n    const k=9; cr.setAttribute(\"d\",`M${x} ${y+k}V${y}H${x+k} M${x+w-k} ${y}H${x+w}V${y+k} M${x+w} ${y+h-k}V${y+h}H${x+w-k} M${x+k} ${y+h}H${x}V${y+h-k}`) };\n  draw(51,15,218,122); const cur=cursor(s,280,140);\n  return async()=>{ await go(cur,269,137,350);\n    await T(800,t=>{ const x=lerp(51,96,t),y=lerp(15,28,t),w=lerp(218,140,t),h=lerp(122,88,t); draw(x,y,w,h); setCur(cur,x+w,y+h) });\n    fadeOut(cur,200); await wait(200); A(L,[{clipPath:\"inset(0% 0% 0% 0%)\"},{clipPath:\"inset(15.6% 26.2% 35.6% 30%)\"}],{duration:400}); fadeOut(mask,400); fadeOut(cr,400,300) } };\nfunction brush(dir){ return ({s})=>{\n  S(\"rect\",{x:40,y:14,width:240,height:122,rx:4,class:\"canvasp\"},s);\n  const d=\"M84 96 C 120 34, 170 130, 236 58\"; const p=S(\"path\",{d,class:\"paint\",pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},s);\n  const r0=dir>0?7:18, r1=dir>0?18:7; const bc=S(\"circle\",{cx:84,cy:96,r:r0,class:\"brushc\"},s);\n  const lb=S(\"text\",{x:270,y:28,\"text-anchor\":\"end\",class:\"lb ink\"},s); lb.textContent=`Taille ${r0*4} px`;\n  return async()=>{ await T(600,t=>{ const r=lerp(r0,r1,t); bc.setAttribute(\"r\",r); lb.textContent=`Taille ${Math.round(r*4)} px` }); await bump(bc,1.08,250).finished;\n    p.setAttribute(\"stroke-width\",r1*2); const L=p.getTotalLength();\n    await T(900,t=>{ p.setAttribute(\"stroke-dashoffset\",1-t); const q=p.getPointAtLength(L*t); bc.setAttribute(\"cx\",q.x); bc.setAttribute(\"cy\",q.y) }) };\n}}\nFX.brushup=brush(1); FX.brushdown=brush(-1);\nfunction swatches(s){ const g=S(\"g\",{},s); const bk=S(\"rect\",{x:152,y:68,width:44,height:44,rx:3,class:\"c3\"},g); bk.style.stroke=\"var(--surface)\"; bk.style.strokeWidth=\"2\";\n  const fr=S(\"rect\",{x:124,y:40,width:44,height:44,rx:3,class:\"c2\"},g); fr.style.stroke=\"var(--surface)\"; fr.style.strokeWidth=\"2\"; return {bk,fr} }\nFX.defcol=({s})=>{ swatches(s);\n  const bw=hide(S(\"rect\",{x:152,y:68,width:44,height:44,rx:3,fill:\"#fff\",stroke:\"#9AA0AA\",\"stroke-width\":1},s)); const fb=hide(S(\"rect\",{x:124,y:40,width:44,height:44,rx:3,fill:\"#111\",stroke:\"#fff\",\"stroke-width\":1.5},s));\n  label(s,146,34,\"premier plan\"); label(s,174,126,\"arri\u00e8re-plan\");\n  return async()=>{ fadeIn(bw,350); await fadeIn(fb,350,120).finished; bump(fb) } };\nFX.swapcol=({s})=>{ const {bk,fr}=swatches(s);\n  const fr2=hide(S(\"rect\",{x:124,y:40,width:44,height:44,rx:3,class:\"c2\"},s)); fr2.style.stroke=\"var(--surface)\"; fr2.style.strokeWidth=\"2\";\n  const bk2=hide(S(\"rect\",{x:152,y:68,width:44,height:44,rx:3,class:\"c3\"},s)); bk2.style.stroke=\"var(--surface)\"; bk2.style.strokeWidth=\"2\";\n  s.insertBefore(fr2,bk.parentNode); \/* c2 copy drawn under everything *\/\n  const pf=[{transform:\"translate(0,0)\"},{transform:\"translate(30px,-4px)\"},{transform:\"translate(28px,28px)\"}], pb=[{transform:\"translate(0,0)\"},{transform:\"translate(-4px,30px)\"},{transform:\"translate(-28px,-28px)\"}];\n  return async()=>{ [fr,fr2].forEach(e=>A(e,pf,{duration:700})); [bk,bk2].forEach(e=>A(e,pb,{duration:700})); await wait(350);\n    fr.style.opacity=0; fr2.style.opacity=1; bk2.style.opacity=1; bk.style.opacity=0; await wait(400) } };\nFX.imgsize=c=>{ const {L}=photo(c,50,14,220,124); const p=tpill(c.s,\"3000 \u00d7 2000 px\");\n  return async()=>{ A(L,[{transform:\"scale(1)\"},{transform:\"scale(.55)\"}],{duration:850}); await T(850,t=>p.set(`${Math.round(lerp(3000,1500,t))} \u00d7 ${Math.round(lerp(2000,1000,t))} px`)) } };\n\n\n\n\/* ================== shared helpers for the new scenes ================== *\/\nconst MATS={clay:[\"var(--bt)\",\"var(--bl)\",\"var(--br)\"],wax:[\"#F3C7AE\",\"#B9573C\",\"#3A120B\"],metal:[\"#F1F5FB\",\"#8A97AB\",\"#161C28\"],\n  gold:[\"#FFEAB0\",\"#D39B2D\",\"#3E2604\"],gray:[\"#E8E8E8\",\"#8A8A8A\",\"#1C1C1C\"],normal:[\"#D7D2FF\",\"#8080FF\",\"#3E33A0\"],ao:[\"#FFFFFF\",\"#C2C2C2\",\"#2E2E2E\"],idm:[\"#FFB454\",\"#FF6F9F\",\"#3D7BFF\"]};\nfunction sphere(p,cx,cy,r,mat=\"clay\"){ const id=uid(); const g=S(\"g\",{},p); const rg=S(\"radialGradient\",{id,cx:\"38%\",cy:\"34%\",r:\"72%\"},S(\"defs\",{},g));\n  const M=MATS[mat]; [[0,M[0]],[.55,M[1]],[1,M[2]]].forEach(([o,c])=>{ const st=S(\"stop\",{offset:o},rg); st.style.stopColor=c });\n  g.circ=S(\"circle\",{cx,cy,r,fill:`url(#${id})`},g); g.grad=rg; return g }\nfunction drawLandscape(v){ const id=uid();\n  const gd=S(\"linearGradient\",{id,x1:0,y1:0,x2:0,y2:1},S(\"defs\",{},v)); S(\"stop\",{offset:0,\"stop-color\":\"#8FB8DE\"},gd); S(\"stop\",{offset:1,\"stop-color\":\"#F6D2A2\"},gd);\n  S(\"rect\",{width:240,height:150,fill:`url(#${id})`},v); S(\"circle\",{cx:172,cy:54,r:17,fill:\"#FFC266\"},v);\n  S(\"polygon\",{points:\"0,112 48,72 92,100 138,56 188,96 240,74 240,150 0,150\",fill:\"#62819F\"},v);\n  S(\"polygon\",{points:\"0,126 70,104 130,128 198,110 240,122 240,150 0,150\",fill:\"#2F4B5F\"},v) }\nfunction miniImg(p,x,y,w,h){ const v=S(\"svg\",{x,y,width:w,height:h,viewBox:\"0 0 240 150\",preserveAspectRatio:\"none\"},p); drawLandscape(v); return v }\nconst NAMES={my:[\"pCube1\",\"pSphere1\",\"pCone1\"],c4:[\"Cube\",\"Sph\u00e8re\",\"C\u00f4ne\"],mx:[\"Box001\",\"Sphere001\",\"Cone001\"],bl:[\"Cube\",\"Sph\u00e8re\",\"C\u00f4ne\"]};\nfunction moveEl(el,x,y){ el.setAttribute(\"x\",x); el.setAttribute(\"y\",y) }\n\n\/* ================== 3D display & view ================== *\/\nfunction edges(o){ const {x,y,w,d,h}=o, B=[[x,y],[x+w,y],[x+w,y+d],[x,y+d]], out=[];\n  for(let i=0;i<4;i++){ const a=B[i],b=B[(i+1)%4]; out.push([ISO(a[0],a[1],0),ISO(b[0],b[1],0),i===0||i===3]); out.push([ISO(a[0],a[1],h),ISO(b[0],b[1],h),false]); out.push([ISO(a[0],a[1],0),ISO(a[0],a[1],h),i===0]) } return out }\nFX.wire=({s},mode=\"wire\")=>{ floor(s); const o=CUBE(); const b=box(s,{...o}); const wg=S(\"g\",{},s);\n  edges(o).forEach(([p,q,hid])=>{ if(hid&&mode===\"edged\") return; S(\"line\",{...xy(p,q),class:\"wedge\"+(hid&&mode===\"xray\"?\" h\":\"\")},wg) });\n  const H=hdr(s,mode===\"shade\"?\"Filaire\":\"Ombr\u00e9\");\n  if(mode===\"shade\") b.g.style.opacity=0; else hide(wg);\n  return async()=>{ if(mode===\"wire\"){ fadeOut(b.g,450); await fadeIn(wg,450).finished; H(\"Filaire\") }\n    else if(mode===\"shade\"){ fadeOut(wg,450); await fadeIn(b.g,450).finished; H(\"Ombr\u00e9\") }\n    else if(mode===\"edged\"){ await fadeIn(wg,400).finished; H(\"Faces + ar\u00eates\") }\n    else { A(b.g,[{opacity:1},{opacity:.35}],{duration:450}); await fadeIn(wg,450).finished; H(\"Transparence\") } };\n};\nFX.smooth=({s})=>{ floor(s); const b=box(s,CUBE(),\"on\"); const sp=hide(sphere(s,160,91,36,\"clay\")); const ring=hide(S(\"circle\",{cx:160,cy:91,r:36,class:\"sphs\"},s)); const H=hdr(s,\"Affichage 1 \u00b7 brut\");\n  return async()=>{ A(b.g,[{opacity:1,transform:\"scale(1)\"},{opacity:0,transform:\"scale(.82)\"}],{duration:450}); pop(sp,120); await pop(ring,120).finished; H(\"Affichage 3 \u00b7 liss\u00e9\") } };\nFX.quad=({s},mode=\"split\")=>{\n  const w=world(s); floor(w); box(w,CUBE(),\"on\");\n  const pan=S(\"g\",{},s);\n  [[0,0,\"Dessus\"],[160,0,\"Face\"],[0,90,\"Droite\"]].forEach(([x,y,t])=>{ S(\"rect\",{x:x+2,y:y+2,width:156,height:86,rx:4,class:\"pane\"},pan);\n    if(t!==\"Dessus\") S(\"line\",{x1:x+14,y1:y+66,x2:x+146,y2:y+66,class:\"orthg\"},pan);\n    S(\"rect\",{x:x+60,y:y+26,width:40,height:40,class:\"orth\"},pan); const l=S(\"text\",{x:x+10,y:y+14,class:\"lb\"},pan); l.textContent=t });\n  S(\"rect\",{x:162,y:92,width:156,height:86,rx:4,class:\"pane\"},pan); const pl=S(\"text\",{x:170,y:104,class:\"lb\"},pan); pl.textContent=\"Persp\";\n  const full=setW(1,160,95,95), small=setW(.48,160,95,140,240);\n  if(mode===\"split\"){ hide(pan); w.style.transform=full } else w.style.transform=small;\n  return async()=>{ if(mode===\"split\"){ A(w,[{transform:full},{transform:small}],{duration:700}); await fadeIn(pan,450,250).finished }\n    else { fadeOut(pan,300); await A(w,[{transform:small},{transform:full}],{duration:700,delay:150}).finished } };\n};\nFX.viewsw=({s},v=\"top\")=>{\n  const per=S(\"g\",{},s); floor(per); box(per,CUBE(),\"on\");\n  const top=S(\"g\",{},s); const g=S(\"g\",{class:\"grid\"},top); for(let i=0;i<=16;i++)S(\"line\",{x1:i*20,y1:0,x2:i*20,y2:180},g); for(let j=0;j<=9;j++)S(\"line\",{x1:0,y1:j*20,x2:320,y2:j*20},g);\n  S(\"rect\",{x:132,y:52,width:56,height:56,class:\"orthf\"},top);\n  const H=hdr(s,v===\"top\"?\"Perspective\":\"Dessus\");\n  if(v===\"top\") hide(top); else hide(per);\n  const [a,b]=v===\"top\"?[per,top]:[top,per];\n  return async()=>{ A(a,[{opacity:1,transform:\"scale(1)\"},{opacity:0,transform:\"scale(.92)\"}],{duration:400}); await A(b,[{opacity:0,transform:\"scale(1.08)\"},{opacity:1,transform:\"scale(1)\"}],{duration:450,delay:150}).finished; H(v===\"top\"?\"Dessus\":\"Perspective\") };\n};\nFX.gridt=({s})=>{ const f=floor(s); box(s,CUBE()); const H=hdr(s,\"Grille : affich\u00e9e\"); return async()=>{ await fadeOut(f,500).finished; H(\"Grille : masqu\u00e9e\") } };\nFX.snap=({s})=>{ floor(s); const b=box(s,{...CUBE(),x:-45},\"on\"); const H=hdr(s,\"Accrochage : off\");\n  const mg=hide(S(\"g\",{},s)); S(\"path\",{d:\"M126 152 v6 a6 6 0 0 0 12 0 v-6 h-4 v6 a2 2 0 0 1 -4 0 v-6 z\",class:\"mag\"},mg);\n  return async()=>{ H(\"Accrochage : on\"); await pop(mg).finished; for(let k=1;k<=3;k++){ await wait(240); b.set({x:-45+20*k}); const c=ISO(-45+20*k,25,0); click(s,c[0],c[1]); bump(b.g,1.03,200) } } };\nFX.hide3d=({s})=>{ floor(s); box(s,{x:-62,y:12,w:34,d:34,h:34}); const b=box(s,{x:10,y:-44,w:36,d:36,h:36},\"on\"); const H=hdr(s,\"2 objets visibles\");\n  return async()=>{ await A(b.g,[{opacity:1,transform:\"scale(1)\"},{opacity:0,transform:\"scale(.9)\"}],{duration:450}).finished; H(\"1 objet masqu\u00e9\") } };\nFX.liveselect=({s})=>{ floor(s); box(s,CUBE(),\"ed\"); const n=3, st=50\/n, cells=[];\n  for(let j=0;j<n;j++)for(let i=0;i<n;i++){ const x=-25+i*st,y=-25+j*st; cells.push(S(\"polygon\",{points:P([ISO(x,y,50),ISO(x+st,y,50),ISO(x+st,y+st,50),ISO(x,y+st,50)]),class:\"cell\"},s)) }\n  const cc=(i,j)=>ISO(-25+(i+.5)*st,-25+(j+.5)*st,50);\n  const br=S(\"ellipse\",{rx:12,ry:7,class:\"brushc\"},s); const p0=cc(0,0); br.setAttribute(\"cx\",p0[0]-50); br.setAttribute(\"cy\",p0[1]-12);\n  return async()=>{ let prev=[p0[0]-50,p0[1]-12];\n    for(const [i,j] of [[0,0],[1,0],[1,1],[2,1],[2,2],[1,2]]){ const p=cc(i,j), a=prev; await T(170,t=>{ br.setAttribute(\"cx\",lerp(a[0],p[0],t)); br.setAttribute(\"cy\",lerp(a[1],p[1],t)) },0,ez.lin); cells[j*n+i].setAttribute(\"class\",\"cell on\"); prev=p } } };\nFX.editable=({s})=>{ floor(s); box(s,CUBE(),\"on\"); const g=hide(S(\"g\",{class:\"subd\"},s));\n  [50\/3,100\/3].forEach(v=>{ S(\"line\",xy(ISO(-25+v,-25,50),ISO(-25+v,25,50)),g); S(\"line\",xy(ISO(-25,-25+v,50),ISO(25,-25+v,50)),g);\n    S(\"line\",xy(ISO(-25+v,25,0),ISO(-25+v,25,50)),g); S(\"line\",xy(ISO(-25,25,v),ISO(25,25,v)),g);\n    S(\"line\",xy(ISO(25,-25+v,0),ISO(25,-25+v,50)),g); S(\"line\",xy(ISO(25,-25,v),ISO(25,25,v)),g) });\n  const p=tpill(s,\"Cube \u00b7 param\u00e9trique\");\n  return async()=>{ await fadeIn(g,500).finished; p.set(\"Cube \u00b7 polygones\"); bump(p,1.08,300) } };\nFX.frameall=({s})=>{ const w=world(s); floor(w); box(w,{x:-60,y:6,w:40,d:40,h:40}); box(w,{x:30,y:-50,w:20,d:20,h:20},\"on\");\n  const c=ISO(40,-40,10); const a=setW(2.2,c[0],c[1]), b=setW(.95,160,96,92); w.style.transform=a;\n  return async()=>{ await A(w,[{transform:a},{transform:b}],{duration:850,easing:\"cubic-bezier(.6,0,.2,1)\"}).finished } };\n\n\/* ================== outliner (group \/ parent \/ unparent) ================== *\/\nFX.outliner=({s,soft},mode=\"group\")=>{\n  const nm=NAMES[soft]||[\"Objet 1\",\"Objet 2\",\"Objet 3\"];\n  const w=world(s); w.style.transform=setW(.8,160,100,96,100); floor(w);\n  const A1=box(w,{x:-55,y:0,w:32,d:32,h:32},\"on\"), B1=box(w,{x:10,y:-50,w:28,d:28,h:28},mode===\"unparent\"?\"on\":\"on\");\n  const pa=S(\"g\",{},s); S(\"rect\",{x:204,y:12,width:108,height:96,rx:6,class:\"panel\"},pa); const tt=S(\"text\",{x:212,y:25,class:\"lb\"},pa); tt.textContent=soft===\"c4\"?\"Objets\":\"Outliner\";\n  const rows=nm.map((t,i)=>{ const g=S(\"g\",{},pa); const y=32+i*18; const bg=S(\"rect\",{x:208,y,width:100,height:15,rx:3,class:\"row-bg\"+(i<2&&mode!==\"unparent\"||i===1?\" on\":\"\")},g);\n    S(\"rect\",{x:213,y:y+4.5,width:6,height:6,rx:1,class:\"oico\"},g); const tx=S(\"text\",{x:224,y:y+10.5,class:\"rt\"},g); tx.textContent=t; return g });\n  const tree=S(\"path\",{d:\"M215 41 V57 H222\",class:\"tree\"},pa);\n  let link=null; try{ const a=A1.g.getBBox(), b=B1.g.getBBox(); link=S(\"line\",{x1:a.x+a.width\/2,y1:a.y+a.height\/2,x2:b.x+b.width\/2,y2:b.y+b.height\/2,class:\"link\"},w) }catch(e){}\n  if(mode===\"unparent\"){ rows[1].style.transform=\"translateX(10px)\" } else { hide(tree); if(link) hide(link) }\n  return async()=>{\n    if(mode===\"group\"){ const g=hide(S(\"g\",{},pa)); S(\"rect\",{x:208,y:32,width:100,height:15,rx:3,class:\"row-bg on\"},g); S(\"path\",{d:\"M213 37 l3 4 l3 -4 z\",class:\"oico\"},g);\n      const t=S(\"text\",{x:224,y:42.5,class:\"rt\"},g); t.textContent=soft===\"c4\"?\"Null\":\"group1\";\n      rows.forEach((r,i)=>{ r.querySelector(\".row-bg\").classList.remove(\"on\"); slide(r,i<2?10:0,18,400) }); await pop(g,250).finished;\n      try{ const a=A1.g.getBBox(), b=B1.g.getBBox(); const x=Math.min(a.x,b.x)-4,y=Math.min(a.y,b.y)-4; pop(S(\"rect\",{x,y,width:Math.max(a.x+a.width,b.x+b.width)-x+4,height:Math.max(a.y+a.height,b.y+b.height)-y+4,class:\"bbox\"},w)) }catch(e){} }\n    else if(mode===\"parent\"){ await slide(rows[1],10,0,400).finished; fadeIn(tree,250); if(link){ await fadeIn(link,300).finished } }\n    else { A(rows[1],[{transform:\"translateX(10px)\"},{transform:\"translateX(0)\"}],{duration:400}); fadeOut(tree,250); if(link) await fadeOut(link,300).finished } };\n};\n\n\/* ================== panels & dialogs ================== *\/\nfunction idPage(s,o={}){ const x=o.x??104, y=o.y??12, w=o.w??112, h=o.h??156; const g=S(\"g\",{},s);\n  S(\"rect\",{x,y,width:w,height:h,class:\"page\"},g);\n  const guides=S(\"g\",{},g); S(\"rect\",{x:x-4,y:y-4,width:w+8,height:h+8,class:\"bleed\"},guides); S(\"rect\",{x:x+10,y:y+10,width:w-20,height:h-20,class:\"guide\"},guides); S(\"line\",{x1:x+w\/2,y1:y+10,x2:x+w\/2,y2:y+h-10,class:\"colg\"},guides);\n  return {g,guides,x,y,w,h} }\nFX.panel=({s,soft},kind=\"char\")=>{\n  if(kind===\"brush\") sphere(s,100,90,46,\"wax\");\n  else if(kind===\"char\"||kind===\"swatch\"){ const pg=idPage(s,{x:40,y:14,w:110,h:152}); const t=S(\"text\",{x:52,y:48,class:\"ptx\",\"font-size\":12},s); t.textContent=\"Vos id\u00e9es.\"; for(let i=0;i<6;i++) S(\"rect\",{x:52,y:60+i*8,width:i%3===2?50:86,height:3,rx:1.5,class:\"pbar\"},s) }\n  else { floor(s); box(s,{...CUBE(),x:-70,y:20},\"on\") }\n  const pg=S(\"g\",{},s); pg.style.opacity=0;\n  S(\"rect\",{x:176,y:10,width:136,height:160,rx:8,class:\"panel\"},pg);\n  const tt=S(\"text\",{x:186,y:25,class:\"lb ink\"},pg);\n  const hl=S(\"rect\",{x:182,y:30,width:124,height:16,rx:3,class:\"hlr\"},pg); hl.style.opacity=0;\n  let steps=[];\n  const field=(i,lab,val)=>{ const y=40+i*22; const l=S(\"text\",{x:186,y:y+9,class:\"lb\"},pg); l.textContent=lab; S(\"rect\",{x:236,y,width:70,height:13,rx:3,class:\"field\"},pg); const v=S(\"text\",{x:241,y:y+9,class:\"rt\"},pg); v.textContent=val; return {y,v} };\n  if(kind===\"char\"){ tt.textContent=\"Caract\u00e8re\"; const f=[[\"Police\",\"Unbounded\"],[\"Style\",\"Bold\"],[\"Corps\",\"48 pt\"],[\"Interligne\",\"56 pt\"],[\"Approche\",\"0\"]].map((r,i)=>field(i,...r));\n    steps=[[235,f[2].y-1.5,72,16],[235,f[4].y-1.5,72,16]]; pg._after=()=>{ f[4].v.textContent=\"25\" } }\n  else if(kind===\"render\"){ tt.textContent=\"R\u00e9glages du rendu\"; const f=[[\"Moteur\",soft===\"mx\"?\"Arnold\":\"Redshift\"],[\"R\u00e9solution\",\"1920 \u00d7 1080\"],[\"Images\/s\",\"25\"],[\"Format\",\"PNG \u00b7 16 bits\"],[\"Qualit\u00e9\",\"Haute\"]].map((r,i)=>field(i,...r));\n    steps=[[235,f[1].y-1.5,72,16],[235,f[3].y-1.5,72,16]] }\n  else if(kind===\"swatch\"){ tt.textContent=\"Nuancier\"; [[\"[Aucune]\",null],[\"[Papier]\",\"#FFFFFF\"],[\"[Noir]\",\"#111111\"],[\"OK3D Bleu\",\"#2E6BFF\"],[\"Glace\",\"#7CC4FF\"],[\"Ambre\",\"#FFB454\"],[\"Corail\",\"#FF6F9F\"]].forEach(([n,c],i)=>{\n      const y=34+i*18; const sw=S(\"rect\",{x:188,y:y+3,width:10,height:10,rx:2,fill:c||\"none\",stroke:\"#5A6478\",\"stroke-width\":.6},pg); if(!c) S(\"line\",{x1:188,y1:y+13,x2:198,y2:y+3,stroke:\"#FF5A6E\",\"stroke-width\":.8},pg);\n      const t=S(\"text\",{x:204,y:y+11,class:\"rt\"},pg); t.textContent=n });\n    steps=[[182,34+3*18,124,16],[182,34+5*18,124,16]] }\n  else if(kind===\"objects\"||kind===\"names\"){ tt.textContent=kind===\"objects\"?\"Objets\":\"S\u00e9lectionner d'apr\u00e8s le nom\";\n    const L=kind===\"objects\"?[\"Cam\u00e9ra\",\"Lumi\u00e8re\",\"Cube\",\"Sph\u00e8re\",\"Sol\"]:[\"Box001\",\"Camera001\",\"Light001\",\"Sphere001\",\"Plane001\"];\n    const boxes=L.map((n,i)=>{ const y=34+i*18; let cb=null; if(kind===\"names\") cb=S(\"rect\",{x:188,y:y+4,width:8,height:8,rx:1.5,class:\"swatch\"},pg); else S(\"rect\",{x:189,y:y+5,width:6,height:6,rx:1,class:\"oico\"},pg); const t=S(\"text\",{x:202,y:y+11,class:\"rt\"},pg); t.textContent=n; return cb });\n    steps=[[182,34+2*18,124,16],[182,34+3*18,124,16]]; if(kind===\"names\") pg._after=()=>boxes[3].classList.add(\"on\") }\n  else if(kind===\"mat\"){ tt.textContent=\"\u00c9diteur de mat\u00e9riaux\"; const mats=[\"clay\",\"metal\",\"gold\",\"wax\",\"normal\",\"ao\"];\n    mats.forEach((m,i)=>{ const cx=200+(i%3)*44, cy=54+Math.floor(i\/3)*48; sphere(pg,cx,cy,16,m) });\n    hl.setAttribute(\"class\",\"ring2\"); hl.setAttribute(\"rx\",20); steps=[[180,34,40,40],[268,82,40,40]] }\n  else if(kind===\"brush\"){ tt.textContent=\"Pinceaux\"; const B=[\"Std\",\"Move\",\"Clay\",\"Dam\",\"Smooth\",\"Inflat\",\"Pinch\",\"Polish\"];\n    B.forEach((n,i)=>{ const x=184+(i%4)*31, y=34+Math.floor(i\/4)*44; S(\"rect\",{x,y,width:27,height:27,rx:4,class:\"field\"},pg); sphere(pg,x+13.5,y+13.5,9,\"wax\"); const t=S(\"text\",{x:x+13.5,y:y+36,\"text-anchor\":\"middle\",class:\"lb\"},pg); t.textContent=n });\n    hl.setAttribute(\"class\",\"ring2\"); hl.setAttribute(\"rx\",5); steps=[[182,32,31,31],[244,76,31,31]] }\n  const go2=([x,y,w,h])=>{ hl.setAttribute(\"x\",x); hl.setAttribute(\"y\",y); hl.setAttribute(\"width\",w); hl.setAttribute(\"height\",h) };\n  go2(steps[0]);\n  return async()=>{ await A(pg,[{opacity:0,transform:\"translateX(24px)\"},{opacity:1,transform:\"translateX(0)\"}],{duration:450}).finished;\n    await fadeIn(hl,200).finished; await wait(350);\n    const a=steps[0], b=steps[1]; await T(380,t=>go2(a.map((v,i)=>lerp(v,b[i],t)))); if(pg._after) pg._after(); bump(hl,1.06,260) };\n};\nFX.progress=({s},kind=\"pdf\")=>{\n  if(kind===\"pdf\"){ idPage(s); const t=S(\"text\",{x:116,y:44,class:\"ptx\",\"font-size\":12},s); t.textContent=\"Vos id\u00e9es.\"; for(let i=0;i<8;i++) S(\"rect\",{x:116,y:56+i*8,width:i%3===2?50:88,height:3,rx:1.5,class:\"pbar\"},s) }\n  else if(kind===\"tex\") sphere(s,160,90,50,\"metal\");\n  else if(kind===\"pv\"){ floor(s); box(s,CUBE()) }\n  else { S(\"rect\",{x:96,y:30,width:128,height:72,rx:2,class:\"comp\"},s); S(\"circle\",{cx:160,cy:66,r:12,class:\"c1\"},s) }\n  const dim=hide(S(\"rect\",{x:0,y:0,width:320,height:180,class:\"dim\"},s));\n  const dg=hide(S(\"g\",{},s)); S(\"rect\",{x:60,y:34,width:200,height:112,rx:10,class:\"panel\"},dg);\n  const title=S(\"text\",{x:72,y:52,class:\"lb ink\"},dg); title.textContent={pdf:\"Exporter \u00b7 Adobe PDF (impression)\",rq:\"File d'attente de rendu\",tex:\"Exporter les textures\",pv:\"Visionneuse d'images\"}[kind];\n  const st=S(\"text\",{x:72,y:72,class:\"rt\"},dg); st.textContent={pdf:\"Export en cours\u2026\",rq:\"Comp 1 \u00b7 H.264 \u00b7 Rendu\u2026\",tex:\"Texture set : Casque\",pv:\"Rendu image 1 \/ 1\"}[kind];\n  S(\"rect\",{x:72,y:82,width:176,height:5,rx:2.5,class:\"track\"},dg); const fl=S(\"rect\",{x:72,y:82,width:0,height:5,rx:2.5,class:\"fillb\"},dg);\n  const pc=S(\"text\",{x:248,y:72,\"text-anchor\":\"end\",class:\"lb br\"},dg); pc.textContent=\"0 %\";\n  const files={pdf:[\"\u2713 affiche_A3.pdf\"],rq:[\"\u2713 Comp 1.mp4\"],tex:[\"\u2713 Casque_BaseColor.png\",\"\u2713 Casque_Roughness.png\",\"\u2713 Casque_Normal.png\"],pv:[\"\u2713 Rendu_0001.png\"]}[kind]\n    .map((f,i)=>{ const t=hide(S(\"text\",{x:72,y:104+i*12,class:\"lb good\"},dg)); t.textContent=f; return t });\n  return async()=>{ fadeIn(dim,250); await pop(dg).finished; await T(1100,t=>{ fl.setAttribute(\"width\",176*t); pc.textContent=Math.round(t*100)+\" %\" });\n    st.textContent=\"Termin\u00e9\"; for(let i=0;i<files.length;i++){ pop(files[i]); await wait(120) } };\n};\nFX.commander=({s})=>{ floor(s); const base=CUBE(); const b=box(s,{...base}); scaleBox(b,0,base); hide(b.g);\n  const g=hide(S(\"g\",{},s)); S(\"rect\",{x:70,y:16,width:180,height:22,rx:11,class:\"panel\"},g); S(\"circle\",{cx:84,cy:26.5,r:4,class:\"mgl\"},g); S(\"line\",{x1:87,y1:29.5,x2:90,y2:32.5,class:\"mgl\"},g);\n  const tx=S(\"text\",{x:96,y:30,class:\"rt\"},g); const caret=S(\"line\",{x1:96,y1:21,x2:96,y2:32,class:\"caret\"},g);\n  const res=hide(S(\"g\",{},s)); S(\"rect\",{x:70,y:42,width:180,height:56,rx:8,class:\"panel\"},res); S(\"rect\",{x:74,y:46,width:172,height:15,rx:3,class:\"hlr\"},res);\n  [\"Cube\",\"Cube de volume\",\"Cubes (Cloneur)\"].forEach((t,i)=>{ const e=S(\"text\",{x:82,y:56.5+i*16,class:\"rt\"},res); e.textContent=t });\n  return async()=>{ await pop(g).finished; const w=\"cube\"; for(let i=1;i<=w.length;i++){ tx.textContent=w.slice(0,i); const L=tx.getComputedTextLength(); caret.setAttribute(\"x1\",98+L); caret.setAttribute(\"x2\",98+L); await wait(110) }\n    await fadeIn(res,200).finished; await wait(380); fadeOut(res,200); fadeOut(g,200); fadeIn(b.g,100); await T(550,t=>scaleBox(b,t,base),0,ez.out); b.g.classList.add(\"on\") };\n};\n\n\/* ================== timeline (After Effects, C4D, Maya) ================== *\/\nFX.tl=({s},kind=\"play\")=>{\n  const is3d=kind.startsWith(\"3d\");\n  const comp=S(\"rect\",{x:96,y:8,width:128,height:72,rx:2,class:\"comp\"},s);\n  const cid=uid(); S(\"rect\",{x:96,y:8,width:128,height:72},S(\"clipPath\",{id:cid},S(\"defs\",{},s)));\n  const vw=S(\"g\",{\"clip-path\":`url(#${cid})`},s);\n  let obj=null, cube=null;\n  if(is3d){ const w=world(vw); w.style.transform=setW(.5,160,91,46); floor(w); cube=box(w,CUBE(),\"on\") }\n  else obj=S(\"circle\",{cx:120,cy:44,r:9,class:\"c1\"},vw);\n  const readout=hide(S(\"g\",{class:\"readout\"},s)); S(\"rect\",{x:230,y:14,width:82,height:30,rx:5},readout); const ro1=S(\"text\",{x:236,y:26,class:\"lb\"},readout), ro2=S(\"text\",{x:236,y:37,class:\"lb br\"},readout);\n  S(\"rect\",{x:6,y:86,width:308,height:90,rx:6,class:\"panel\"},s);\n  const tab=S(\"g\",{},s); S(\"rect\",{x:12,y:90,width:52,height:11,rx:3,class:\"tab\"},tab); const tbt=S(\"text\",{x:17,y:98,class:\"lb ink\"},tab); tbt.textContent=is3d?\"Timeline\":\"Comp 1\";\n  const ruler=S(\"g\",{class:\"ruler\"},s); S(\"line\",{x1:92,y1:103,x2:308,y2:103},ruler); for(let x=92;x<=308;x+=12) S(\"line\",{x1:x,y1:x%48===44?99:101,x2:x,y2:103},ruler);\n  [[92,\"0s\"],[188,\"2s\"],[284,\"4s\"]].forEach(([x,t])=>{ const e=S(\"text\",{x:x+2,y:98,class:\"lb\"},ruler); e.textContent=t });\n  const rowsG=S(\"g\",{},s);\n  const Y=i=>108+i*15;\n  const row=(i,name,cls,x0,x1,on)=>{ const g=S(\"g\",{},rowsG); const y=Y(i); g._bg=S(\"rect\",{x:10,y,width:300,height:13,rx:2,class:\"row-bg\"+(on?\" on\":\"\")},g);\n    S(\"rect\",{x:14,y:y+3.5,width:6,height:6,rx:1,class:cls||\"cs\"},g); g._t=S(\"text\",{x:24,y:y+9.5,class:\"rt\"},g); g._t.textContent=name;\n    g._bar=x1>x0?S(\"rect\",{x:x0,y:y+2,width:x1-x0,height:9,rx:2,class:\"tbar \"+(cls||\"cs\")},g):null; return g };\n  const prop=(i,name)=>{ const g=S(\"g\",{},rowsG); const y=Y(i); g._sw=S(\"circle\",{cx:30,cy:y+6.5,r:2.8,class:\"swatch\"},g); const t=S(\"text\",{x:37,y:y+9.5,class:\"rt dim\"},g); t.textContent=name; g._y=y; return g };\n  const kf=(p,x,y,on)=>S(\"path\",{d:`M${x} ${y+2.5} l4 4 l-4 4 l-4 -4 z`,class:\"kf\"+(on?\" on\":\"\")},p);\n  const ph=S(\"g\",{class:\"ph\"},s); S(\"line\",{x1:0,y1:100,x2:0,y2:174},ph); S(\"rect\",{x:-3,y:99,width:6,height:5,rx:1},ph);\n  const setPh=x=>{ ph.style.transform=`translateX(${x}px)` }; let phx=160; setPh(phx);\n  const mapObj=x=>x<=130?110:x<=210?lerp(110,160,(x-130)\/80):x<=270?lerp(160,210,(x-210)\/60):210;\n  const show=(a,b)=>{ ro1.textContent=a; ro2.textContent=b; return pop(readout) };\n  let play=async()=>{};\n  switch(kind){\n    case \"newcomp\":{ hide(comp); hide(vw); hide(tab); hide(ruler); hide(ph); const em=S(\"text\",{x:160,y:136,\"text-anchor\":\"middle\",class:\"lb\"},s); em.textContent=\"Aucune composition ouverte\";\n      play=async()=>{ fadeOut(em,200); await pop(comp).finished; fadeIn(vw,300); pop(tab); fadeIn(ruler,300); fadeIn(ph,300); row(0,\"Calque vide\",\"cs\",92,308,false); show(\"Comp 1\",\"1920 \u00d7 1080 \u00b7 25 i\/s\") }; break }\n    case \"precomp\":{ const r=[row(0,\"Titre\",\"c2\",100,290,true),row(1,\"Forme 1\",\"c1\",120,260,true),row(2,\"Fond\",\"c3\",92,308,true)];\n      play=async()=>{ slide(r[1],0,-15,400); slide(r[2],0,-30,400); fadeOut(r[1],400); await fadeOut(r[2],400).finished;\n        r[0]._t.textContent=\"Pr\u00e9comp 1\"; r[0]._bar.setAttribute(\"class\",\"tbar c4\"); await T(350,t=>{ r[0]._bar.setAttribute(\"x\",lerp(100,92,t)); r[0]._bar.setAttribute(\"width\",lerp(190,216,t)) }); bump(r[0],1.03,260) }; break }\n    case \"split\":{ phx=190; setPh(phx); const r=row(1,\"Forme 1\",\"c1\",100,290,true); row(2,\"Fond\",\"c3\",92,308,false);\n      const p2=hide(S(\"rect\",{x:190,y:Y(1)+2,width:100,height:9,rx:2,class:\"tbar c1\"},rowsG)); const nm=hide(S(\"text\",{x:24,y:Y(0)+9.5,class:\"rt\"},rowsG)); nm.textContent=\"Forme 1\";\n      play=async()=>{ p2.style.opacity=1; r._bar.setAttribute(\"width\",90); await wait(150); await slide(p2,0,-15,400).finished; fadeIn(nm,250) }; break }\n    case \"trimin\": case \"trimout\":{ phx=kind===\"trimin\"?150:250; setPh(phx); const r=row(0,\"Forme 1\",\"c1\",100,290,true); row(1,\"Fond\",\"c3\",92,308,false);\n      play=async()=>{ await T(450,t=>{ if(kind===\"trimin\"){ r._bar.setAttribute(\"x\",lerp(100,150,t)); r._bar.setAttribute(\"width\",lerp(190,140,t)) } else r._bar.setAttribute(\"width\",lerp(190,150,t)) }); bump(r._bar,1.04,240) }; break }\n    case \"prop-P\": case \"prop-S\": case \"prop-R\": case \"prop-T\":{ const L=kind.slice(5); const r0=row(0,\"Forme 1\",\"c1\",92,308,true); const r1=row(1,\"Fond\",\"c3\",92,308,false);\n      const nm={P:[\"Position\",\"960,0 \u00b7 540,0\"],S:[\"\u00c9chelle\",\"100,0 %\"],R:[\"Rotation\",\"0x +0,0\u00b0\"],T:[\"Opacit\u00e9\",\"100 %\"]}[L];\n      const pr=hide(prop(1,nm[0])); const sb=hide(sel(vw,108,32,24,24));\n      play=async()=>{ slide(r1,0,15,350); await fadeIn(pr,300,150).finished; pop(sb); show(nm[0],nm[1]) }; break }\n    case \"showkeys\":{ row(0,\"Forme 1\",\"c1\",92,308,true); const r1=row(1,\"Fond\",\"c3\",92,308,false); const pr=hide(prop(1,\"Position\")); const ks=[130,210,270].map(x=>hide(kf(rowsG,x,Y(1))));\n      play=async()=>{ slide(r1,0,15,350); await fadeIn(pr,300,150).finished; for(const k of ks){ pop(k); await wait(110) } }; break }\n    case \"ease\":{ row(0,\"Forme 1\",\"c1\",92,308,true); prop(1,\"Position\"); const k1=kf(rowsG,130,Y(1)), k2=kf(rowsG,270,Y(1));\n      const hg=x=>hide(S(\"path\",{d:`M${x-4} ${Y(1)+2.5} h8 l-8 9 h8 z`,class:\"kf\"},rowsG)); const h1=hg(130), h2=hg(270); phx=130; setPh(phx); obj.setAttribute(\"cx\",110);\n      play=async()=>{ fadeOut(k1,200); fadeOut(k2,200); pop(h1); await pop(h2,80).finished; show(\"Influence\",\"33,33 %\"); await wait(250);\n        await T(1100,t=>{ const x=lerp(130,270,t); setPh(x); obj.setAttribute(\"cx\",lerp(110,210,t)) },0,ez.io) }; break }\n    case \"solid\": case \"null\": case \"adjust\": case \"dup\":{ const r0=row(0,\"Forme 1\",\"c1\",92,308,kind===\"dup\"); const r1=row(1,\"Fond\",\"c3\",92,308,false);\n      const meta={solid:[\"Solide 1\",\"c3\"],null:[\"Nul 1\",\"cs\"],adjust:[\"Calque d'effets 1\",\"cs\"],dup:[\"Forme 2\",\"c1\"]}[kind];\n      let vis=null; if(kind===\"solid\") vis=hide(S(\"rect\",{x:96,y:8,width:128,height:72,class:\"c3\"},vw));\n      else if(kind===\"null\") vis=hide(S(\"rect\",{x:152,y:36,width:16,height:16,class:\"nul\"},vw));\n      else if(kind===\"adjust\") vis=hide(S(\"rect\",{x:96,y:8,width:128,height:72,fill:\"#7CC4FF\",\"fill-opacity\":.28},vw));\n      else vis=hide(S(\"circle\",{cx:120,cy:44,r:9,class:\"c1\"},vw));\n      play=async()=>{ r0._bg.classList.remove(\"on\"); slide(r0,0,15,350); slide(r1,0,15,350); await wait(200); const n=row(0,meta[0],meta[1],92,308,true); pop(n);\n        if(kind===\"dup\"){ vis.style.opacity=1; await slide(vis,14,8,400).finished } else await fadeIn(vis,400).finished; if(kind===\"adjust\") show(\"Effet\",\"Courbes\") }; break }\n    case \"play\": case \"nextkey\":{ row(0,\"Forme 1\",\"c1\",92,308,true); prop(1,\"Position\"); const ks=[130,210,270].map(x=>kf(rowsG,x,Y(1)));\n      if(kind===\"play\"){ phx=92; setPh(phx); obj.setAttribute(\"cx\",110);\n        play=async()=>{ await T(1500,t=>{ const x=lerp(92,300,t); setPh(x); obj.setAttribute(\"cx\",mapObj(x)); const i=[130,210,270].findIndex(k=>Math.abs(k-x)<3); if(i>=0) bump(ks[i],1.6,200) },0,ez.lin) } }\n      else { phx=150; setPh(phx); obj.setAttribute(\"cx\",mapObj(150));\n        play=async()=>{ for(const [x,i] of [[210,1],[270,2]]){ await wait(250); setPh(x); obj.setAttribute(\"cx\",mapObj(x)); ks[i].classList.add(\"on\"); bump(ks[i],1.6,260) } } }\n      break }\n    case \"addkey\":{ phx=200; setPh(phx); row(0,\"Forme 1\",\"c1\",92,308,true); const pr=prop(1,\"Position\"); const k=hide(kf(rowsG,200,Y(1),true)); obj.setAttribute(\"cx\",160);\n      play=async()=>{ pr._sw.classList.add(\"on\"); bump(pr._sw,1.5,240); await pop(k).finished; show(\"Position\",\"1200,0 \u00b7 540,0\") }; break }\n    case \"3dkey\":{ phx=180; setPh(phx); row(0,soft3d(),\"c1\",92,308,true); const pr=prop(1,\"Position\"); const k=hide(kf(rowsG,180,Y(1),true)); kf(rowsG,110,Y(1));\n      play=async()=>{ pr._sw.classList.add(\"on\"); bump(cube.g,1.06,260); await pop(k).finished; show(\"Cl\u00e9 enregistr\u00e9e\",\"P \u00b7 R \u00b7 \u00c9\") }; break }\n    case \"3dplay\":{ phx=92; setPh(phx); row(0,soft3d(),\"c1\",92,308,true); prop(1,\"Position\"); kf(rowsG,110,Y(1)); kf(rowsG,280,Y(1));\n      play=async()=>{ await T(1500,t=>{ const x=lerp(92,300,t); setPh(x); const u=Math.min(1,Math.max(0,(x-110)\/170)); cube.set({x:-25+lerp(-30,30,u),y:-25-lerp(-30,30,u)*.2,r:u*Math.PI\/2}) },0,ez.lin) }; break }\n  }\n  return play;\n};\nfunction soft3d(){ return \"Cube\" }\n\n\/* ================== InDesign page scenes ================== *\/\nFX.id=({s},kind=\"place\")=>{\n  const board=hide(S(\"rect\",{x:0,y:0,width:320,height:180,class:\"pboard\"},s));\n  const pg=idPage(s); const X=pg.x, Y0=pg.y;\n  const title=(t,y=40,size=11)=>{ const e=S(\"text\",{x:X+12,y,class:\"ptx\",\"font-size\":size},s); e.textContent=t; return e };\n  const bars=(y,n,w=88,gap=8)=>{ const out=[]; for(let i=0;i<n;i++) out.push(S(\"rect\",{x:X+12,y:y+i*gap,width:i%4===3?w*.6:w,height:3,rx:1.5,class:\"pbar\"},s)); return out };\n  const H=hdr(s,\"\");  H.call; const hdrG=s.querySelector(\".hdr\"); hdrG.style.opacity=0;\n  const setH=t=>{ hdrG.style.opacity=1; H(t) };\n  switch(kind){\n    case \"place\":{ title(\"Vos id\u00e9es.\"); bars(108,5); const cur=cursor(s,262,150); const th=S(\"g\",{},s); miniImg(th,270,134,24,15); th.style.transform=\"translate(0,0)\";\n      const fr=S(\"rect\",{x:116,y:50,width:0,height:0,class:\"fedge\"},s); const im=S(\"svg\",{x:116,y:50,width:0,height:0,viewBox:\"0 0 240 150\",preserveAspectRatio:\"xMidYMid slice\"},s); drawLandscape(im);\n      return async()=>{ A(th,[{transform:\"translate(0,0)\"},{transform:\"translate(-146px,-100px)\"}],{duration:600}); await go(cur,116,50,600); click(s,116,50); fadeOut(th,150);\n        await T(650,t=>{ const w=88*t, h=52*t; [fr,im].forEach(e=>{ e.setAttribute(\"width\",w); e.setAttribute(\"height\",h) }); setCur(cur,116+w,50+h) }); setH(\"image.jpg \u00b7 100 %\") } }\n    case \"frame\":{ title(\"Vos id\u00e9es.\"); bars(110,5); const fr=S(\"rect\",{x:116,y:50,width:0,height:0,class:\"fedge\"},s); const x1=S(\"line\",{class:\"fedge\"},s), x2=S(\"line\",{class:\"fedge\"},s); const cur=cursor(s,104,40);\n      return async()=>{ await go(cur,116,50,350); await T(700,t=>{ const w=88*t,h=50*t; fr.setAttribute(\"width\",w); fr.setAttribute(\"height\",h); x1.setAttribute(\"x1\",116); x1.setAttribute(\"y1\",50); x1.setAttribute(\"x2\",116+w); x1.setAttribute(\"y2\",50+h);\n        x2.setAttribute(\"x1\",116+w); x2.setAttribute(\"y1\",50); x2.setAttribute(\"x2\",116); x2.setAttribute(\"y2\",50+h); setCur(cur,116+w,50+h) }); setH(\"Bloc graphique vide\") } }\n    case \"fit\": case \"fill\": case \"fitframe\":{ title(\"Vos id\u00e9es.\"); bars(118,5);\n      const F={x:118,y:46,w:84,h:54}; const cid=uid(); const cr=S(\"rect\",{x:F.x,y:F.y,width:F.w,height:F.h},S(\"clipPath\",{id:cid},S(\"defs\",{},s)));\n      const g=S(\"g\",{\"clip-path\":`url(#${cid})`},s); const im=S(\"svg\",{viewBox:\"0 0 240 150\",preserveAspectRatio:\"none\"},g); drawLandscape(im);\n      const fe=S(\"rect\",{x:F.x,y:F.y,width:F.w,height:F.h,class:\"fedge\"},s);\n      const set=(e,x,y,w,h)=>{ e.setAttribute(\"x\",x); e.setAttribute(\"y\",y); e.setAttribute(\"width\",w); e.setAttribute(\"height\",h) };\n      let from,to; if(kind===\"fit\"){ from=[98,30,150,93.75]; to=[118,46+(54-52.5)\/2,84,52.5] } else if(kind===\"fill\"){ from=[124,58,48,30]; to=[118-1.2,46,86.4,54] } else { from=[118,46,104,65]; to=from }\n      set(im,...from); if(kind===\"fitframe\"){ set(cr,118,46,70,42); set(fe,118,46,70,42) }\n      return async()=>{ await wait(120); if(kind===\"fitframe\"){ await T(600,t=>{ const w=lerp(70,104,t), h=lerp(42,65,t); set(cr,118,46,w,h); set(fe,118,46,w,h) }) }\n        else await T(650,t=>set(im,...from.map((v,i)=>lerp(v,to[i],t)))); bump(fe,1.03,240); setH(kind===\"fit\"?\"Contenu ajust\u00e9\":kind===\"fill\"?\"Bloc rempli\":\"Bloc ajust\u00e9 au contenu\") } }\n    case \"preview\":{ title(\"Vos id\u00e9es.\"); bars(110,5); const ig=miniImg(s,116,48,88,52); const e1=S(\"rect\",{x:116,y:48,width:88,height:52,class:\"fedge\"},s), e2=S(\"rect\",{x:114,y:30,width:92,height:13,class:\"fedge\"},s), e3=S(\"rect\",{x:114,y:106,width:92,height:38,class:\"fedge\"},s);\n      setH(\"Mode : Normal\"); return async()=>{ [pg.guides,e1,e2,e3].forEach(e=>fadeOut(e,400)); await fadeIn(board,500).finished; setH(\"Mode : Aper\u00e7u\") } }\n    case \"hidden\":{ title(\"Vos id\u00e9es.\");\n      const L=[\"Une autre dimension pour\",\"vos projets, du concept\",\"\u00e0 l'image finale.\"]; const T0=L.map((t,i)=>{ const e=S(\"text\",{x:X+12,y:62+i*10,class:\"ptxt\"},s); e.textContent=t; return e });\n      const marks=[]; T0.forEach((e,i)=>{ const str=e.textContent; try{ for(let k=0;k<str.length;k++){ if(str[k]===\" \"){ const x=X+12+e.getSubStringLength(0,k)+e.getSubStringLength(k,1)\/2; const m=hide(S(\"text\",{x,y:62+i*10,\"text-anchor\":\"middle\",class:\"hid\"},s)); m.textContent=\"\u00b7\"; marks.push(m) } }\n        const end=hide(S(\"text\",{x:X+13+e.getComputedTextLength(),y:62+i*10,class:\"hid\"},s)); end.textContent=i===2?\"#\":\"\u00b6\"; marks.push(end) }catch(err){} });\n      return async()=>{ for(const m of marks){ pop(m); await wait(40) } setH(\"Caract\u00e8res masqu\u00e9s : on\") } }\n    case \"wrap\":{ title(\"Vos id\u00e9es.\",34); const bs=[]; for(let i=0;i<12;i++) bs.push(S(\"rect\",{x:X+12,y:46+i*9,width:88,height:3,rx:1.5,class:\"pbar\"},s));\n      miniImg(s,152,64,50,38); S(\"rect\",{x:152,y:64,width:50,height:38,class:\"fedge\"},s); const wb=hide(S(\"rect\",{x:146,y:58,width:62,height:50,class:\"fedge\",\"stroke-dasharray\":\"2 2\"},s));\n      return async()=>{ await fadeIn(wb,250).finished; await T(600,t=>bs.forEach(b=>{ const y=+b.getAttribute(\"y\"); if(y>=54&&y<=110) b.setAttribute(\"width\",lerp(88,28,t)) })); setH(\"Habillage : cadre de s\u00e9lection\") } }\n    case \"pagenum\":{ title(\"Vos id\u00e9es.\"); bars(56,8); const fb=S(\"rect\",{x:X+44,y:Y0+136,width:24,height:11,class:\"fedge\"},s);\n      const a=hide(S(\"text\",{x:X+56,y:Y0+144.5,\"text-anchor\":\"middle\",class:\"ptx\",\"font-size\":8},s)); a.textContent=\"A\"; const n=hide(S(\"text\",{x:X+56,y:Y0+144.5,\"text-anchor\":\"middle\",class:\"ptx\",\"font-size\":8},s)); n.textContent=\"12\";\n      const p=hide(tpill(s,\"Gabarit A\"));\n      return async()=>{ fadeIn(p,200); await pop(a).finished; await wait(500); p.set(\"Page 12\"); fadeOut(a,250); await pop(n).finished } }\n    case \"edges\":{ title(\"Vos id\u00e9es.\"); bars(110,5); miniImg(s,116,48,88,52); const E=[S(\"rect\",{x:116,y:48,width:88,height:52,class:\"fedge\"},s),S(\"rect\",{x:114,y:30,width:92,height:13,class:\"fedge\"},s),S(\"rect\",{x:114,y:106,width:92,height:38,class:\"fedge\"},s)];\n      return async()=>{ E.forEach((e,i)=>fadeOut(e,350,i*90)); await wait(500); setH(\"Bords de bloc : masqu\u00e9s\") } }\n    case \"track\":{ const t=S(\"text\",{x:X+56,y:70,\"text-anchor\":\"middle\",class:\"ptx\",\"font-size\":12},s); t.textContent=\"DIMENSION\"; bars(86,6);\n      const hl=S(\"rect\",{class:\"tsel\"},s); s.insertBefore(hl,t); const fit=()=>{ try{ const b=t.getBBox(); hl.setAttribute(\"x\",b.x-1); hl.setAttribute(\"y\",b.y); hl.setAttribute(\"width\",b.width+2); hl.setAttribute(\"height\",b.height) }catch(e){} }; fit();\n      const p=tpill(s,\"Approche 0\");\n      return async()=>{ for(let k=1;k<=5;k++){ await T(130,u=>{ t.setAttribute(\"letter-spacing\",((k-1+u)*.7).toFixed(2)); fit() },0,ez.out); p.set(`Approche ${k*20}`); await wait(90) } } }\n  }\n  return async()=>{};\n};\n\n\/* ================== ZBrush ================== *\/\nFX.zb=({s},kind=\"size\")=>{\n  const w=world(s); w.style.transform=setW(1,160,86,86);\n  const cid=uid(); S(\"circle\",{cx:160,cy:86,r:56},S(\"clipPath\",{id:cid},S(\"defs\",{},w)));\n  const sp=sphere(w,160,86,56,\"wax\"); const layer=S(\"g\",{\"clip-path\":`url(#${cid})`},w);\n  const H=hdr(s,\"\");\n  const ell=(n,seg)=>{ const g=S(\"g\",{},layer); for(let i=1;i<n;i++){ const k=Math.cos(Math.PI*i\/n); const pts=[]; for(let j=0;j<=seg;j++){ const a=j\/seg*2*Math.PI; pts.push([160+56*Math.cos(a), 86+56*k+Math.sin(a)*56*Math.sqrt(1-k*k)*.18]) } S(\"polyline\",{points:P(pts),class:\"zw\"},g) }\n    for(let i=1;i<n;i++){ const k=Math.cos(Math.PI*i\/n); const pts=[]; for(let j=0;j<=seg;j++){ const a=j\/seg*2*Math.PI; pts.push([160+56*k+Math.sin(a)*56*Math.sqrt(1-k*k)*.18, 86+56*Math.cos(a)]) } S(\"polyline\",{points:P(pts),class:\"zw\"},g) } return g };\n  const outer=S(\"circle\",{cx:184,cy:66,r:14,class:\"zbo\"},s), inner=S(\"circle\",{cx:184,cy:66,r:7,class:\"zbi\"},s);\n  const follow=(path,pre)=>{ const L=path.getTotalLength(); return t=>{ const q=path.getPointAtLength(L*t); [outer,inner].forEach(c=>{ c.setAttribute(\"cx\",q.x); c.setAttribute(\"cy\",q.y) }) } };\n  const stroke=(d,color,width)=>{ const sh=S(\"path\",{d,class:\"zst\",stroke:\"#3A120B\",\"stroke-opacity\":.5,\"stroke-width\":width+2,transform:\"translate(0 2)\",pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},layer);\n    const st=S(\"path\",{d,class:\"zst\",stroke:color,\"stroke-width\":width,pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},layer); return [sh,st] };\n  const draw=async(paths,d)=>{ const f=follow(paths[1]); await T(900,t=>{ paths.forEach(p=>p.setAttribute(\"stroke-dashoffset\",1-t)); f(t) }) };\n  switch(kind){\n    case \"size\": case \"focal\":{ H(kind===\"size\"?\"Draw Size 64\":\"Focal Shift 0\");\n      return async()=>{ await T(700,t=>{ if(kind===\"size\"){ outer.setAttribute(\"r\",lerp(14,26,t)); inner.setAttribute(\"r\",lerp(7,13,t)); H(`Draw Size ${Math.round(lerp(64,120,t))}`) } else { inner.setAttribute(\"r\",lerp(7,2.5,t)); H(`Focal Shift ${Math.round(lerp(0,-60,t))}`) } }) } }\n    case \"zint\": case \"rgb\":{ const d=\"M118 76 C 140 56, 178 54, 204 72\"; H(kind===\"zint\"?\"Z Intensity 25\":\"Rgb Intensity 0\");\n      const ps=kind===\"zint\"?stroke(d,\"#F7D2BE\",6):[(()=>{const e=S(\"path\",{d,class:\"zst\",\"stroke-width\":9,\"stroke-opacity\":.85,pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},layer); e.style.stroke=\"var(--s1)\"; return e})()];\n      return async()=>{ await T(450,t=>H(kind===\"zint\"?`Z Intensity ${Math.round(lerp(25,70,t))}`:`Rgb Intensity ${Math.round(lerp(0,80,t))}`)); await draw(ps.length>1?ps:[ps[0],ps[0]]) } }\n    case \"divide\": case \"subup\": case \"subdown\":{ const low=S(\"polygon\",{points:P([...Array(12)].map((_,i)=>[160+56*Math.cos(i\/12*2*Math.PI),86+56*Math.sin(i\/12*2*Math.PI)])),fill:sp.circ.getAttribute(\"fill\")},w);\n      w.insertBefore(low,layer); const wl=ell(3,12), wh=ell(6,40); const down=kind===\"subdown\";\n      if(down){ hide(low); hide(wl) } else { hide(wh) } sp.circ.style.opacity=down?1:0;\n      const lab={divide:[\"SDiv 1 \u00b7 1 536 polys\",\"SDiv 2 \u00b7 6 144 polys\"],subup:[\"SDiv 2 \u00b7 6 144 polys\",\"SDiv 3 \u00b7 24 576 polys\"],subdown:[\"SDiv 3 \u00b7 24 576 polys\",\"SDiv 2 \u00b7 6 144 polys\"]}[kind]; H(lab[0]);\n      return async()=>{ if(down){ fadeIn(low,400); fadeIn(wl,400); fadeOut(wh,400); await fadeOut(sp.circ,400).finished } else { fadeOut(low,400); fadeOut(wl,400); fadeIn(wh,400); await fadeIn(sp.circ,400).finished } H(lab[1]) } }\n    case \"polyframe\":{ const wh=hide(ell(6,40)); const pg=hide(S(\"rect\",{x:100,y:20,width:60,height:140,fill:\"#2FD3C0\",\"fill-opacity\":.32},layer)); H(\"PolyF : off\");\n      return async()=>{ fadeIn(pg,400); await fadeIn(wh,400).finished; H(\"PolyF : on\") } }\n    case \"sym\":{ const ax=S(\"line\",{x1:160,y1:20,x2:160,y2:152,class:\"link\"},s); const o2=S(\"circle\",{cx:136,cy:66,r:14,class:\"zbo\"},s), i2=S(\"circle\",{cx:136,cy:66,r:7,class:\"zbi\"},s);\n      outer.setAttribute(\"cx\",184); hide(o2); hide(i2); H(\"Sym\u00e9trie : off\");\n      const L=stroke(\"M200 100 C 196 82, 186 68, 172 60\",\"#F7D2BE\",6), R=stroke(\"M120 100 C 124 82, 134 68, 148 60\",\"#F7D2BE\",6);\n      return async()=>{ H(\"Sym\u00e9trie : X\"); fadeIn(o2,200); await fadeIn(i2,200).finished; const fL=follow(L[1]), fR=follow(R[1]);\n        await T(900,t=>{ [...L,...R].forEach(p=>p.setAttribute(\"stroke-dashoffset\",1-t)); fL(t); const q=R[1].getPointAtLength(R[1].getTotalLength()*t); o2.setAttribute(\"cx\",q.x); o2.setAttribute(\"cy\",q.y); i2.setAttribute(\"cx\",q.x); i2.setAttribute(\"cy\",q.y) }) } }\n    case \"edit\":{ const p=tpill(s,\"Edit : off\"); const wh=hide(ell(5,40));\n      return async()=>{ p.set(\"Edit : on\"); bump(p,1.1,260); fadeIn(wh,300); await T(1000,t=>{ sp.grad.setAttribute(\"cx\",lerp(38,64,t)+\"%\"); wh.style.transform=`translateX(${lerp(0,10,t)}px)` }) } }\n    case \"invmask\": case \"groupmask\":{ const m1=S(\"rect\",{x:100,y:20,width:60,height:140,class:\"zmask\"},layer), m2=hide(S(\"rect\",{x:160,y:20,width:60,height:140,class:\"zmask\"},layer));\n      H(\"Masque actif\");\n      if(kind===\"invmask\") return async()=>{ fadeOut(m1,400); await fadeIn(m2,400).finished; H(\"Masque invers\u00e9\") };\n      const g1=hide(S(\"rect\",{x:100,y:20,width:60,height:140,fill:\"#2FD3C0\",\"fill-opacity\":.45},layer)), g2=hide(S(\"rect\",{x:160,y:20,width:60,height:140,fill:\"#FF6F9F\",\"fill-opacity\":.4},layer));\n      return async()=>{ fadeOut(m1,400); fadeIn(g1,400); await fadeIn(g2,400,100).finished; H(\"2 polygroupes \u00b7 masque effac\u00e9\") } }\n    case \"frame\":{ const a=setW(.42,160,86,124,252), b=setW(1,160,86,86); w.style.transform=a; hide(outer); hide(inner); H(\"Cadrage\");\n      return async()=>{ await A(w,[{transform:a},{transform:b}],{duration:800,easing:\"cubic-bezier(.6,0,.2,1)\"}).finished } }\n    case \"clear\":{ hide(outer); hide(inner); H(\"Calque : 1 outil\");\n      return async()=>{ await A(w,[{opacity:1,transform:setW(1,160,86,86)},{opacity:0,transform:setW(.85,160,86,86)}],{duration:500}).finished; H(\"Calque vid\u00e9\") } }\n    case \"move\":{ hide(outer); hide(inner); const gz=hide(S(\"g\",{},w)); S(\"line\",{x1:160,y1:86,x2:226,y2:86,class:\"link\"},gz); S(\"circle\",{cx:160,cy:86,r:5,class:\"ring2\"},gz); S(\"circle\",{cx:226,cy:86,r:4,class:\"ring2\"},gz); H(\"Gizmo : off\");\n      return async()=>{ await pop(gz).finished; H(\"Gizmo : d\u00e9placement\"); await A(w,[{transform:setW(1,160,86,86)},{transform:setW(1,160,86,86,196)}],{duration:700}).finished } }\n  }\n  return async()=>{};\n};\n\n\/* ================== Substance 3D Painter ================== *\/\nFX.sp=({s},kind=\"paint\")=>{\n  const w=world(s); w.style.transform=setW(1,160,86,86);\n  const cid=uid(); S(\"circle\",{cx:160,cy:86,r:54},S(\"clipPath\",{id:cid},S(\"defs\",{},w)));\n  const sp=sphere(w,160,86,54,\"metal\"); const layer=S(\"g\",{\"clip-path\":`url(#${cid})`},w);\n  const H=hdr(s,\"\");\n  const brush=S(\"circle\",{cx:96,cy:60,r:9,class:\"brushc\"},s);\n  const follow=(path)=>{ const L=path.getTotalLength(); return t=>{ const q=path.getPointAtLength(L*t); brush.setAttribute(\"cx\",q.x); brush.setAttribute(\"cy\",q.y) } };\n  const paint=(d,wd=11,cls=\"spaint\")=>S(\"path\",{d,class:cls,\"stroke-width\":wd,pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},layer);\n  const D=\"M112 72 C 136 50, 176 116, 210 86\";\n  const swap=(mat,lab0,lab1)=>{ const g=hide(sphere(w,160,86,54,mat)); w.insertBefore(g,layer); H(lab0); return async()=>{ await fadeIn(g,550).finished; H(lab1) } };\n  const uv=()=>{ const g=S(\"g\",{},s); S(\"rect\",{x:166,y:14,width:146,height:152,rx:4,class:\"uvpane\"},g);\n    [[176,24,58,40],[242,24,60,26],[242,56,30,40],[278,56,24,40],[176,72,42,60],[224,104,78,54],[176,138,42,20]].forEach(([x,y,ww,hh])=>S(\"rect\",{x,y,width:ww,height:hh,rx:3,class:\"uvi\"},g)); return g };\n  switch(kind){\n    case \"paint\":{ H(\"Pinceau \u00b7 1\"); const p=paint(D); return async()=>{ const f=follow(p); await T(1000,t=>{ p.setAttribute(\"stroke-dashoffset\",1-t); f(t) }) } }\n    case \"erase\":{ H(\"Gomme \u00b7 2\"); const mid=uid(); const mk=S(\"mask\",{id:mid},S(\"defs\",{},w)); S(\"rect\",{x:0,y:0,width:320,height:180,fill:\"#fff\"},mk);\n      const er=S(\"path\",{d:\"M108 98 C 140 66, 172 70, 214 60\",stroke:\"#000\",\"stroke-width\":18,fill:\"none\",\"stroke-linecap\":\"round\",pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},mk);\n      const p=S(\"path\",{d:D,class:\"spaint\",\"stroke-width\":13,mask:`url(#${mid})`},layer); const guide=S(\"path\",{d:\"M108 98 C 140 66, 172 70, 214 60\",fill:\"none\"},s); brush.setAttribute(\"r\",11);\n      return async()=>{ const f=follow(guide); await T(1000,t=>{ er.setAttribute(\"stroke-dashoffset\",1-t); f(t) }) } }\n    case \"project\":{ H(\"Projection \u00b7 3\"); hide(brush); const st=S(\"g\",{},s); S(\"rect\",{x:116,y:58,width:88,height:52,class:\"link\"},st); const t1=S(\"text\",{x:160,y:93,\"text-anchor\":\"middle\",class:\"tx\",\"font-size\":22,fill:\"#FF6F9F\",\"fill-opacity\":.45},st); t1.textContent=\"OK3D\"; t1.style.fill=\"var(--s4)\";\n      const pr=hide(S(\"text\",{x:160,y:93,\"text-anchor\":\"middle\",class:\"tx\",\"font-size\":22},layer)); pr.textContent=\"OK3D\"; pr.style.fill=\"var(--s4)\";\n      return async()=>{ click(s,160,84); await wait(200); fadeOut(st,400); await fadeIn(pr,500).finished; bump(pr,1.05,260) } }\n    case \"polyfill\":{ H(\"Remplissage polygone \u00b7 4\"); hide(brush); const pt=(la,lo)=>[160+54*Math.cos(la)*Math.sin(lo),86-54*Math.sin(la)]; const R=Math.PI\/180, cells={};\n      for(let a=-60;a<60;a+=30) for(let o=-75;o<75;o+=25){ const q=[pt(a*R,o*R),pt(a*R,(o+25)*R),pt((a+30)*R,(o+25)*R),pt((a+30)*R,o*R)]; cells[a+\",\"+o]=S(\"polygon\",{points:P(q),class:\"pcell\"},layer) }\n      const cur=cursor(s,250,140); const picks=[\"0,-25\",\"0,0\",\"-30,0\",\"-30,-25\",\"0,25\"];\n      return async()=>{ for(const k of picks){ const c=cells[k]; const bb=c.getBBox(); await go(cur,bb.x+bb.width\/2,bb.y+bb.height\/2,200); click(s,bb.x+bb.width\/2,bb.y+bb.height\/2); c.setAttribute(\"class\",\"pcell on\") } } }\n    case \"smudge\":{ H(\"Estompe \u00b7 5\"); const fid=uid(); const fl=S(\"filter\",{id:fid,x:\"-30%\",y:\"-60%\",width:\"160%\",height:\"220%\"},S(\"defs\",{},w)); S(\"feGaussianBlur\",{stdDeviation:3.5},fl);\n      S(\"path\",{d:\"M126 96 C 136 80, 150 76, 164 80\",class:\"spaint\",\"stroke-width\":12},layer); const sm=S(\"path\",{d:\"M160 80 C 176 84, 192 94, 206 104\",class:\"spaint\",\"stroke-width\":12,filter:`url(#${fid})`,pathLength:1,\"stroke-dasharray\":1,\"stroke-dashoffset\":1},layer);\n      sm.style.strokeOpacity=.75; brush.setAttribute(\"cx\",160); brush.setAttribute(\"cy\",80);\n      return async()=>{ const f=follow(sm); await T(900,t=>{ sm.setAttribute(\"stroke-dashoffset\",1-t); f(t) }) } }\n    case \"clone\":{ H(\"Clonage \u00b7 6\"); S(\"circle\",{cx:140,cy:68,r:10,class:\"c4\"},layer); const src=S(\"g\",{},s); S(\"line\",{x1:132,y1:68,x2:148,y2:68,class:\"link\"},src); S(\"line\",{x1:140,y1:60,x2:140,y2:76,class:\"link\"},src);\n      const ccid=uid(); const cc=S(\"circle\",{cx:188,cy:104,r:0},S(\"clipPath\",{id:ccid},S(\"defs\",{},w))); const g=S(\"g\",{\"clip-path\":`url(#${ccid})`},layer); S(\"circle\",{cx:188,cy:104,r:10,class:\"c4\"},g);\n      return async()=>{ await T(400,t=>{ brush.setAttribute(\"cx\",lerp(96,188,t)); brush.setAttribute(\"cy\",lerp(60,104,t)) }); await T(600,t=>cc.setAttribute(\"r\",14*t)) } }\n    case \"picker\":{ hide(brush); const gold=sphere(s,268,90,24,\"gold\"); const lb=S(\"text\",{x:268,y:128,\"text-anchor\":\"middle\",class:\"lb\"},s); lb.textContent=\"Or bross\u00e9\";\n      const run=swap(\"gold\",\"Mat\u00e9riau : Acier\",\"Mat\u00e9riau : Or bross\u00e9\"); const cur=cursor(s,210,150);\n      return async()=>{ await go(cur,268,90,500); click(s,268,90); await wait(150); await run() } }\n    case \"split\": case \"3d\": case \"2d\":{ hide(brush); const u=uv(); const full=setW(1,160,86,86), left=setW(.82,160,86,90,84);\n      if(kind===\"split\"){ hide(u); H(\"Vue 3D\") ; return async()=>{ A(w,[{transform:full},{transform:left}],{duration:600}); await A(u,[{opacity:0,transform:\"translateX(30px)\"},{opacity:1,transform:\"translateX(0)\"}],{duration:500,delay:150}).finished; H(\"Vues 3D + 2D\") } }\n      if(kind===\"3d\"){ w.style.transform=left; H(\"Vues 3D + 2D\"); return async()=>{ A(u,[{opacity:1,transform:\"translateX(0)\"},{opacity:0,transform:\"translateX(30px)\"}],{duration:400}); await A(w,[{transform:left},{transform:full}],{duration:600,delay:120}).finished; H(\"Vue 3D\") } }\n      hide(u); H(\"Vue 3D\"); return async()=>{ fadeOut(w,400); await A(u,[{opacity:0,transform:\"translateX(-79px) scale(.9)\"},{opacity:1,transform:\"translateX(-79px) scale(1)\"}],{duration:500,delay:200}).finished; H(\"Vue 2D \u00b7 UV\") } }\n    case \"channel\":{ hide(brush); return swap(\"gray\",\"Base Color\",\"Roughness\") }\n    case \"material\":{ hide(brush); const g=sphere(w,160,86,54,\"gray\"); w.insertBefore(g,layer); H(\"Roughness\"); return async()=>{ await fadeOut(g,550).finished; H(\"Material\") } }\n    case \"meshmap\":{ hide(brush); return swap(\"normal\",\"Material\",\"Normal\") }\n    case \"bake\":{ hide(brush); w.style.transform=setW(.8,160,86,90,96); H(\"Cuisson : pr\u00eat\");\n      const pg=S(\"g\",{},s); S(\"rect\",{x:196,y:14,width:116,height:152,rx:6,class:\"panel\"},pg); const tt=S(\"text\",{x:204,y:28,class:\"lb ink\"},pg); tt.textContent=\"Cartes de maillage\";\n      const maps=[[\"Normal\",\"normal\"],[\"Occlusion\",\"ao\"],[\"Courbure\",\"gray\"],[\"ID\",\"idm\"]].map(([n,m],i)=>{ const y=38+i*31; sphere(pg,212,y+12,10,m); const t=S(\"text\",{x:228,y:y+10,class:\"rt\"},pg); t.textContent=n;\n        S(\"rect\",{x:228,y:y+15,width:76,height:3,rx:1.5,class:\"track\"},pg); return S(\"rect\",{x:228,y:y+15,width:0,height:3,rx:1.5,class:\"fillb\"},pg) });\n      return async()=>{ for(let i=0;i<maps.length;i++){ await T(320,t=>maps[i].setAttribute(\"width\",76*t)); H(`Cuisson ${Math.round((i+1)\/maps.length*100)} %`) } } }\n  }\n  return async()=>{};\n};\n\n\n\/* =====================================================================\n   APP \u2014 \u00e9tat, stockage, vues\n   ===================================================================== *\/\n\nconst esc=s=>String(s).replace(\/[&<>\"]\/g,c=>({\"&\":\"&amp;\",\"<\":\"&lt;\",\">\":\"&gt;\",'\"':\"&quot;\"}[c]));\nconst normalize=s=>String(s).normalize(\"NFD\").replace(\/[\\u0300-\\u036f]\/g,\"\").toLowerCase();\nconst fmt=n=>Math.round(n).toLocaleString(\"fr-FR\");\nconst comboId=k=>k.join(\"+\");\nconst shuffle=a=>{ a=a.slice(); for(let i=a.length-1;i>0;i--){ const j=Math.random()*(i+1)|0; [a[i],a[j]]=[a[j],a[i]] } return a };\nconst TOTAL=Object.values(DATA).reduce((n,a)=>n+a.items.length,0);\n\nlet PS={};\ntry { const parsed=JSON.parse(localStorage.getItem(CONFIG.storageKey)); if(parsed && typeof parsed==='object' && !Array.isArray(parsed)) PS=parsed; } catch(e){storageAvailable=false}\nconst U={os:['mac','win'].includes(PS.os)?PS.os:null,sound:PS.sound===true,rev:typeof PS.rev==='boolean'?PS.rev:true,xp:Number.isFinite(PS.xp)&&PS.xp>=0?PS.xp:0,apps:{},lastApp:Object.prototype.hasOwnProperty.call(DATA,PS.lastApp)?PS.lastApp:null};\nfor(const id of Object.keys(DATA)){\n  const p=PS.apps&&typeof PS.apps==='object'?PS.apps[id]:null;\n  const ap=U.apps[id]={best:p&&Number.isFinite(p.best)&&p.best>=0?p.best:0,cert:!!(p&&p.cert===true),known:{}};\n  for(const it of DATA[id].items){const n=p&&p.known?p.known[it[0]]:0;if(Number.isFinite(n)&&n>=0)ap.known[it[0]]=n}\n}\nif(!U.os) U.os=\/Mac|iPhone|iPad\/.test(navigator.platform||navigator.userAgent)?'mac':'win';\nconst save=()=>{try{localStorage.setItem(CONFIG.storageKey,JSON.stringify(U))}catch(e){storageAvailable=false}};\nconst AP=id=>U.apps[id];\nconst knownCount=id=>DATA[id].items.filter(it=>(AP(id).known[it[0]]||0)>=2).length;\nconst level=xp=>{ const l=Math.floor(Math.sqrt(xp\/400))+1; const a=(l-1)**2*400, b=l**2*400; return {l,p:Math.round((xp-a)\/(b-a)*100)} };\n\nconst MAC={Ctrl:\"\u2318\",Alt:\"\u2325\",Shift:\"\u21e7\"};\nconst macStyle=soft=>U.os===\"mac\"&&DATA[soft].mac;\nconst cap=(k,soft)=>macStyle(soft)&&MAC[k]?MAC[k]:k;\nconst keysHTML=(k,soft)=>`<span class=\"keys\">${k.map(x=>`<kbd>${esc(cap(x,soft))}<\/kbd>`).join(macStyle(soft)?\"\":'<span class=\"plus\">+<\/span>')}<\/span>`;\nconst keysText=(k,soft)=>k.map(x=>cap(x,soft)).join(macStyle(soft)?\"\":\" + \");\n\n\/* ---------- sound (WebAudio, off by default) ---------- *\/\nconst Snd={ctx:null,\n  ok(){ if(!U.sound) return false; try{ this.ctx=this.ctx||new (window.AudioContext||window.webkitAudioContext)(); if(this.ctx.state===\"suspended\") this.ctx.resume(); return true }catch(e){ return false } },\n  tone(f,d,type=\"sine\",g=.05,dl=0){ if(!this.ok()) return; const c=this.ctx,t=c.currentTime+dl,o=c.createOscillator(),v=c.createGain(); o.type=type; o.frequency.setValueAtTime(f,t); v.gain.setValueAtTime(0,t); v.gain.linearRampToValueAtTime(g,t+.01); v.gain.exponentialRampToValueAtTime(.0001,t+d); o.connect(v).connect(c.destination); o.start(t); o.stop(t+d+.02) },\n  key(){ if(!this.ok()) return; const c=this.ctx,t=c.currentTime,b=c.createBuffer(1,c.sampleRate*.03,c.sampleRate),d=b.getChannelData(0); for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i\/d.length,4);\n    const s=c.createBufferSource(),f=c.createBiquadFilter(),v=c.createGain(); f.type=\"bandpass\"; f.frequency.value=2400; v.gain.value=.22; s.buffer=b; s.connect(f).connect(v).connect(c.destination); s.start(t) },\n  good(){ this.tone(660,.14,\"sine\",.05); this.tone(990,.22,\"sine\",.045,.08) },\n  bad(){ this.tone(170,.26,\"triangle\",.06); this.tone(130,.3,\"triangle\",.04,.06) },\n  fanfare(){ [523,659,784,1047].forEach((f,i)=>this.tone(f,.3,\"sine\",.05,i*.11)) }\n};\n\n\/* ---------- particles & floating text ---------- *\/\nfunction burst(el,n=26){ if(RM||!el) return; const host=$(\"#fxl\"), hr=$(\"#okk\").getBoundingClientRect(), r=el.getBoundingClientRect();\n  const cx=r.left+r.width\/2-hr.left, cy=r.top+r.height\/2-hr.top, cols=[\"#7CC4FF\",\"#2E6BFF\",\"#35E0A1\",\"#FFFFFF\"];\n  for(let i=0;i<n;i++){ const d=document.createElement(\"i\"); const a=Math.random()*Math.PI*2, dist=40+Math.random()*100, sz=2+Math.random()*5;\n    d.style.cssText=`left:${cx}px;top:${cy}px;width:${sz}px;height:${sz}px;background:${cols[i%4]};box-shadow:0 0 8px ${cols[i%4]}`; host.appendChild(d);\n    d.animate([{transform:\"translate(-50%,-50%) scale(1)\",opacity:1},{transform:`translate(calc(-50% + ${Math.cos(a)*dist}px),calc(-50% + ${Math.sin(a)*dist}px)) scale(.2)`,opacity:0}],{duration:650+Math.random()*450,easing:\"cubic-bezier(.2,.8,.2,1)\"}).onfinish=()=>d.remove() } }\nfunction floatText(el,txt,color){ if(!el) return; const host=$(\"#fxl\"), hr=$(\"#okk\").getBoundingClientRect(), r=el.getBoundingClientRect(); const b=document.createElement(\"b\");\n  b.textContent=txt; if(color) b.style.color=color; b.style.left=(r.left+r.width\/2-hr.left)+\"px\"; b.style.top=(r.top+r.height\/2-hr.top)+\"px\"; host.appendChild(b);\n  b.animate([{opacity:0,transform:\"translate(-50%,-30%) scale(.8)\"},{opacity:1,transform:\"translate(-50%,-90%) scale(1.05)\",offset:.25},{opacity:0,transform:\"translate(-50%,-220%) scale(1)\"}],{duration:RM?1:1100,easing:\"cubic-bezier(.2,.8,.2,1)\"}).onfinish=()=>b.remove() }\nfunction countTo(el,from,to,d=600){ if(!el) return; const t0=performance.now(); const step=n=>{ const t=RM?1:Math.min(1,(n-t0)\/d); el.textContent=fmt(from+(to-from)*ez.out(t)); if(t<1) requestAnimationFrame(step) }; requestAnimationFrame(step) }\n\n\/* ---------- stage ---------- *\/\nlet play=async()=>{};\nfunction stageHTML(soft,extra=\"\"){ return `<div class=\"stage\" id=\"stage\"><span class=\"crn c-tl\"><\/span><span class=\"crn c-tr\"><\/span><span class=\"crn c-bl\"><\/span><span class=\"crn c-br\"><\/span>\n  <span class=\"vp\" id=\"vp\">${esc(DATA[soft].vp)}<\/span><button class=\"rep\" id=\"rep\" hidden>\u21bb Rejouer<\/button><div class=\"q\">?<\/div>${extra}<\/div>` }\nfunction buildStage(st,fx,soft){\n  GEN++; st.querySelectorAll(\":scope>svg\").forEach(e=>e.remove());\n  const s=S(\"svg\",{viewBox:\"0 0 320 180\",\"aria-hidden\":\"true\"}); st.insertBefore(s,st.firstChild);\n  const ctx={s,soft,layer(){ const l=S(\"svg\",{viewBox:\"0 0 320 180\"}); st.insertBefore(l,s); return l }};\n  const [name,arg]=fx.split(\":\");\n  try{ play=(FX[name]||(()=>async()=>{}))(ctx,arg) }catch(e){ console.error(fx,e); play=async()=>{} }\n}\n\n\/* ---------- header \/ footer ---------- *\/\nfunction header(){\n  const lv=level(U.xp);\n  $(\"#bar\").innerHTML=`\n    <button class=\"appmark\" id=\"home-link\" aria-label=\"Retour \u00e0 l'accueil du quiz Keystroke\" title=\"Accueil Keystroke\">\n      <span class=\"appmark-icon\" aria-hidden=\"true\">\u2318<\/span>\n      <span class=\"appmark-text\"><strong>KEYSTROKE<\/strong><small>LAB \/ BY OK3D<\/small><\/span>\n    <\/button>\n    <div class=\"tools\">\n      <button type=\"button\" class=\"screen-btn\" data-okk-fullscreen aria-pressed=\"false\">\u26f6 Plein \u00e9cran<\/button>\n      <div class=\"lvl\" title=\"${fmt(U.xp)} XP au total\"><span class=\"ring\" style=\"--p:${lv.p}\"><span>${lv.l}<\/span><\/span><small>${fmt(U.xp)} XP<\/small><\/div>\n      <button class=\"icon-btn\" id=\"snd\" aria-pressed=\"${U.sound}\" aria-label=\"Son ${U.sound?\"activ\u00e9\":\"coup\u00e9\"}\">${U.sound\n        ?'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M4 9v6h4l5 4V5L8 9H4z\"\/><path d=\"M16.5 8.5a5 5 0 0 1 0 7M19 6a8.5 8.5 0 0 1 0 12\"\/><\/svg>'\n        :'<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\"><path d=\"M4 9v6h4l5 4V5L8 9H4z\"\/><path d=\"M17 9l5 6M22 9l-5 6\"\/><\/svg>'}<\/button>\n      <div class=\"seg\" role=\"group\" aria-label=\"Clavier\"><button data-os=\"mac\" aria-pressed=\"${U.os===\"mac\"}\">Mac<\/button><button data-os=\"win\" aria-pressed=\"${U.os===\"win\"}\">Windows<\/button><\/div>\n      <a class=\"studio-link\" href=\"${esc(CONFIG.siteUrl)}\">Le studio OK3D \u2197<\/a>\n    <\/div>`;\n  $(\"#home-link\").onclick=()=>{ if(Q&&!Q.done&&Q.n>0) endQuiz(true); else home() };\n  $(\"#snd\").onclick=()=>{ U.sound=!U.sound; save(); header(); if(U.sound) Snd.good() };\n  $$('[data-os]').forEach(b=>{b.disabled=current==='quiz';b.title=current==='quiz'?'Change de clavier entre deux s\u00e9ries.':'';b.onclick=()=>{if(current==='quiz')return;U.os=b.dataset.os;save();header();rerender()}});\n}\nfunction footer(){ $(\"#foot\").innerHTML=`<span>${esc(CONFIG.product)} \u00b7 un outil ${esc(CONFIG.brand)} Studio, Paris<\/span><span>${TOTAL} raccourcis \u00b7 ${Object.keys(DATA).length} logiciels<br>${storageAvailable?\"Progression conserv\u00e9e localement, sans compte.\":\"Stockage indisponible : progression conserv\u00e9e pour cette visite.\"}<\/span>` }\nlet current=\"home\", currentArg=null;\nfunction rerender(){ if(current===\"home\") home(); else if(current===\"app\") appView(currentArg); else if(current===\"quiz\"&&Q&&!Q.answered) renderQ(true); else if(current===\"results\") results() }\nfunction toTop(){ const r=root.getBoundingClientRect(); if(r.top<60)root.scrollIntoView({block:'start',behavior:RM?'auto':'smooth'}) }\n\n\/* ---------- HOME ---------- *\/\nlet heroGen=0, filter=\"all\", search=\"\";\nfunction home(){\n  current=\"home\"; Q=null; GEN++; captureKeys=false; header();\n  const appsCount=Object.keys(DATA).length;\n  $(\"#view\").innerHTML=`\n    <section class=\"hero\">\n      <div>\n        <span class=\"eyebrow\">${esc(CONFIG.brand)} Lab \u00b7 Raccourcis clavier<\/span>\n        <h1>Moins de clics.<br><span class=\"grad\">Plus de cr\u00e9ation.<\/span><\/h1>\n        <p class=\"lede\">Apprends les raccourcis de ${appsCount} logiciels de design graphique, de motion et de 3D. Chaque bonne r\u00e9ponse s'ex\u00e9cute sous tes yeux, chaque erreur te montre le bon geste.<\/p>\n        <div class=\"cta\"><button class=\"btn pri\" id=\"go-grid\">Choisir mon logiciel <span aria-hidden=\"true\">\u2198<\/span><\/button><button class=\"btn gho\" id=\"go-rand\">Un sprint au hasard<\/button><\/div><p class=\"microcopy\">Gratuit \u00b7 Sans compte \u00b7 Progression enregistr\u00e9e sur cet appareil<\/p>\n        <div class=\"stats\"><div><b>${appsCount}<\/b><span>Logiciels<\/span><\/div><div><b>${TOTAL}<\/b><span>Raccourcis anim\u00e9s<\/span><\/div><div><b>2<\/b><span>Modes de jeu<\/span><\/div><\/div>\n      <\/div>\n      <div class=\"hviz\" id=\"hviz\" aria-hidden=\"true\"><div class=\"floor\"><\/div><div class=\"plane\" id=\"plane\"><\/div><div class=\"hcap\"><span class=\"a\" id=\"hca\"><\/span><span class=\"s\" id=\"hcs\"><\/span><\/div><\/div>\n    <\/section>\n    <section id=\"grid\" aria-label=\"Choisir un logiciel\">\n      ${U.lastApp?`<div class=\"resume\"><div><b>On continue avec ${esc(DATA[U.lastApp].name)} ?<\/b><small>${knownCount(U.lastApp)} raccourcis acquis \u00b7 retrouve ta biblioth\u00e8que et tes entra\u00eenements.<\/small><\/div><button class=\"btn gho\" id=\"resume\">Continuer \u2192<\/button><\/div>`:''}\n      <div class=\"sec-h\"><h2>Choisis ton logiciel<\/h2><div class=\"filters\" role=\"group\" aria-label=\"Cat\u00e9gorie\">${CATS.map(([k,l])=>`<button data-f=\"${k}\" aria-pressed=\"${filter===k}\">${l}<\/button>`).join(\"\")}<\/div><\/div>\n      <div class=\"discovery\"><label class=\"search\"><span aria-hidden=\"true\">\u2315<\/span><input id=\"app-search\" type=\"search\" value=\"${esc(search)}\" placeholder=\"Un logiciel, un outil, un raccourci\u2026\" aria-label=\"Rechercher un logiciel ou une action\"><\/label><small class=\"muted\">\u00c0 la souris, au toucher ou au clavier<\/small><\/div><p id=\"app-count\" class=\"result-count\" role=\"status\"><\/p><div class=\"agrid\" id=\"agrid\"><\/div>\n    <\/section>\n    <section class=\"studio glass\">\n      <div><span class=\"eyebrow\">Studio ${esc(CONFIG.brand)}<\/span><h3 style=\"margin-top:12px\">Ces raccourcis, on les tape tous les jours.<\/h3>\n        <p>${esc(CONFIG.brand)} est un studio de cr\u00e9ation 3D et motion design bas\u00e9 \u00e0 Paris : visuels produits, animations, identit\u00e9s. Un projet en t\u00eate ?<\/p><\/div>\n      <a class=\"btn pri\" href=\"${esc(CONFIG.contactUrl)}?source=keystroke\">Parler de mon projet \u2192<\/a>\n    <\/section>`;\n  cards();\n  $('#app-search').oninput=e=>{search=e.target.value;cards()};\n  if($('#resume')) $('#resume').onclick=()=>appView(U.lastApp);\n  $$(\"[data-f]\").forEach(b=>b.onclick=()=>{ filter=b.dataset.f; $$(\"[data-f]\").forEach(x=>x.setAttribute(\"aria-pressed\",x.dataset.f===filter)); cards() });\n  $(\"#go-grid\").onclick=()=>$(\"#grid\").scrollIntoView({behavior:RM?\"auto\":\"smooth\",block:\"start\"});\n  $(\"#go-rand\").onclick=()=>{ const ids=Object.keys(DATA); startQuiz(ids[Math.random()*ids.length|0],\"sprint\") };\n  hero();\n}\nfunction cards(){\n  const g=$(\"#agrid\"); g.innerHTML=Object.entries(DATA).filter(([,a])=>(filter===\"all\"||a.cat===filter)&&(!search||normalize(a.name+\" \"+a.sub+\" \"+a.items.flat().join(\" \")).includes(normalize(search)))).map(([id,a])=>{ const k=knownCount(id), n=a.items.length, ap=AP(id);\n    return `<button class=\"acard\" data-app=\"${id}\" style=\"--hue:${a.hue}\">${ap.cert?'<span class=\"badge\">\u2605 Sans faute<\/span>':\"\"}\n      <span class=\"top\"><span class=\"mono\">${a.mono}<\/span><span><b>${esc(a.name)}<\/b><small>${esc(a.sub)} \u00b7 ${n} raccourcis<\/small><\/span><\/span>\n      <span class=\"abar\"><i style=\"width:${k\/n*100}%\"><\/i><\/span>\n      <span class=\"ameta\"><span>${k}\/${n} acquis<\/span><span>${ap.best?`Record ${fmt(ap.best)}`:\"Pas encore jou\u00e9\"}<\/span><\/span><\/button>` }).join(\"\");\n  const count=g.querySelectorAll('.acard').length; $('#app-count').textContent=count+' logiciel'+(count>1?'s':'')+' disponible'+(count>1?'s':'');\n  if(!count) g.innerHTML='<div class=\"empty\">Aucun logiciel trouv\u00e9. Essaie \u00ab Blender \u00bb, \u00ab Plume \u00bb ou change de cat\u00e9gorie.<\/div>';\n  g.querySelectorAll(\".acard\").forEach(c=>{ c.onclick=()=>appView(c.dataset.app);\n    c.onpointermove=e=>{ const r=c.getBoundingClientRect(); c.style.setProperty(\"--mx\",(e.clientX-r.left)+\"px\"); c.style.setProperty(\"--my\",(e.clientY-r.top)+\"px\") } });\n}\nasync function hero(){\n  const my=++heroGen, plane=$(\"#plane\"), viz=$(\"#hviz\"); if(!plane) return;\n  viz.onpointermove=e=>{ const r=viz.getBoundingClientRect(), dx=(e.clientX-r.left)\/r.width-.5, dy=(e.clientY-r.top)\/r.height-.5; plane.style.setProperty(\"--rx\",(46-dy*14)+\"deg\"); plane.style.setProperty(\"--rz\",(-20+dx*22)+\"deg\") };\n  viz.onpointerleave=()=>{ plane.style.setProperty(\"--rx\",\"46deg\"); plane.style.setProperty(\"--rz\",\"-20deg\") };\n  const pool=[]; Object.entries(DATA).forEach(([id,a])=>a.items.forEach(it=>{ if(it[3].length>=2) pool.push([id,it]) }));\n  const sl=ms=>new Promise(r=>setTimeout(r,ms));\n  let first=true;\n  while(my===heroGen&&document.body.contains(plane)){\n    if(document.hidden||!heroVisible){await sl(800);continue}\n    const [id,it]=pool[Math.random()*pool.length|0];\n    plane.innerHTML=it[3].map(k=>`<div class=\"hk\"><span>${esc(cap(k,id))}<\/span><\/div>`).join(\"\");\n    const ks=[...plane.children]; $(\"#hca\").textContent=it[0]; $(\"#hcs\").textContent=`${DATA[id].name} \u00b7 ${keysText(it[3],id)}`;\n    if(RM||matchMedia(\"(hover:none)\").matches){ ks.forEach(k=>k.classList.add(\"down\")); return }\n    ks.forEach((k,i)=>k.animate([{opacity:0,transform:\"translateZ(120px)\"},{opacity:1,transform:\"translateZ(0)\"}],{duration:520,delay:i*90,easing:\"cubic-bezier(.2,.8,.2,1)\",fill:\"backwards\"}));\n    $(\".hcap\").animate([{opacity:0,transform:\"translateY(8px)\"},{opacity:1,transform:\"translateY(0)\"}],{duration:500,delay:250,fill:\"backwards\"});\n    await sl(first?900:750); first=false;\n    for(const k of ks){ if(my!==heroGen) return; k.classList.add(\"down\"); await sl(170) }\n    await sl(1100); ks.forEach(k=>k.classList.remove(\"down\")); await sl(500);\n    if(my!==heroGen) return;\n    await Promise.all(ks.map((k,i)=>k.animate([{opacity:1,transform:\"translateZ(0)\"},{opacity:0,transform:\"translateZ(80px)\"}],{duration:380,delay:i*50,fill:\"forwards\"}).finished));\n  }\n}\n\n\/* ---------- APP DETAIL ---------- *\/\nfunction appView(id){\n  current=\"app\"; currentArg=id; heroGen++; GEN++; Q=null; captureKeys=false; U.lastApp=id; save(); header();\n  const a=DATA[id], ap=AP(id), n=a.items.length, k=knownCount(id), sl=Math.min(CONFIG.sprintLength,n);\n  $(\"#view\").innerHTML=`\n    <button class=\"crumb\" id=\"back\">\u2190 Tous les logiciels<\/button>\n    <div class=\"ahead\" style=\"--hue:${a.hue}\"><span class=\"mono\">${a.mono}<\/span>\n      <div><h1>${esc(a.name)}<\/h1><p>${esc(a.sub)} \u00b7 ${n} raccourcis${!a.mac&&U.os===\"mac\"?\" \u00b7 notation Windows\":\"\"}<\/p>\n        <div class=\"chips\"><span>${k}\/${n} acquis<\/span><span>Record ${ap.best?fmt(ap.best)+\" pts\":\"\u2014\"}<\/span>${ap.cert?'<span class=\"badge\">\u2605 Sans faute<\/span>':\"<span>Badge \u00e0 d\u00e9crocher<\/span>\"}<\/div><\/div><\/div>\n    <div class=\"modes\">\n      <button class=\"mode\" data-mode=\"sprint\"><h3>Sprint <span class=\"go\">\u2192<\/span><\/h3><p>${sl} questions tir\u00e9es au hasard, en priorit\u00e9 celles que tu connais le moins. Vitesse et s\u00e9ries font grimper le score.<\/p>\n        <div class=\"pills\"><span>${sl} questions<\/span><span>Bonus de vitesse<\/span><span>Combo \u00d73<\/span><\/div><\/button>\n      <button class=\"mode m2\" data-mode=\"mastery\"><h3>Ma\u00eetrise <span class=\"go\">\u2192<\/span><\/h3><p>Les ${n} raccourcis, dans un ordre al\u00e9atoire. Chaque erreur revient plus tard. Termine sans aucune faute pour d\u00e9crocher ton badge personnel Sans faute.<\/p>\n        <div class=\"pills\"><span>${n} raccourcis<\/span><span>Les erreurs reviennent<\/span><span>\u2605 Sans faute<\/span><\/div><\/button>\n    <\/div>\n    <div class=\"opts-row\"><label><input type=\"checkbox\" class=\"swi\" id=\"rev\" ${U.rev?\"checked\":\"\"}><span class=\"sw\"><\/span><span>Questions invers\u00e9es<br><small>Une partie des questions montre les touches : \u00e0 toi de trouver l'action.<\/small><\/span><\/label>\n      <small class=\"kbonly\">Astuce : tape directement le raccourci sur ton clavier.<\/small><\/div>\n    <section class=\"lib\">\n      <div class=\"sticky\"><div class=\"glass scard\">${stageHTML(id)}<div class=\"strip\" id=\"strip\"><span class=\"lab\">Raccourci<\/span><span class=\"ks\" id=\"ks\"><\/span><\/div><\/div><\/div>\n      <div><div class=\"libh\"><h2>Biblioth\u00e8que anim\u00e9e<\/h2><p>Touche un raccourci pour voir son effet. R\u00e9vise avant de te lancer.<\/p><\/div>\n        <label class=\"search library-search\"><span aria-hidden=\"true\">\u2315<\/span><input id=\"library-search\" type=\"search\" placeholder=\"Chercher une action ou des touches\" aria-label=\"Rechercher dans les raccourcis de ${esc(a.name)}\"><\/label><div class=\"library-tools\"><span id=\"library-count\">${n} raccourcis<\/span><button id=\"unlearned\" aria-pressed=\"false\">\u00c0 apprendre<\/button><\/div><div class=\"list\">${a.items.map((it,i)=>`<button class=\"li\" data-i=\"${i}\"><span class=\"pl\">\u25b6<\/span><span class=\"nm\">${esc(it[0])}<small>${esc(it[1])}<\/small><\/span>${keysHTML(it[3],id)}<\/button>`).join(\"\")}<\/div><p class=\"library-note\">Un raccourci est marqu\u00e9 acquis apr\u00e8s deux bonnes r\u00e9ponses. Raccourcis par d\u00e9faut : ils peuvent varier selon la version, la langue et la disposition du clavier. Les animations illustrent le geste, sans ouvrir le logiciel.<\/p><\/div>\n    <\/section>`;\n  $(\"#back\").onclick=home;\n  $$(\"[data-mode]\").forEach(b=>b.onclick=()=>startQuiz(id,b.dataset.mode));\n  $('#rev').onchange=e=>{U.rev=e.target.checked;save()};\n  let onlyUnlearned=false;\n  const libraryFilter=()=>{const term=normalize($('#library-search').value);let count=0;\n    $$('.li').forEach(b=>{const it=a.items[+b.dataset.i];const visible=(!onlyUnlearned||(ap.known[it[0]]||0)<2)&&normalize(it.slice(0,4).flat().join(' ')).includes(term);b.hidden=!visible;if(visible)count++});\n    $('#library-count').textContent=count+' \/ '+n+' raccourcis';\n  };\n  $('#library-search').oninput=libraryFilter;\n  $('#unlearned').onclick=e=>{onlyUnlearned=!onlyUnlearned;e.currentTarget.setAttribute('aria-pressed',String(onlyUnlearned));libraryFilter()};\n  const st=$(\"#stage\"); let cur=0;\n  const show=async(i,run)=>{ cur=i; $$(\".li\").forEach(x=>x.classList.toggle(\"on\",+x.dataset.i===i)); const it=a.items[i];\n    $(\"#vp\").textContent=it[0]; $(\"#ks\").innerHTML=it[3].map(x=>`<kbd>${esc(cap(x,id))}<\/kbd>`).join(\"\"); $(\"#rep\").hidden=true; buildStage(st,it[4],id);\n    if(run){ const g=GEN; const ks=[...$(\"#ks\").children]; for(const kb of ks){ kb.classList.add(\"down\"); Snd.key(); await wait(110) } await wait(160); ks.forEach(kb=>kb.classList.remove(\"down\")); if(g!==GEN) return; await play(); if(g===GEN) $(\"#rep\").hidden=false } };\n  $$(\".li\").forEach(b=>b.onclick=()=>{ show(+b.dataset.i,true); if(innerWidth<1000) $(\".sticky\").scrollIntoView({behavior:RM?\"auto\":\"smooth\",block:\"start\"}) });\n  $(\"#rep\").onclick=()=>show(cur,true);\n  show(0,false);\n  toTop();\n}\n\n\/* ---------- QUIZ ENGINE ---------- *\/\nlet Q=null, timerT=null;\nfunction startQuiz(id,mode,reviewItems){\n  heroGen++; GEN++; captureKeys=false; U.lastApp=id;save();\n  const items=DATA[id].items, ap=AP(id);\n  let list;\n  if(mode===\"review\") list=shuffle(reviewItems&&reviewItems.length?reviewItems:items.filter(it=>(ap.known[it[0]]||0)<2));\n  else if(mode===\"mastery\") list=shuffle(items);\n  else list=shuffle(items.map(it=>({it,w:(ap.known[it[0]]||0)+Math.random()*1.6})).sort((a,b)=>a.w-b.w).slice(0,Math.min(CONFIG.sprintLength,items.length)).map(o=>o.it));\n  if(!list.length){appView(id);uiToast(\"Tous les raccourcis sont d\u00e9j\u00e0 acquis.\");return}\n  Q={soft:id,mode,queue:list.map(it=>({it,retry:false})),total:list.length,n:0,score:0,combo:0,bestCombo:0,right:0,attempts:0,times:[],missed:new Set(),acquired:new Set(),log:[],done:false};\n  current=\"quiz\"; header(); nextQ(true);\n}\nfunction mult(c){ return c>=8?3:c>=5?2:c>=3?1.5:1 }\nfunction makeQuestion(entry){\n  const it=entry.it, items=DATA[Q.soft].items;\n  const reverse=U.rev&&Math.random()<CONFIG.reverseRatio;\n  let opts, correct;\n  if(reverse){ const others=shuffle(items.filter(o=>o!==it&&o[0]!==it[0])).slice(0,3); opts=shuffle([it,...others]).map(o=>({id:\"a:\"+o[0],label:o[0]})); correct=\"a:\"+it[0] }\n  else { const seen=new Set([comboId(it[3])]), out=[it[3]];\n    if(it[3].length>1){ const k=it[3].includes(\"Shift\")?it[3].filter(x=>x!==\"Shift\"):[\"Ctrl\",\"Alt\",\"Shift\"].filter(m=>m===\"Shift\"||it[3].includes(m)).concat(it[3].filter(x=>![\"Ctrl\",\"Alt\",\"Shift\"].includes(x)));\n      if(k.length&&!seen.has(comboId(k))&&Math.random()<.85){ seen.add(comboId(k)); out.push(k) } }\n    for(const o of shuffle(items)){ if(out.length>=4) break; const c=comboId(o[3]); if(!seen.has(c)){ seen.add(c); out.push(o[3]) } }\n    opts=shuffle(out).map(k=>({id:comboId(k),keys:k})); correct=comboId(it[3]) }\n  return {it,retry:entry.retry,reverse,opts,correct};\n}\nfunction nextQ(first){\n  if(!Q.queue.length) return endQuiz();\n  Q.cur=makeQuestion(Q.queue[0]); Q.answered=false; renderQ(false); if(first) toTop();\n}\nfunction progressInfo(){ return (Q.mode===\"mastery\"||Q.mode===\"review\")?{v:Q.acquired.size\/Q.total,t:`${Q.acquired.size}\/${Q.total} acquis`}:{v:Q.n\/Q.total,t:`Question ${Math.min(Q.n+(Q.answered?0:1),Q.total)}\/${Q.total}`} }\nfunction renderQ(keepState){\n  const q=Q.cur, it=q.it, a=DATA[Q.soft], pi=progressInfo();\n  $(\"#view\").innerHTML=`\n    <div class=\"qbar\">\n      <button class=\"qx\" id=\"quit\" aria-label=\"Quitter la s\u00e9rie\">\u2715<\/button>\n      <div class=\"qinfo\" style=\"--hue:${a.hue}\"><span class=\"mono\">${a.mono}<\/span><div class=\"t\"><b>${esc(a.name)} \u00b7 ${Q.mode===\"mastery\"?\"Ma\u00eetrise\":Q.mode===\"review\"?\"R\u00e9vision\":\"Sprint\"}<\/b>\n        <small id=\"ptext\">${pi.t}<\/small><div class=\"prog\"><i id=\"pbar\" style=\"width:${pi.v*100}%\"><\/i><\/div><\/div><\/div>\n      <div class=\"score\"><b id=\"score\">${fmt(Q.score)}<\/b><span>points<\/span><\/div>\n      <div class=\"combo ${Q.combo>=3?\"hot\":\"\"}\" id=\"combo\">\u00d7${mult(Q.combo)}<small>${Q.combo} s\u00e9rie<\/small><\/div>\n    <\/div>\n    <div class=\"qtools\"><button type=\"button\" class=\"capture\" id=\"capture\" aria-pressed=\"${captureKeys}\">${captureKeys?'\u2328 Clavier activ\u00e9':'\u2328 Activer le clavier'}<\/button><span class=\"capture-help\">${captureKeys?'La saisie reste limit\u00e9e \u00e0 ce module. Tab permet de naviguer. Certains raccourcis sont r\u00e9serv\u00e9s au navigateur ou au syst\u00e8me.':'Clique ou touche une r\u00e9ponse. Active le clavier seulement si tu veux taper les raccourcis.'}<\/span><\/div>\n    <div class=\"qgrid\">\n      <div class=\"glass scard\" id=\"card\">${stageHTML(Q.soft)}<div class=\"strip\" id=\"strip\"><span class=\"lab\">Raccourci<\/span><span class=\"ks\" id=\"ks\">${q.reverse?it[3].map(x=>`<kbd>${esc(cap(x,Q.soft))}<\/kbd>`).join(\"\"):'<kbd class=\"ph\">?<\/kbd>'}<\/span><\/div><div class=\"timer-label\">Bonus de vitesse \u00b7 sans limite pour r\u00e9pondre<\/div><div class=\"timer\" aria-hidden=\"true\"><i id=\"timer\"><\/i><\/div><\/div>\n      <div class=\"glass qpanel\">\n        <span class=\"qkind\">${q.retry?\"\u21bb Deuxi\u00e8me chance \u00b7 \":\"\"}${q.reverse?\"Que fait ce raccourci ?\":\"Quel est le raccourci ?\"}<\/span>\n        ${q.reverse?`<div class=\"qkeys\">${it[3].map(x=>`<kbd>${esc(cap(x,Q.soft))}<\/kbd>`).join(\"\")}<\/div><p class=\"qmenu\">${esc(a.name)}<\/p>`\n          :`<h2>${esc(it[0])}<\/h2><p class=\"qd\">${esc(it[2])}<\/p><p class=\"qmenu\">${esc(it[1])}<\/p>`}\n        <div class=\"opts\" id=\"opts\">${q.opts.map((o,i)=>`<button class=\"opt\" data-id=\"${esc(o.id)}\"><span class=\"n\">${i+1}<\/span>${o.keys?keysHTML(o.keys,Q.soft):esc(o.label)}<\/button>`).join(\"\")}<\/div>\n        <div class=\"hint\" id=\"verdict\"><span class=\"kbonly\">${captureKeys?(q.reverse?\"Touches 1 \u00e0 4 pour r\u00e9pondre.\":\"Tape le raccourci ou choisis une r\u00e9ponse.\"):\"Choisis une r\u00e9ponse ci-dessus.\"}<\/span><\/div>\n      <\/div>\n    <\/div>`;\n  $('#quit').onclick=()=>endQuiz(true);\n  $('#capture').onclick=()=>{captureKeys=!captureKeys;$('#capture').setAttribute('aria-pressed',String(captureKeys));$('#capture').textContent=captureKeys?'\u2328 Clavier activ\u00e9':'\u2328 Activer le clavier';$('.capture-help').textContent=captureKeys?'Saisie active dans ce module. Tab reste disponible. Les raccourcis r\u00e9serv\u00e9s au syst\u00e8me se choisissent avec les boutons.':'Clique ou touche une r\u00e9ponse. Le clavier est d\u00e9sactiv\u00e9.';if(captureKeys)root.focus({preventScroll:true})};\n  $('#view').querySelectorAll('button:not([type])').forEach(b=>b.type='button');\n  announce((Q.mode==='review'?'R\u00e9vision. ':'')+pi.t+'. '+(q.reverse?'Quelle action correspond \u00e0 ces touches ?':it[0]));\n  requestAnimationFrame(()=>{if(current==='quiz'&&Q&&!Q.answered){$('.qpanel').setAttribute('tabindex','-1');$('.qpanel').focus({preventScroll:true})}});\n  $$(\".opt\").forEach(b=>b.onclick=()=>answer(b.dataset.id));\n  const st=$(\"#stage\"); if(q.reverse) st.classList.add(\"mystery\");\n  buildStage(st,it[4],Q.soft);\n  $(\"#rep\").onclick=async()=>{ buildStage(st,it[4],Q.soft); $(\"#rep\").hidden=true; const g=GEN; await play(); if(g===GEN) $(\"#rep\").hidden=false };\n  if(keepState&&Q.answered){ return }\n  Q.t0=performance.now(); Q.pausedAt=null; startTimer();\n}\nfunction startTimer(){ const t=$(\"#timer\"); if(!t) return; t.style.transition=\"none\"; t.style.width=\"100%\"; void t.offsetWidth; t.style.transition=`width ${Math.max(0,CONFIG.timePerQuestion-(performance.now()-Q.t0)\/1000)}s linear`; t.style.width=\"0%\" }\nfunction stopTimer(){ const t=$(\"#timer\"); if(!t) return; const w=getComputedStyle(t).width; t.style.transition=\"none\"; t.style.width=w }\n\nasync function pressStrip(keys,ok){ const ks=$(\"#ks\"); ks.innerHTML=\"\"; $(\"#strip\").classList.toggle(\"ok\",ok);\n  const caps=keys.map(x=>{ const e=document.createElement(\"kbd\"); e.textContent=cap(x,Q.soft); e.style.opacity=0; ks.appendChild(e); return e });\n  for(const e of caps){ e.style.opacity=\"\"; e.animate([{opacity:0,transform:\"translateY(-10px)\"},{opacity:1,transform:\"translateY(0)\"}],{duration:RM?1:170,easing:EASE}); await wait(130); e.classList.add(\"down\"); Snd.key(); await wait(100) }\n  await wait(220); caps.forEach(e=>e.classList.remove(\"down\")); await wait(80) }\n\nasync function answer(id,typed){\n  if(!Q||Q.answered||current!==\"quiz\") return; Q.answered=true; stopTimer();\n  const q=Q.cur, it=q.it, right=id===q.correct, dt=(performance.now()-Q.t0)\/1000, ap=AP(Q.soft);\n  Q.attempts++; Q.times.push(dt); Q.queue.shift(); Q.n++;\n  let pts=0;\n  if(right){ Q.right++; Q.combo++; Q.bestCombo=Math.max(Q.bestCombo,Q.combo); const speed=Math.round(100*Math.max(0,1-dt\/CONFIG.timePerQuestion)); pts=Math.round((100+speed)*mult(Q.combo)*(q.retry?.5:1)); Q.score+=pts;\n    ap.known[it[0]]=(ap.known[it[0]]||0)+1; Q.acquired.add(it[0]); }\n  else { Q.combo=0; ap.known[it[0]]=0; Q.missed.add(it[0]);\n    if(Q.mode===\"mastery\"||Q.mode===\"review\"){ const pos=Math.min(Q.queue.length,2+(Math.random()*3|0)); Q.queue.splice(pos,0,{it,retry:true}) } }\n  Q.log.push({it,right,reverse:q.reverse}); save();\n  const g=GEN, btns=[...$$(\".opt\")]; btns.forEach(b=>b.disabled=true);\n  const chosen=btns.find(b=>b.dataset.id===id), good=btns.find(b=>b.dataset.id===q.correct), st=$(\"#stage\"), vp=$(\"#vp\"), card=$(\"#card\");\n  const sc=$(\"#score\"); countTo(sc,Q.score-pts,Q.score,600);\n  const cb=$(\"#combo\"); cb.innerHTML=`\u00d7${mult(Q.combo)}<small>${Q.combo} s\u00e9rie<\/small>`; cb.classList.toggle(\"hot\",Q.combo>=3); if(right&&Q.combo>=3) bump(cb,1.15,300);\n  const pi=progressInfo(); $(\"#pbar\").style.width=pi.v*100+\"%\"; $(\"#ptext\").textContent=pi.t;\n  const v=$(\"#verdict\"); v.className=\"verdict\"; announce(right?\"Bonne r\u00e9ponse.\":\"\u00c0 revoir : \"+it[0]+\", \"+keysText(it[3],Q.soft)); const last=!Q.queue.length;\n  v.innerHTML=`<span class=\"msg ${right?\"good\":\"bad\"}\">${right?(Q.combo>=3?`Exact \u00b7 s\u00e9rie de ${Q.combo}`:\"Exact.\"):typed?`Tu as tap\u00e9 ${esc(typed)}`:\"Rat\u00e9.\"}\n    <small>${right?`+${fmt(pts)} pts${mult(Q.combo)>1?` \u00b7 combo \u00d7${mult(Q.combo)}`:\"\"}`:(Q.mode===\"mastery\"||Q.mode===\"review\")?\"Il reviendra plus tard dans la s\u00e9rie.\":`Bonne r\u00e9ponse : ${q.reverse?esc(it[0]):esc(keysText(it[3],Q.soft))}`}<\/small><\/span>\n    <button class=\"btn pri\" id=\"next\">${last?\"Voir le r\u00e9sultat\":\"Suivant\"} <kbd class=\"kbonly\">Entr\u00e9e<\/kbd><\/button>`;\n  $(\"#next\").onclick=next;\n  st.classList.remove(\"mystery\");\n  if(right){ chosen.classList.add(\"ok\"); bump(chosen,1.04,320); burst(chosen); floatText(sc,`+${fmt(pts)}`); Snd.good(); vp.classList.add(\"good\"); vp.textContent=\"\u2713 Exact\"; st.classList.add(\"sweep\") }\n  else { if(chosen){ chosen.classList.add(\"ko\"); A(chosen,[{transform:\"translateX(0)\"},{transform:\"translateX(-8px)\"},{transform:\"translateX(8px)\"},{transform:\"translateX(-4px)\"},{transform:\"translateX(0)\"}],{duration:380}) }\n    Snd.bad(); vp.classList.add(\"bad\"); vp.textContent=\"\u2715 Rat\u00e9\"; st.classList.add(\"hit\"); A(card,[{transform:\"translateX(0)\"},{transform:\"translateX(-6px)\"},{transform:\"translateX(5px)\"},{transform:\"translateX(0)\"}],{duration:320});\n    await wait(560); if(g!==GEN) return; st.classList.remove(\"hit\"); good.classList.add(\"ok\"); bump(good,1.05,360); vp.classList.remove(\"bad\"); vp.classList.add(\"good\"); vp.textContent=\"Bonne r\u00e9ponse\" }\n  btns.forEach(b=>{ if(!b.classList.contains(\"ok\")&&!b.classList.contains(\"ko\")) b.classList.add(\"dim\") });\n  await pressStrip(it[3],true); if(g!==GEN) return;\n  await play(); if(g!==GEN) return;\n  vp.classList.remove(\"good\"); vp.textContent=it[0]; $(\"#rep\").hidden=false;\n}\nfunction next(){ if(!Q||!Q.answered) return; nextQ(false) }\nfunction endQuiz(aborted){\n  if(!Q) return home();\n  if(Q.done) return;\n  captureKeys=false;\n  if(aborted&&Q.n===0){ const id=Q.soft; Q=null; return appView(id) }\n  Q.done=true; Q.aborted=!!aborted; const ap=AP(Q.soft);\n  Q.newBest=Q.score>ap.best; if(Q.newBest) ap.best=Q.score;\n  Q.certified=Q.mode===\"mastery\"&&!aborted&&Q.missed.size===0; if(Q.certified) ap.cert=true;\n  U.xp+=Q.score; save(); current=\"results\"; header(); results(true);\n}\nfunction results(fresh){\n  GEN++;\n  const a=DATA[Q.soft], acc=Q.attempts?Q.right\/Q.attempts:0, avg=Q.times.length?Q.times.reduce((x,y)=>x+y,0)\/Q.times.length:0;\n  const rank=Q.certified?\"S\":acc>=.9?\"S\":acc>=.75?\"A\":acc>=.55?\"B\":\"C\";\n  const msg=Q.certified?`Sans aucune faute sur les ${Q.total} raccourcis. Un badge personnel pour cette s\u00e9rie r\u00e9ussie.`\n    :Q.mode===\"review\"?\"Tu as repris les raccourcis \u00e0 revoir. Reviens demain pour les m\u00e9moriser durablement.\"\n    :Q.aborted?\"S\u00e9rie interrompue. Les points gagn\u00e9s sont conserv\u00e9s.\"\n    :acc>=.9?\"Excellent r\u00e9flexe. Tente le mode Ma\u00eetrise pour d\u00e9crocher le badge Sans faute.\"\n    :acc>=.6?\"Solide. Les raccourcis rat\u00e9s reviendront en priorit\u00e9 au prochain Sprint.\"\n    :\"\u00c7a viendra vite : passe par la biblioth\u00e8que anim\u00e9e, puis relance une s\u00e9rie.\";\n  const missed=[...new Set(Q.log.filter(l=>!l.right).map(l=>l.it))];\n  $(\"#view\").innerHTML=`\n    <div class=\"res\">\n      <div class=\"glass rhero\">\n        <span class=\"eyebrow\">${esc(a.name)} \u00b7 ${Q.mode===\"mastery\"?\"Ma\u00eetrise\":Q.mode===\"review\"?\"R\u00e9vision\":\"Sprint\"} \u2014 r\u00e9sultat<\/span>\n        <div class=\"rank-row\"><div class=\"rank grad\">${rank}<\/div><div class=\"rscore\"><b id=\"rs\">0<\/b><span>points ${Q.newBest?\"\u00b7 nouveau record\":\"\"}<\/span><\/div><\/div>\n        <div class=\"rstats\"><div><b>${Math.round(acc*100)} %<\/b><span>Pr\u00e9cision<\/span><\/div><div><b>\u00d7${mult(Q.bestCombo)}<\/b><span>Meilleur combo<\/span><\/div><div><b>${avg.toFixed(1)} s<\/b><span>Temps moyen<\/span><\/div><div><b>${knownCount(Q.soft)}\/${a.items.length}<\/b><span>Acquis<\/span><\/div><\/div>\n        ${Q.certified?`<div class=\"cert\"><div class=\"medal\">\u2605<\/div><div><b>Sans faute ${esc(a.name)}<\/b><p>Tous les raccourcis connus en une seule session, sans erreur.<\/p><\/div><\/div>`:\"\"}\n        <p class=\"rmsg\">${msg}<\/p>\n        <div class=\"ract\"><button class=\"btn pri\" id=\"again\">Rejouer<\/button><button class=\"btn gho\" id=\"mode2\">${Q.mode===\"mastery\"?\"Faire un Sprint\":\"Tenter la Ma\u00eetrise\"}<\/button><button class=\"btn gho\" id=\"share\">Copier mon score<\/button>${missed.length?'<button class=\"btn gho\" id=\"review\">Revoir mes erreurs<\/button>':\"\"}<\/div>\n      <\/div>\n      <div class=\"rside\">\n        <div class=\"glass rlist\"><h3>${missed.length?\"\u00c0 revoir\":\"Rien \u00e0 revoir\"}<\/h3>\n          ${missed.length?missed.map(it=>`<div class=\"row\"><span>${esc(it[0])}<\/span>${keysHTML(it[3],Q.soft)}<\/div>`).join(\"\"):'<div class=\"row\"><span>Toutes tes r\u00e9ponses \u00e9taient justes.<\/span><span class=\"ok\">\u2713<\/span><\/div>'}<\/div>\n        <div class=\"glass rcta\"><span class=\"eyebrow\">Studio ${esc(CONFIG.brand)}<\/span><h3>Besoin d'un visuel 3D ou d'une animation ?<\/h3><p>Le studio qui a con\u00e7u ${esc(CONFIG.product)} cr\u00e9e des visuels produits, des films 3D et des identit\u00e9s anim\u00e9es.<\/p>\n          <div><a class=\"btn gho\" href=\"${esc(CONFIG.contactUrl)}?source=keystroke&logiciel=${encodeURIComponent(a.name)}\">Pr\u00e9parer mon projet \u2192<\/a><\/div><\/div>\n        <button class=\"btn gho\" id=\"others\">\u2190 Changer de logiciel<\/button>\n      <\/div>\n    <\/div>`;\n  const id=Q.soft, mode=Q.mode;\n  countTo($(\"#rs\"),0,Q.score,fresh?1100:1);\n  if(fresh){ if(Q.certified){ Snd.fanfare(); setTimeout(()=>burst($(\".medal\"),60),300) } else if(acc>=.75) setTimeout(()=>burst($(\".rank\"),40),200);\n    $(\".rank\").animate([{opacity:0,transform:\"scale(.6)\"},{opacity:1,transform:\"scale(1)\"}],{duration:RM?1:600,easing:BOUNCE}) }\n  $(\"#again\").onclick=()=>startQuiz(id,mode,mode===\"review\"?Q.log.map(l=>l.it).filter((it,i,a)=>a.indexOf(it)===i):null);\n  if($(\"#review\")) $(\"#review\").onclick=()=>startQuiz(id,\"review\",missed); $(\"#mode2\").onclick=()=>startQuiz(id,mode===\"mastery\"?\"sprint\":\"mastery\"); $(\"#others\").onclick=home;\n  announce(\"S\u00e9rie termin\u00e9e. \"+Math.round(acc*100)+\" pour cent de pr\u00e9cision.\");\n  $(\"#share\").onclick=async e=>{ const txt=`${fmt(Q.score)} points en ${a.name} sur ${CONFIG.product} by ${CONFIG.brand} (${Math.round(acc*100)} % de pr\u00e9cision${Q.certified?\", sans faute\":\"\"}). \u00c0 toi de jouer : ${CONFIG.shareUrl}`;\n    let ok=false; try{ await navigator.clipboard.writeText(txt); ok=true }catch(err){}\n    uiToast(ok?\"Score copi\u00e9, colle-le o\u00f9 tu veux\":\"Copie impossible ici : \"+txt) };\n  toTop();\n}\nfunction uiToast(t){ announce(t);const d=document.createElement(\"div\"); d.className=\"toast\"; d.textContent=t; $(\"#okk\").appendChild(d); setTimeout(()=>d.remove(),2600) }\n\n\/* ---------- keyboard ---------- *\/\nconst CODE={BracketRight:\"]\",BracketLeft:\"[\",Backslash:\"\\\\\",NumpadDecimal:\"Num .\",NumpadDivide:\"Num \/\",Tab:\"Tab\",Space:\"Espace\",ArrowRight:\"\u2192\",ArrowLeft:\"\u2190\"};\nfunction pressed(e){\n  const mods=[]; if(e.ctrlKey)mods.push(\"Ctrl\");if(e.metaKey)mods.push(Q&&macStyle(Q.soft)?\"Ctrl\":\"Meta\"); if(e.altKey) mods.push(\"Alt\"); if(e.shiftKey) mods.push(\"Shift\");\n  const c=[];\n  if(\/^F\\d{1,2}$\/.test(e.code)) c.push(e.code); if(CODE[e.code]) c.push(CODE[e.code]); if(\/^Digit\\d$\/.test(e.code)) c.push(e.code.slice(5)); if(\/^Numpad\\d$\/.test(e.code)) c.push(e.code.slice(6));\n  if(e.key&&e.key.length===1&&\/[a-z]\/i.test(e.key)) c.push(e.key.toUpperCase()); if(!c.length&&\/^Key[A-Z]$\/.test(e.code)) c.push(e.code.slice(3));\n  if(e.key&&e.key.length===1&&\"[]\\\\\".includes(e.key)) c.push(e.key);\n  return c.map(k=>mods.concat(k));\n}\nroot.addEventListener('keydown',e=>{\n  if(current!=='quiz'||!Q||e.repeat||e.isComposing||e.key==='Tab')return;\n  if(e.key==='Enter'&&e.target.closest('button,a'))return;\n  if(!captureKeys)return;\n  if(e.target instanceof Element && e.target.closest('input, textarea, select, [contenteditable=\"true\"]')) return;\n  if(Q.answered){if(e.key===\"Enter\"){e.preventDefault();next()}return}\n  if([\"Control\",\"Alt\",\"Shift\",\"Meta\"].includes(e.key)) return;\n  if(e.key===\"Escape\"){captureKeys=false;$(\"#capture\").setAttribute(\"aria-pressed\",\"false\");$(\"#capture\").textContent=\"\u2328 Activer le clavier\";announce(\"Saisie clavier d\u00e9sactiv\u00e9e.\");return}\n  const q=Q.cur;\n  if(q.reverse){ const n=+e.key; if(n>=1&&n<=4&&!e.ctrlKey&&!e.metaKey&&!e.altKey){ e.preventDefault(); answer(q.opts[n-1].id) } return }\n  const cands=pressed(e); if(!cands.length) return;\n  e.preventDefault();\n  if(cands.some(c=>comboId(c)===q.correct)) return answer(q.correct);\n  const asOpt=cands.find(c=>q.opts.some(o=>o.id===comboId(c)));\n  answer(asOpt?comboId(asOpt):\"__typed__\",keysText(asOpt||cands[0],Q.soft));\n});\n\n\/* ---------- Elementor integration ---------- *\/\nfunction fitLayout(){\n  if(!root.isConnected||root.dataset.layout!=='full'||root.classList.contains('is-app-screen')||(document.fullscreenElement||document.webkitFullscreenElement)===root)return;\n  const width=document.documentElement.clientWidth;\n  root.style.setProperty('--okk-viewport',width+'px');\n  const left=root.getBoundingClientRect().left;\n  const margin=parseFloat(getComputedStyle(root).marginLeft)||0;\n  if(Math.abs(left)>.5)root.style.setProperty('--okk-offset',(margin-left)+'px');\n}\nlet fitFrame=0;\nconst scheduleFit=()=>{cancelAnimationFrame(fitFrame);fitFrame=requestAnimationFrame(fitLayout)};\nwindow.addEventListener('resize',scheduleFit,{passive:true});\nconst layoutObserver=typeof ResizeObserver!=='undefined'?new ResizeObserver(scheduleFit):null;\nif(layoutObserver){layoutObserver.observe(root.parentElement);layoutObserver.observe(document.documentElement)}\nif(typeof IntersectionObserver!=='undefined')new IntersectionObserver(es=>{heroVisible=es[0].isIntersecting},{rootMargin:'200px'}).observe(root);\ndocument.addEventListener('visibilitychange',()=>{\n  if(!Q||current!=='quiz'||Q.answered)return;\n  if(document.hidden){Q.pausedAt=performance.now();stopTimer()}\n  else if(Q.pausedAt){Q.t0+=performance.now()-Q.pausedAt;Q.pausedAt=null;const t=$('#timer');if(t){const left=Math.max(0,1-(performance.now()-Q.t0)\/(CONFIG.timePerQuestion*1000));t.style.transition='none';t.style.width=(left*100)+'%';void t.offsetWidth;t.style.transition='width '+(left*CONFIG.timePerQuestion)+'s linear';t.style.width='0%'}}\n});\nif(window.parent!==window){let last=0;const send=()=>{const height=Math.ceil(root.getBoundingClientRect().height);if(Math.abs(height-last)>2){last=height;parent.postMessage({type:'ok3d-keystroke:height',height},'*')}};if(typeof ResizeObserver!=='undefined')new ResizeObserver(send).observe(root)}\nconst observer=new MutationObserver(()=>{root.querySelectorAll('button:not([type])').forEach(b=>b.type='button')});observer.observe(root,{childList:true,subtree:true});\nscheduleFit();\n(function setupOK3DFullscreen(root) {\n  const doc = root.ownerDocument;\n  const win = doc.defaultView;\n  let appState = null;\n  const nativeFull = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === root;\n  const active = () => nativeFull() || root.classList.contains('is-app-screen');\n  const attribute = (node, name, value) => {\n    if (node.getAttribute(name) !== value) node.setAttribute(name, value);\n  };\n  const sync = () => {\n    const full = active();\n    root.querySelectorAll('[data-okk-fullscreen]').forEach(button => {\n      const label = full ? '\u2922 Quitter le plein \u00e9cran' : '\u26f6 Plein \u00e9cran';\n      if (button.textContent !== label) button.textContent = label;\n      button.hidden = false;\n      attribute(button, 'aria-pressed', String(full));\n      attribute(button, 'aria-label', full ? 'Quitter le plein \u00e9cran' : 'Afficher en plein \u00e9cran');\n      attribute(button, 'title', full ? 'Quitter le plein \u00e9cran (\u00c9chap)' : 'Afficher en plein \u00e9cran');\n    });\n  };\n  const changed = () => {\n    sync();\n    win.dispatchEvent(new Event('resize'));\n  };\n  const saveOverflow = node => [node.style.getPropertyValue('overflow'), node.style.getPropertyPriority('overflow')];\n  const restoreOverflow = (node, value) => {\n    if (value[0]) node.style.setProperty('overflow', value[0], value[1]);\n    else node.style.removeProperty('overflow');\n  };\n  const focusButton = () => {\n    const button = root.querySelector('[data-okk-fullscreen]');\n    if (button) button.focus({ preventScroll: true });\n  };\n  const enterApp = () => {\n    if (appState) return;\n    const marker = doc.createComment('OK3D \u2014 retour du mode app');\n    appState = {\n      marker, parent: root.parentNode, next: root.nextSibling,\n      bodyOverflow: saveOverflow(doc.body), htmlOverflow: saveOverflow(doc.documentElement),\n      x: win.scrollX, y: win.scrollY\n    };\n    root.before(marker);\n    \/\/ Sortir des conteneurs transform\u00e9s d'Elementor pour couvrir toute la fen\u00eatre.\n    doc.body.appendChild(root);\n    root.classList.add('is-app-screen');\n    doc.body.style.setProperty('overflow', 'hidden', 'important');\n    doc.documentElement.style.setProperty('overflow', 'hidden', 'important');\n    root.scrollTop = 0;\n    changed();\n    focusButton();\n  };\n  const leaveApp = () => {\n    if (!appState) return;\n    const saved = appState;\n    appState = null;\n    root.classList.remove('is-app-screen');\n    if (saved.marker.isConnected) saved.marker.replaceWith(root);\n    else if (saved.parent && saved.parent.isConnected) {\n      saved.parent.insertBefore(root, saved.next && saved.next.parentNode === saved.parent ? saved.next : null);\n    }\n    restoreOverflow(doc.body, saved.bodyOverflow);\n    restoreOverflow(doc.documentElement, saved.htmlOverflow);\n    changed();\n    win.scrollTo(saved.x, saved.y);\n    focusButton();\n  };\n  root.addEventListener('click', async event => {\n    const button = event.target.closest && event.target.closest('[data-okk-fullscreen]');\n    if (!button || !root.contains(button)) return;\n    event.preventDefault();\n    if (appState) { leaveApp(); return; }\n    if (nativeFull()) {\n      const exit = doc.exitFullscreen || doc.webkitExitFullscreen;\n      if (exit) {\n        try { await exit.call(doc); } catch (_) { attribute(button, 'title', 'Appuie sur \u00c9chap pour quitter le plein \u00e9cran'); }\n      }\n      return;\n    }\n    const request = root.requestFullscreen || root.webkitRequestFullscreen;\n    if (request) {\n      try { await request.call(root); sync(); return; } catch (_) { \/* Aper\u00e7u ou appareil sans API : mode app. *\/ }\n    }\n    enterApp();\n  });\n  doc.addEventListener('keydown', event => {\n    if (event.key === 'Escape' && appState) {\n      event.preventDefault();\n      event.stopPropagation();\n      leaveApp();\n      return;\n    }\n    if (event.key !== 'Tab' || !active()) return;\n    const items = Array.from(root.querySelectorAll('button:not(:disabled),a[href],input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex=\"0\"]'))\n      .filter(node => !node.hidden && node.getClientRects().length);\n    if (!items.length) return;\n    const first = items[0], last = items[items.length - 1];\n    if (event.shiftKey && (doc.activeElement === first || !root.contains(doc.activeElement))) {\n      event.preventDefault(); last.focus();\n    } else if (!event.shiftKey && (doc.activeElement === last || !root.contains(doc.activeElement))) {\n      event.preventDefault(); first.focus();\n    }\n  }, true);\n  doc.addEventListener('fullscreenchange', changed);\n  doc.addEventListener('webkitfullscreenchange', changed);\n  \/\/ Les barres de jeu (Keystroke notamment) sont reconstruites apr\u00e8s chaque s\u00e9rie.\n  if (typeof MutationObserver !== 'undefined') new MutationObserver(sync).observe(root, { childList: true, subtree: true });\n  sync();\n  return { active };\n})(root);\nheader(); footer(); home();\n\n})();\n<\/script>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-15668","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15668","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=15668"}],"version-history":[{"count":7,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15668\/revisions"}],"predecessor-version":[{"id":15697,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15668\/revisions\/15697"}],"wp:attachment":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/media?parent=15668"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}