{"id":15673,"date":"2026-10-16T20:24:00","date_gmt":"2026-10-16T18:24:00","guid":{"rendered":"https:\/\/www.ok3d.fr\/?page_id=15673"},"modified":"2026-10-16T20:51:39","modified_gmt":"2026-10-16T18:51:39","slug":"light-lab","status":"publish","type":"page","link":"https:\/\/www.ok3d.fr\/en\/light-lab\/","title":{"rendered":"Light lab"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"15673\" class=\"elementor elementor-15673\" 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<!-- Light Lab by OK3D \u2014 contenu centr\u00e9, sans en-t\u00eate int\u00e9gr\u00e9.\n  Coller TOUT ce fichier dans un widget HTML (sans balises html\/head\/body).\n  Page : Elementor pleine largeur pour conserver l'en-t\u00eate du site.\n  Conteneur : pleine largeur, marges\/padding 0,\n  d\u00e9bordement visible. Le contenu conserve son d\u00e9filement vertical.\n  Cadre maximal : 1600px, marges comprises, comme le configurateur OK3D.\n  Header : 94px + 54px d'espace sur ordinateur ; 78px + 43px sur mobile.\n  R\u00e9glages : --okk-content-max, --okk-header-offset et --okk-top-gap.\n-->\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.okk-scope[data-okk=\"lightlab\"]{display:block;width:100%;font-family:var(--f-body);font-size:15px;line-height:1.5;color:var(--ink);background:var(--bg);text-align:left;-webkit-font-smoothing:antialiased}\n.okk-scope[data-okk=\"lightlab\"] *,.okk-scope[data-okk=\"lightlab\"] *::before,.okk-scope[data-okk=\"lightlab\"] *::after{box-sizing:border-box}\n.okk-scope[data-okk=\"lightlab\"] [hidden]{display:none!important}\n.okk-scope[data-okk=\"lightlab\"] img{max-width:100%;height:auto}\n.okk-scope[data-okk=\"lightlab\"] button,.okk-scope[data-okk=\"lightlab\"] input{font-family:inherit;text-transform:none;letter-spacing:normal;min-height:0;box-shadow:none;margin:0}\n.okk-scope[data-okk=\"lightlab\"] h1,.okk-scope[data-okk=\"lightlab\"] h2,.okk-scope[data-okk=\"lightlab\"] h3{color:inherit;text-transform:none;padding:0}\n.okk-scope[data-okk=\"lightlab\"] p{margin:0}\n.okk-scope[data-okk=\"lightlab\"] a{text-decoration:none}\n.okk-scope[data-okk=\"lightlab\"]{\n  color-scheme:dark;\n  --bg:#06080C; --bg2:#0A0E15; --ink:#EEF2F8; --muted:#8792A6; --faint:#566074; --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  --key:#FFC46B; --fill:#7CC4FF; --rim:#FF7AA8; --fond:#A99BFF;\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-scope[data-okk=\"lightlab\"]{background:var(--bg);color:var(--ink)}\n.okk-scope[data-okk=\"lightlab\"]{font-family:var(--f-body);font-size:15px;line-height:1.5;-webkit-font-smoothing:antialiased}\n.okk-scope[data-okk=\"lightlab\"] *{box-sizing:border-box}\n.okk-scope[data-okk=\"lightlab\"] button{font:inherit;color:inherit;cursor:pointer;background:none;border:0;padding:0}\n.okk-scope[data-okk=\"lightlab\"] a{color:inherit}\n.okk-scope[data-okk=\"lightlab\"] a.btn{text-decoration:none}\n.okk-scope[data-okk=\"lightlab\"] :focus-visible{outline:2px solid var(--brand2);outline-offset:3px;border-radius:6px}\n.okk-scope[data-okk=\"lightlab\"] .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:radial-gradient(1100px 560px at 12% -8%,rgba(46,107,255,.18),transparent 62%),radial-gradient(800px 480px at 105% 4%,rgba(255,196,107,.06),transparent 60%),var(--bg)}\n.okk-scope[data-okk=\"lightlab\"] .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);background-size:56px 56px;\n  mask-image:linear-gradient(180deg,#000,transparent 60%);-webkit-mask-image:linear-gradient(180deg,#000,transparent 60%)}\n.okk-scope[data-okk=\"lightlab\"] .wrap{position:relative;max-width:1280px;margin:0 auto}\n.okk-scope[data-okk=\"lightlab\"] .bar{display:flex;align-items:center;justify-content:space-between;gap:12px;padding-block:18px;flex-wrap:wrap}\n.okk-scope[data-okk=\"lightlab\"] .brand{display:flex;align-items:center;gap:12px;text-decoration:none}\n.okk-scope[data-okk=\"lightlab\"] .brand .mk{font:700 17px var(--f-display);letter-spacing:-.02em;display:flex;align-items:center;gap:8px}\n.okk-scope[data-okk=\"lightlab\"] .brand .mk i{width:26px;height:26px;border-radius:9px;display:grid;place-items:center;font-style:normal;font-size:9px;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-scope[data-okk=\"lightlab\"] .brand img{height:26px;width:auto;display:block}\n.okk-scope[data-okk=\"lightlab\"] .brand .pr{font:500 13px var(--f-body);color:var(--muted);padding-left:12px;border-left:1px solid var(--line2)}\n.okk-scope[data-okk=\"lightlab\"] .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-scope[data-okk=\"lightlab\"] .eyebrow::before{content:\"\";width:6px;height:6px;border-radius:2px;background:var(--brand2);box-shadow:0 0 10px var(--brand2)}\n.okk-scope[data-okk=\"lightlab\"] h1,.okk-scope[data-okk=\"lightlab\"] h2,.okk-scope[data-okk=\"lightlab\"] h3{margin:0;font-family:var(--f-display);letter-spacing:-.03em;text-wrap:balance}\n.okk-scope[data-okk=\"lightlab\"] .grad{background:linear-gradient(100deg,#FFD9A0 0%,var(--brand2) 55%,#E9F5FF 100%);-webkit-background-clip:text;background-clip:text;color:transparent}\n.okk-scope[data-okk=\"lightlab\"] .btn{display:inline-flex;align-items:center;justify-content:center;gap:9px;height:44px;padding:0 20px;border-radius:999px;font-weight:600;font-size:14.5px;transition:transform .15s,box-shadow .2s,border-color .2s;white-space:nowrap}\n.okk-scope[data-okk=\"lightlab\"] .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-scope[data-okk=\"lightlab\"] .btn.pri:hover{transform:translateY(-1px)}\n.okk-scope[data-okk=\"lightlab\"] .btn.gho{border:1px solid var(--line2);background:rgba(255,255,255,.02)}\n.okk-scope[data-okk=\"lightlab\"] .btn.gho:hover{border-color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .btn.sm{height:34px;padding:0 13px;font-size:12.5px}\n.okk-scope[data-okk=\"lightlab\"] .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.okk-scope[data-okk=\"lightlab\"] .intro{display:flex;justify-content:space-between;align-items:flex-end;gap:24px;flex-wrap:wrap;padding-block:18px 26px}\n.okk-scope[data-okk=\"lightlab\"] .intro h1{font-size:clamp(32px,4.6vw,60px);line-height:1.02;font-weight:700;margin-top:16px}\n.okk-scope[data-okk=\"lightlab\"] .intro p{color:var(--muted);max-width:52ch;margin:16px 0 0;font-size:clamp(15px,1.3vw,16.5px)}\n.okk-scope[data-okk=\"lightlab\"] .intro .rules{display:flex;gap:10px;flex-wrap:wrap}\n.okk-scope[data-okk=\"lightlab\"] .intro .rules span{font:500 12px var(--f-mono);color:var(--muted);padding:6px 11px;border:1px solid var(--line);border-radius:999px}\n.okk-scope[data-okk=\"lightlab\"] .lab{display:grid;grid-template-columns:minmax(0,1fr) 380px;gap:16px;align-items:start}\n.okk-scope[data-okk=\"lightlab\"] .viewcol{position:sticky;top:12px;display:flex;flex-direction:column;gap:12px}\n.okk-scope[data-okk=\"lightlab\"] .view{position:relative;overflow:hidden;padding:0;border-radius:24px}\n.okk-scope[data-okk=\"lightlab\"] .vp{position:relative;aspect-ratio:16\/10;max-width:100%;background:#0B0E14}\n.okk-scope[data-okk=\"lightlab\"] .vp>canvas{position:absolute;inset:0;width:100%!important;height:100%!important;display:block;touch-action:none;cursor:grab}\n.okk-scope[data-okk=\"lightlab\"] .vp>canvas:active{cursor:grabbing}\n.okk-scope[data-okk=\"lightlab\"] .vp .crn{position:absolute;width:14px;height:14px;border:0 solid rgba(255,255,255,.4);z-index:3;pointer-events:none}\n.okk-scope[data-okk=\"lightlab\"] .c-tl{top:12px;left:12px;border-top-width:1.5px!important;border-left-width:1.5px!important}\n.okk-scope[data-okk=\"lightlab\"] .c-tr{top:12px;right:12px;border-top-width:1.5px!important;border-right-width:1.5px!important}\n.okk-scope[data-okk=\"lightlab\"] .c-bl{bottom:12px;left:12px;border-bottom-width:1.5px!important;border-left-width:1.5px!important}\n.okk-scope[data-okk=\"lightlab\"] .c-br{bottom:12px;right:12px;border-bottom-width:1.5px!important;border-right-width:1.5px!important}\n.okk-scope[data-okk=\"lightlab\"] .hud{position:absolute;z-index:4;display:flex;gap:6px;align-items:center;flex-wrap:wrap}\n.okk-scope[data-okk=\"lightlab\"] .hud.tl{top:14px;left:34px}\n.okk-scope[data-okk=\"lightlab\"] .hud.tr{top:10px;right:32px}\n.okk-scope[data-okk=\"lightlab\"] .hud.bl{bottom:14px;left:34px}\n.okk-scope[data-okk=\"lightlab\"] .tag{font:600 10.5px var(--f-mono);letter-spacing:.12em;text-transform:uppercase;padding:5px 10px;border-radius:999px;background:rgba(6,8,12,.62);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.1);color:#DDE4EE}\n.okk-scope[data-okk=\"lightlab\"] .tag b{color:#fff;font-weight:600}\n.okk-scope[data-okk=\"lightlab\"] .ibtn{height:32px;padding:0 12px;border-radius:999px;font:600 12px var(--f-body);background:rgba(6,8,12,.62);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);border:1px solid rgba(255,255,255,.12);color:#fff;display:inline-flex;align-items:center;gap:6px}\n.okk-scope[data-okk=\"lightlab\"] .ibtn[aria-pressed=\"true\"]{background:rgba(46,107,255,.6);border-color:rgba(124,196,255,.7)}\n.okk-scope[data-okk=\"lightlab\"] .target{position:absolute;right:14px;bottom:14px;z-index:4;width:32%;min-width:120px;border-radius:12px;overflow:hidden;border:1px solid rgba(255,255,255,.25);box-shadow:0 12px 40px rgba(0,0,0,.6);background:#000}\n.okk-scope[data-okk=\"lightlab\"] .target canvas{display:block;width:100%;height:auto}\n.okk-scope[data-okk=\"lightlab\"] .target span{position:absolute;top:6px;left:6px;font:600 9.5px var(--f-mono);letter-spacing:.12em;text-transform:uppercase;padding:3px 7px;border-radius:999px;background:rgba(0,0,0,.6);color:var(--gold)}\n.okk-scope[data-okk=\"lightlab\"] .drop{position:relative;border:1.5px dashed var(--line2);border-radius:16px;padding:16px 14px;text-align:center;display:flex;flex-direction:column;gap:3px;transition:border-color .2s,background-color .2s}\n.okk-scope[data-okk=\"lightlab\"] .drop input{position:absolute;inset:0;opacity:0;cursor:pointer;width:100%;height:100%}\n.okk-scope[data-okk=\"lightlab\"] .drop:hover,.okk-scope[data-okk=\"lightlab\"] .drop.over{border-color:var(--brand2);background:rgba(46,107,255,.08)}\n.okk-scope[data-okk=\"lightlab\"] .drop b{font:700 14px var(--f-display);letter-spacing:-.02em}\n.okk-scope[data-okk=\"lightlab\"] .drop span{font:500 11.5px var(--f-mono);color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .drop small{font-size:12px;color:var(--faint);margin-top:4px}\n.okk-scope[data-okk=\"lightlab\"] .mtools{display:flex;gap:6px;flex-wrap:wrap;align-items:center}\n.okk-scope[data-okk=\"lightlab\"] .mtools[hidden]{display:none!important}\n.okk-scope[data-okk=\"lightlab\"] .minfo{font:500 11px var(--f-mono);color:var(--muted);width:100%;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}\n.okk-scope[data-okk=\"lightlab\"] .busy,.okk-scope[data-okk=\"lightlab\"] .dz{position:absolute;inset:0;z-index:6;display:grid;place-items:center;text-align:center;padding:20px;background:rgba(4,6,10,.72);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);font:600 14px var(--f-body)}\n.okk-scope[data-okk=\"lightlab\"] .busy[hidden],.okk-scope[data-okk=\"lightlab\"] .dz[hidden]{display:none!important}\n.okk-scope[data-okk=\"lightlab\"] .busy i{display:block;width:34px;height:34px;margin:0 auto 12px;border-radius:50%;border:3px solid rgba(124,196,255,.25);border-top-color:var(--brand2);animation:spin .8s linear infinite}\n@keyframes spin{to{transform:rotate(360deg)}}\n.okk-scope[data-okk=\"lightlab\"] .dz{border:2px dashed var(--brand2);border-radius:24px;font:700 18px var(--f-display)}\n.okk-scope[data-okk=\"lightlab\"] .fallback{position:absolute;inset:0;display:grid;place-items:center;text-align:center;padding:24px;color:var(--muted);z-index:5}\n.okk-scope[data-okk=\"lightlab\"] .explain{padding:18px 20px;display:grid;grid-template-columns:minmax(0,1fr) auto;gap:16px;align-items:center}\n.okk-scope[data-okk=\"lightlab\"] .explain h3{font-size:17px;font-weight:700}\n.okk-scope[data-okk=\"lightlab\"] .explain p{margin:6px 0 0;color:var(--muted);font-size:14px;max-width:62ch}\n.okk-scope[data-okk=\"lightlab\"] .explain .tip{margin-top:10px;font:500 12.5px var(--f-mono);color:var(--brand2)}\n.okk-scope[data-okk=\"lightlab\"] .ratio{display:flex;flex-direction:column;align-items:flex-end;gap:2px;text-align:right}\n.okk-scope[data-okk=\"lightlab\"] .ratio b{font:700 26px var(--f-display);letter-spacing:-.03em;font-variant-numeric:tabular-nums}\n.okk-scope[data-okk=\"lightlab\"] .ratio span{font:500 10px var(--f-mono);letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .desk{padding:16px;display:flex;flex-direction:column;gap:14px}\n.okk-scope[data-okk=\"lightlab\"] .planwrap{position:relative;border-radius:18px;border:1px solid var(--line);background:radial-gradient(closest-side,rgba(46,107,255,.08),transparent),#080B11;overflow:hidden}\n.okk-scope[data-okk=\"lightlab\"] .planwrap svg{display:block;width:100%;height:auto;touch-action:none;user-select:none;-webkit-user-select:none}\n.okk-scope[data-okk=\"lightlab\"] .planfoot{display:flex;justify-content:space-between;gap:8px;padding:0 12px 10px;font:600 10px var(--f-mono);letter-spacing:.1em;text-transform:uppercase;color:var(--faint)}\n.okk-scope[data-okk=\"lightlab\"] .planwrap .cap{position:absolute;left:12px;top:10px;font:600 10px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint)}\n.okk-scope[data-okk=\"lightlab\"] .planwrap .cap2{position:absolute;right:12px;top:10px;font:500 10.5px var(--f-mono);color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .ring{fill:none;stroke:#1A2232;stroke-width:1}\n.okk-scope[data-okk=\"lightlab\"] .ring.d{stroke-dasharray:2 4}\n.okk-scope[data-okk=\"lightlab\"] .beam{stroke-width:10;stroke-linecap:round;opacity:.16}\n.okk-scope[data-okk=\"lightlab\"] .ldot{cursor:grab}\n.okk-scope[data-okk=\"lightlab\"] .ldot:active{cursor:grabbing}\n.okk-scope[data-okk=\"lightlab\"] .ldot circle.h{fill:transparent}\n.okk-scope[data-okk=\"lightlab\"] .ldot text{font:700 9px var(--f-mono);fill:#0A0D14;pointer-events:none}\n.okk-scope[data-okk=\"lightlab\"] .ldot.off{opacity:.3}\n.okk-scope[data-okk=\"lightlab\"] .ldot.sel circle.o{stroke:#fff;stroke-width:2}\n.okk-scope[data-okk=\"lightlab\"] .ghost circle{fill:none;stroke-width:1.4;stroke-dasharray:3 2}\n.okk-scope[data-okk=\"lightlab\"] .cam{fill:#2A3549}\n.okk-scope[data-okk=\"lightlab\"] .plab{font:500 9px var(--f-mono);fill:var(--faint)}\n.okk-scope[data-okk=\"lightlab\"] .tabs{display:grid;grid-template-columns:repeat(4,1fr);gap:4px;padding:4px;border:1px solid var(--line);border-radius:14px;background:rgba(0,0,0,.2)}\n.okk-scope[data-okk=\"lightlab\"] .tabs button{padding:8px 4px;border-radius:10px;font-size:12.5px;font-weight:600;color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .tabs button[aria-selected=\"true\"]{background:var(--ink);color:var(--bg)}\n.okk-scope[data-okk=\"lightlab\"] .pane{display:flex;flex-direction:column;gap:14px}\n.okk-scope[data-okk=\"lightlab\"] .pane[hidden]{display:none!important}\n.okk-scope[data-okk=\"lightlab\"] .lbl{font:600 10px var(--f-mono);letter-spacing:.14em;text-transform:uppercase;color:var(--faint);margin-bottom:8px;display:flex;justify-content:space-between;gap:8px}\n.okk-scope[data-okk=\"lightlab\"] .lbl b{color:var(--ink);font-weight:600;letter-spacing:.02em;text-transform:none;font-variant-numeric:tabular-nums}\n.okk-scope[data-okk=\"lightlab\"] .chips{display:flex;gap:6px;flex-wrap:wrap}\n.okk-scope[data-okk=\"lightlab\"] .chip{padding:7px 11px;border-radius:999px;border:1px solid var(--line2);font-size:12.5px;font-weight:500;color:var(--muted);display:inline-flex;align-items:center;gap:7px;transition:all .15s}\n.okk-scope[data-okk=\"lightlab\"] .chip:hover{color:var(--ink)}\n.okk-scope[data-okk=\"lightlab\"] .chip[aria-pressed=\"true\"]{color:var(--ink);border-color:var(--brand);background:rgba(46,107,255,.14)}\n.okk-scope[data-okk=\"lightlab\"] .chip i{width:10px;height:10px;border-radius:50%;display:inline-block;box-shadow:inset 0 0 0 1px rgba(255,255,255,.25)}\n.okk-scope[data-okk=\"lightlab\"] .lsel{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}\n.okk-scope[data-okk=\"lightlab\"] .lsel button{padding:9px 6px;border-radius:12px;border:1px solid var(--line2);font:600 12px var(--f-body);display:flex;flex-direction:column;align-items:center;gap:4px;color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .lsel button i{width:10px;height:10px;border-radius:50%}\n.okk-scope[data-okk=\"lightlab\"] .lsel button[aria-pressed=\"true\"]{color:var(--ink);border-color:var(--ink);background:rgba(255,255,255,.05)}\n.okk-scope[data-okk=\"lightlab\"] .lsel button.off i{opacity:.3}\n.okk-scope[data-okk=\"lightlab\"] .rowc{display:flex;align-items:center;justify-content:space-between;gap:12px}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]{-webkit-appearance:none;appearance:none;width:100%;height:22px;background:transparent;margin:0}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]::-webkit-slider-runnable-track{height:4px;border-radius:4px;background:linear-gradient(90deg,var(--brand2) var(--v,50%),var(--line2) var(--v,50%))}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]::-moz-range-track{height:4px;border-radius:4px;background:var(--line2)}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]::-moz-range-progress{height:4px;border-radius:4px;background:var(--brand2)}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;border-radius:50%;background:#fff;margin-top:-6px;box-shadow:0 0 0 4px rgba(124,196,255,.25)}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:#fff;box-shadow:0 0 0 4px rgba(124,196,255,.25)}\n.okk-scope[data-okk=\"lightlab\"] input[type=range].kel::-webkit-slider-runnable-track{background:linear-gradient(90deg,#FF9A3C,#FFD7A0,#FFF6EA,#DCE8FF,#A9C6FF)}\n.okk-scope[data-okk=\"lightlab\"] input[type=range]:disabled{opacity:.35}\n.okk-scope[data-okk=\"lightlab\"] input[type=color]{width:34px;height:34px;border:1px solid var(--line2);border-radius:50%;padding:0;background:none;cursor:pointer}\n.okk-scope[data-okk=\"lightlab\"] input[type=color]::-webkit-color-swatch-wrapper{padding:3px}\n.okk-scope[data-okk=\"lightlab\"] input[type=color]::-webkit-color-swatch{border:0;border-radius:50%}\n.okk-scope[data-okk=\"lightlab\"] .sw{position:relative;width:40px;height:22px;border-radius:999px;background:var(--line2);transition:background .2s;flex:none}\n.okk-scope[data-okk=\"lightlab\"] .sw::after{content:\"\";position:absolute;top:3px;left:3px;width:16px;height:16px;border-radius:50%;background:#fff;transition:transform .2s}\n.okk-scope[data-okk=\"lightlab\"] input.swi{position:absolute;opacity:0;width:1px;height:1px}\n.okk-scope[data-okk=\"lightlab\"] input.swi:checked+.sw{background:var(--brand)}\n.okk-scope[data-okk=\"lightlab\"] input.swi:checked+.sw::after{transform:translateX(18px)}\n.okk-scope[data-okk=\"lightlab\"] input.swi:focus-visible+.sw{outline:2px solid var(--brand2);outline-offset:2px}\n.okk-scope[data-okk=\"lightlab\"] .tg{display:flex;align-items:center;gap:10px;cursor:pointer;font-size:13.5px}\n.okk-scope[data-okk=\"lightlab\"] .presets{display:grid;grid-template-columns:1fr 1fr;gap:8px}\n.okk-scope[data-okk=\"lightlab\"] .pcard{text-align:left;padding:12px;border-radius:16px;border:1px solid var(--line2);display:flex;flex-direction:column;gap:8px;transition:all .2s;background:rgba(255,255,255,.015)}\n.okk-scope[data-okk=\"lightlab\"] .pcard:hover{border-color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .pcard[aria-pressed=\"true\"]{border-color:var(--brand2);background:rgba(46,107,255,.1);box-shadow:0 0 0 1px var(--brand2)}\n.okk-scope[data-okk=\"lightlab\"] .pcard b{font:700 13px var(--f-display);letter-spacing:-.02em}\n.okk-scope[data-okk=\"lightlab\"] .pcard small{color:var(--muted);font-size:12px;line-height:1.35}\n.okk-scope[data-okk=\"lightlab\"] .pcard .dots{display:flex;gap:4px}\n.okk-scope[data-okk=\"lightlab\"] .pcard .dots i{width:12px;height:12px;border-radius:50%;box-shadow:0 0 10px currentColor}\n.okk-scope[data-okk=\"lightlab\"] .chal{padding:16px;border-radius:16px;border:1px solid rgba(255,203,107,.35);background:linear-gradient(140deg,rgba(255,203,107,.1),rgba(255,203,107,.02))}\n.okk-scope[data-okk=\"lightlab\"] .chal h3{font-size:16px;font-weight:700;color:var(--gold)}\n.okk-scope[data-okk=\"lightlab\"] .chal p{margin:6px 0 0;color:var(--muted);font-size:13.5px}\n.okk-scope[data-okk=\"lightlab\"] .score{display:flex;align-items:flex-end;gap:14px;margin-top:4px}\n.okk-scope[data-okk=\"lightlab\"] .score b{white-space:nowrap;font:700 44px var(--f-display);letter-spacing:-.04em;line-height:1;font-variant-numeric:tabular-nums}\n.okk-scope[data-okk=\"lightlab\"] .score span{color:var(--muted);font-size:13px;padding-bottom:5px}\n.okk-scope[data-okk=\"lightlab\"] .meter{height:6px;border-radius:6px;background:var(--line2);overflow:hidden}\n.okk-scope[data-okk=\"lightlab\"] .meter i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--bad),var(--gold),var(--good));transition:width .8s cubic-bezier(.2,.8,.2,1)}\n.okk-scope[data-okk=\"lightlab\"] .stats{display:flex;gap:18px;font:500 12px var(--f-mono);color:var(--muted)}\n.okk-scope[data-okk=\"lightlab\"] .stats b{color:var(--ink)}\n.okk-scope[data-okk=\"lightlab\"] .rules3{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;margin-top:40px}\n.okk-scope[data-okk=\"lightlab\"] .rules3 article{padding:22px}\n.okk-scope[data-okk=\"lightlab\"] .rules3 .n{font:700 12px var(--f-mono);color:var(--brand2);letter-spacing:.1em}\n.okk-scope[data-okk=\"lightlab\"] .rules3 h3{font-size:17px;font-weight:700;margin-top:10px}\n.okk-scope[data-okk=\"lightlab\"] .rules3 p{color:var(--muted);font-size:14px;margin:8px 0 0}\n.okk-scope[data-okk=\"lightlab\"] .studio{margin-top:16px;padding:clamp(22px,4vw,36px);display:grid;grid-template-columns:minmax(0,1fr) auto;gap:24px;align-items:center;background:linear-gradient(140deg,rgba(46,107,255,.14),rgba(46,107,255,.02))}\n.okk-scope[data-okk=\"lightlab\"] .studio h3{font-size:clamp(20px,2.4vw,28px);font-weight:700}\n.okk-scope[data-okk=\"lightlab\"] .studio p{color:var(--muted);margin:10px 0 0;max-width:58ch}\n.okk-scope[data-okk=\"lightlab\"] .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.okk-scope[data-okk=\"lightlab\"] .shot{position:absolute;inset:0;z-index:20;display:grid;place-items:center;background:rgba(3,4,7,.82);backdrop-filter:blur(6px);padding:16px}\n.okk-scope[data-okk=\"lightlab\"] .shot[hidden]{display:none!important}\n.okk-scope[data-okk=\"lightlab\"] .shot .in{max-width:min(900px,100%);display:flex;flex-direction:column;gap:12px;align-items:center;text-align:center}\n.okk-scope[data-okk=\"lightlab\"] .shot img{max-width:100%;border-radius:14px;border:1px solid var(--line2);box-shadow:0 30px 80px rgba(0,0,0,.6)}\n.okk-scope[data-okk=\"lightlab\"] .shot p{margin:0;color:var(--muted);font-size:13.5px}\n.okk-scope[data-okk=\"lightlab\"] .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;text-align:center;max-width:90%}\n@media (max-width:1020px){.okk-scope[data-okk=\"lightlab\"] .lab{grid-template-columns:1fr}\n.okk-scope[data-okk=\"lightlab\"] .viewcol{position:relative;top:0}}\n@media (max-width:760px){.okk-scope[data-okk=\"lightlab\"] .rules3{grid-template-columns:1fr}\n.okk-scope[data-okk=\"lightlab\"] .studio{grid-template-columns:1fr}\n.okk-scope[data-okk=\"lightlab\"] .explain{grid-template-columns:1fr}\n.okk-scope[data-okk=\"lightlab\"] .ratio{align-items:flex-start;text-align:left}}\n@media (max-width:560px){.okk-scope[data-okk=\"lightlab\"] .ibtn .t{display:none}\n.okk-scope[data-okk=\"lightlab\"] .ibtn{width:34px;justify-content:center;padding:0}\n.okk-scope[data-okk=\"lightlab\"] .vp{aspect-ratio:4\/3}\n.okk-scope[data-okk=\"lightlab\"] .bar .pr{display:none}\n.okk-scope[data-okk=\"lightlab\"] .presets{grid-template-columns:1fr 1fr}\n.okk-scope[data-okk=\"lightlab\"] .target{width:38%}\n.okk-scope[data-okk=\"lightlab\"] .hud.tl{left:30px;top:12px}}\n@media (prefers-reduced-motion:reduce){.okk-scope[data-okk=\"lightlab\"] *{transition:none!important;animation:none!important}}\n\n\/* OK3D \u2014 fond pleine largeur, contenu centr\u00e9 et largeur ma\u00eetris\u00e9e.\n   M\u00eame cadre de 1600px et m\u00eames marges que le configurateur OK3D.\n   --okk-content-max r\u00e8gle la largeur maximale du cadre, marges comprises.\n   Le fond seul suit la largeur de l'\u00e9cran, m\u00eame sur un \u00e9cran tr\u00e8s large. *\/\n.okk-scope[data-okk=\"lightlab\"] {\n  --okk-content-max: 1600px;\n  --okk-gutter: clamp(22px, 5vw, 88px);\n  --okk-header-offset: 94px;\n  --okk-top-gap: 54px;\n  position: relative;\n  left: var(--okk-shift, 0px);\n  width: var(--okk-width, 100%) !important;\n  max-width: none !important;\n  min-width: 0;\n  min-height: 100vh;\n  min-height: 100dvh;\n  margin: 0 !important;\n  padding: 0 !important;\n}\n.okk-scope[data-okk=\"lightlab\"] .okk {\n  width: 100%;\n  overflow: clip;\n  min-height: 100vh;\n  min-height: 100dvh;\n  padding-left: 0;\n  padding-right: 0;\n  padding-top: calc(var(--okk-header-offset) + var(--okk-top-gap));\n  padding-bottom: max(28px, env(safe-area-inset-bottom, 0px));\n  scroll-margin-top: calc(var(--okk-header-offset) + var(--okk-admin-height, 0px) + 24px);\n}\n.okk-scope[data-okk=\"lightlab\"] .wrap {\n  width: 100%;\n  max-width: var(--okk-content-max);\n  margin: 0 auto;\n  padding-left: max(var(--okk-gutter), env(safe-area-inset-left, 0px));\n  padding-right: max(var(--okk-gutter), env(safe-area-inset-right, 0px));\n}\n.okk-scope[data-okk=\"lightlab\"] .intro { padding-top: 0; }\n.okk-scope[data-okk=\"lightlab\"] .lab { grid-template-columns: minmax(0, 1fr) clamp(360px, 28%, 400px); }\n.okk-scope[data-okk=\"lightlab\"] .lab > *, .okk-scope[data-okk=\"lightlab\"] .intro > *, .okk-scope[data-okk=\"lightlab\"] .studio > *,\n.okk-scope[data-okk=\"lightlab\"] .explain > *, .okk-scope[data-okk=\"lightlab\"] .rowc > *, .okk-scope[data-okk=\"lightlab\"] .res > *,\n.okk-scope[data-okk=\"lightlab\"] .cta > *, .okk-scope[data-okk=\"lightlab\"] .ghead > *, .okk-scope[data-okk=\"lightlab\"] .fb > * { min-width: 0; }\n.okk-scope[data-okk=\"lightlab\"] .viewcol { top: calc(var(--okk-header-offset) + var(--okk-admin-height, 0px) + 12px); }\n.okk-scope[data-okk=\"lightlab\"] .intro h1, .okk-scope[data-okk=\"lightlab\"] .studio h3,\n.okk-scope[data-okk=\"lightlab\"] .drop span, .okk-scope[data-okk=\"lightlab\"] .planfoot { overflow-wrap: anywhere; }\n.okk-scope[data-okk=\"lightlab\"] .rowc > .chips { flex: 1 1 0; }\n.okk-scope[data-okk=\"lightlab\"] input[type=color] { flex: 0 0 34px; }\n.okk-scope[data-okk=\"lightlab\"] .btn { max-width: 100%; height: auto; min-height: 44px; padding-block: 10px; white-space: normal; text-align: center; }\n.okk-scope[data-okk=\"lightlab\"] .btn.sm { min-height: 34px; padding-block: 6px; }\n.okk-scope[data-okk=\"lightlab\"] .explain .btn { white-space: normal; }\n.okk-scope[data-okk=\"lightlab\"] .bar .screen-btn { margin-left: auto; }\n.okk-scope[data-okk=\"lightlab\"] .game { width: 100%; max-width: 680px; margin-inline: auto; }\n.okk-scope[data-okk=\"lightlab\"] .icons .ico span { overflow-wrap: anywhere; }\n.okk-scope[data-okk=\"lightlab\"] .hint {\n  width: max-content;\n  max-width: calc(100% - 24px);\n  white-space: normal;\n  text-align: center;\n}\n.okk-scope[data-okk=\"lightlab\"] .fb .tx { overflow-wrap: anywhere; }\n.okk-scope[data-okk=\"lightlab\"] .shot { overflow-y: auto; }\n.okk-scope[data-okk=\"lightlab\"] .shot img, .okk-scope[data-okk=\"lightlab\"] .shot video { max-height: 60vh; max-height: 60dvh; object-fit: contain; }\n.okk-scope[data-okk=\"lightlab\"]:fullscreen {\n  left: 0 !important;\n  width: 100% !important;\n  height: 100vh;\n  height: 100dvh;\n  overflow: auto;\n  overscroll-behavior: contain;\n  --okk-admin-height: 0px !important;\n  --okk-header-offset: 0px !important;\n  --okk-top-gap: 32px !important;\n}\n.okk-scope[data-okk=\"lightlab\"]:-webkit-full-screen {\n  left: 0 !important;\n  width: 100% !important;\n  height: 100vh;\n  overflow: auto;\n  --okk-admin-height: 0px !important;\n  --okk-header-offset: 0px !important;\n  --okk-top-gap: 32px !important;\n}\n\/* Ces ajustements ne concernent que le widget contenant ce laboratoire. *\/\n.elementor-widget-html:has(.okk-scope[data-okk=\"lightlab\"]) { width: 100%; min-width: 0; padding: 0; }\n.elementor-widget-container:has(> .okk-scope[data-okk=\"lightlab\"]) { padding: 0; margin: 0; }\n@media (max-width: 1020px) {\n  .okk-scope[data-okk=\"lightlab\"] .lab { grid-template-columns: minmax(0, 1fr); }\n  .okk-scope[data-okk=\"lightlab\"] .viewcol { position: relative; top: 0; }\n}\n@media (max-width: 700px) {\n  .okk-scope[data-okk=\"lightlab\"] { --okk-header-offset: 78px; --okk-top-gap: 43px; }\n}\n@media (max-width: 560px) {\n  .okk-scope[data-okk=\"lightlab\"] .bar { gap: 10px; }\n  .okk-scope[data-okk=\"lightlab\"] .bar > a.btn { flex-basis: 100%; }\n  .okk-scope[data-okk=\"lightlab\"] .intro { padding-block: 0 22px; }\n  .okk-scope[data-okk=\"lightlab\"] .intro h1 { font-size: clamp(26px, 7.6vw, 40px); }\n  .okk-scope[data-okk=\"lightlab\"] .desk { padding: 12px; }\n  .okk-scope[data-okk=\"lightlab\"] .planfoot { flex-wrap: wrap; }\n  .okk-scope[data-okk=\"lightlab\"] .explain { padding: 16px; }\n  .okk-scope[data-okk=\"lightlab\"] .stats { flex-wrap: wrap; }\n  .okk-scope[data-okk=\"lightlab\"] .hud.tr { right: 12px; }\n  .okk-scope[data-okk=\"lightlab\"] .hud.tl { left: 14px; top: auto; bottom: 54px; }\n}\n\n\/* Affichage app lorsque le plein \u00e9cran natif n'est pas disponible. *\/\n.okk-scope[data-okk=\"lightlab\"].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-scope[data-okk=\"lightlab\"]:fullscreen, .okk-scope[data-okk=\"lightlab\"].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  \n}\n.okk-scope[data-okk=\"lightlab\"]:-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  \n}\n.okk-scope[data-okk=\"lightlab\"] .appbar {\n  display: flex; justify-content: flex-end; align-items: center; gap: 12px;\n  flex-wrap: wrap; padding: 0 0 18px;\n}\n.okk-scope[data-okk=\"lightlab\"] .screen-btn { margin-left: auto; flex: 0 0 auto; }\n.okk-scope[data-okk=\"lightlab\"]:fullscreen .appbar, .okk-scope[data-okk=\"lightlab\"].is-app-screen .appbar,\n.okk-scope[data-okk=\"lightlab\"]:fullscreen .bar, .okk-scope[data-okk=\"lightlab\"].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\n<\/style>\n<div class=\"okk-scope\" data-okk=\"lightlab\">\n<div class=\"okk\" id=\"okk\">\n  <div class=\"wrap\">\n    <div class=\"appbar\"><button type=\"button\" class=\"btn gho sm screen-btn\" data-okk-fullscreen aria-pressed=\"false\">\u26f6 Plein \u00e9cran<\/button><\/div>\n\n    <section class=\"intro\">\n      <div>\n        <span class=\"eyebrow\">OK3D Lab \u00b7 Studio lumi\u00e8re<\/span>\n        <h1>\u00c9claire comme un studio.<br><span class=\"grad\">Dans ton navigateur.<\/span><\/h1>\n        <p>Importe ton propre mod\u00e8le 3D ou pars d'un objet de d\u00e9mo, place les lumi\u00e8res sur le plan et regarde le rendu r\u00e9agir en direct. Six ambiances de studio pour apprendre, un d\u00e9fi pour t'entra\u00eener l'\u0153il.<\/p>\n      <\/div>\n      <div class=\"rules\"><span>Ton mod\u00e8le 3D<\/span><span>Temps r\u00e9el<\/span><span>Reflets fid\u00e8les<\/span><span>6 ambiances<\/span><span>Mode d\u00e9fi<\/span><\/div>\n    <\/section>\n\n    <section class=\"lab\">\n      <div class=\"viewcol\">\n        <div class=\"glass view\">\n          <div class=\"vp\" id=\"vp\">\n            <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            <div class=\"hud tl\"><span class=\"tag\" id=\"ptag\">Trois points<\/span><\/div>\n            <div class=\"hud tr\"><button type=\"button\" class=\"ibtn\" id=\"imp\" aria-label=\"Importer ton mod\u00e8le\">\u21ea<span class=\"t\"> Ton mod\u00e8le<\/span><\/button><button type=\"button\" class=\"ibtn\" id=\"spin\" aria-pressed=\"false\" aria-label=\"Tournette\">\u21bb<span class=\"t\"> Tournette<\/span><\/button><button type=\"button\" class=\"ibtn\" id=\"snap\" aria-label=\"Capture\">\u25c9<span class=\"t\"> Capture<\/span><\/button><\/div>\n            <div class=\"hud bl\"><span class=\"tag\" id=\"ctag\">50 mm \u00b7 \u0192\/8<\/span><\/div>\n            <div class=\"busy\" id=\"busy\" hidden><div><i><\/i><span id=\"busyt\">Chargement\u2026<\/span><\/div><\/div><div class=\"dz\" id=\"dz\" hidden>D\u00e9pose ton mod\u00e8le 3D<\/div><div class=\"target\" id=\"target\" hidden><span>Cible<\/span><canvas id=\"tcan\" width=\"320\" height=\"200\"><\/canvas><\/div>\n            <div class=\"shot\" id=\"shot\" hidden><div class=\"in\"><img id=\"shotimg\" alt=\"Capture du rendu\"><p id=\"shotp\">Clic droit ou appui long sur l'image pour l'enregistrer.<\/p><div style=\"display:flex;gap:8px;flex-wrap:wrap;justify-content:center\"><a class=\"btn pri sm\" id=\"shotdl\" download=\"ok3d-lightlab.png\" hidden>T\u00e9l\u00e9charger<\/a><button type=\"button\" class=\"btn gho sm\" id=\"shotx\">Fermer<\/button><\/div><\/div><\/div>\n          <\/div>\n        <\/div>\n        <div class=\"glass explain\">\n          <div><h3 id=\"ex-t\">Trois points<\/h3><p id=\"ex-p\"><\/p><div class=\"tip\" id=\"ex-tip\"><\/div><\/div>\n          <div class=\"ratio\"><b id=\"ratio\">3:1<\/b><span>Ratio key \/ fill<\/span><\/div>\n        <\/div>\n      <\/div>\n\n      <aside class=\"glass desk\">\n        <div class=\"planwrap\"><svg id=\"plan\" viewBox=\"0 0 300 260\" aria-label=\"Plan des lumi\u00e8res, \u00e0 faire glisser\"><\/svg><div class=\"planfoot\"><span>Plan de studio \u00b7 vue de dessus<\/span><span>Glisse les lumi\u00e8res<\/span><\/div><\/div>\n        <div class=\"tabs\" role=\"tablist\">\n          <button type=\"button\" role=\"tab\" data-tab=\"amb\" aria-selected=\"true\">Ambiances<\/button>\n          <button type=\"button\" role=\"tab\" data-tab=\"lum\" aria-selected=\"false\">Lumi\u00e8res<\/button>\n          <button type=\"button\" role=\"tab\" data-tab=\"prod\" aria-selected=\"false\">Produit<\/button>\n          <button type=\"button\" role=\"tab\" data-tab=\"defi\" aria-selected=\"false\">D\u00e9fi<\/button>\n        <\/div>\n        <div class=\"pane\" id=\"p-amb\"><div class=\"presets\" id=\"presets\"><\/div><\/div>\n        <div class=\"pane\" id=\"p-lum\" hidden>\n          <div class=\"lsel\" id=\"lsel\"><\/div>\n          <label class=\"tg\"><input type=\"checkbox\" class=\"swi\" id=\"l-on\"><span class=\"sw\"><\/span><span id=\"l-on-t\">Allum\u00e9e<\/span><\/label>\n          <div><div class=\"lbl\">Intensit\u00e9 <b id=\"v-i\"><\/b><\/div><input type=\"range\" id=\"l-i\" min=\"0\" max=\"2\" step=\"0.01\"><\/div>\n          <div><div class=\"lbl\">Hauteur <b id=\"v-el\"><\/b><\/div><input type=\"range\" id=\"l-el\" min=\"-15\" max=\"85\" step=\"1\"><\/div>\n          <div><div class=\"lbl\">Distance <b id=\"v-d\"><\/b><\/div><input type=\"range\" id=\"l-d\" min=\"2\" max=\"7\" step=\"0.05\"><\/div>\n          <div><div class=\"lbl\">Temp\u00e9rature <b id=\"v-t\"><\/b><\/div><input type=\"range\" class=\"kel\" id=\"l-t\" min=\"2500\" max=\"9500\" step=\"50\"><\/div>\n          <div><div class=\"lbl\">G\u00e9latine<\/div><div class=\"chips\" id=\"gels\"><\/div><\/div>\n        <\/div>\n        <div class=\"pane\" id=\"p-prod\" hidden>\n          <div><div class=\"lbl\">Objet<\/div><div class=\"chips\" id=\"models\"><\/div><\/div>\n          <div class=\"drop\" id=\"drop\"><input type=\"file\" id=\"mfile\" multiple accept=\".glb,.gltf,.obj,.mtl,.fbx,.stl,.ply,.dae,.3ds,.bin,.png,.jpg,.jpeg,.tga,.webp\" aria-label=\"Importer un mod\u00e8le 3D\"><b>Importer ton mod\u00e8le 3D<\/b><span>GLB \u00b7 glTF \u00b7 FBX \u00b7 OBJ (+MTL) \u00b7 STL \u00b7 PLY \u00b7 DAE \u00b7 3DS<\/span><small>Le fichier reste dans ton navigateur, rien n'est envoy\u00e9.<\/small><\/div>\n          <div class=\"mtools\" id=\"mtools\" hidden><button type=\"button\" class=\"chip\" id=\"m-up\">Redresser (Z vers le haut)<\/button><button type=\"button\" class=\"chip\" id=\"m-rot\">Pivoter de 90\u00b0<\/button><span class=\"minfo\" id=\"m-info\"><\/span><\/div>\n          <div><div class=\"lbl\">Mati\u00e8re<\/div><div class=\"chips\" id=\"mats\"><\/div><\/div>\n          <div><div class=\"lbl\">Couleur du produit<\/div><div class=\"rowc\"><div class=\"chips\" id=\"cols\"><\/div><input type=\"color\" id=\"col\" aria-label=\"Couleur personnalis\u00e9e\"><\/div><\/div>\n          <div><div class=\"lbl\">Fond de studio<\/div><div class=\"rowc\"><div class=\"chips\" id=\"bgs\"><\/div><input type=\"color\" id=\"bgc\" aria-label=\"Fond personnalis\u00e9\"><\/div><\/div>\n          <div><div class=\"lbl\">Focale<\/div><div class=\"chips\" id=\"focals\"><\/div><\/div>\n          <div><div class=\"lbl\">Exposition <b id=\"v-ex\"><\/b><\/div><input type=\"range\" id=\"ex\" min=\"0.4\" max=\"2\" step=\"0.01\"><\/div>\n        <\/div>\n        <div class=\"pane\" id=\"p-defi\" hidden>\n          <div class=\"chal\"><h3>Retrouve l'\u00e9clairage<\/h3><p>Une image cible s'affiche dans le viewport. D\u00e9place et r\u00e8gle tes lumi\u00e8res pour la reproduire, puis compare. \u00c0 partir de <span id=\"d-win\">80<\/span> %, le d\u00e9fi est r\u00e9ussi.<\/p><\/div>\n          <div style=\"display:flex;gap:8px;flex-wrap:wrap\"><button type=\"button\" class=\"btn pri\" id=\"d-new\">Nouveau d\u00e9fi<\/button><button type=\"button\" class=\"btn gho\" id=\"d-cmp\" disabled>Comparer<\/button><button type=\"button\" class=\"btn gho\" id=\"d-rev\" disabled>R\u00e9v\u00e9ler<\/button><\/div>\n          <div id=\"d-res\" hidden><div class=\"score\"><b id=\"d-score\">0 %<\/b><span id=\"d-msg\"><\/span><\/div><div class=\"meter\"><i id=\"d-bar\"><\/i><\/div><\/div>\n          <div class=\"stats\"><span>R\u00e9ussis <b id=\"d-won\">0<\/b><\/span><span>Meilleur <b id=\"d-best\">\u2014<\/b><\/span><\/div>\n        <\/div>\n        <div style=\"display:flex;gap:8px;flex-wrap:wrap\"><button type=\"button\" class=\"btn gho sm\" id=\"copy\">Copier la fiche d'\u00e9clairage<\/button><button type=\"button\" class=\"btn gho sm\" id=\"reset\">R\u00e9initialiser la vue<\/button><\/div>\n      <\/aside>\n    <\/section>\n\n    <section class=\"rules3\">\n      <article class=\"glass\"><span class=\"n\">R\u00c8GLE 01<\/span><h3>Plus la source est proche, plus elle est douce.<\/h3><p>Rapproche une lumi\u00e8re sur le plan : ses ombres s'adoucissent et son reflet grandit sur le produit. \u00c9loigne-la : la lumi\u00e8re devient dure et pr\u00e9cise.<\/p><\/article>\n      <article class=\"glass\"><span class=\"n\">R\u00c8GLE 02<\/span><h3>Le ratio key \/ fill r\u00e8gle le contraste.<\/h3><p>\u00c0 2:1 le rendu reste doux et commercial. \u00c0 8:1 il devient dramatique. Le ratio s'affiche sous le viewport pendant que tu r\u00e8gles.<\/p><\/article>\n      <article class=\"glass\"><span class=\"n\">R\u00c8GLE 03<\/span><h3>Le contre-jour d\u00e9tache le produit du fond.<\/h3><p>Une lumi\u00e8re plac\u00e9e derri\u00e8re dessine un liser\u00e9 sur les bords. C'est elle qui s\u00e9pare un flacon sombre d'un fond sombre.<\/p><\/article>\n    <\/section>\n\n    <section class=\"glass studio\">\n      <div><span class=\"eyebrow\">Studio OK3D<\/span><h3 style=\"margin-top:12px\">Ton produit m\u00e9rite ce niveau de lumi\u00e8re.<\/h3>\n        <p>OK3D r\u00e9alise des packshots 3D, des films produits et des visuels de lancement \u00e0 partir de tes fichiers CAO ou de simples photos.<\/p><\/div>\n      <a class=\"btn pri\" id=\"cta-bottom\" href=\"#\" target=\"_blank\" rel=\"noopener\">Demander un devis \u2192<\/a>\n    <\/section>\n    <footer class=\"foot\"><span>Light Lab \u00b7 un outil OK3D Studio, Paris<\/span><span>Rendu temps r\u00e9el WebGL<\/span><\/footer>\n  <\/div>\n<\/div>\n<\/div>\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"><\/script>\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.128.0\/examples\/js\/controls\/OrbitControls.js\"><\/script>\n<script>\n(function(){\nconst OKK=document.querySelector('[data-okk=\"lightlab\"]:not([data-okk-ready])');\nif(!OKK) return;\nOKK.setAttribute('data-okk-ready', 'true');\n\n\n\/* =====================================================================\n   CONFIG \u2014 r\u00e9glages modifiables\n   ===================================================================== *\/\nconst CONFIG = {\n  brand: \"OK3D\",\n  logoUrl: \"https:\/\/www.ok3d.fr\/wp-content\/uploads\/2026\/09\/ok3d_logo_arrondi_blanc.png\",                                   \/\/ logo blanc (version WordPress)\n  siteUrl: \"https:\/\/www.ok3d.fr\",\n  contactUrl: \"https:\/\/www.ok3d.fr\/#contact\",\n  allowDownload: true,                          \/\/ true sur WordPress : bouton \u00ab T\u00e9l\u00e9charger \u00bb sur les captures\n  winScore: 80,                                  \/\/ % minimum pour r\u00e9ussir un d\u00e9fi\n  storageKey: \"okk-lightlab-v1\"\n};\n\n\/* =====================================================================\n   PRESETS \u2014 az : angle autour du produit (0 = c\u00f4t\u00e9 cam\u00e9ra, + = droite), el : hauteur (\u00b0), d : distance (m), i : intensit\u00e9, t : Kelvin, gel : couleur\n   ===================================================================== *\/\nconst L0={on:true,az:0,el:30,d:4,i:1,t:5600,gel:null};\nconst PRESETS=[\n  {id:\"trois\",name:\"Trois points\",short:\"Le classique : key, fill, contre-jour.\",bg:\"#1A1F29\",ex:1,\n   desc:\"La key, \u00e0 40\u00b0 du produit, sculpte la forme. La fill, de l'autre c\u00f4t\u00e9 et plus faible, d\u00e9bouche les ombres. Le contre-jour dessine un liser\u00e9 qui d\u00e9tache l'objet du fond.\",\n   tip:\"Essaie de baisser la fill \u00e0 0,1 : le produit gagne en relief, l'ambiance devient plus dramatique.\",\n   lights:{key:{az:40,el:35,d:3.5,i:1,t:5200},fill:{az:-55,el:15,d:4.5,i:.32,t:6200},rim:{az:160,el:40,d:4,i:1.15,t:6800},fond:{i:.45,t:5600}}},\n  {id:\"pack\",name:\"Packshot e-commerce\",short:\"Fond clair, lumi\u00e8re douce et \u00e9gale.\",bg:\"#EEF0F3\",ex:1.05,\n   desc:\"Deux grandes sources proches, \u00e0 gauche et \u00e0 droite, enveloppent le produit d'une lumi\u00e8re douce. Une lumi\u00e8re de fond blanchit le cyclo pour un d\u00e9tourage facile.\",\n   tip:\"Ratio proche de 1:1 : c'est voulu. Le client doit voir le produit, pas la lumi\u00e8re.\",\n   lights:{key:{az:68,el:30,d:2.6,i:.85,t:5600},fill:{az:-68,el:30,d:2.6,i:.8,t:5600},rim:{az:180,el:70,d:3.5,i:.45,t:5600},fond:{i:1,t:5600}}},\n  {id:\"rim\",name:\"Contre-jour dramatique\",short:\"Deux liser\u00e9s, aucune face \u00e9clair\u00e9e.\",bg:\"#05070A\",ex:1.1,\n   desc:\"Pas de key : deux lumi\u00e8res plac\u00e9es derri\u00e8re, de chaque c\u00f4t\u00e9, ne touchent que les bords. Le volume se lit uniquement par ses contours.\",\n   tip:\"Passe le produit en chrome ou en verre : les liser\u00e9s deviennent de longues lignes de lumi\u00e8re.\",\n   lights:{key:{on:false,az:30,el:30,d:4,i:.6,t:5600},fill:{az:-150,el:25,d:3,i:1.35,t:7200},rim:{az:150,el:25,d:3,i:1.45,t:7200},fond:{on:false,i:.3,t:5600}}},\n  {id:\"clam\",name:\"Clamshell beaut\u00e9\",short:\"Lumi\u00e8re frontale haute et basse.\",bg:\"#D8CFC6\",ex:1,\n   desc:\"Une source au-dessus et une autre en dessous, toutes deux face au produit, forment une coquille de lumi\u00e8re. Les ombres disparaissent, les mati\u00e8res brillent sans duret\u00e9.\",\n   tip:\"Le ratio reste faible : la fill basse remplit les ombres sous l'objet.\",\n   lights:{key:{az:0,el:55,d:2.8,i:1,t:5400},fill:{az:0,el:-5,d:2.8,i:.45,t:5400},rim:{az:180,el:30,d:4,i:.55,t:6000},fond:{i:.7,t:4800}}},\n  {id:\"neon\",name:\"N\u00e9on cyber\",short:\"G\u00e9latines satur\u00e9es, fond nuit.\",bg:\"#0A0B1A\",ex:1.05,\n   desc:\"Deux g\u00e9latines compl\u00e9mentaires, magenta et cyan, se croisent sur le produit. Un violet en contre-jour unifie la sc\u00e8ne.\",\n   tip:\"Les couleurs compl\u00e9mentaires se renforcent : magenta et cyan, orange et bleu.\",\n   lights:{key:{az:75,el:15,d:3,i:1.1,gel:\"#FF2E88\"},fill:{az:-75,el:15,d:3,i:1,gel:\"#22D3FF\"},rim:{az:180,el:45,d:4,i:.7,gel:\"#7C5CFF\"},fond:{i:.6,gel:\"#7C5CFF\"}}},\n  {id:\"gold\",name:\"Golden hour\",short:\"Soleil rasant chaud, ombre froide.\",bg:\"#2A1E17\",ex:1.05,\n   desc:\"Une key tr\u00e8s chaude et tr\u00e8s basse imite un soleil de fin de journ\u00e9e. Une fill froide et faible rappelle le ciel dans les ombres.\",\n   tip:\"Le contraste chaud\/froid donne plus de profondeur qu'un simple contraste de lumi\u00e8re.\",\n   lights:{key:{az:85,el:8,d:4.5,i:1.4,t:2800},fill:{az:-60,el:25,d:5,i:.22,t:9200},rim:{az:150,el:10,d:5,i:.6,t:3000},fond:{i:.35,t:3200}}},\n];\nconst GELS=[[null,\"Aucune\"],[\"#FFB347\",\"Ambre\"],[\"#FF2E88\",\"Magenta\"],[\"#22D3FF\",\"Cyan\"],[\"#7C5CFF\",\"Violet\"],[\"#3CFF9A\",\"Vert\"]];\nconst MODELS=[[\"flacon\",\"Flacon\"],[\"canette\",\"Canette\"],[\"sphere\",\"Sph\u00e8re\"],[\"noeud\",\"N\u0153ud\"]];\nlet custom=null;\nconst MATS=[[\"chrome\",\"Chrome\"],[\"alu\",\"Alu satin\u00e9\"],[\"ceram\",\"C\u00e9ramique\"],[\"plast\",\"Plastique brillant\"],[\"verre\",\"Verre\"]];\nconst COLS=[\"#C9D2E0\",\"#1B1E24\",\"#E9E2D6\",\"#2E6BFF\",\"#C1272D\",\"#2F8F6B\"];\nconst BGS=[[\"#1A1F29\",\"Ardoise\"],[\"#EEF0F3\",\"Blanc studio\"],[\"#05070A\",\"Noir\"],[\"#D8CFC6\",\"Sable\"],[\"#0A0B1A\",\"Nuit\"]];\nconst FOCALS=[35,50,85,135];\nconst LNAMES={key:\"Key\",fill:\"Fill\",rim:\"Contre\",fond:\"Fond\"};\nconst LCOL={key:\"var(--key)\",fill:\"var(--fill)\",rim:\"var(--rim)\",fond:\"var(--fond)\"};\n\n\/* ---------- state ---------- *\/\nconst clone=o=>JSON.parse(JSON.stringify(o));\nfunction presetLights(p){ const o={}; for(const k of [\"key\",\"fill\",\"rim\",\"fond\"]) o[k]=Object.assign({},L0,k===\"fond\"?{az:180,el:20,d:4}:{},p.lights[k]||{}); return o }\nconst ST={preset:\"trois\",lights:presetLights(PRESETS[0]),bg:PRESETS[0].bg,ex:1,model:\"flacon\",mat:\"chrome\",col:\"#C9D2E0\",focal:50,spin:false,sel:\"key\"};\nlet SAVE={won:0,best:0}; try{ SAVE=Object.assign(SAVE,JSON.parse(localStorage.getItem(CONFIG.storageKey))||{}) }catch(e){}\nconst persist=()=>{ try{ localStorage.setItem(CONFIG.storageKey,JSON.stringify(SAVE)) }catch(e){} };\nconst $=s=>OKK.querySelector(s);\n\n\/* ---------- colour helpers ---------- *\/\nfunction kelvin(k){ const t=k\/100; let r,g,b;\n  if(t<=66){ r=255; g=99.4708025861*Math.log(t)-161.1195681661; b=t<=19?0:138.5177312231*Math.log(t-10)-305.0447927307 }\n  else { r=329.698727446*Math.pow(t-60,-.1332047592); g=288.1221695283*Math.pow(t-60,-.0755148492); b=255 }\n  const c=v=>Math.max(0,Math.min(255,v))\/255; return [c(r),c(g),c(b)] }\nconst hex2rgb=h=>[1,3,5].map(i=>parseInt(h.slice(i,i+2),16)\/255);\nconst rgb2hex=c=>\"#\"+c.map(v=>Math.round(Math.max(0,Math.min(1,v))*255).toString(16).padStart(2,\"0\")).join(\"\");\nconst lightRGB=l=>l.gel?hex2rgb(l.gel):kelvin(l.t);\nconst lerp=(a,b,t)=>a+(b-a)*t;\nconst angLerp=(a,b,t)=>{ let d=((b-a+540)%360)-180; return a+d*t };\nconst eff=l=>(l.on?l.i:0)*Math.pow(3.5\/l.d,2);\nfunction resolve(lights){ const o={}; for(const k in lights){ const l=lights[k]; o[k]={az:l.az,el:l.el,d:l.d,ie:l.on?l.i:0,rgb:lightRGB(l)} } return o }\n\n\/* =====================================================================\n   THREE.JS SCENE\n   ===================================================================== *\/\nconst vp=$(\"#vp\");\nlet envBg=null,R,scene,camera,controls,pmrem,envRT,envScene,product,prodMats={},spots={},envPanels={},cyc,cycMat,envFloor,envDirty=true,lastEnv=0,dirty=true,cur=null;\nconst TGT=new THREE.Vector3(0,.72,0);\nfunction lin(hex){ return new THREE.Color(hex).convertSRGBToLinear() }\n\nfunction initThree(){\n  R=new THREE.WebGLRenderer({antialias:true,preserveDrawingBuffer:false});\n  R.setPixelRatio(Math.min(devicePixelRatio||1,2)); R.outputEncoding=THREE.sRGBEncoding; R.toneMapping=THREE.ACESFilmicToneMapping;\n  R.shadowMap.enabled=true; R.shadowMap.type=THREE.PCFSoftShadowMap; vp.insertBefore(R.domElement,vp.firstChild);\n  scene=new THREE.Scene();\n  camera=new THREE.PerspectiveCamera(27,16\/10,.1,100);\n  controls=new THREE.OrbitControls(camera,R.domElement); controls.target.copy(TGT); controls.enableDamping=true; controls.dampingFactor=.08; controls.enablePan=false;\n  controls.minPolarAngle=.35; controls.maxPolarAngle=1.5; controls.minAzimuthAngle=-1.15; controls.maxAzimuthAngle=1.15;\n  controls.addEventListener(\"change\",()=>dirty=true);\n  \/\/ cyclorama\n  const prof=[]; for(let z=14;z>=-4;z-=1) prof.push([0,z]); for(let i=1;i<=24;i++){ const a=-Math.PI\/2+i\/24*Math.PI\/2; prof.push([5+5*Math.sin(a),-4-5*Math.cos(a)]) } for(let y=6;y<=18;y+=2) prof.push([y,-9]);\n  const pos=[],idx=[]; prof.forEach(([y,z])=>{ pos.push(-22,y,z, 22,y,z) }); for(let i=0;i<prof.length-1;i++){ const a=i*2; idx.push(a,a+1,a+2, a+1,a+3,a+2) }\n  const g=new THREE.BufferGeometry(); g.setAttribute(\"position\",new THREE.Float32BufferAttribute(pos,3)); g.setIndex(idx); g.computeVertexNormals();\n  cycMat=new THREE.MeshStandardMaterial({color:lin(ST.bg),roughness:.92,metalness:0,envMapIntensity:.4}); cyc=new THREE.Mesh(g,cycMat); cyc.receiveShadow=true; scene.add(cyc);\n  \/\/ lights\n  for(const k of [\"key\",\"fill\",\"rim\",\"fond\"]){ const s=new THREE.SpotLight(0xffffff,1,0,.95,.6,1); s.target.position.copy(k===\"fond\"?new THREE.Vector3(0,2.6,-9):TGT);\n    if(k===\"key\"||k===\"rim\"){ s.castShadow=true; s.shadow.mapSize.set(1024,1024); s.shadow.bias=-.0004; s.shadow.normalBias=.02; s.shadow.camera.near=.5; s.shadow.camera.far=20 }\n    if(k===\"fond\") s.angle=.75;\n    scene.add(s); scene.add(s.target); spots[k]=s }\n  scene.add(new THREE.AmbientLight(0xffffff,.015));\n  \/\/ environment scene (reflections follow the lights)\n  pmrem=new THREE.PMREMGenerator(R); envScene=new THREE.Scene();\n  const efl=new THREE.Mesh(new THREE.PlaneGeometry(60,60),new THREE.MeshBasicMaterial({color:0x000000,side:THREE.DoubleSide})); efl.rotation.x=-Math.PI\/2; efl.position.y=-.01; envScene.add(efl); envFloor=efl;\n  for(const k of [\"key\",\"fill\",\"rim\",\"fond\"]){ const m=new THREE.Mesh(new THREE.PlaneGeometry(1,1),new THREE.MeshBasicMaterial({color:0xffffff,side:THREE.DoubleSide})); envScene.add(m); envPanels[k]=m }\n  buildProduct(); setFocal(ST.focal,true); resize();\n  new ResizeObserver(resize).observe(vp);\n  cur=resolve(ST.lights); applyLights(cur); applyLook();\n  loop();\n}\nfunction resize(){ if(!R) return; const w=vp.clientWidth, h=vp.clientHeight; if(!w||!h) return; R.setSize(w,h,false); camera.aspect=w\/h; camera.updateProjectionMatrix(); dirty=true }\n\n\/* ---------- products ---------- *\/\nfunction lathe(pts,seg=96){ return new THREE.LatheGeometry(pts.map(([x,y])=>new THREE.Vector2(x,y)),seg) }\nfunction smooth(pts,n=6){ \/\/ rounded corners on a polyline via simple chaikin\n  let p=pts; for(let k=0;k<2;k++){ const q=[p[0]]; for(let i=0;i<p.length-1;i++){ const a=p[i],b=p[i+1]; q.push([a[0]*.75+b[0]*.25,a[1]*.75+b[1]*.25],[a[0]*.25+b[0]*.75,a[1]*.25+b[1]*.75]) } q.push(p[p.length-1]); p=q } return p }\nfunction makeMat(kind,color){ const c=lin(color);\n  switch(kind){\n    case \"chrome\": return new THREE.MeshStandardMaterial({color:c.clone().lerp(new THREE.Color(1,1,1),.55),metalness:1,roughness:.06});\n    case \"alu\": return new THREE.MeshStandardMaterial({color:c.clone().lerp(new THREE.Color(1,1,1),.35),metalness:.95,roughness:.34});\n    case \"ceram\": return new THREE.MeshPhysicalMaterial({color:c,metalness:0,roughness:.5,clearcoat:.15,clearcoatRoughness:.4});\n    case \"plast\": return new THREE.MeshPhysicalMaterial({color:c,metalness:0,roughness:.28,clearcoat:1,clearcoatRoughness:.06});\n    case \"verre\": return new THREE.MeshPhysicalMaterial({color:c.clone().lerp(new THREE.Color(1,1,1),.8),metalness:0,roughness:.03,transmission:1,transparent:true,opacity:1,clearcoat:1,clearcoatRoughness:0,envMapIntensity:1.6,depthWrite:false});\n  } }\nfunction buildProduct(){ if(!R) return;\n  if(product){ scene.remove(product); product.traverse(o=>{ if(o.userData.keep) return; if(o.geometry) o.geometry.dispose(); if(o.material&&!o.userData.keepMat) o.material.dispose() }) }\n  product=new THREE.Group();\n  if(ST.model===\"custom\"&&custom){ buildCustom(); scene.add(product); dirty=true; envDirty=true; return }\n  const m=makeMat(ST.mat===\"orig\"?\"ceram\":ST.mat,ST.col); const add=(geo,mat,y=0)=>{ const me=new THREE.Mesh(geo,mat); me.position.y=y; me.castShadow=true; me.receiveShadow=true; product.add(me); return me };\n  if(ST.model===\"flacon\"){\n    add(lathe(smooth([[0,0],[.52,0],[.6,.06],[.6,.96],[.5,1.06],[.2,1.12],[.17,1.16],[.17,1.26],[0,1.26]])),m);\n    if(ST.mat===\"verre\") add(lathe(smooth([[0,.06],[.5,.06],[.53,.1],[.53,.7],[0,.7]])),new THREE.MeshPhysicalMaterial({color:lin(\"#E0A040\"),roughness:.1,transmission:.6,metalness:0}));\n    add(lathe(smooth([[0,1.24],[.24,1.24],[.27,1.28],[.27,1.62],[.22,1.66],[0,1.66]])),new THREE.MeshStandardMaterial({color:lin(ST.mat===\"chrome\"?\"#16181C\":\"#C8A35A\"),metalness:ST.mat===\"chrome\"?.2:1,roughness:ST.mat===\"chrome\"?.35:.22}));\n  } else if(ST.model===\"canette\"){\n    add(lathe(smooth([[0,.02],[.34,.02],[.4,.1],[.43,.22],[.43,1.2],[.36,1.34],[.33,1.36],[.33,1.4],[0,1.4]])),m);\n  } else if(ST.model===\"sphere\"){ add(new THREE.SphereGeometry(.68,96,64),m,.68) }\n  else { const me=add(new THREE.TorusKnotGeometry(.44,.16,260,40),m,.86); me.rotation.x=.35 }\n  scene.add(product); dirty=true; envDirty=true;\n}\nfunction setFocal(f,instant){ ST.focal=f; if(!R) return; const fov=2*Math.atan(12\/f)*180\/Math.PI; camera.fov=fov; const dist=1.22\/Math.tan(fov*Math.PI\/360);\n  const dir=instant?new THREE.Vector3(0,.26,1).normalize():camera.position.clone().sub(controls.target).normalize();\n  camera.position.copy(controls.target).add(dir.multiplyScalar(dist)); controls.minDistance=dist*.6; controls.maxDistance=dist*1.6; camera.updateProjectionMatrix();\n  $(\"#ctag\").textContent=`${f} mm \u00b7 \u0192\/8`; dirty=true }\n\n\/* ---------- lights ---------- *\/\nfunction posOf(l,k){ const a=l.az*Math.PI\/180, e=l.el*Math.PI\/180; if(k===\"fond\") return new THREE.Vector3(0,.35,-1.7);\n  return new THREE.Vector3(l.d*Math.cos(e)*Math.sin(a), .6+l.d*Math.sin(e), l.d*Math.cos(e)*Math.cos(a)) }\nfunction applyLights(res){ if(!R) return;\n  for(const k in res){ const l=res[k], s=spots[k], p=posOf(l,k); s.position.copy(p);\n    const c=new THREE.Color(...l.rgb).convertSRGBToLinear(); s.color.copy(c);\n    const soft=Math.max(0,Math.min(1,1-(l.d-2)\/5));\n    s.intensity=k===\"fond\"?l.ie*3.2:l.ie*2.4*Math.pow(3.5\/l.d,2); s.penumbra=k===\"fond\"?1:.25+.75*soft;\n    const pn=envPanels[k]; const ed=k===\"fond\"?6:6; const dir=k===\"fond\"?new THREE.Vector3(0,.25,-1).normalize():p.clone().sub(TGT).normalize();\n    pn.position.copy(dir.multiplyScalar(ed)); pn.lookAt(0,.6,0); const sz=k===\"fond\"?6:2.2*(4.4\/l.d); pn.scale.set(sz,sz*1.25,1);\n    pn.material.color.copy(c).multiplyScalar(k===\"fond\"?l.ie*.6:l.ie*3.2*Math.pow(3.5\/l.d,2)); pn.visible=l.ie>0.001 }\n  envDirty=true; dirty=true;\n}\nfunction applyLook(){ if(!R) return; cycMat.color.copy(lin(ST.bg)); envFloor.material.color.copy(lin(ST.bg)).multiplyScalar(.6); envBg=new THREE.Color(ST.bg).multiplyScalar(.85);\n  R.toneMappingExposure=ST.ex; envDirty=true; dirty=true }\nfunction updateEnv(force){ const now=performance.now(); if(!force&&now-lastEnv<90) return; lastEnv=now; envDirty=false;\n  envScene.background=envBg.clone(); \/* r128 PMREM clears scene.background after use *\/ const rt=pmrem.fromScene(envScene,.035); if(envRT) envRT.dispose(); envRT=rt; scene.environment=rt.texture; dirty=true }\nlet tween=null;\nfunction loop(){ requestAnimationFrame(loop);\n  if(tween){ const t=Math.min(1,(performance.now()-tween.t0)\/tween.d), e=t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)\/2; const o={};\n    for(const k in tween.a){ const a=tween.a[k], b=tween.b[k]; o[k]={az:angLerp(a.az,b.az,e),el:lerp(a.el,b.el,e),d:lerp(a.d,b.d,e),ie:lerp(a.ie,b.ie,e),rgb:a.rgb.map((v,i)=>lerp(v,b.rgb[i],e))} }\n    cur=o; applyLights(o); drawPlan(); if(t>=1){ tween=null; cur=resolve(ST.lights) } }\n  if(ST.spin&&product){ product.rotation.y+=.008; dirty=true }\n  if(controls.update()) dirty=true;\n  if(envDirty) updateEnv(false);\n  if(dirty){ R.render(scene,camera); dirty=false }\n}\nfunction animateTo(lights,d=750){ tween={t0:performance.now(),d,a:cur||resolve(ST.lights),b:resolve(lights)}; ST.lights=lights }\nfunction setNow(){ cur=resolve(ST.lights); applyLights(cur); drawPlan(); syncLightUI(); updateRatio() }\n\n\/* =====================================================================\n   PLAN (top view, draggable)\n   ===================================================================== *\/\nconst plan=$(\"#plan\"), NS=\"http:\/\/www.w3.org\/2000\/svg\", CX=150, CY=124;\nconst rOf=d=>34+(d-2)\/5*84, dOf=r=>Math.max(2,Math.min(7,2+(r-34)\/84*5));\nfunction el(t,a,p){ const e=document.createElementNS(NS,t); for(const k in a) e.setAttribute(k,a[k]); if(p) p.appendChild(e); return e }\nlet ghost=null;\nfunction drawPlan(){\n  plan.innerHTML=\"\";\n  const defs=el(\"defs\",{},plan);\n  [2,3.5,5,7].forEach((d,i)=>el(\"circle\",{cx:CX,cy:CY,r:rOf(d),class:\"ring\"+(i%2?\" d\":\"\")},plan));\n  [2,4,6].forEach(d=>{ const t=el(\"text\",{x:CX+rOf(d)+3,y:CY-3,class:\"plab\"},plan); t.textContent=d+\" m\" });\n  el(\"rect\",{x:20,y:10,width:260,height:4,rx:2,fill:\"#1E2638\"},plan); const tw=el(\"text\",{x:CX,y:24,\"text-anchor\":\"middle\",class:\"plab\"},plan); tw.textContent=\"cyclo\";\n  \/\/ camera\n  el(\"path\",{d:`M${CX-12} 246 h24 v-12 l8 -5 v22 l-8 -5 v0 h-24 z`,class:\"cam\"},plan); const ct=el(\"text\",{x:CX+40,y:244,class:\"plab\"},plan); ct.textContent=\"cam\u00e9ra\";\n  el(\"line\",{x1:CX,y1:228,x2:CX,y2:CY+18,stroke:\"#1E2638\",\"stroke-dasharray\":\"2 3\"},plan);\n  if(ghost){ for(const k of [\"key\",\"fill\",\"rim\"]){ const l=ghost[k]; if(!l.on) continue; const r=rOf(l.d), a=l.az*Math.PI\/180; const g=el(\"g\",{class:\"ghost\"},plan); el(\"circle\",{cx:CX+r*Math.sin(a),cy:CY+r*Math.cos(a),r:11,stroke:getComputedStyle(OKK).getPropertyValue(LCOL[k].slice(4,-1))},g) } }\n  const res=cur||resolve(ST.lights);\n  for(const k of [\"key\",\"fill\",\"rim\"]){ const l=ST.lights[k], rl=res[k], r=rOf(rl.d), a=rl.az*Math.PI\/180, x=CX+r*Math.sin(a), y=CY+r*Math.cos(a), hex=rgb2hex(rl.rgb);\n    const gid=\"g\"+k; const lg=el(\"linearGradient\",{id:gid,x1:x,y1:y,x2:CX,y2:CY,gradientUnits:\"userSpaceOnUse\"},defs); el(\"stop\",{offset:0,\"stop-color\":hex},lg); el(\"stop\",{offset:1,\"stop-color\":hex,\"stop-opacity\":0},lg);\n    if(l.on){ const bw=8+14*(1-(rl.d-2)\/5); const ang=Math.atan2(CY-y,CX-x), nx=-Math.sin(ang), ny=Math.cos(ang), sp=bw;\n      el(\"polygon\",{points:`${x+nx*4},${y+ny*4} ${x-nx*4},${y-ny*4} ${CX-nx*sp},${CY-ny*sp} ${CX+nx*sp},${CY+ny*sp}`,fill:`url(#${gid})`,opacity:.35*Math.min(1,rl.ie+.2)},plan) } }\n  \/\/ product\n  el(\"circle\",{cx:CX,cy:CY,r:15,fill:\"#2A3549\",stroke:\"#7CC4FF\",\"stroke-width\":1},plan);\n  for(const k of [\"key\",\"fill\",\"rim\"]){ const l=ST.lights[k], rl=res[k], r=rOf(rl.d), a=rl.az*Math.PI\/180, x=CX+r*Math.sin(a), y=CY+r*Math.cos(a);\n    const g=el(\"g\",{class:\"ldot\"+(l.on?\"\":\" off\")+(ST.sel===k?\" sel\":\"\"),\"data-k\":k,tabindex:0,role:\"slider\",\"aria-label\":`${LNAMES[k]} : ${Math.round(l.az)}\u00b0, ${l.d.toFixed(1)} m`},plan);\n    el(\"circle\",{cx:x,cy:y,r:22,class:\"h\"},g); el(\"circle\",{cx:x,cy:y,r:16,fill:rgb2hex(rl.rgb),opacity:.18},g);\n    el(\"circle\",{cx:x,cy:y,r:11,fill:rgb2hex(rl.rgb),class:\"o\"},g); const t=el(\"text\",{x,y:y+3.2,\"text-anchor\":\"middle\"},g); t.textContent=k===\"rim\"?\"C\":k[0].toUpperCase() }\n  const fl=ST.lights.fond; const fb=el(\"g\",{opacity:fl.on?1:.3},plan); el(\"circle\",{cx:CX,cy:46,r:6,fill:rgb2hex(lightRGB(fl))},fb); const ft=el(\"text\",{x:CX+12,y:49,class:\"plab\"},fb); ft.textContent=\"fond\";\n}\nlet drag=null;\nplan.addEventListener(\"pointerdown\",e=>{ const g=e.target.closest(\".ldot\"); if(!g) return; e.preventDefault(); drag=g.dataset.k; selectLight(drag); plan.setPointerCapture(e.pointerId); tween=null; cur=null });\nplan.addEventListener(\"pointermove\",e=>{ if(!drag) return; const r=plan.getBoundingClientRect(), x=(e.clientX-r.left)\/r.width*300-CX, y=(e.clientY-r.top)\/r.height*260-CY;\n  const l=ST.lights[drag]; l.az=Math.round(Math.atan2(x,y)*180\/Math.PI); l.d=Math.round(dOf(Math.hypot(x,y))*20)\/20; markCustom(); setNow() });\nconst endDrag=()=>{ drag=null }; plan.addEventListener(\"pointerup\",endDrag); plan.addEventListener(\"pointercancel\",endDrag);\nplan.addEventListener(\"keydown\",e=>{ const g=e.target.closest(\".ldot\"); if(!g) return; const l=ST.lights[g.dataset.k]; const m={ArrowLeft:[-5,0],ArrowRight:[5,0],ArrowUp:[0,-.25],ArrowDown:[0,.25]}[e.key]; if(!m) return; e.preventDefault();\n  l.az+=m[0]; l.d=Math.max(2,Math.min(7,l.d+m[1])); markCustom(); setNow(); plan.querySelector(`[data-k=\"${g.dataset.k}\"]`)?.focus() });\n\n\/* =====================================================================\n   UI\n   ===================================================================== *\/\nfunction setRange(id,v,txt){ const r=$(id); r.value=v; r.style.setProperty(\"--v\",((v-r.min)\/(r.max-r.min)*100)+\"%\"); if(txt!==undefined) $(id.replace(\"#l-\",\"#v-\").replace(\"#ex\",\"#v-ex\")).textContent=txt }\nfunction selectLight(k){ ST.sel=k; OKK.querySelectorAll(\"#lsel button\").forEach(b=>b.setAttribute(\"aria-pressed\",b.dataset.k===k)); syncLightUI(); drawPlan() }\nfunction syncLightUI(){ const l=ST.lights[ST.sel]; $(\"#l-on\").checked=l.on;\n  setRange(\"#l-i\",l.i,l.i.toFixed(2)); setRange(\"#l-el\",l.el,Math.round(l.el)+\"\u00b0\"); setRange(\"#l-d\",l.d,l.d.toFixed(1)+\" m\"); setRange(\"#l-t\",l.t,l.gel?\"g\u00e9latine\":Math.round(l.t)+\" K\");\n  $(\"#l-t\").disabled=!!l.gel; $(\"#l-el\").disabled=$(\"#l-d\").disabled=ST.sel===\"fond\";\n  OKK.querySelectorAll(\"#gels .chip\").forEach(c=>c.setAttribute(\"aria-pressed\",(c.dataset.g||null)===(l.gel||null)));\n  OKK.querySelectorAll(\"#lsel button\").forEach(b=>b.classList.toggle(\"off\",!ST.lights[b.dataset.k].on)) }\nfunction updateRatio(){ const k=eff(ST.lights.key), f=eff(ST.lights.fill); let t;\n  if(k<.02&&f<.02) t=\"\u2014\"; else if(f<.02) t=\"\u221e\"; else { const r=k\/f; t=r>=1?`${r>=9.5?Math.round(r):Math.round(r*2)\/2}:1`.replace(\".\",\",\"):`1:${Math.round(1\/r*2)\/2}`.replace(\".\",\",\") } $(\"#ratio\").textContent=t }\nfunction markCustom(){ if(ST.preset!==\"perso\"){ ST.preset=\"perso\"; $(\"#ptag\").textContent=\"R\u00e9glage perso\"; OKK.querySelectorAll(\".pcard\").forEach(c=>c.setAttribute(\"aria-pressed\",\"false\")) } }\nfunction showPreset(p){ $(\"#ex-t\").textContent=p.name; $(\"#ex-p\").textContent=p.desc; $(\"#ex-tip\").textContent=\"\u2192 \"+p.tip; $(\"#ptag\").textContent=p.name }\nfunction applyPreset(id){ const p=PRESETS.find(x=>x.id===id); ST.preset=id; OKK.querySelectorAll(\".pcard\").forEach(c=>c.setAttribute(\"aria-pressed\",c.dataset.p===id));\n  animateTo(presetLights(p)); ST.bg=p.bg; ST.ex=p.ex; applyLook(); syncProdUI(); showPreset(p); syncLightUI(); updateRatio() }\nfunction chipRow(sel,items,attr,onClick){ const box=$(sel); box.innerHTML=items.map(([v,l,sw])=>`<button type=\"button\" class=\"chip\" data-${attr}=\"${v??\"\"}\">${sw?`<i style=\"background:${sw}\"><\/i>`:\"\"}${l}<\/button>`).join(\"\");\n  box.querySelectorAll(\".chip\").forEach(c=>c.onclick=()=>onClick(c.dataset[attr])) }\nfunction pressed(sel,attr,val){ OKK.querySelectorAll(sel+\" .chip\").forEach(c=>c.setAttribute(\"aria-pressed\",String(c.dataset[attr])===String(val))) }\nfunction syncProdUI(){ $(\"#mtools\").hidden=ST.model!==\"custom\"; pressed(\"#models\",\"m\",ST.model); pressed(\"#mats\",\"t\",ST.mat); pressed(\"#cols\",\"c\",ST.col); pressed(\"#bgs\",\"b\",ST.bg); pressed(\"#focals\",\"f\",ST.focal);\n  $(\"#col\").value=ST.col; $(\"#bgc\").value=ST.bg; setRange(\"#ex\",ST.ex); $(\"#v-ex\").textContent=ST.ex.toFixed(2) }\n\nfunction buildUI(){\n  $(\"#presets\").innerHTML=PRESETS.map(p=>{ const dots=[\"key\",\"fill\",\"rim\"].map(k=>{ const l=presetLights(p)[k]; return l.on?`<i style=\"background:${rgb2hex(lightRGB(l))};color:${rgb2hex(lightRGB(l))}\"><\/i>`:\"\" }).join(\"\");\n    return `<button type=\"button\" class=\"pcard\" data-p=\"${p.id}\" aria-pressed=\"${p.id===ST.preset}\"><span class=\"dots\">${dots}<\/span><b>${p.name}<\/b><small>${p.short}<\/small><\/button>` }).join(\"\");\n  OKK.querySelectorAll(\".pcard\").forEach(c=>c.onclick=()=>applyPreset(c.dataset.p));\n  $(\"#lsel\").innerHTML=[\"key\",\"fill\",\"rim\",\"fond\"].map(k=>`<button type=\"button\" data-k=\"${k}\" aria-pressed=\"${k===ST.sel}\"><i style=\"background:${LCOL[k]}\"><\/i>${LNAMES[k]}<\/button>`).join(\"\");\n  OKK.querySelectorAll(\"#lsel button\").forEach(b=>b.onclick=()=>selectLight(b.dataset.k));\n  chipRow(\"#gels\",GELS.map(([v,l])=>[v,l,v||\"conic-gradient(#FF9A3C,#FFF6EA,#A9C6FF,#FF9A3C)\"]),\"g\",v=>{ ST.lights[ST.sel].gel=v||null; markCustom(); setNow() });\n  $(\"#l-on\").onchange=e=>{ ST.lights[ST.sel].on=e.target.checked; markCustom(); setNow() };\n  const bind=(id,key,fmt)=>{ $(id).oninput=e=>{ ST.lights[ST.sel][key]=+e.target.value; markCustom(); setNow() } };\n  bind(\"#l-i\",\"i\"); bind(\"#l-el\",\"el\"); bind(\"#l-d\",\"d\"); bind(\"#l-t\",\"t\");\n  renderModelChips();\n  chipRow(\"#cols\",COLS.map(c=>[c,\"\",c]),\"c\",v=>{ ST.col=v; buildProduct(); syncProdUI() });\n  $(\"#col\").oninput=e=>{ ST.col=e.target.value; buildProduct(); syncProdUI() };\n  chipRow(\"#bgs\",BGS.map(([c,l])=>[c,l,c]),\"b\",v=>{ ST.bg=v; applyLook(); syncProdUI() });\n  $(\"#bgc\").oninput=e=>{ ST.bg=e.target.value; applyLook(); syncProdUI() };\n  chipRow(\"#focals\",FOCALS.map(f=>[f,f+\" mm\"]),\"f\",v=>{ setFocal(+v); syncProdUI() });\n  $(\"#ex\").oninput=e=>{ ST.ex=+e.target.value; applyLook(); syncProdUI() };\n  OKK.querySelectorAll(\"[data-tab]\").forEach(b=>b.onclick=()=>{ OKK.querySelectorAll(\"[data-tab]\").forEach(x=>x.setAttribute(\"aria-selected\",x===b)); [\"amb\",\"lum\",\"prod\",\"defi\"].forEach(t=>$(\"#p-\"+t).hidden=t!==b.dataset.tab) });\n  $(\"#spin\").onclick=()=>{ ST.spin=!ST.spin; $(\"#spin\").setAttribute(\"aria-pressed\",ST.spin) };\n  $(\"#reset\").onclick=()=>{ if(product) product.rotation.y=0; setFocal(ST.focal,true); controls.target.copy(TGT); dirty=true };\n  $(\"#snap\").onclick=capture; $(\"#shotx\").onclick=()=>$(\"#shot\").hidden=true;\n  $(\"#copy\").onclick=copySheet;\n  $(\"#d-new\").onclick=newChallenge; $(\"#d-cmp\").onclick=compare; $(\"#d-rev\").onclick=reveal;\n  $(\"#d-win\").textContent=CONFIG.winScore; $(\"#d-won\").textContent=SAVE.won; $(\"#d-best\").textContent=SAVE.best?SAVE.best+\" %\":\"\u2014\";\n  [\"#cta-bottom\"].forEach(s=>$(s).href=CONFIG.contactUrl);\n\n  syncProdUI(); syncLightUI(); showPreset(PRESETS[0]); updateRatio(); drawPlan();\n}\n\n\/* ---------- capture & sheet ---------- *\/\nfunction capture(){ if(!R) return; R.render(scene,camera); const url=R.domElement.toDataURL(\"image\/png\"); $(\"#shotimg\").src=url; $(\"#shot\").hidden=false;\n  const dl=$(\"#shotdl\"); dl.hidden=!CONFIG.allowDownload; if(CONFIG.allowDownload) dl.href=url; $(\"#shotp\").textContent=CONFIG.allowDownload?\"Ton rendu est pr\u00eat.\":\"Clic droit ou appui long sur l'image pour l'enregistrer.\" }\nfunction copySheet(){ const p=PRESETS.find(x=>x.id===ST.preset);\n  const L=[\"key\",\"fill\",\"rim\",\"fond\"].map(k=>{ const l=ST.lights[k]; if(!l.on) return `${LNAMES[k]} : \u00e9teinte`; return k===\"fond\"?`${LNAMES[k]} : intensit\u00e9 ${l.i.toFixed(2)}, ${l.gel?\"g\u00e9latine \"+l.gel:Math.round(l.t)+\" K\"}`\n    :`${LNAMES[k]} : ${Math.round(l.az)}\u00b0, hauteur ${Math.round(l.el)}\u00b0, ${l.d.toFixed(1)} m, intensit\u00e9 ${l.i.toFixed(2)}, ${l.gel?\"g\u00e9latine \"+l.gel:Math.round(l.t)+\" K\"}` });\n  const txt=[`Fiche d'\u00e9clairage \u00b7 ${p?p.name:\"R\u00e9glage perso\"}`,`Ratio key\/fill ${$(\"#ratio\").textContent} \u00b7 Focale ${ST.focal} mm \u00b7 Fond ${ST.bg}`,...L,`Objet : ${ST.model===\"custom\"&&custom?custom.name:MODELS.find(m=>m[0]===ST.model)[1]} \u00b7 Mati\u00e8re : ${ST.mat===\"orig\"?\"d'origine\":MATS.find(m=>m[0]===ST.mat)[1]} \u00b7 Light Lab by ${CONFIG.brand} \u00b7 ${CONFIG.siteUrl}`].join(\"\\n\");\n  navigator.clipboard.writeText(txt).then(()=>toast(\"Fiche d'\u00e9clairage copi\u00e9e\")).catch(()=>toast(\"Copie impossible dans cette fen\u00eatre\")) }\nfunction toast(t){ const d=document.createElement(\"div\"); d.className=\"toast\"; d.textContent=t; $(\"#okk\").appendChild(d); setTimeout(()=>d.remove(),2400) }\n\n\/* =====================================================================\n   D\u00c9FI \u2014 the target is rendered from hidden lights; the score compares pixels\n   ===================================================================== *\/\nlet CH=null; const SW=96, SH=60; let rtSmall=null;\nfunction rnd(a,b){ return a+Math.random()*(b-a) }\nfunction newChallenge(){ if(!R) return;\n  const p=PRESETS[Math.random()*PRESETS.length|0], L=presetLights(p);\n  for(const k of [\"key\",\"fill\",\"rim\"]){ const l=L[k]; if(!l.on) continue; l.az=Math.round(l.az+rnd(-35,35)); l.el=Math.round(Math.max(-10,Math.min(80,l.el+rnd(-12,12)))); l.d=+Math.max(2,Math.min(7,l.d+rnd(-1,1))).toFixed(2); l.i=+Math.min(2,l.i*rnd(.75,1.3)).toFixed(2) }\n  const camPos=new THREE.Vector3(0,.26,1).normalize().multiplyScalar(1.22\/Math.tan(camera.fov*Math.PI\/360)).add(TGT);\n  CH={lights:L,camPos,tries:0}; ghost=null;\n  if(product) product.rotation.y=0; ST.spin=false; $(\"#spin\").setAttribute(\"aria-pressed\",\"false\");\n  \/\/ neutral start for the player\n  const start=presetLights(PRESETS[0]); [\"key\",\"fill\",\"rim\",\"fond\"].forEach(k=>{ start[k].i=k===\"fond\"?.4:.7; start[k].t=5600 }); start.fond.on=L.fond.on; start.fond.gel=L.fond.gel; start.fond.t=L.fond.t; start.fond.i=L.fond.i;\n  ST.lights=start; ST.preset=\"perso\"; $(\"#ptag\").textContent=\"D\u00e9fi en cours\"; OKK.querySelectorAll(\".pcard\").forEach(c=>c.setAttribute(\"aria-pressed\",\"false\"));\n  \/\/ target thumbnail\n  withPose(()=>{ applyLights(resolve(L)); updateEnv(true); R.render(scene,camera); const tc=$(\"#tcan\"); tc.width=320; tc.height=Math.round(320*R.domElement.height\/R.domElement.width); const x=tc.getContext(\"2d\"); x.drawImage(R.domElement,0,0,tc.width,tc.height) });\n  setNow(); updateEnv(true); $(\"#target\").hidden=false; $(\"#d-cmp\").disabled=false; $(\"#d-rev\").disabled=false; $(\"#d-res\").hidden=true;\n  $(\"#ex-t\").textContent=\"D\u00e9fi : retrouve l'\u00e9clairage\"; $(\"#ex-p\").textContent=`Indice : la cible s'inspire d'une ambiance de la liste, avec des r\u00e9glages d\u00e9cal\u00e9s. Fond et contre-jour de fond sont d\u00e9j\u00e0 en place.`; $(\"#ex-tip\").textContent=\"\u2192 Commence par la direction de la key : regarde de quel c\u00f4t\u00e9 tombent les ombres.\";\n}\nfunction withPose(fn){ const p=camera.position.clone(), t=controls.target.clone(), ry=product?product.rotation.y:0; camera.position.copy(CH.camPos); controls.target.copy(TGT); camera.lookAt(TGT); if(product) product.rotation.y=0;\n  fn(); camera.position.copy(p); controls.target.copy(t); camera.lookAt(t); if(product) product.rotation.y=ry; dirty=true }\nfunction pixels(lights){ if(!rtSmall) rtSmall=new THREE.WebGLRenderTarget(SW,SH); const buf=new Uint8Array(SW*SH*4);\n  withPose(()=>{ applyLights(resolve(lights)); updateEnv(true); const a=camera.aspect; camera.aspect=SW\/SH; camera.updateProjectionMatrix(); R.setRenderTarget(rtSmall); R.render(scene,camera); R.readRenderTargetPixels(rtSmall,0,0,SW,SH,buf); R.setRenderTarget(null); camera.aspect=a; camera.updateProjectionMatrix() });\n  return buf }\nfunction similarity(){ const a=pixels(CH.lights), b=pixels(ST.lights); let s=0,n=0; for(let i=0;i<a.length;i+=4){ for(let c=0;c<3;c++){ s+=Math.abs(a[i+c]-b[i+c]) } n+=3 }\n  const mad=s\/n; return Math.max(0,Math.min(100,Math.round(100*Math.exp(-mad\/22)))) }\nfunction compare(){ if(!CH) return; const sc=similarity(); CH.tries++; setNow(); updateEnv(true);\n  $(\"#d-res\").hidden=false; $(\"#d-bar\").style.width=sc+\"%\"; const won=sc>=CONFIG.winScore;\n  let v=0; const t0=performance.now(); const step=n=>{ const t=Math.min(1,(n-t0)\/700); v=Math.round(sc*(1-Math.pow(1-t,3))); $(\"#d-score\").textContent=v+\" %\"; if(t<1) requestAnimationFrame(step) }; requestAnimationFrame(step);\n  $(\"#d-msg\").textContent=won?\"D\u00e9fi r\u00e9ussi. \u0152il de directeur photo.\":sc>=70?\"Presque. Affine l'intensit\u00e9 et la hauteur.\":\"Regarde o\u00f9 tombent les ombres et d'o\u00f9 vient le reflet.\";\n  if(won&&!CH.won){ CH.won=true; SAVE.won++; } if(sc>SAVE.best) SAVE.best=sc; persist(); $(\"#d-win\").textContent=CONFIG.winScore; $(\"#d-won\").textContent=SAVE.won; $(\"#d-best\").textContent=SAVE.best+\" %\" }\nfunction reveal(){ if(!CH) return; ghost=CH.lights; drawPlan(); toast(\"Les positions cibles sont en pointill\u00e9s sur le plan\") }\n\n\n\/* =====================================================================\n   IMPORT DE MOD\u00c8LE \u2014 les loaders three.js sont charg\u00e9s \u00e0 la demande\n   ===================================================================== *\/\nconst LBASE=\"https:\/\/cdn.jsdelivr.net\/npm\/three@0.128.0\/examples\/js\/\";\nconst FMT={glb:\"GLTFLoader\",gltf:\"GLTFLoader\",obj:\"OBJLoader\",fbx:\"FBXLoader\",stl:\"STLLoader\",ply:\"PLYLoader\",dae:\"ColladaLoader\",\"3ds\":\"TDSLoader\"};\nconst DEPS={GLTFLoader:[\"libs\/meshopt_decoder.js\",\"loaders\/DRACOLoader.js\"],OBJLoader:[\"loaders\/MTLLoader.js\"],FBXLoader:[\"libs\/fflate.min.js\",\"curves\/NURBSUtils.js\",\"curves\/NURBSCurve.js\"],ColladaLoader:[\"loaders\/TGALoader.js\"]};\nconst scriptP={};\nfunction loadScript(u){ return scriptP[u]||(scriptP[u]=new Promise((res,rej)=>{ const sc=document.createElement(\"script\"); sc.src=u; sc.async=true; sc.onload=res; sc.onerror=()=>{ delete scriptP[u]; rej(new Error(\"Chargement du lecteur impossible\")) }; document.head.appendChild(sc) })) }\nasync function ensureLoader(n){ for(const d of DEPS[n]||[]){ try{ await loadScript(LBASE+d) }catch(e){} } if(!THREE[n]) await loadScript(LBASE+\"loaders\/\"+n+\".js\") }\nfunction busy(on,t){ $(\"#busy\").hidden=!on; if(t) $(\"#busyt\").textContent=t }\nfunction renderModelChips(){\n  const list=custom?[...MODELS,[\"custom\",\"Mon mod\u00e8le\"]]:MODELS;\n  chipRow(\"#models\",list,\"m\",v=>{ ST.model=v; if(v!==\"custom\"&&ST.mat===\"orig\") ST.mat=\"ceram\"; renderModelChips(); buildProduct(); syncProdUI() });\n  const mats=ST.model===\"custom\"&&custom&&custom.hasOrig?[[\"orig\",\"D'origine\"],...MATS]:MATS;\n  chipRow(\"#mats\",mats,\"t\",v=>{ ST.mat=v; buildProduct(); syncProdUI() });\n  syncProdUI();\n}\nfunction disposeCustom(){ if(!custom) return; custom.obj.traverse(o=>{ if(o.geometry) o.geometry.dispose(); const ms=o.userData.orig?(Array.isArray(o.userData.orig)?o.userData.orig:[o.userData.orig]):[]; ms.forEach(m=>{ if(!m) return; [\"map\",\"normalMap\",\"roughnessMap\",\"metalnessMap\",\"aoMap\",\"emissiveMap\"].forEach(k=>m[k]&&m[k].dispose()); m.dispose() }) }); custom.urls.forEach(u=>URL.revokeObjectURL(u)) }\nfunction buildCustom(){\n  const pivot=new THREE.Group(); pivot.add(custom.obj); pivot.rotation.set(custom.up?-Math.PI\/2:0,0,0);\n  const yaw=new THREE.Group(); yaw.add(pivot); yaw.rotation.y=custom.yaw;\n  const studio=ST.mat===\"orig\"?null:makeMat(ST.mat,ST.col);\n  custom.obj.traverse(o=>{ if(o.isMesh){ o.userData.keep=true; o.material=studio||o.userData.orig; o.userData.keepMat=!studio } });\n  yaw.updateMatrixWorld(true); const box=new THREE.Box3().setFromObject(yaw), size=box.getSize(new THREE.Vector3()), c=box.getCenter(new THREE.Vector3());\n  const k=Math.min(1.45\/Math.max(size.y,1e-6),1.75\/Math.max(size.x,size.z,1e-6));\n  const holder=new THREE.Group(); holder.add(yaw); yaw.position.set(-c.x,-box.min.y,-c.z); holder.scale.setScalar(k); product.add(holder);\n}\nasync function importFiles(fl){\n  const files=[...fl]; if(!files.length||!R) return;\n  const ext=f=>f.name.split(\".\").pop().toLowerCase();\n  const main=[\"glb\",\"gltf\",\"fbx\",\"obj\",\"dae\",\"3ds\",\"stl\",\"ply\"].map(e=>files.find(f=>ext(f)===e)).find(Boolean);\n  if(!main){ toast(\"Format non reconnu. Formats accept\u00e9s : GLB, glTF, FBX, OBJ, STL, PLY, DAE, 3DS.\"); return }\n  if(main.size>200e6){ toast(\"Fichier trop lourd (plus de 200 Mo). Exporte une version all\u00e9g\u00e9e en GLB.\"); return }\n  const e=ext(main), L=FMT[e]; busy(true,`Chargement de ${main.name}\u2026`);\n  const urls={}; files.forEach(f=>urls[f.name.toLowerCase()]=URL.createObjectURL(f));\n  try{\n    await ensureLoader(L);\n    const mgr=new THREE.LoadingManager(); mgr.setURLModifier(u=>{ if(\/^(blob|data):\/.test(u)) return u; const n=decodeURIComponent(u.split(\/[\\\\\/]\/).pop().split(\"?\")[0]).toLowerCase(); return urls[n]||u });\n    const buf=await main.arrayBuffer(), text=()=>new TextDecoder().decode(buf);\n    let obj, hasOrig=true;\n    if(L===\"GLTFLoader\"){ const gl=new THREE.GLTFLoader(mgr); if(window.MeshoptDecoder) gl.setMeshoptDecoder(window.MeshoptDecoder);\n      if(THREE.DRACOLoader){ const dr=new THREE.DRACOLoader(); dr.setDecoderPath(\"https:\/\/www.gstatic.com\/draco\/versioned\/decoders\/1.4.1\/\"); gl.setDRACOLoader(dr) }\n      const cib=window.createImageBitmap; try{ window.createImageBitmap=undefined }catch(_){} \/* force <img> textures (blob URLs) *\/\n      try{ obj=await new Promise((res,rej)=>gl.parse(e===\"glb\"?buf:text(),\"\",g=>res(g.scene),rej)) } finally { try{ window.createImageBitmap=cib }catch(_){} } }\n    else if(L===\"OBJLoader\"){ const ol=new THREE.OBJLoader(mgr); const mtl=files.find(f=>ext(f)===\"mtl\"); if(mtl&&THREE.MTLLoader){ const m=new THREE.MTLLoader(mgr).parse(await mtl.text(),\"\"); m.preload(); ol.setMaterials(m) } else hasOrig=false; obj=ol.parse(text()) }\n    else if(L===\"FBXLoader\") obj=new THREE.FBXLoader(mgr).parse(buf,\"\");\n    else if(L===\"ColladaLoader\") obj=new THREE.ColladaLoader(mgr).parse(text(),\"\").scene;\n    else if(L===\"TDSLoader\") obj=new THREE.TDSLoader(mgr).parse(buf,\"\");\n    else { const geo=new THREE[L]().parse(buf); if(!geo.attributes.normal) geo.computeVertexNormals(); const vc=!!geo.attributes.color; hasOrig=vc;\n      obj=new THREE.Mesh(geo,new THREE.MeshStandardMaterial({color:0xcccccc,vertexColors:vc,roughness:.5})); if(e===\"stl\") obj.rotation.x=0 }\n    let meshes=0, tris=0;\n    obj.traverse(o=>{ if(!o.isMesh) return; meshes++; o.castShadow=o.receiveShadow=true; const g=o.geometry; if(!g.attributes.normal) g.computeVertexNormals();\n      tris+=g.index?g.index.count\/3:g.attributes.position.count\/3; const ms=Array.isArray(o.material)?o.material:[o.material];\n      ms.forEach(m=>{ if(!m) return; [\"map\",\"emissiveMap\"].forEach(k=>{ if(m[k]) m[k].encoding=THREE.sRGBEncoding }); m.needsUpdate=true }); o.userData.orig=o.material });\n    if(!meshes) throw new Error(\"Aucun maillage trouv\u00e9 dans ce fichier.\");\n    disposeCustom();\n    custom={obj,name:main.name,up:[\"stl\",\"3ds\"].includes(e),yaw:0,tris:Math.round(tris),hasOrig,urls:Object.values(urls)};\n    ST.model=\"custom\"; ST.mat=hasOrig?\"orig\":\"ceram\"; if(product) product.rotation.y=0;\n    renderModelChips(); buildProduct(); syncProdUI(); envDirty=true;\n    $(\"#m-info\").textContent=`${custom.name} \u00b7 ${custom.tris.toLocaleString(\"fr-FR\")} triangles`;\n    OKK.querySelector('[data-tab=\"prod\"]').click();\n    toast(custom.up?\"Mod\u00e8le import\u00e9. S'il est couch\u00e9, clique sur \u00ab Redresser \u00bb.\":\"Mod\u00e8le import\u00e9\");\n  }catch(err){ console.error(err); Object.values(urls).forEach(u=>URL.revokeObjectURL(u));\n    toast(\/draco\/i.test(String(err))?\"Ce GLB est compress\u00e9 en Draco : exporte-le sans compression.\":\"Lecture impossible : \"+(err&&err.message?err.message:\"fichier non pris en charge\")) }\n  finally{ busy(false) }\n}\nfunction bindImport(){\n  $(\"#mfile\").onchange=e=>{ importFiles(e.target.files); e.target.value=\"\" };\n  $(\"#imp\").onclick=()=>{ OKK.querySelector('[data-tab=\"prod\"]').click(); $(\"#mfile\").click() };\n  const drop=$(\"#drop\"); [\"dragenter\",\"dragover\"].forEach(t=>drop.addEventListener(t,()=>drop.classList.add(\"over\"))); [\"dragleave\",\"drop\"].forEach(t=>drop.addEventListener(t,()=>drop.classList.remove(\"over\")));\n  let depth=0; const dz=$(\"#dz\");\n  vp.addEventListener(\"dragenter\",e=>{ e.preventDefault(); depth++; dz.hidden=false }); vp.addEventListener(\"dragover\",e=>e.preventDefault());\n  vp.addEventListener(\"dragleave\",()=>{ if(--depth<=0){ depth=0; dz.hidden=true } });\n  vp.addEventListener(\"drop\",e=>{ e.preventDefault(); depth=0; dz.hidden=true; importFiles(e.dataTransfer.files) });\n  $(\"#m-up\").onclick=()=>{ if(!custom) return; custom.up=!custom.up; buildProduct() };\n  $(\"#m-rot\").onclick=()=>{ if(!custom) return; custom.yaw+=Math.PI\/2; buildProduct() };\n}\n\/* ---------- iframe auto-height (WordPress \/ Elementor) ---------- *\/\n(function(){ if(window.parent===window) return; let last=0; const send=()=>{ const h=Math.ceil($(\"#okk\").getBoundingClientRect().height); if(Math.abs(h-last)>2){ last=h; try{ parent.postMessage({type:\"ok3d-lightlab:height\",height:h},\"*\") }catch(e){} } };\n  try{ new ResizeObserver(send).observe($(\"#okk\")) }catch(e){ setInterval(send,800) } })();\n\n\/* ---------- boot ---------- *\/\n\n\/* Pleine largeur, m\u00eame dans une colonne Elementor limit\u00e9e.\n   Aucune modification globale du th\u00e8me ou du d\u00e9filement de la page. *\/\n(function setupElementorScreen() {\n  let pending = 0;\n  const isFull = () => (document.fullscreenElement || document.webkitFullscreenElement) === OKK || OKK.classList.contains('is-app-screen');\n  const setVar = (name, value) => {\n    if (OKK.style.getPropertyValue(name) !== value) OKK.style.setProperty(name, value);\n  };\n  const measure = () => {\n    pending = 0;\n    if (!OKK.isConnected) { if (observer) observer.disconnect(); return; }\n    const full = isFull();\n    if (!full) {\n      const width = document.documentElement.clientWidth;\n      const shift = parseFloat(OKK.style.getPropertyValue('--okk-shift')) || 0;\n      const left = OKK.getBoundingClientRect().left;\n      setVar('--okk-width', width + 'px');\n      setVar('--okk-shift', (Math.round((shift - left) * 100) \/ 100) + 'px');\n    }\n    const admin = document.getElementById('wpadminbar');\n    const adminVisible = admin && getComputedStyle(admin).position === 'fixed';\n    setVar('--okk-admin-height', (!full && adminVisible ? admin.getBoundingClientRect().height : 0) + 'px');\n  };\n  const schedule = () => { if (!pending) pending = requestAnimationFrame(measure); };\n  const observer = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(schedule) : null;\n  if (observer) {\n    observer.observe(document.documentElement);\n    if (OKK.parentElement) observer.observe(OKK.parentElement);\n  }\n  window.addEventListener('resize', schedule);\n  window.addEventListener('orientationchange', schedule);\n  window.addEventListener('load', schedule, { once: true });\n  if (window.visualViewport) window.visualViewport.addEventListener('resize', schedule);\n  if (document.fonts && document.fonts.ready) document.fonts.ready.then(schedule);\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})(OKK);\n  measure();\n})();\n\nbuildUI(); bindImport();\nif(window.THREE&&THREE.OrbitControls){ try{ initThree() }catch(e){ console.error(e); vp.insertAdjacentHTML(\"beforeend\",'<div class=\"fallback\">Ton navigateur ne permet pas le rendu 3D (WebGL). Essaie avec une version r\u00e9cente de Chrome, Safari ou Firefox.<\/div>') } }\nelse vp.insertAdjacentHTML(\"beforeend\",'<div class=\"fallback\">Le moteur 3D n\\'a pas pu se charger. V\u00e9rifie ta connexion puis recharge la page.<\/div>');\n\n})();\n<\/script>\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>\u26f6 Plein \u00e9cran OK3D Lab \u00b7 Studio lumi\u00e8re \u00c9claire comme un studio.Dans ton navigateur. Importe ton propre mod\u00e8le 3D ou pars d&rsquo;un objet de d\u00e9mo, place les lumi\u00e8res sur le plan et regarde le rendu r\u00e9agir en direct. Six ambiances de studio pour apprendre, un d\u00e9fi pour t&rsquo;entra\u00eener l&rsquo;\u0153il. Ton mod\u00e8le 3DTemps r\u00e9elReflets fid\u00e8les6 ambiancesMode [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-15673","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15673","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=15673"}],"version-history":[{"count":14,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15673\/revisions"}],"predecessor-version":[{"id":15707,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15673\/revisions\/15707"}],"wp:attachment":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/media?parent=15673"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}