{"id":15721,"date":"2026-10-16T22:01:35","date_gmt":"2026-10-16T20:01:35","guid":{"rendered":"https:\/\/www.ok3d.fr\/?page_id=15721"},"modified":"2026-10-16T22:02:21","modified_gmt":"2026-10-16T20:02:21","slug":"sculpt-lab","status":"publish","type":"page","link":"https:\/\/www.ok3d.fr\/en\/sculpt-lab\/","title":{"rendered":"Sculpt Lab"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"15721\" class=\"elementor elementor-15721\" 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<!-- Sculpt Lab by OK3D \u2014 atelier de sculpture interactif pour Elementor.\n  Coller TOUT ce fichier dans un widget HTML. Page : Elementor pleine largeur.\n  Conteneur : largeur 100 %, marges\/padding 0, d\u00e9bordement visible.\n  Le header du site reste pr\u00e9sent. Cadre : 1600px, marges comprises.\n  Espace haut : 94 + 54px ; mobile : 78 + 43px.\n  Personnalisation : --okk-content-max, --okk-header-offset, --okk-top-gap.\n  Aucun compte ni biblioth\u00e8que JavaScript externe. Sauvegarde sur cet appareil.\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;600;700;800&family=Geist:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap\">\n<style>.okm[data-okm=\"matiere\"]{--bg:#06080c;--ink:#eef2f8;--muted:#929dae;--line:#232a37;--blue:#2e6bff;--ice:#a3caff;--okk-content-max:1600px;--okk-header-offset:94px;--okk-top-gap:54px;--gutter:clamp(22px,5vw,88px);--body:\"Geist\",system-ui,-apple-system,\"Segoe UI\",sans-serif;--display:\"Unbounded\",system-ui,sans-serif;--mono:\"Geist Mono\",ui-monospace,monospace;position:relative;left:var(--m-shift,0px);width:var(--m-width,100%)!important;max-width:none!important;min-width:0;margin:0!important;padding:0!important;background:var(--bg);color:var(--ink);font:400 15px\/1.5 var(--body);text-align:left;color-scheme:dark;-webkit-font-smoothing:antialiased;isolation:isolate}\n.okm[data-okm=\"matiere\"] *,.okm[data-okm=\"matiere\"] *::before,.okm[data-okm=\"matiere\"] *::after{box-sizing:border-box}\n.okm[data-okm=\"matiere\"] [hidden]{display:none!important}\n.okm[data-okm=\"matiere\"] :focus-visible{outline:2px solid var(--ice);outline-offset:5px}\n.okm[data-okm=\"matiere\"] button{font:500 13px\/1.3 var(--body);border:0;border-radius:0;padding:0;margin:0;color:inherit;background:none;cursor:pointer;box-shadow:none;letter-spacing:normal;text-transform:none;transition:background .18s,border-color .18s,transform .18s,color .18s;touch-action:manipulation}\n.okm[data-okm=\"matiere\"] button:disabled{cursor:default;opacity:.42}\n.okm[data-okm=\"matiere\"] button:enabled:active{transform:translateY(1px)}\n.okm[data-okm=\"matiere\"] p,.okm[data-okm=\"matiere\"] h1,.okm[data-okm=\"matiere\"] h2{margin:0;padding:0;color:inherit;letter-spacing:normal;text-transform:none}\n.okm[data-okm=\"matiere\"] h1,.okm[data-okm=\"matiere\"] h2{font-family:var(--display)}\n.okm[data-okm=\"matiere\"] canvas{display:block;max-width:100%}\n.okm[data-okm=\"matiere\"] .m-page{min-height:100vh;overflow:clip;background:radial-gradient(ellipse 800px 600px at 15% 0,rgba(46,107,255,.075),transparent 70%),var(--bg);padding-top:calc(var(--okk-header-offset) + var(--okk-top-gap) + var(--okk-admin-height,0px));padding-bottom:max(28px,env(safe-area-inset-bottom,0px))}\n.okm[data-okm=\"matiere\"] .m-wrap{max-width:var(--okk-content-max);margin-inline:auto;padding-inline:max(var(--gutter),env(safe-area-inset-left,0px)) max(var(--gutter),env(safe-area-inset-right,0px))}\n.okm[data-okm=\"matiere\"] .appbar{display:flex;justify-content:flex-end;align-items:center;gap:12px;flex-wrap:wrap;padding:0 0 25px}\n.okm[data-okm=\"matiere\"] .m-record{display:flex;gap:8px;align-items:center;color:var(--muted);font-size:12px;white-space:nowrap}\n.okm[data-okm=\"matiere\"] .m-record strong{color:var(--ink);font:600 14px var(--mono)}\n.okm[data-okm=\"matiere\"] .m-star{color:var(--ice);font-size:21px;line-height:1;margin-right:2px}\n.okm[data-okm=\"matiere\"] .m-record-unit{font:400 10px var(--mono);margin-left:-5px}\n.okm[data-okm=\"matiere\"] .m-btn{min-height:46px;display:inline-flex;align-items:center;justify-content:center;gap:9px;padding:12px 19px;border:1px solid #344055;border-radius:999px;background:#0e131c;font-size:13px;font-weight:600;line-height:1.4;text-align:center}\n.okm[data-okm=\"matiere\"] .m-btn:enabled:hover{border-color:#76869f;background:#1a2434}\n.okm[data-okm=\"matiere\"] .m-small{min-height:34px;font-size:11px;padding:7px 12px}\n.okm[data-okm=\"matiere\"] .screen-btn{margin-left:0}\n.okm[data-okm=\"matiere\"] .m-primary{background:#2e6bff;border-color:#4d82ff;color:#fff;box-shadow:inset 0 1px 0 #ffffff24,0 7px 24px #2e6bff24}\n.okm[data-okm=\"matiere\"] .m-primary:enabled:hover{background:#4079ff;border-color:#87b1ff}\n.okm[data-okm=\"matiere\"] .m-intro{display:flex;align-items:flex-end;justify-content:space-between;gap:28px;padding-bottom:36px}\n.okm[data-okm=\"matiere\"] .m-kicker{font:500 10px\/1.6 var(--mono);letter-spacing:.15em;color:var(--ice);display:flex;align-items:center;gap:8px}\n.okm[data-okm=\"matiere\"] .m-kicker i{height:5px;width:5px;border-radius:50%;background:var(--ice)}\n.okm[data-okm=\"matiere\"] h1{font-size:clamp(34px,5vw,64px);line-height:1.08;letter-spacing:-.05em;font-weight:600;margin-top:12px}\n.okm[data-okm=\"matiere\"] h1>span{color:var(--blue)}\n.okm[data-okm=\"matiere\"] .m-intro-copy{padding-bottom:4px;max-width:390px}\n.okm[data-okm=\"matiere\"] .m-intro-copy p{font-size:17px;letter-spacing:-.025em;margin-bottom:8px}\n.okm[data-okm=\"matiere\"] .m-intro-copy>span{font-size:14px;line-height:1.65;color:var(--muted)}\n.okm[data-okm=\"matiere\"] .m-modebar{display:flex;align-items:center;justify-content:space-between;gap:16px;margin-bottom:18px}\n.okm[data-okm=\"matiere\"] .m-segment{display:flex;padding:4px;border:1px solid var(--line);border-radius:999px;background:#0c1018}\n.okm[data-okm=\"matiere\"] .m-segment button{padding:10px 19px;border-radius:999px;color:var(--muted);min-height:40px;font-size:12px}\n.okm[data-okm=\"matiere\"] .m-segment button[aria-pressed=\"true\"]{background:#e9eff8;color:#0c111b}\n.okm[data-okm=\"matiere\"] .m-segment button>span{padding-left:9px}\n.okm[data-okm=\"matiere\"] .m-mode-note{font-size:12px;color:var(--muted);text-align:right}\n.okm[data-okm=\"matiere\"] .m-studio{display:grid;grid-template-columns:minmax(0,1fr) 348px;gap:18px;align-items:start}\n.okm[data-okm=\"matiere\"] .m-left,.okm[data-okm=\"matiere\"] .m-panel{min-width:0}\n.okm[data-okm=\"matiere\"] .m-left{position:sticky;top:calc(var(--okk-header-offset) + var(--okk-admin-height,0px) + 16px)}\n.okm[data-okm=\"matiere\"] .m-stage{aspect-ratio:1.32;max-height:740px;position:relative;border:1px solid #2b3444;border-radius:24px;overflow:hidden;background:radial-gradient(ellipse at 50% 40%,#1c2340,#0c1019);isolation:isolate}\n.okm[data-okm=\"matiere\"] .m-canvas{width:100%!important;height:100%!important;position:absolute;inset:0;touch-action:none;cursor:grab;z-index:1;outline-offset:-5px}\n.okm[data-okm=\"matiere\"] .m-canvas:active{cursor:grabbing}\n.okm[data-okm=\"matiere\"] .m-stage-top{position:absolute;left:27px;right:27px;top:26px;display:flex;justify-content:space-between;align-items:center;gap:12px;pointer-events:none;z-index:2}\n.okm[data-okm=\"matiere\"] .m-edition{font:500 10px var(--mono);letter-spacing:.15em;color:#c3cbd9}\n.okm[data-okm=\"matiere\"] .m-live{display:flex;gap:7px;align-items:center;font:400 9px var(--mono);letter-spacing:.09em;color:#bec8d8}\n.okm[data-okm=\"matiere\"] .m-live i{width:5px;height:5px;border-radius:50%;background:#b4eccd;box-shadow:0 0 12px #7cebb555}\n.okm[data-okm=\"matiere\"] .m-stage-word{position:absolute;left:0;right:0;bottom:18%;font:500 clamp(24px,4.6vw,70px)\/1 var(--display);letter-spacing:-.055em;color:#d6e1ff09;text-align:center;z-index:2;pointer-events:none}\n.okm[data-okm=\"matiere\"] .m-stage-bottom{position:absolute;bottom:25px;left:27px;right:27px;display:flex;justify-content:space-between;align-items:center;pointer-events:none;z-index:3}\n.okm[data-okm=\"matiere\"] .m-piece-title{display:block;font:500 clamp(17px,2vw,23px) var(--display);letter-spacing:-.04em}\n.okm[data-okm=\"matiere\"] .m-piece-sub{display:block;font:400 11px var(--mono);color:#b5bfd0;margin-top:5px}\n.okm[data-okm=\"matiere\"] .m-iconbtn{pointer-events:auto;width:45px;height:45px;border:1px solid #ffffff33;border-radius:50%;font-size:26px;background:#080c1640;backdrop-filter:blur(10px)}\n.okm[data-okm=\"matiere\"] .m-iconbtn:hover{background:#ffffff20}\n.okm[data-okm=\"matiere\"] .m-viewbar{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;padding:12px 3px;color:var(--muted);font-size:11px}\n.okm[data-okm=\"matiere\"] .m-drag-icon{color:var(--ice);font-size:16px;margin-right:5px}\n.okm[data-okm=\"matiere\"] .m-viewtools{display:flex;gap:4px;align-items:center}\n.okm[data-okm=\"matiere\"] .m-viewtools button{min-width:34px;min-height:36px;border:1px solid var(--line);border-radius:9px;font-size:16px}\n.okm[data-okm=\"matiere\"] .m-viewtools button:hover{background:#1d2534;color:#fff}\n.okm[data-okm=\"matiere\"] .m-viewtools .m-pause{font-size:11px;min-width:82px;padding-inline:8px}\n.okm[data-okm=\"matiere\"] .m-viewtools button[aria-pressed=\"true\"]{color:var(--ice);border-color:#6482b0}\n.okm[data-okm=\"matiere\"] .m-tool-separator{height:18px;width:1px;background:var(--line);margin:0 4px}\n.okm[data-okm=\"matiere\"] .m-panel{background:linear-gradient(155deg,#141a25,#0b1018 65%);border:1px solid var(--line);border-radius:24px;padding:25px 24px 17px}\n.okm[data-okm=\"matiere\"] .m-panel-heading{padding-bottom:24px}\n.okm[data-okm=\"matiere\"] .m-panel-heading h2{font-size:19px;font-weight:500;letter-spacing:-.045em}\n.okm[data-okm=\"matiere\"] .m-panel-heading h2>span{color:var(--blue)}\n.okm[data-okm=\"matiere\"] .m-panel-count{display:block;font:400 8px var(--mono);letter-spacing:.15em;color:var(--muted);margin-top:7px}\n.okm[data-okm=\"matiere\"] .m-fieldset{border:0;border-top:1px solid var(--line);margin:0 0 20px;padding:13px 0 0;min-width:0;width:100%}\n.okm[data-okm=\"matiere\"] legend{float:left;width:100%;margin-bottom:12px;padding:0;font-size:12px;font-weight:500;display:flex;align-items:center;gap:9px;line-height:1.4}\n.okm[data-okm=\"matiere\"] legend>span{font:400 9px var(--mono);color:#8393ac}\n.okm[data-okm=\"matiere\"] legend>b{font-size:10px;font-weight:400;color:var(--muted);margin-left:auto}\n.okm[data-okm=\"matiere\"] .m-shapes,.okm[data-okm=\"matiere\"] .m-materials{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;clear:both}\n.okm[data-okm=\"matiere\"] .m-shapes button{border:1px solid #2c3545;border-radius:11px;padding:4px 0 8px;color:#aab6ca;font-size:10px;display:flex;align-items:center;flex-direction:column}\n.okm[data-okm=\"matiere\"] .m-shapes svg{width:43px;height:37px;fill:none;stroke:currentColor;stroke-width:1.2;stroke-linecap:round;stroke-linejoin:round}\n.okm[data-okm=\"matiere\"] .m-shapes button[aria-pressed=\"true\"]{color:#dbe9ff;border-color:#547bb9;background:#1b2e4a}\n.okm[data-okm=\"matiere\"] .m-shapes button:enabled:hover{border-color:#829ac1}\n.okm[data-okm=\"matiere\"] .m-materials button{font-size:10px;display:flex;flex-direction:column;align-items:center;gap:7px;padding:3px 0 7px;border-radius:9px;color:var(--muted);border:1px solid transparent}\n.okm[data-okm=\"matiere\"] .m-materials button[aria-pressed=\"true\"]{color:var(--ink);border-color:#51627f;background:#ffffff05}\n.okm[data-okm=\"matiere\"] .m-orb{width:30px;height:30px;border-radius:50%;display:block;box-shadow:inset -4px -5px 8px #0005,0 3px 6px #0003}\n.okm[data-okm=\"matiere\"] .m-pearl{background:radial-gradient(circle at 30% 25%,#fff7ef,#ddb1d2 25%,#98b5f0 48%,#757799 75%,#e5cfdb)}\n.okm[data-okm=\"matiere\"] .m-chrome{background:linear-gradient(140deg,#f8fbff 4%,#c3ceda 20%,#151d2a 43%,#101722 50%,#e1e6f3 56%,#6d7b92 72%,#111c2f)}\n.okm[data-okm=\"matiere\"] .m-clay{background:radial-gradient(circle at 32% 25%,#e8e4da,#aca9ad 60%,#6a7283)}\n.okm[data-okm=\"matiere\"] .m-velvet{background:radial-gradient(circle at 36% 25%,#bd9eaf,#725874 58%,#a68eaf 95%)}\n.okm[data-okm=\"matiere\"] .m-palettes{display:flex;gap:12px;clear:both;padding:3px 3px 1px}\n.okm[data-okm=\"matiere\"] .m-palettes button{width:32px;height:32px;flex:0 0 32px;border-radius:50%;background:var(--swatch);position:relative}\n.okm[data-okm=\"matiere\"] .m-palettes button::after{content:\"\";position:absolute;inset:-4px;border:1px solid transparent;border-radius:50%;transition:border-color .2s}\n.okm[data-okm=\"matiere\"] .m-palettes button[aria-pressed=\"true\"]::after{border-color:#e7efff}\n.okm[data-okm=\"matiere\"] .m-sculpt{margin-bottom:18px}\n.okm[data-okm=\"matiere\"] .m-slider{display:block;clear:both;margin:0 0 15px;font-size:12px;cursor:pointer}\n.okm[data-okm=\"matiere\"] .m-slider:last-child{margin-bottom:0}\n.okm[data-okm=\"matiere\"] .m-slider>span{display:flex;align-items:center;justify-content:space-between}\n.okm[data-okm=\"matiere\"] .m-slider output{color:#c5d7f5;font:500 10px var(--mono);min-width:24px;text-align:right}\n.okm[data-okm=\"matiere\"] .m-slider small{display:flex;justify-content:space-between;color:#8792a6;font-size:9px;line-height:1.3}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]{appearance:none;-webkit-appearance:none;width:100%;height:24px;margin:4px 0 0;border:0;padding:0;background:transparent;box-shadow:none;cursor:pointer;accent-color:var(--ice);min-height:0}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]::-webkit-slider-runnable-track{height:3px;border-radius:3px;background:linear-gradient(90deg,#accbff var(--value,50%),#303c51 var(--value,50%))}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]::-webkit-slider-thumb{appearance:none;-webkit-appearance:none;width:13px;height:13px;border-radius:50%;border:0;margin-top:-5px;background:#e7f0ff;box-shadow:0 0 0 4px #b4d3ff12}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]::-moz-range-track{height:3px;background:#303c51}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]::-moz-range-progress{height:3px;background:#accbff}\n.okm[data-okm=\"matiere\"] input[type=\"range\"]::-moz-range-thumb{width:13px;height:13px;border:0;border-radius:50%;background:#e7f0ff}\n.okm[data-okm=\"matiere\"] .m-random{width:100%;background:#e3ebfa;color:#0a1221;border-color:#e3ebfa;justify-content:space-between;border-radius:12px;font-size:12px}\n.okm[data-okm=\"matiere\"] .m-random:enabled:hover{background:#fff;color:#0a1221;border-color:#fff}\n.okm[data-okm=\"matiere\"] .m-random>span:first-child{font-size:20px}\n.okm[data-okm=\"matiere\"] .m-random>span:last-child{font-size:17px}\n.okm[data-okm=\"matiere\"] .m-panel-foot{text-align:center;font-size:9px;color:var(--muted);margin-top:10px;line-height:1.5}\n.okm[data-okm=\"matiere\"] .m-lesson{grid-column:1\/-1;padding:14px 15px;margin-bottom:16px;border:1px solid #314361;border-radius:12px;background:#13213a60}\n.okm[data-okm=\"matiere\"] .m-lesson .m-kicker{font-size:8px;margin-bottom:6px}\n.okm[data-okm=\"matiere\"] .m-lesson strong{display:block;font-size:12px;font-weight:600;margin-bottom:5px}\n.okm[data-okm=\"matiere\"] .m-lesson p{font-size:11px;line-height:1.65;color:#b6c7df}\n.okm[data-okm=\"matiere\"] .m-caption{display:flex;gap:20px;align-items:center;padding:24px 5px 4px}\n.okm[data-okm=\"matiere\"] .m-caption-num{font:400 8px var(--mono);letter-spacing:.12em;color:var(--muted);align-self:flex-start;padding-top:5px}\n.okm[data-okm=\"matiere\"] .m-caption p{font-size:16px;font-weight:400;letter-spacing:-.02em;color:var(--muted)}\n.okm[data-okm=\"matiere\"] .m-caption strong{font-weight:400;color:#e8eef9}\n.okm[data-okm=\"matiere\"] .m-caption svg{width:38px;height:38px;margin-left:auto;fill:none;stroke:#8496b7;stroke-width:1.3}\n.okm[data-okm=\"matiere\"] .m-keep{display:flex;justify-content:space-between;align-items:center;gap:30px;border-top:1px solid var(--line);border-bottom:1px solid var(--line);padding:34px 0;margin-top:48px}\n.okm[data-okm=\"matiere\"] .m-keep h2{font-size:clamp(21px,2.3vw,31px);line-height:1.4;font-weight:500;letter-spacing:-.05em;margin-top:10px}\n.okm[data-okm=\"matiere\"] .m-keep h2 span{color:var(--muted)}\n.okm[data-okm=\"matiere\"] .m-keep-actions{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:10px}\n.okm[data-okm=\"matiere\"] .m-keep-actions>span{font:400 9px var(--mono);color:var(--muted);width:100%;text-align:right;margin-top:3px}\n.okm[data-okm=\"matiere\"] .m-collection{margin-top:32px}\n.okm[data-okm=\"matiere\"] .m-collection-heading{display:flex;justify-content:space-between;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:18px}\n.okm[data-okm=\"matiere\"] .m-collection h2{font-size:13px;font-weight:500;letter-spacing:-.025em}\n.okm[data-okm=\"matiere\"] .m-collection h2>span{font:400 9px var(--mono);color:var(--muted);margin-left:12px}\n.okm[data-okm=\"matiere\"] .m-collection-heading p{font-size:10px;color:var(--muted)}\n.okm[data-okm=\"matiere\"] .m-empty{display:flex;gap:16px;align-items:center;padding:23px;border:1px dashed #30394a;border-radius:16px;color:var(--muted)}\n.okm[data-okm=\"matiere\"] .m-empty>span{border:1px solid var(--line);width:37px;height:37px;border-radius:50%;display:grid;place-items:center;font-size:20px;flex-shrink:0}\n.okm[data-okm=\"matiere\"] .m-empty p{font-size:12px;color:#c3cddd}\n.okm[data-okm=\"matiere\"] .m-empty small{display:block;color:var(--muted);font-size:10px;margin-top:4px}\n.okm[data-okm=\"matiere\"] .m-gallery{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:12px}\n.okm[data-okm=\"matiere\"] .m-card{position:relative;border:1px solid var(--line);border-radius:13px;overflow:hidden;background:#101621}\n.okm[data-okm=\"matiere\"] .m-card>button:first-child{display:block;text-align:left;width:100%;padding-bottom:10px}\n.okm[data-okm=\"matiere\"] .m-card img{display:block;width:100%;height:auto;aspect-ratio:1.25;object-fit:cover;max-width:100%}\n.okm[data-okm=\"matiere\"] .m-card strong{display:block;padding:8px 10px 0;font-size:11px;font-weight:500}\n.okm[data-okm=\"matiere\"] .m-card small{display:block;padding:4px 10px 0;color:var(--muted);font-size:9px}\n.okm[data-okm=\"matiere\"] .m-card .m-remove{position:absolute;top:6px;right:6px;background:#080b14b3;border:1px solid #ffffff30;border-radius:50%;width:28px;height:28px;font-size:16px;color:#fff}\n.okm[data-okm=\"matiere\"] .m-card button:focus-visible{outline-offset:-3px}\n.okm[data-okm=\"matiere\"] .m-endnote{margin-top:34px;display:flex;justify-content:space-between;gap:12px;flex-wrap:wrap;font:400 8px var(--mono);letter-spacing:.1em;color:#7e8a9e}\n.okm[data-okm=\"matiere\"] .m-endnote i{font-size:15px;font-style:normal;margin-left:7px;color:var(--ice)}\n.okm[data-okm=\"matiere\"] .m-challenge{display:grid;grid-template-columns:150px minmax(0,1fr);gap:22px;align-items:center;border:1px solid #38517a;border-radius:18px;padding:18px;background:#101a2a;margin-top:12px}\n.okm[data-okm=\"matiere\"] .m-target{min-width:0}\n.okm[data-okm=\"matiere\"] .m-target canvas{width:100%;height:auto;aspect-ratio:1;border-radius:12px;border:1px solid #ffffff15}\n.okm[data-okm=\"matiere\"] .m-target>span{display:block;text-align:center;font:400 7px\/1.5 var(--mono);letter-spacing:.05em;color:#b4c4df;margin-top:8px}\n.okm[data-okm=\"matiere\"] .m-challenge h2{font-size:19px;font-weight:500;letter-spacing:-.04em;margin:6px 0 10px}\n.okm[data-okm=\"matiere\"] .m-challenge p{font-size:12px;color:#a6b8d0;line-height:1.6}\n.okm[data-okm=\"matiere\"] .m-challenge .m-kicker{font-size:8px}\n.okm[data-okm=\"matiere\"] .m-challenge-actions{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin-top:14px}\n.okm[data-okm=\"matiere\"] .m-challenge-actions .m-btn{min-height:39px;font-size:11px;padding:9px 13px}\n.okm[data-okm=\"matiere\"] .m-score{font:600 22px var(--mono);color:#e1edff}\n.okm[data-okm=\"matiere\"] .m-score small{font:400 10px var(--mono);color:var(--muted);margin-left:5px}\n.okm[data-okm=\"matiere\"] .m-challenge .m-feedback{font-size:10px;margin-top:11px;min-height:32px;color:#c8d9f1}\n.okm[data-okm=\"matiere\"] .m-loading,.okm[data-okm=\"matiere\"] .m-fallback{position:absolute;inset:0;z-index:5;background:#0c1019;display:flex;flex-direction:column;align-items:center;justify-content:center;text-align:center;gap:14px;padding:30px;color:var(--muted);font-size:12px}\n.okm[data-okm=\"matiere\"] .m-fallback strong{color:#e3ebf8;font:500 17px\/1.5 var(--display);max-width:370px}\n.okm[data-okm=\"matiere\"] .m-fallback p{max-width:360px}\n.okm[data-okm=\"matiere\"] .m-loader{width:31px;height:31px;border:2px solid #accbff20;border-top-color:var(--ice);border-radius:50%;animation:okm-spin 1s linear infinite}\n.okm[data-okm=\"matiere\"] .m-toast{position:fixed;bottom:max(28px,env(safe-area-inset-bottom,0px));left:50%;transform:translateX(-50%);z-index:2147483647;border:1px solid #516789;background:#152136f5;color:#edf4ff;border-radius:14px;box-shadow:0 12px 60px #0008;padding:14px 22px;width:max-content;max-width:calc(100% - 36px);font-size:13px;text-align:center;pointer-events:none}\n.elementor-widget-html:has(.okm[data-okm=\"matiere\"]){width:100%;min-width:0;padding:0}\n.elementor-widget-container:has(>.okm[data-okm=\"matiere\"]){padding:0;margin:0}\n@keyframes okm-spin{to{transform:rotate(360deg)}}\n@media(min-width:1450px){.okm[data-okm=\"matiere\"] .m-studio{grid-template-columns:minmax(0,1fr) 370px}.okm[data-okm=\"matiere\"] .m-panel{padding:28px 28px 21px}.okm[data-okm=\"matiere\"] .m-stage{aspect-ratio:1.38}.okm[data-okm=\"matiere\"] .m-fieldset{margin-bottom:24px}}\n@media(max-width:1050px){.okm[data-okm=\"matiere\"] .m-studio{grid-template-columns:minmax(0,1fr) 310px;gap:14px}.okm[data-okm=\"matiere\"] .m-panel{padding:20px 18px 17px}.okm[data-okm=\"matiere\"] .m-stage{aspect-ratio:.9}.okm[data-okm=\"matiere\"] .m-caption-num{display:none}.okm[data-okm=\"matiere\"] .m-challenge{grid-template-columns:110px minmax(0,1fr);padding:13px;gap:14px}.okm[data-okm=\"matiere\"] .m-gallery{grid-template-columns:repeat(3,minmax(0,1fr))}}\n@media(max-width:820px){.okm[data-okm=\"matiere\"] .m-studio{grid-template-columns:minmax(0,1fr)}.okm[data-okm=\"matiere\"] .m-left{position:relative;top:0}.okm[data-okm=\"matiere\"] .m-stage{aspect-ratio:1.25}.okm[data-okm=\"matiere\"] .m-panel{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));column-gap:24px;padding:23px}.okm[data-okm=\"matiere\"] .m-panel-heading{grid-column:1\/-1;padding-bottom:19px}.okm[data-okm=\"matiere\"] .m-sculpt{grid-column:2;grid-row:2\/5}.okm[data-okm=\"matiere\"] .m-random,.okm[data-okm=\"matiere\"] .m-panel-foot{grid-column:1\/-1}.okm[data-okm=\"matiere\"] .m-caption{display:none}.okm[data-okm=\"matiere\"] .m-intro-copy{max-width:300px}.okm[data-okm=\"matiere\"] .m-mode-note{display:none}.okm[data-okm=\"matiere\"] .m-keep{margin-top:32px}.okm[data-okm=\"matiere\"] .m-keep-actions{max-width:340px}}\n@media(max-width:700px){.okm[data-okm=\"matiere\"]{--okk-header-offset:78px;--okk-top-gap:43px}.okm[data-okm=\"matiere\"] .appbar{padding-bottom:24px}.okm[data-okm=\"matiere\"] .m-intro{align-items:flex-start;flex-direction:column;gap:18px;padding-bottom:25px}.okm[data-okm=\"matiere\"] .m-intro-copy{max-width:100%}.okm[data-okm=\"matiere\"] .m-intro-copy p{font-size:15px}.okm[data-okm=\"matiere\"] .m-intro-copy>span{font-size:13px}.okm[data-okm=\"matiere\"] .m-kicker{font-size:9px}.okm[data-okm=\"matiere\"] .m-keep{align-items:flex-start;flex-direction:column;gap:22px}.okm[data-okm=\"matiere\"] .m-keep-actions{max-width:100%;justify-content:flex-start}.okm[data-okm=\"matiere\"] .m-keep-actions>span{text-align:left}}\n@media(max-width:480px){.okm[data-okm=\"matiere\"] .appbar{gap:8px}.okm[data-okm=\"matiere\"] .m-record{font-size:10px;gap:6px}.okm[data-okm=\"matiere\"] .m-small{font-size:10px;padding-inline:10px}.okm[data-okm=\"matiere\"] .m-panel{display:block;padding:22px 20px 17px}.okm[data-okm=\"matiere\"] .m-shapes button{font-size:11px}.okm[data-okm=\"matiere\"] .m-materials button{font-size:11px}.okm[data-okm=\"matiere\"] .m-stage{aspect-ratio:1;min-height:290px;border-radius:20px}.okm[data-okm=\"matiere\"] .m-stage-top{inset:19px 19px auto}.okm[data-okm=\"matiere\"] .m-live{font-size:7px}.okm[data-okm=\"matiere\"] .m-edition{font-size:8px}.okm[data-okm=\"matiere\"] .m-stage-bottom{bottom:19px;left:19px;right:19px}.okm[data-okm=\"matiere\"] .m-stage-word{font-size:30px;bottom:22%}.okm[data-okm=\"matiere\"] .m-viewbar{justify-content:center;padding:12px 0 18px;gap:9px}.okm[data-okm=\"matiere\"] .m-viewbar>span{width:100%;text-align:center}.okm[data-okm=\"matiere\"] .m-segment{width:100%;justify-content:center}.okm[data-okm=\"matiere\"] .m-segment button{padding-inline:14px;flex:1;font-size:11px;white-space:nowrap}.okm[data-okm=\"matiere\"] .m-segment button>span{padding-left:3px}.okm[data-okm=\"matiere\"] .m-gallery{grid-template-columns:repeat(2,minmax(0,1fr))}.okm[data-okm=\"matiere\"] .m-challenge{grid-template-columns:88px minmax(0,1fr);gap:12px}.okm[data-okm=\"matiere\"] .m-challenge h2{font-size:16px}.okm[data-okm=\"matiere\"] .m-challenge p{font-size:11px}.okm[data-okm=\"matiere\"] .m-challenge .m-kicker{font-size:7px}.okm[data-okm=\"matiere\"] .m-challenge-actions{gap:10px}.okm[data-okm=\"matiere\"] .m-challenge-actions .m-btn{font-size:10px;width:100%}.okm[data-okm=\"matiere\"] .m-keep-actions{width:100%}.okm[data-okm=\"matiere\"] .m-keep-actions .m-btn{width:100%}.okm[data-okm=\"matiere\"] .m-keep-actions>span{text-align:center;font-size:8px}.okm[data-okm=\"matiere\"] .m-empty{padding:17px}.okm[data-okm=\"matiere\"] .m-endnote{font-size:7px}}\n@media(prefers-reduced-motion:reduce){.okm[data-okm=\"matiere\"] button{transition:none}.okm[data-okm=\"matiere\"] .m-loader{animation:none}}\n\/* Sur \u00e9cran tactile, la pi\u00e8ce accompagne les gestes jusqu'au bas des r\u00e9glages. *\/\n@media(max-width:820px){\n .okm[data-okm=\"matiere\"] .m-studio{display:block}\n .okm[data-okm=\"matiere\"] .m-left{display:contents}\n .okm[data-okm=\"matiere\"] .m-stage{position:sticky;top:calc(var(--okk-header-offset) + var(--okk-admin-height,0px) + 10px);z-index:5;height:clamp(230px,38svh,380px);min-height:230px;max-height:380px;aspect-ratio:auto;box-shadow:0 12px 26px #06080c90}\n .okm[data-okm=\"matiere\"] .m-panel{margin-top:8px}\n .okm[data-okm=\"matiere\"] .m-challenge{margin-bottom:18px}\n .okm[data-okm=\"matiere\"]:fullscreen .m-stage,.okm[data-okm=\"matiere\"].is-app-screen .m-stage{top:68px}\n .okm[data-okm=\"matiere\"]:-webkit-full-screen .m-stage{top:68px}\n}\n\n\n\/* Affichage app lorsque le plein \u00e9cran natif n'est pas disponible. *\/\n.okm[data-okm=\"matiere\"].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.okm[data-okm=\"matiere\"]:fullscreen, .okm[data-okm=\"matiere\"].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.okm[data-okm=\"matiere\"]:-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.okm[data-okm=\"matiere\"] .appbar {\n  display: flex; justify-content: flex-end; align-items: center; gap: 12px;\n  flex-wrap: wrap; padding: 0 0 18px;\n}\n.okm[data-okm=\"matiere\"] .screen-btn { margin-left: auto; flex: 0 0 auto; }\n.okm[data-okm=\"matiere\"]:fullscreen .appbar, .okm[data-okm=\"matiere\"].is-app-screen .appbar,\n.okm[data-okm=\"matiere\"]:fullscreen .bar, .okm[data-okm=\"matiere\"].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.okm[data-okm=\"matiere\"] .screen-btn{margin-left:0}.okm[data-okm=\"matiere\"]:fullscreen .m-left,.okm[data-okm=\"matiere\"].is-app-screen .m-left{top:68px}.okm[data-okm=\"matiere\"]:-webkit-full-screen .appbar{position:sticky;top:0;z-index:40;background:var(--bg);padding-block:10px;}\n<\/style>\n<div class=\"okm\" data-okm=\"matiere\">\n <div class=\"m-page\">\n  <div class=\"m-wrap\">\n   <div class=\"appbar\">\n    <button type=\"button\" class=\"m-btn m-small screen-btn\" data-okk-fullscreen aria-pressed=\"false\">\u26f6 Plein \u00e9cran<\/button>\n    <span class=\"m-record\"><span class=\"m-star\" aria-hidden=\"true\">\u2733<\/span> Ton record <strong data-ui=\"record\">\u2014<\/strong><span class=\"m-record-unit\">\/ 100<\/span><\/span>\n   <\/div>\n   <div class=\"m-intro\">\n    <div><span class=\"m-kicker\"><i aria-hidden=\"true\"><\/i> JOUE AVEC LA MATI\u00c8RE.<\/span><h1>Sculpt Lab<span>.<\/span><\/h1><\/div>\n    <div class=\"m-intro-copy\"><p>La beaut\u00e9 n\u2019a pas de mode d\u2019emploi.<\/p><span>Tourne. D\u00e9forme. Laisse-toi surprendre.<br>La prochaine pi\u00e8ce de collection, c\u2019est la tienne.<\/span><\/div>\n   <\/div>\n   <div class=\"m-modebar\">\n    <div class=\"m-segment\" role=\"group\" aria-label=\"Mode de cr\u00e9ation\"><button type=\"button\" data-mode=\"free\" aria-pressed=\"true\">Cr\u00e9ation libre<\/button><button type=\"button\" data-mode=\"challenge\" aria-pressed=\"false\">Le d\u00e9fi du jour <span aria-hidden=\"true\">\u2197<\/span><\/button><\/div>\n    <span class=\"m-mode-note\" data-ui=\"mode-note\">Aucun mauvais geste. Juste des possibilit\u00e9s.<\/span>\n   <\/div>\n   <div class=\"m-studio\">\n    <div class=\"m-left\">\n     <div class=\"m-stage\" data-ui=\"stage\" aria-busy=\"true\">\n      <canvas class=\"m-canvas\" data-ui=\"canvas\" tabindex=\"0\" aria-label=\"Sculpture 3D interactive. Glisse pour tourner, touche pour cr\u00e9er une onde. Au clavier : fl\u00e8ches pour tourner, plus et moins pour zoomer, espace pour une onde.\">Sculpture interactive : utilise les commandes de forme, mati\u00e8re et couleur.<\/canvas>\n      <div class=\"m-stage-top\"><span class=\"m-edition\" data-ui=\"edition\">\u00c9TUDE 001<\/span><span class=\"m-live\"><i aria-hidden=\"true\"><\/i><span data-ui=\"live\">MATI\u00c8RE VIVANTE<\/span><\/span><\/div>\n      <div class=\"m-stage-word\" aria-hidden=\"true\">touch &amp; feel.<\/div>\n      <div class=\"m-stage-bottom\"><div><span class=\"m-piece-title\" data-ui=\"piece-title\">\u00c9closion<\/span><span class=\"m-piece-sub\" data-ui=\"piece-sub\">Nacre \u00b7 Aurore<\/span><\/div><button type=\"button\" class=\"m-iconbtn\" data-action=\"pulse\" title=\"Cr\u00e9er une onde\" aria-label=\"Cr\u00e9er une onde dans la sculpture\">\u2733<\/button><\/div>\n      <div class=\"m-loading\" data-ui=\"loading\"><span class=\"m-loader\"><\/span><span>La mati\u00e8re prend vie\u2026<\/span><\/div>\n      <div class=\"m-fallback\" data-ui=\"fallback\" hidden><strong>La 3D a besoin d\u2019un petit coup de pouce.<\/strong><p>Essaie de recharger la page ou de l\u2019ouvrir dans un navigateur avec l\u2019acc\u00e9l\u00e9ration graphique activ\u00e9e.<\/p><button type=\"button\" class=\"m-btn\" data-action=\"retry\">R\u00e9essayer<\/button><\/div>\n     <\/div>\n     <div class=\"m-viewbar\"><span><span class=\"m-drag-icon\" aria-hidden=\"true\">\u2194<\/span> Glisse pour tourner. Touche pour vibrer.<\/span><div class=\"m-viewtools\"><button type=\"button\" data-action=\"zoom-out\" aria-label=\"\u00c9loigner la sculpture\" title=\"\u00c9loigner\">\u2212<\/button><button type=\"button\" data-action=\"zoom-in\" aria-label=\"Rapprocher la sculpture\" title=\"Rapprocher\">+<\/button><button type=\"button\" data-action=\"camera\" aria-label=\"Recentrer la vue\" title=\"Recentrer\">\u21ba<\/button><span class=\"m-tool-separator\"><\/span><button type=\"button\" class=\"m-pause\" data-action=\"pause\" aria-pressed=\"false\">\u2161 Figer<\/button><\/div><\/div>\n     <div class=\"m-challenge\" data-ui=\"challenge\" hidden>\n      <div class=\"m-target\"><canvas data-ui=\"target\" width=\"200\" height=\"200\" aria-label=\"Sculpture \u00e0 retrouver avec les trois curseurs\"><\/canvas><span>LA PI\u00c8CE \u00c0 RETROUVER<\/span><\/div>\n      <div class=\"m-challenge-copy\"><span class=\"m-kicker\" data-ui=\"challenge-date\">LE D\u00c9FI DU JOUR<\/span><h2>Tu as l\u2019\u0153il ?<\/h2><p>Retrouve cette forme avec les trois curseurs. La mati\u00e8re et la couleur sont d\u00e9j\u00e0 r\u00e9gl\u00e9es.<\/p><div class=\"m-challenge-actions\"><button type=\"button\" class=\"m-btn m-primary\" data-action=\"compare\">Comparer ma pi\u00e8ce<\/button><strong class=\"m-score\" data-ui=\"score\">\u2014<small>\/ 100<\/small><\/strong><\/div><p class=\"m-feedback\" data-ui=\"feedback\" role=\"status\">Observe les volumes, les plis et la torsion.<\/p><\/div>\n     <\/div>\n     <div class=\"m-caption\" data-ui=\"caption\"><span class=\"m-caption-num\">01 \/ LE GESTE<\/span><p>Pas besoin d\u2019\u00eatre designer.<br><strong>Il suffit d\u2019\u00eatre curieux.<\/strong><\/p><svg viewBox=\"0 0 64 64\" aria-hidden=\"true\"><path d=\"M8 32h48M32 8v48M15 15l34 34M15 49l34-34\"\/><\/svg><\/div>\n    <\/div>\n    <aside class=\"m-panel\" aria-label=\"Commandes de la sculpture\">\n     <div class=\"m-panel-heading\"><h2>\u00c0 toi de jouer<span>.<\/span><\/h2><span class=\"m-panel-count\">\u221e POSSIBILIT\u00c9S<\/span><\/div>\n     <fieldset class=\"m-fieldset\" data-ui=\"shapes\"><legend><span>01<\/span> Une silhouette<\/legend><div class=\"m-shapes\" role=\"group\" aria-label=\"Silhouette\">\n      <button type=\"button\" data-shape=\"0\" aria-pressed=\"false\"><svg viewBox=\"0 0 48 40\" aria-hidden=\"true\"><ellipse cx=\"24\" cy=\"20\" rx=\"12\" ry=\"13\" transform=\"rotate(-25 24 20)\"\/><path d=\"M17 11c10-1 17 9 11 19\"\/><\/svg>Galet<\/button>\n      <button type=\"button\" data-shape=\"1\" aria-pressed=\"false\"><svg viewBox=\"0 0 48 40\" aria-hidden=\"true\"><ellipse cx=\"24\" cy=\"20\" rx=\"15\" ry=\"10\" transform=\"rotate(-30 24 20)\"\/><ellipse cx=\"24\" cy=\"20\" rx=\"8\" ry=\"4\" transform=\"rotate(-30 24 20)\"\/><\/svg>Anneau<\/button>\n      <button type=\"button\" data-shape=\"2\" aria-pressed=\"true\"><svg viewBox=\"0 0 48 40\" aria-hidden=\"true\"><path d=\"M24 6c4 0 4 6 7 6s8 1 7 5-5 3-5 7 0 8-4 8-4-4-8-3-7 2-9-2 3-5 2-8-5-5-2-8 7 1 9-1 1-4 3-4Z\"\/><circle cx=\"24\" cy=\"20\" r=\"5\"\/><\/svg>Fleur<\/button>\n      <button type=\"button\" data-shape=\"3\" aria-pressed=\"false\"><svg viewBox=\"0 0 48 40\" aria-hidden=\"true\"><path d=\"M15 26c-13-7 0-18 10-12s11 17 3 18S11 10 20 8s25 17 14 20S13 21 15 14\"\/><\/svg>N\u0153ud<\/button>\n     <\/div><\/fieldset>\n     <fieldset class=\"m-fieldset\" data-ui=\"materials\"><legend><span>02<\/span> Une sensation<\/legend><div class=\"m-materials\" role=\"group\" aria-label=\"Mati\u00e8re\">\n      <button type=\"button\" data-material=\"0\" aria-pressed=\"true\"><i class=\"m-orb m-pearl\" aria-hidden=\"true\"><\/i>Nacre<\/button><button type=\"button\" data-material=\"1\" aria-pressed=\"false\"><i class=\"m-orb m-chrome\" aria-hidden=\"true\"><\/i>Chrome<\/button><button type=\"button\" data-material=\"2\" aria-pressed=\"false\"><i class=\"m-orb m-clay\" aria-hidden=\"true\"><\/i>C\u00e9ramique<\/button><button type=\"button\" data-material=\"3\" aria-pressed=\"false\"><i class=\"m-orb m-velvet\" aria-hidden=\"true\"><\/i>Velours<\/button>\n     <\/div><\/fieldset>\n     <fieldset class=\"m-fieldset\" data-ui=\"palettes\"><legend><span>03<\/span> Une atmosph\u00e8re <b data-ui=\"palette-name\">Aurore<\/b><\/legend><div class=\"m-palettes\" role=\"group\" aria-label=\"Atmosph\u00e8re color\u00e9e\">\n      <button type=\"button\" data-palette=\"0\" aria-pressed=\"true\" aria-label=\"Aurore, bleu et rose\" title=\"Aurore\" style=\"--swatch:linear-gradient(135deg,#768cff,#ffc1b6)\"><\/button><button type=\"button\" data-palette=\"1\" aria-pressed=\"false\" aria-label=\"Infra, rouge et violet\" title=\"Infra\" style=\"--swatch:linear-gradient(135deg,#ff644e,#8c4dff)\"><\/button><button type=\"button\" data-palette=\"2\" aria-pressed=\"false\" aria-label=\"Glacier, turquoise et bleu\" title=\"Glacier\" style=\"--swatch:linear-gradient(135deg,#b9f5ce,#368ad5)\"><\/button><button type=\"button\" data-palette=\"3\" aria-pressed=\"false\" aria-label=\"Solaire, ambre et cr\u00e8me\" title=\"Solaire\" style=\"--swatch:linear-gradient(135deg,#ee8c3a,#fff0bd)\"><\/button><button type=\"button\" data-palette=\"4\" aria-pressed=\"false\" aria-label=\"Monochrome, argent et graphite\" title=\"Monochrome\" style=\"--swatch:linear-gradient(135deg,#f4f1e6,#5d6984)\"><\/button>\n     <\/div><\/fieldset>\n     <fieldset class=\"m-fieldset m-sculpt\"><legend><span>04<\/span> Ton geste<\/legend>\n      <label class=\"m-slider\"><span>Relief <output data-value=\"relief\">62<\/output><\/span><input type=\"range\" min=\"0\" max=\"100\" value=\"62\" data-param=\"relief\" aria-label=\"Relief\"><small>Lisse <span>Pliss\u00e9<\/span><\/small><\/label>\n      <label class=\"m-slider\"><span>Torsion <output data-value=\"twist\">35<\/output><\/span><input type=\"range\" min=\"0\" max=\"100\" value=\"35\" data-param=\"twist\" aria-label=\"Torsion\"><small>Calme <span>Vertige<\/span><\/small><\/label>\n      <label class=\"m-slider\"><span>Volume <output data-value=\"volume\">48<\/output><\/span><input type=\"range\" min=\"0\" max=\"100\" value=\"48\" data-param=\"volume\" aria-label=\"Volume\"><small>D\u00e9licat <span>G\u00e9n\u00e9reux<\/span><\/small><\/label>\n     <\/fieldset>\n     <div class=\"m-lesson\" aria-live=\"polite\" aria-atomic=\"true\"><span class=\"m-kicker\">LE PETIT D\u00c9CLIC<\/span><strong data-ui=\"lesson-title\">Les reflets iris\u00e9s<\/strong><p data-ui=\"lesson-text\">Fais tourner la nacre : les teintes changent avec l\u2019angle de vue. C\u2019est l\u2019effet iris\u00e9.<\/p><\/div>\n     <button type=\"button\" class=\"m-btn m-random\" data-action=\"random\"><span aria-hidden=\"true\">\u2733<\/span> Surprends-moi <span aria-hidden=\"true\">\u2197<\/span><\/button>\n     <p class=\"m-panel-foot\" data-ui=\"panel-foot\">Le hasard a parfois beaucoup de go\u00fbt.<\/p>\n    <\/aside>\n   <\/div>\n   <div class=\"m-keep\"><div><span class=\"m-kicker\">\u00c7A M\u00c9RITE D\u2019\u00caTRE GARD\u00c9.<\/span><h2>Une pi\u00e8ce unique.<br><span>La tienne.<\/span><\/h2><\/div><div class=\"m-keep-actions\"><button type=\"button\" class=\"m-btn\" data-action=\"save\"><span aria-hidden=\"true\">\uff0b<\/span> Garder cette pi\u00e8ce<\/button><button type=\"button\" class=\"m-btn m-primary\" data-action=\"export\"><span aria-hidden=\"true\">\u2193<\/span> Exporter l\u2019affiche<\/button><span>PNG 1 800 \u00d7 2 100 \u00b7 sans inscription<\/span><\/div><\/div>\n   <section class=\"m-collection\" aria-label=\"Ta collection personnelle\"><div class=\"m-collection-heading\"><h2>Ta collection <span data-ui=\"collection-count\">00 \/ 06<\/span><\/h2><p data-ui=\"storage-note\">Enregistr\u00e9e dans ce navigateur, sur cet appareil.<\/p><\/div><div class=\"m-gallery\" data-ui=\"gallery\"><\/div><div class=\"m-empty\" data-ui=\"empty\"><span aria-hidden=\"true\">\uff0b<\/span><p>Le d\u00e9but d\u2019une belle collection.<small>Garde une pi\u00e8ce, puis retrouve-la ici quand tu veux.<\/small><\/p><\/div><\/section>\n   <div class=\"m-endnote\"><span>SCULPT LAB \u2014 UNE EXP\u00c9RIENCE OK3D<\/span><span>Fait pour le plaisir de cr\u00e9er. <i aria-hidden=\"true\">\u2733<\/i><\/span><\/div>\n  <\/div>\n <\/div>\n <div class=\"m-toast\" data-ui=\"toast\" role=\"status\" aria-live=\"polite\" hidden><\/div>\n <noscript><p style=\"padding:32px\">Active JavaScript dans ton navigateur pour jouer avec Sculpt Lab.<\/p><\/noscript>\n<\/div>\n<script>(() => {\n 'use strict';\n const root=document.querySelector('[data-okm=\"matiere\"]:not([data-okm-ready])');\n if(!root)return;\n root.setAttribute('data-okm-ready','true');\n const Core = (() => {\n  const clamp = (n, lo, hi) => Math.max(lo, Math.min(hi, n));\n  const shapes = ['L\u00e9vitation', 'Orbital', '\u00c9closion', 'Entrelacs'];\n  const materials = ['Nacre', 'Chrome', 'C\u00e9ramique', 'Velours'];\n  const palettes = [\n    { name: 'Aurore', a: '#8c9dff', b: '#ffbdac' },\n    { name: 'Infra', a: '#ff684e', b: '#975bff' },\n    { name: 'Glacier', a: '#b8f5d3', b: '#4b9cd7' },\n    { name: 'Solaire', a: '#e9a052', b: '#fff1c8' },\n    { name: 'Monochrome', a: '#e5e7e4', b: '#7b8ba8' }\n  ];\n  const defaults = { shape: 2, material: 0, palette: 0, relief: 62, twist: 35, volume: 48 };\n  function recipe(value) {\n    if (!value || typeof value !== 'object') return null;\n    const result = {};\n    for (const [key, max] of [['shape',3],['material',3],['palette',4],['relief',100],['twist',100],['volume',100]]) {\n      if (typeof value[key] !== 'number' || !Number.isFinite(value[key])) return null;\n      result[key] = Math.round(clamp(value[key], 0, max));\n    }\n    return result;\n  }\n  function rng(seed) {\n    let h = 2166136261;\n    for (const c of String(seed)) { h ^= c.charCodeAt(0); h = Math.imul(h, 16777619); }\n    return () => {\n      h += 0x6D2B79F5;\n      let n = h;\n      n = Math.imul(n ^ n >>> 15, n | 1);\n      n ^= n + Math.imul(n ^ n >>> 7, n | 61);\n      return ((n ^ n >>> 14) >>> 0) \/ 4294967296;\n    };\n  }\n  function randomRecipe(random = Math.random) {\n    return { shape:Math.floor(random()*4), material:Math.floor(random()*4), palette:Math.floor(random()*5), relief:Math.round(18+random()*76), twist:Math.round(random()*85), volume:Math.round(15+random()*75) };\n  }\n  function dailyRecipe(date) { return randomRecipe(rng('OK3D-matiere-v1:' + date)); }\n  function compare(value, target) {\n    const keys = ['relief','twist','volume'];\n    const diffs = keys.map(key => ({ key, delta:value[key]-target[key] }));\n    const error = Math.sqrt(diffs.reduce((sum,d) => sum+d.delta*d.delta,0)\/3);\n    const exact = diffs.every(d => Math.abs(d.delta) <= 1);\n    const score = exact ? 100 : Math.min(99, Math.max(0, Math.round(100-error*1.65)));\n    const largest = diffs.sort((a,b) => Math.abs(b.delta)-Math.abs(a.delta))[0];\n    return { score, key:largest.key, direction:Math.sign(largest.delta) };\n  }\n  const linearColor = hex => [1,3,5].map(start => Math.pow(parseInt(hex.slice(start,start+2),16)\/255,2.2));\n  const localDay = (date = new Date()) => `${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,'0')}-${String(date.getDate()).padStart(2,'0')}`;\n  function validPiece(piece) {\n    if (!piece || typeof piece !== 'object' || !recipe(piece.recipe) || typeof piece.id !== 'string' || !\/^[a-z0-9-]{1,70}$\/i.test(piece.id)) return null;\n    if (typeof piece.thumb !== 'string' || !\/^data:image\\\/jpeg;base64,[A-Za-z0-9+\/=]+$\/.test(piece.thumb) || piece.thumb.length > 150000) return null;\n    const finite = (value, fallback, min, max) => typeof value === 'number' && Number.isFinite(value) ? clamp(value,min,max) : fallback;\n    return { id:piece.id, recipe:recipe(piece.recipe), thumb:piece.thumb, yaw:finite(piece.yaw,.4,-1e6,1e6), pitch:finite(piece.pitch,.84,-1.45,1.45), zoom:finite(piece.zoom,1,.65,1.35), time:finite(piece.time,0,0,1e7) };\n  }\n  return { clamp, shapes, materials, palettes, defaults, recipe, rng, randomRecipe, dailyRecipe, compare, linearColor, localDay, validPiece };\n})();\n\n const SCULPTURE_VERTEX=\"precision highp float;\\nattribute vec2 aUV;\\nuniform vec4 uShape;\\nuniform vec3 uParams;\\nuniform float uTime;\\nuniform float uPulse;\\nuniform float uYaw;\\nuniform float uPitch;\\nuniform float uZoom;\\nuniform float uAspect;\\nvarying mediump vec3 vNormal;\\nvarying mediump vec3 vPosition;\\nvarying mediump vec3 vLocal;\\nconst float PI = 3.14159265359;\\nvec3 sculpture(vec2 uv) {\\n  float u = uv.x * 2.0 * PI;\\n  float v = uv.y * 2.0 * PI;\\n  float relief = uParams.x;\\n  float volume = uParams.z;\\n  vec3 p = vec3(0.0);\\n  if (uShape.x > 0.0001) {\\n    float lat = (uv.y - 0.5) * PI;\\n    float c = cos(lat);\\n    float rib = sin(u*9.0 + lat*3.0 + uParams.y*lat*6.0);\\n    float radius = 0.86 + relief * 0.14 * rib * c*c;\\n    vec3 ball = vec3(c*cos(u),sin(lat)*(0.74+volume*0.65),c*sin(u));\\n    p += ball * radius * uShape.x;\\n  }\\n  if (uShape.y > 0.0001) {\\n    float tube = 0.18 + volume*0.26;\\n    tube *= 1.0 + relief*0.2*sin(u*14.0 + v*2.0);\\n    float ring = 0.68 + tube*cos(v);\\n    p += vec3(ring*cos(u),tube*sin(v),ring*sin(u)) * uShape.y;\\n  }\\n  if (uShape.z > 0.0001) {\\n    float tube = (0.21 + volume*0.2) * (1.0 + relief*0.19*cos(u*18.0 + v*3.0));\\n    float ring = 0.65 + (0.06+relief*0.13)*cos(u*6.0) + tube*cos(v);\\n    p += vec3(ring*cos(u),tube*sin(v)*(0.8+volume*0.3)+0.07*sin(u*3.0),ring*sin(u)) * uShape.z;\\n  }\\n  if (uShape.w > 0.0001) {\\n    float radius = 0.61 + 0.19*cos(u*3.0);\\n    vec3 center = vec3(radius*cos(u*2.0),0.32*sin(u*3.0),radius*sin(u*2.0));\\n    float dr = -0.57*sin(u*3.0);\\n    vec3 tangent = normalize(vec3(dr*cos(u*2.0)-2.0*radius*sin(u*2.0),0.96*cos(u*3.0),dr*sin(u*2.0)+2.0*radius*cos(u*2.0)));\\n    vec3 side = normalize(cross(tangent,vec3(0.0,1.0,0.0)));\\n    vec3 up = normalize(cross(tangent,side));\\n    float tube = (0.1+volume*0.12)*(1.0+relief*0.2*cos(u*24.0+v*3.0));\\n    p += (center+tube*(cos(v)*side+sin(v)*up))*uShape.w;\\n  }\\n  float twist = uParams.y * (2.3*p.y + 0.4*sin(p.y*3.0));\\n  p.xz = mat2(cos(twist),-sin(twist),sin(twist),cos(twist))*p.xz;\\n  float breathing = 1.0 + 0.014*sin(uTime*1.25 + p.y*2.0);\\n  float ripple = uPulse * 0.055 * sin(length(p-vec3(0.3,0.5,0.6))*12.0 - uTime*10.0);\\n  return p*(breathing+ripple)*1.04;\\n}\\nvec3 rotateView(vec3 p) {\\n  float c=cos(uYaw),s=sin(uYaw);\\n  p=vec3(c*p.x+s*p.z,p.y,-s*p.x+c*p.z);\\n  c=cos(uPitch);s=sin(uPitch);\\n  return vec3(p.x,c*p.y-s*p.z,s*p.y+c*p.z);\\n}\\nvoid main() {\\n  vec3 p=sculpture(aUV);\\n  float e=0.0008;\\n  vec3 du=sculpture(aUV+vec2(e,0.0))-sculpture(aUV-vec2(e,0.0));\\n  vec3 dv=sculpture(aUV+vec2(0.0,e))-sculpture(aUV-vec2(0.0,e));\\n  vec3 n=cross(dv,du);\\n  if (dot(n,n)<0.00000000001) n=normalize(p+vec3(0.00001));\\n  vNormal=rotateView(normalize(n));\\n  vLocal=p;\\n  p=rotateView(p);\\n  vPosition=p;\\n  float distance=4.5\/uZoom;\\n  float z=distance-p.z;\\n  float f=2.95;\\n  \/\/ Keep the object inside both a portrait and a landscape viewport.\\n  float fit=min(1.0,uAspect);\\n  gl_Position=vec4(p.x*f*fit\/uAspect,p.y*f*fit,z*1.01005025-0.201005,z);\\n}\\n\";\nconst SCULPTURE_FRAGMENT=\"precision highp float;\\nuniform vec4 uMaterial;\\nuniform vec3 uColorA;\\nuniform vec3 uColorB;\\nuniform float uZoom;\\nvarying mediump vec3 vNormal;\\nvarying mediump vec3 vPosition;\\nvarying mediump vec3 vLocal;\\nvec3 tonemap(vec3 x){return clamp((x*(2.51*x+0.03))\/(x*(2.43*x+0.59)+0.14),0.0,1.0);}\\nvec3 studio(vec3 r) {\\n  vec3 sky=mix(vec3(0.018,0.025,0.045),vec3(0.25,0.29,0.36),smoothstep(-0.4,0.9,r.y));\\n  sky += uColorA*0.24*pow(max(r.x,0.0),1.3)+uColorB*0.22*pow(max(-r.x,0.0),1.3);\\n  \/\/ Large photographic softboxes, expressed directly as an environment.\\n  float key=pow(max(dot(r,normalize(vec3(-0.55,0.9,1.1))),0.0),18.0);\\n  float strip=exp(-pow((r.x-0.52)\/0.13,2.0)-pow((r.y-0.25)\/0.8,6.0));\\n  float rim=pow(max(dot(r,normalize(vec3(-0.8,0.15,-0.8))),0.0),28.0);\\n  return sky+vec3(1.0,0.93,0.85)*key*3.3+mix(vec3(0.75,0.86,1.0),uColorA,0.2)*strip*2.6+uColorB*rim*2.5;\\n}\\nvoid main() {\\n  vec3 n=normalize(vNormal);\\n  vec3 eye=normalize(vec3(0.0,0.0,4.5\/uZoom)-vPosition);\\n  if(dot(n,eye)<0.0)n=-n;\\n  float ndv=max(dot(n,eye),0.0);\\n  float fres=pow(1.0-ndv,4.0);\\n  vec3 r=reflect(-eye,n);\\n  float key=max(dot(n,normalize(vec3(-0.5,0.85,1.2))),0.0);\\n  float fill=max(dot(n,normalize(vec3(1.0,0.2,0.4))),0.0);\\n  float back=max(dot(n,normalize(vec3(-0.6,0.6,-1.1))),0.0);\\n  vec3 tint=mix(uColorA,uColorB,clamp(0.5+n.y*0.34+n.x*0.22,0.0,1.0));\\n  float shade=0.17+0.65*key+0.2*fill;\\n  vec3 env=studio(r);\\n  vec3 interference=0.5+0.5*cos(6.28318*(vec3(0.06,0.31,0.53)+ndv*0.83+vLocal.y*0.08));\\n  vec3 pearlBase=mix(tint,interference,0.2+fres*0.36);\\n  vec3 pearl=pearlBase*shade*0.65+env*mix(vec3(0.26),mix(tint,vec3(1.0),0.65),0.35+fres*0.65);\\n  pearl+=tint*back*0.23;\\n  vec3 chrome=env*mix(vec3(0.72),tint,0.2)+tint*shade*0.065;\\n  vec3 halfVector=normalize(eye+normalize(vec3(-0.5,0.85,1.2)));\\n  float ceramicSpec=pow(max(dot(n,halfVector),0.0),38.0);\\n  vec3 ceramic=mix(tint,vec3(0.83),0.19)*shade+vec3(ceramicSpec*0.45)+env*fres*0.12;\\n  float fiber=pow(1.0-ndv,2.5);\\n  vec3 velvet=tint*(0.13+key*0.58+fill*0.15)+mix(tint,vec3(0.65),0.45)*fiber*0.9;\\n  float grain=fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453)-0.5;\\n  velvet*=1.0+grain*0.055;\\n  vec3 color=pearl*uMaterial.x+chrome*uMaterial.y+ceramic*uMaterial.z+velvet*uMaterial.w;\\n  color=pow(tonemap(color),vec3(1.0\/2.2));\\n  gl_FragColor=vec4(color+grain\/450.0,1.0);\\n}\\n\";\nconst BACKGROUND_FRAGMENT=\"precision highp float;\\nvarying mediump vec2 vUV;\\nuniform vec3 uColorA;\\nuniform vec3 uColorB;\\nuniform float uAspect;\\nvoid main(){\\n  vec2 p=(vUV-0.5)*vec2(uAspect,1.0);\\n  float halo=exp(-dot(p-vec2(-0.1,0.09),p-vec2(-0.1,0.09))*3.9);\\n  vec3 color=vec3(0.0055,0.008,0.014)+mix(uColorA,uColorB,clamp(vUV.x*0.7,0.0,1.0))*halo*0.024;\\n  float shadow=exp(-pow(p.x\/0.32,2.0)-pow((p.y+0.34)\/0.027,2.0));\\n  color*=1.0-shadow*0.55;\\n  float vignette=1.0-0.32*smoothstep(0.25,0.9,length(p));\\n  color=pow(color*vignette,vec3(1.0\/2.2));\\n  float noise=fract(sin(dot(gl_FragCoord.xy,vec2(12.9898,78.233)))*43758.5453)-0.5;\\n  gl_FragColor=vec4(color+noise\/320.0,1.0);\\n}\\n\";\n \/\/ Self-contained WebGL 1 renderer. No remote 3D library or image assets.\nclass SculptureRenderer {\n  constructor(canvas, detail = 144) {\n    this.canvas=canvas;\n    const gl=canvas.getContext('webgl',{alpha:false,antialias:true,powerPreference:'low-power',preserveDrawingBuffer:false});\n    if(!gl) throw new Error('WebGL indisponible');\n    this.gl=gl;this.resources=[];\n    const high=gl.getShaderPrecisionFormat(gl.FRAGMENT_SHADER,gl.HIGH_FLOAT);\n    this.high=!!(high&&high.precision);\n    try {\n      this.background=this.program('attribute vec2 aPosition; varying mediump vec2 vUV; void main(){vUV=aPosition*0.5+0.5;gl_Position=vec4(aPosition,0.0,1.0);}',BACKGROUND_FRAGMENT);\n      this.object=this.program(SCULPTURE_VERTEX,SCULPTURE_FRAGMENT);\n      this.quad=gl.createBuffer();this.resources.push(['buffer',this.quad]);\n      gl.bindBuffer(gl.ARRAY_BUFFER,this.quad);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,1,-1,-1,1,1,1]),gl.STATIC_DRAW);\n      const cols=detail,rows=Math.round(detail*.75),uv=[],indices=[];\n      for(let y=0;y<=rows;y++)for(let x=0;x<=cols;x++)uv.push(x\/cols,y\/rows);\n      for(let y=0;y<rows;y++)for(let x=0;x<cols;x++){\n        const a=y*(cols+1)+x,b=a+cols+1;\n        indices.push(a,b,a+1,a+1,b,b+1);\n      }\n      this.vertices=gl.createBuffer();this.resources.push(['buffer',this.vertices]);\n      gl.bindBuffer(gl.ARRAY_BUFFER,this.vertices);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array(uv),gl.STATIC_DRAW);\n      this.indices=gl.createBuffer();this.resources.push(['buffer',this.indices]);\n      gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,this.indices);gl.bufferData(gl.ELEMENT_ARRAY_BUFFER,new Uint16Array(indices),gl.STATIC_DRAW);\n      this.count=indices.length;\n    } catch(error) {this.dispose();throw error;}\n  }\n  program(vertex,fragment){\n    const gl=this.gl;\n    const compile=(type,source)=>{\n      const shader=gl.createShader(type);\n      if(type===gl.FRAGMENT_SHADER&&!this.high)source=source.replace('precision highp float;','precision mediump float;');\n      gl.shaderSource(shader,source);gl.compileShader(shader);\n      if(!gl.getShaderParameter(shader,gl.COMPILE_STATUS)){\n        const message=gl.getShaderInfoLog(shader);gl.deleteShader(shader);throw Error(message);\n      }\n      return shader;\n    };\n    const vs=compile(gl.VERTEX_SHADER,vertex),fs=compile(gl.FRAGMENT_SHADER,fragment),program=gl.createProgram();\n    gl.attachShader(program,vs);gl.attachShader(program,fs);gl.linkProgram(program);gl.deleteShader(vs);gl.deleteShader(fs);\n    if(!gl.getProgramParameter(program,gl.LINK_STATUS)){const message=gl.getProgramInfoLog(program);gl.deleteProgram(program);throw Error(message);}\n    this.resources.push(['program',program]);\n    const uniforms={};\n    for(const name of ['uShape','uParams','uTime','uPulse','uYaw','uPitch','uZoom','uAspect','uMaterial','uColorA','uColorB'])uniforms[name]=gl.getUniformLocation(program,name);\n    return {program,uniforms,position:gl.getAttribLocation(program,'aPosition'),uv:gl.getAttribLocation(program,'aUV')};\n  }\n  resize(width,height){\n    width=Math.max(1,Math.round(width));height=Math.max(1,Math.round(height));\n    if(this.canvas.width!==width||this.canvas.height!==height){this.canvas.width=width;this.canvas.height=height;}\n  }\n  draw(options){\n    const gl=this.gl;\n    if(gl.isContextLost())return false;\n    gl.viewport(0,0,this.canvas.width,this.canvas.height);\n    const aspect=this.canvas.width\/this.canvas.height;\n    const colors=program=>{\n      gl.uniform3fv(program.uniforms.uColorA,options.colorA);gl.uniform3fv(program.uniforms.uColorB,options.colorB);gl.uniform1f(program.uniforms.uAspect,aspect);\n    };\n    gl.disable(gl.DEPTH_TEST);gl.depthMask(false);\n    gl.useProgram(this.background.program);colors(this.background);\n    gl.bindBuffer(gl.ARRAY_BUFFER,this.quad);gl.enableVertexAttribArray(this.background.position);gl.vertexAttribPointer(this.background.position,2,gl.FLOAT,false,0,0);gl.drawArrays(gl.TRIANGLE_STRIP,0,4);gl.disableVertexAttribArray(this.background.position);\n    gl.enable(gl.DEPTH_TEST);gl.depthMask(true);gl.clear(gl.DEPTH_BUFFER_BIT);gl.disable(gl.CULL_FACE);\n    gl.useProgram(this.object.program);colors(this.object);\n    const u=this.object.uniforms;\n    gl.uniform4fv(u.uShape,options.weights);gl.uniform4fv(u.uMaterial,options.materials);gl.uniform3fv(u.uParams,options.params);\n    for(const [name,key]of [['uTime','time'],['uPulse','pulse'],['uYaw','yaw'],['uPitch','pitch'],['uZoom','zoom']])gl.uniform1f(u[name],options[key]);\n    gl.bindBuffer(gl.ARRAY_BUFFER,this.vertices);gl.enableVertexAttribArray(this.object.uv);gl.vertexAttribPointer(this.object.uv,2,gl.FLOAT,false,0,0);\n    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER,this.indices);gl.drawElements(gl.TRIANGLES,this.count,gl.UNSIGNED_SHORT,0);gl.disableVertexAttribArray(this.object.uv);\n    return true;\n  }\n  dispose(){\n    const gl=this.gl;\n    for(const[type,resource]of this.resources){if(type==='buffer')gl.deleteBuffer(resource);else gl.deleteProgram(resource);}\n    this.resources=[];\n  }\n  release(){this.dispose();const ext=this.gl.getExtension('WEBGL_lose_context');if(ext)ext.loseContext();}\n}\n\n const $=selector=>root.querySelector(selector),all=selector=>Array.from(root.querySelectorAll(selector)),ui=name=>$('[data-ui=\"'+name+'\"]');\n const reduce=window.matchMedia('(prefers-reduced-motion: reduce)');\n const storageKey='ok3d-matiere-v1';\n let storageAvailable=true,saved={pieces:[],record:0};\n try {\n   const raw=JSON.parse(localStorage.getItem(storageKey)||'null');\n   if(raw&&typeof raw==='object'){\n     if(Array.isArray(raw.pieces))saved.pieces=raw.pieces.map(Core.validPiece).filter(Boolean).filter((p,i,a)=>a.findIndex(x=>x.id===p.id)===i).slice(0,6);\n     if(Number.isFinite(raw.record))saved.record=Core.clamp(Math.round(raw.record),0,100);\n   }\n }catch(_){storageAvailable=false;}\n const state={recipe:{...Core.defaults},yaw:.4,pitch:.84,zoom:1,time:0,pulse:0,paused:reduce.matches,mode:'free',edition:1};\n let current=renderState(state.recipe),renderer=null,targetRenderer=null,targetRecipe=null,freeSnapshot=null,available=false;\n let raf=0,lastFrame=0,visible=true,toastTimer=0,layoutRAF=0,exporting=false;\n let dragging=false,pinchDistance=0,touchStart=null;\n const pointers=new Map();\n const canvas=ui('canvas');\n const lessons={\n   shape:[\n     ['Lire une silhouette','Une forme simple permet de mieux observer les variations de mati\u00e8re. Fais tourner le galet et suis les reflets.'],\n     ['Le vide dessine aussi','Le trou de l\u2019anneau fait partie du dessin : c\u2019est l\u2019espace n\u00e9gatif. Change le volume et observe sa taille.'],\n     ['Le rythme visuel','La r\u00e9p\u00e9tition des p\u00e9tales cr\u00e9e un rythme. Le relief et la torsion rendent cette r\u00e9p\u00e9tition plus ou moins r\u00e9guli\u00e8re.'],\n     ['Comprendre la profondeur','Les passages devant et derri\u00e8re nous aident \u00e0 lire la profondeur. Tourne le n\u0153ud pour suivre le parcours de la forme.']\n   ],\n   material:[\n     ['Les reflets iris\u00e9s','Fais tourner la nacre : les teintes changent avec l\u2019angle de vue. C\u2019est l\u2019effet iris\u00e9.'],\n     ['Le m\u00e9tal se lit dans ses reflets','Un m\u00e9tal poli refl\u00e8te son environnement. Les bandes claires sugg\u00e8rent ici les grandes sources de lumi\u00e8re du studio.'],\n     ['La mati\u00e8re r\u00e9v\u00e8le le volume','Dans ce rendu c\u00e9ramique, les reflets sont plus doux. Compare avec le chrome : les m\u00eames volumes se lisent autrement.'],\n     ['Une impression de douceur','Dans ce rendu velours, les bords paraissent plus clairs que le centre. Cet effet \u00e9voque la fa\u00e7on dont les fibres accrochent la lumi\u00e8re.']\n   ],\n   palette:[\n     ['Le contraste chaud-froid','Le rose et le bleu cr\u00e9ent un contraste chaud-froid. Observe comment cette opposition accompagne les volumes.'],\n     ['La force de la saturation','Les couleurs intenses prennent beaucoup de place dans l\u2019image. Compare avec Monochrome pour voir ce qui attire ton regard.'],\n     ['Une palette, plusieurs lectures','Passe du bleu au turquoise en faisant tourner la pi\u00e8ce. Les changements de lumi\u00e8re modifient aussi notre lecture des couleurs.'],\n     ['L\u2019unit\u00e9 des couleurs voisines','L\u2019ambre et le cr\u00e8me restent proches dans cette palette. Les \u00e9carts de luminosit\u00e9 aident \u00e0 distinguer les volumes.'],\n     ['Voir sans la distraction de la couleur','Avec peu de couleur, les contours et les variations de lumi\u00e8re deviennent plus faciles \u00e0 comparer.']\n   ],\n   relief:['Le relief d\u00e9coupe la lumi\u00e8re','Les creux et les bosses fragmentent les reflets. Glisse vers \u00ab Lisse \u00bb, puis vers \u00ab Pliss\u00e9 \u00bb pour comparer.'],\n   twist:['La torsion donne une direction','La torsion fait tourner les volumes autour d\u2019un axe. Observe l\u2019orientation des plis, pas seulement le contour.'],\n   volume:['Tout est une question de proportions','Changer le volume modifie l\u2019\u00e9paisseur de la pi\u00e8ce. La m\u00eame silhouette peut para\u00eetre plus l\u00e9g\u00e8re ou plus massive.'],\n   challenge:['Apprendre \u00e0 comparer','Observe trois indices : les creux pour le relief, l\u2019orientation des plis pour la torsion et l\u2019\u00e9paisseur pour le volume.']\n };\n function showLesson(kind,index){\n   const lesson=index===undefined?lessons[kind]:lessons[kind][index];\n   if(!lesson)return;\n   if(ui('lesson-title').textContent!==lesson[0])ui('lesson-title').textContent=lesson[0];\n   if(ui('lesson-text').textContent!==lesson[1])ui('lesson-text').textContent=lesson[1];\n }\n function renderState(recipe){\n   const palette=Core.palettes[recipe.palette];\n   return {weights:[0,1,2,3].map(i=>i===recipe.shape?1:0),materials:[0,1,2,3].map(i=>i===recipe.material?1:0),params:[recipe.relief\/100,recipe.twist\/100,recipe.volume\/100],colorA:Core.linearColor(palette.a),colorB:Core.linearColor(palette.b)};\n }\n function options(render=current){return {...render,yaw:state.yaw,pitch:state.pitch,zoom:state.zoom,time:state.time,pulse:state.pulse};}\n function notify(message){clearTimeout(toastTimer);ui('toast').textContent=message;ui('toast').hidden=false;toastTimer=setTimeout(()=>{ui('toast').hidden=true;},4200);}\n function persist(){\n   try{localStorage.setItem(storageKey,JSON.stringify(saved));storageAvailable=true;}\n   catch(_){storageAvailable=false;}\n   ui('storage-note').textContent=storageAvailable?'Enregistr\u00e9e dans ce navigateur, sur cet appareil.':'Stockage indisponible : collection conserv\u00e9e pour cette session seulement.';\n   return storageAvailable;\n }\n function updateUI(){\n   const r=state.recipe,challenging=state.mode==='challenge';\n   for(const field of ['shape','material','palette'])for(const button of all('[data-'+field+']'))button.setAttribute('aria-pressed',String(Number(button.dataset[field])===r[field]));\n   for(const input of all('[data-param]')){\n     const value=r[input.dataset.param];input.value=String(value);input.style.setProperty('--value',value+'%');\n     $('[data-value=\"'+input.dataset.param+'\"]').textContent=String(value);input.disabled=!available;\n   }\n   for(const mode of all('[data-mode]')){mode.setAttribute('aria-pressed',String(mode.dataset.mode===state.mode));mode.disabled=!available;}\n   for(const field of ['shapes','materials','palettes'])ui(field).disabled=challenging||!available;\n   for(const button of all('[data-action]'))if(button.dataset.action!=='retry')button.disabled=!available;\n   $('[data-action=\"pause\"]').disabled=challenging||!available;\n   $('[data-action=\"random\"]').disabled=challenging||!available;\n   $('[data-action=\"export\"]').disabled=exporting||!available;\n   $('[data-action=\"pause\"]').setAttribute('aria-pressed',String(state.paused));\n   $('[data-action=\"pause\"]').textContent=state.paused?'\u25b7 Animer':'\u2161 Figer';\n   ui('live').textContent=challenging?'\u00c0 TOI DE RETROUVER':state.paused?'INSTANT SUSPENDU':'MATI\u00c8RE VIVANTE';\n   ui('record').textContent=saved.record?String(saved.record):'\u2014';\n   ui('piece-title').textContent=Core.shapes[r.shape];\n   ui('piece-sub').textContent=Core.materials[r.material]+' \u00b7 '+Core.palettes[r.palette].name;\n   ui('palette-name').textContent=Core.palettes[r.palette].name;\n   ui('edition').textContent=challenging?'\u00c9TUDE DU JOUR':'\u00c9TUDE '+String(state.edition).padStart(3,'0');\n   ui('challenge').hidden=!challenging;ui('caption').hidden=challenging;\n   ui('mode-note').textContent=challenging?'Trois curseurs. Un \u0153il. \u00c0 toi de trouver.':'Aucun mauvais geste. Juste des possibilit\u00e9s.';\n   ui('panel-foot').textContent=challenging?'Dans ce d\u00e9fi, seuls les trois curseurs changent la forme.':'Le hasard a parfois beaucoup de go\u00fbt.';\n   if(!storageAvailable)ui('storage-note').textContent='Stockage indisponible : collection conserv\u00e9e pour cette session seulement.';\n }\n function draw(){\n   if(renderer&&available)renderer.draw(options());\n   if(state.mode==='challenge'&&targetRenderer&&targetRecipe)targetRenderer.draw({...options(renderState(targetRecipe)),time:0,pulse:0});\n }\n function schedule(){if(!raf&&visible&&!document.hidden&&available)raf=requestAnimationFrame(frame);}\n function frame(now){\n   raf=0;\n   if(!root.isConnected){cleanup();return;}\n   if(!visible||document.hidden||!available)return;\n   const dt=Math.min(.05,lastFrame?(now-lastFrame)\/1000:1\/60);lastFrame=now;\n   if(!state.paused){state.time+=dt;if(!dragging)state.yaw+=dt*.09;}\n   state.pulse*=Math.exp(-dt*3.6);\n   if(state.pulse<.001)state.pulse=0;\n   const goal=renderState(state.recipe),amount=reduce.matches?1:1-Math.exp(-dt*12);\n   let difference=0;\n   for(const key of ['weights','materials','params','colorA','colorB'])for(let i=0;i<goal[key].length;i++){\n     const delta=goal[key][i]-current[key][i];difference+=Math.abs(delta);\n     current[key][i]=Math.abs(delta)<.00005?goal[key][i]:current[key][i]+delta*amount;\n   }\n   draw();\n   if(!state.paused||state.pulse||difference>.0005)schedule();\n }\n function resize(){\n   if(!renderer)return;\n   const rect=canvas.getBoundingClientRect();\n   if(rect.width<1||rect.height<1)return;\n   const ratio=Math.min(window.devicePixelRatio||1,1.65,Math.sqrt(1100000\/(rect.width*rect.height)));\n   renderer.resize(rect.width*ratio,rect.height*ratio);\n   if(targetRenderer){const box=ui('target').getBoundingClientRect();if(box.width)targetRenderer.resize(box.width*Math.min(devicePixelRatio||1,2),box.height*Math.min(devicePixelRatio||1,2));}\n   \/\/ A canvas resize clears its bitmap, even while off screen or paused.\n   \/\/ Repaint once immediately; continuous animation still sleeps off screen.\n   if(available)draw();\n   schedule();\n }\n function rendererFailed(){\n   available=false;ui('loading').hidden=true;ui('fallback').hidden=false;ui('stage').setAttribute('aria-busy','false');\n   if(raf){cancelAnimationFrame(raf);raf=0;}\n   updateUI();\n }\n function init(){\n   try{\n     if(renderer)renderer.dispose();\n     renderer=new SculptureRenderer(canvas);\n     available=true;ui('fallback').hidden=true;ui('loading').hidden=true;ui('stage').setAttribute('aria-busy','false');\n     updateUI();resize();schedule();\n   }catch(error){console.warn('Mati\u00e8re : moteur 3D indisponible.',error.message);rendererFailed();}\n }\n canvas.addEventListener('webglcontextlost',event=>{event.preventDefault();rendererFailed();});\n canvas.addEventListener('webglcontextrestored',()=>{init();notify('La mati\u00e8re est de retour.');});\n ui('target').addEventListener('webglcontextlost',event=>{event.preventDefault();if(state.mode==='challenge'){setMode('free');notify('Le d\u00e9fi est momentan\u00e9ment indisponible. Ta cr\u00e9ation reste disponible.');}});\n ui('target').addEventListener('webglcontextrestored',()=>{if(targetRenderer)targetRenderer.dispose();targetRenderer=null;});\n function setMode(mode){\n   if(mode===state.mode)return;\n   if(mode==='challenge'){\n     try{\n       if(!targetRenderer||targetRenderer.gl.isContextLost())targetRenderer=new SculptureRenderer(ui('target'),64);\n     }catch(_){notify('Le d\u00e9fi est indisponible sur cet appareil. Continue \u00e0 cr\u00e9er librement.');return;}\n     freeSnapshot={...state,recipe:{...state.recipe}};\n     targetRecipe=Core.dailyRecipe(Core.localDay());\n     state.recipe={...targetRecipe,relief:32,twist:12,volume:40};\n     if(Core.compare(state.recipe,targetRecipe).score>75)state.recipe={...targetRecipe,relief:80,twist:75,volume:18};\n     Object.assign(state,{yaw:.4,pitch:.84,zoom:1,time:0,pulse:0,paused:true,mode:'challenge'});\n     ui('score').replaceChildren(document.createTextNode('\u2014'),Object.assign(document.createElement('small'),{textContent:'\/ 100'}));\n     ui('feedback').textContent='Observe les volumes, les plis et la torsion.';\n     ui('challenge-date').textContent='LE D\u00c9FI DU '+new Date().toLocaleDateString('fr-FR',{day:'numeric',month:'long'}).toUpperCase();\n     notify('Retrouve la pi\u00e8ce de r\u00e9f\u00e9rence avec les trois curseurs.');\n     showLesson('challenge');\n   }else{\n     if(freeSnapshot)Object.assign(state,freeSnapshot,{recipe:{...freeSnapshot.recipe}});\n     state.mode='free';freeSnapshot=null;\n     showLesson('material',state.recipe.material);\n   }\n   updateUI();resize();schedule();\n }\n function compare(){\n   if(state.mode!=='challenge'||!targetRecipe)return;\n   const result=Core.compare(state.recipe,targetRecipe),score=result.score;\n   ui('score').replaceChildren(document.createTextNode(String(score)),Object.assign(document.createElement('small'),{textContent:'\/ 100'}));\n   const higher=score>saved.record;\n   if(higher){saved.record=score;persist();}\n   const labels={relief:['Ajoute du relief : compare les creux et les ruptures dans les reflets.','Adoucis le relief : les plis de la r\u00e9f\u00e9rence sont moins marqu\u00e9s.'],twist:['Augmente la torsion : observe la direction des plis.','D\u00e9tends la torsion : observe la direction des plis.'],volume:['Augmente le volume : la r\u00e9f\u00e9rence est plus \u00e9paisse.','R\u00e9duis le volume : la r\u00e9f\u00e9rence est plus fine.']};\n   showLesson(score===100?'challenge':result.key);\n   let feedback;\n   if(score===100){feedback='100 \/ 100. L\u2019\u0153il et le geste : tu as retrouv\u00e9 la pi\u00e8ce !';state.pulse=1;notify(higher?'100 \/ 100 \u2014 nouveau record !':'100 \/ 100 \u2014 pi\u00e8ce retrouv\u00e9e !');}\n   else if(score>=90)feedback='Tout pr\u00e8s du geste juste. '+labels[result.key][result.direction>0?1:0];\n   else feedback=(score>=65?'La forme se rapproche. ':'Observe encore la r\u00e9f\u00e9rence. ')+labels[result.key][result.direction>0?1:0];\n   if(higher&&score<100)feedback+=' Nouveau record !';\n   ui('feedback').textContent=feedback;updateUI();schedule();\n }\n function pulse(){if(state.mode==='challenge'){notify('Pour comparer pr\u00e9cis\u00e9ment, la sculpture reste immobile dans le d\u00e9fi.');return;}state.pulse=1.2;schedule();}\n function camera(){state.yaw=.4;state.pitch=.84;state.zoom=1;schedule();}\n function settle(){current=renderState(state.recipe);draw();}\n function renderGallery(){\n   const gallery=ui('gallery');gallery.replaceChildren();\n   ui('empty').hidden=saved.pieces.length>0;\n   ui('collection-count').textContent=String(saved.pieces.length).padStart(2,'0')+' \/ 06';\n   for(const piece of saved.pieces){\n     const card=document.createElement('div');card.className='m-card';\n     const restore=document.createElement('button');restore.type='button';restore.dataset.restore=piece.id;\n     restore.setAttribute('aria-label','Retrouver '+Core.shapes[piece.recipe.shape]+' en '+Core.materials[piece.recipe.material]);\n     const img=document.createElement('img');img.src=piece.thumb;img.alt='';img.width=300;img.height=240;img.loading='lazy';\n     const title=document.createElement('strong');title.textContent=Core.shapes[piece.recipe.shape];\n     const desc=document.createElement('small');desc.textContent=Core.materials[piece.recipe.material]+' \u00b7 '+Core.palettes[piece.recipe.palette].name;\n     restore.append(img,title,desc);\n     const remove=document.createElement('button');remove.type='button';remove.className='m-remove';remove.dataset.remove=piece.id;remove.textContent='\u00d7';remove.setAttribute('aria-label','Retirer '+title.textContent+' de la collection');\n     card.append(restore,remove);gallery.append(card);\n   }\n }\n function save(){\n   if(saved.pieces.length>=6){notify('Ta collection contient d\u00e9j\u00e0 6 pi\u00e8ces. Retire-en une pour faire de la place.');return;}\n   try{\n     settle();\n     const thumb=document.createElement('canvas');thumb.width=300;thumb.height=240;\n     const context=thumb.getContext('2d');\n     context.fillStyle='#0c1019';context.fillRect(0,0,300,240);\n     const scale=Math.min(300\/canvas.width,240\/canvas.height),w=canvas.width*scale,h=canvas.height*scale;\n     context.drawImage(canvas,(300-w)\/2,(240-h)\/2,w,h);\n     saved.pieces.push({id:Date.now().toString(36)+'-'+Math.random().toString(36).slice(2,9),recipe:{...state.recipe},thumb:thumb.toDataURL('image\/jpeg',.8),yaw:state.yaw,pitch:state.pitch,zoom:state.zoom,time:state.time});\n     const stored=persist();renderGallery();notify(stored?'Une nouvelle pi\u00e8ce dans ta collection.':'Pi\u00e8ce gard\u00e9e pour cette session. Le navigateur ne permet pas la sauvegarde.');\n   }catch(_){notify('Cette pi\u00e8ce n\u2019a pas pu \u00eatre gard\u00e9e. Essaie \u00e0 nouveau.');}\n }\n function restorePiece(id){\n   const piece=saved.pieces.find(p=>p.id===id);if(!piece)return;\n   if(state.mode==='challenge')setMode('free');\n   Object.assign(state,{recipe:{...piece.recipe},yaw:piece.yaw,pitch:piece.pitch,zoom:piece.zoom,time:piece.time,paused:true,pulse:0,edition:state.edition+1});\n   updateUI();schedule();\n   canvas.scrollIntoView({behavior:reduce.matches?'instant':'smooth',block:'center'});\n   canvas.focus({preventScroll:true});notify('Ta pi\u00e8ce est de retour, dans la pose que tu avais gard\u00e9e.');\n }\n async function exportPoster(){\n   if(exporting)return;\n   exporting=true;const button=$('[data-action=\"export\"]');button.textContent='Pr\u00e9paration de l\u2019affiche\u2026';updateUI();\n   let exportRenderer=null;\n   try{\n     await new Promise(resolve=>requestAnimationFrame(resolve));\n     const recipe={...state.recipe},shot=options(renderState(recipe));shot.pulse=0;\n     const surface=document.createElement('canvas');exportRenderer=new SculptureRenderer(surface,192);exportRenderer.resize(1800,1800);exportRenderer.draw(shot);\n     const poster=document.createElement('canvas');poster.width=1800;poster.height=2100;\n     const ctx=poster.getContext('2d');if(!ctx)throw Error('Canvas indisponible');\n     ctx.fillStyle='#0b1019';ctx.fillRect(0,0,1800,2100);ctx.drawImage(surface,0,0);\n     exportRenderer.release();exportRenderer=null;\n     ctx.fillStyle='#dbe6f7';ctx.font='500 24px \"Geist Mono\", monospace';\n     ctx.fillText('OK3D \/ SCULPT LAB',96,110);\n     ctx.textAlign='right';ctx.fillStyle='#b9c8e0';ctx.fillText('UNE PI\u00c8CE UNIQUE. LA TIENNE.',1704,110);ctx.textAlign='left';\n     ctx.strokeStyle='#2b3547';ctx.lineWidth=1;ctx.beginPath();ctx.moveTo(96,1806);ctx.lineTo(1704,1806);ctx.stroke();\n     ctx.fillStyle='#f0f3fb';ctx.font='500 84px \"Unbounded\", sans-serif';ctx.fillText(Core.shapes[recipe.shape],92,1930);\n     ctx.fillStyle='#b7c4d9';ctx.font='400 28px \"Geist\", sans-serif';ctx.fillText(Core.materials[recipe.material]+' \u00b7 '+Core.palettes[recipe.palette].name,96,1990);\n     ctx.textAlign='right';ctx.font='400 20px \"Geist Mono\", monospace';ctx.fillText('RELIEF '+recipe.relief+' \/ TORSION '+recipe.twist+' \/ VOLUME '+recipe.volume,1704,1988);\n     ctx.fillStyle='#8395b3';ctx.font='400 18px \"Geist Mono\", monospace';ctx.fillText(Core.localDay()+' \u2014 CR\u00c9\u00c9 AVEC SCULPT LAB',1704,2040);\n     const blob=await new Promise((resolve,reject)=>poster.toBlob(value=>value?resolve(value):reject(Error('Export impossible')),'image\/png'));\n     const url=URL.createObjectURL(blob),link=document.createElement('a');link.href=url;link.download='OK3D-Sculpt-Lab-'+Core.localDay()+'-'+Date.now().toString(36)+'.png';\n     root.append(link);link.click();link.remove();setTimeout(()=>URL.revokeObjectURL(url),60000);\n     notify('Ton affiche est pr\u00eate. Le t\u00e9l\u00e9chargement a \u00e9t\u00e9 lanc\u00e9.');\n   }catch(error){console.warn('Mati\u00e8re : export indisponible.',error.message);notify('L\u2019export n\u2019a pas abouti sur cet appareil. Ta pi\u00e8ce reste dans l\u2019atelier.');}\n   finally{if(exportRenderer)exportRenderer.release();exporting=false;button.innerHTML='<span aria-hidden=\"true\">\u2193<\/span> Exporter l\u2019affiche';updateUI();schedule();}\n }\n root.addEventListener('input',event=>{\n   const input=event.target.closest('[data-param]');if(!input)return;\n   state.recipe[input.dataset.param]=Core.clamp(Number(input.value),0,100);\n   $('[data-value=\"'+input.dataset.param+'\"]').textContent=input.value;input.style.setProperty('--value',input.value+'%');\n   showLesson(input.dataset.param);\n   if(state.mode==='challenge')ui('feedback').textContent='Compare \u00e0 nouveau pour \u00e9valuer ton dernier geste.';\n   schedule();\n });\n \/\/ Keep keyboard-focused controls below the sticky sculpture on mobile.\n root.addEventListener('focusin',event=>{\n   if(window.innerWidth>820||!event.target.closest('.m-panel'))return;\n   const control=event.target.closest('.m-slider')||event.target;\n   const top=control.getBoundingClientRect().top,bottom=ui('stage').getBoundingClientRect().bottom;\n   if(top<bottom+16){\n     const full=(document.fullscreenElement||document.webkitFullscreenElement)===root||root.classList.contains('is-app-screen');\n     (full?root:window).scrollBy({top:top-bottom-16,behavior:'instant'});\n   }\n });\n root.addEventListener('click',event=>{\n   const button=event.target.closest('button');if(!button||button.disabled||!root.contains(button))return;\n   if(button.hasAttribute('data-okk-fullscreen'))return;\n   if(button.dataset.mode){setMode(button.dataset.mode);return;}\n   if(button.dataset.restore){if(available)restorePiece(button.dataset.restore);return;}\n   if(button.dataset.remove){\n     const index=saved.pieces.findIndex(p=>p.id===button.dataset.remove);if(index<0)return;\n     saved.pieces.splice(index,1);persist();renderGallery();notify('Pi\u00e8ce retir\u00e9e de la collection.');\n     const next=ui('gallery').querySelector('button')||$('[data-action=\"save\"]');next.focus();return;\n   }\n   for(const key of ['shape','material','palette'])if(button.hasAttribute('data-'+key)){\n     if(state.mode==='challenge'||!available)return;\n     state.recipe[key]=Number(button.dataset[key]);if(key==='shape')state.edition++;\n     showLesson(key,state.recipe[key]);\n     updateUI();schedule();return;\n   }\n   const action=button.dataset.action;\n   if(action==='retry'){init();return;}if(!available)return;\n   if(action==='random'){state.recipe=Core.randomRecipe();state.edition++;state.pulse=.6;showLesson('material',state.recipe.material);updateUI();schedule();}\n   else if(action==='pulse')pulse();\n   else if(action==='camera')camera();\n   else if(action==='zoom-in'){state.zoom=Core.clamp(state.zoom+.1,.65,1.35);schedule();}\n   else if(action==='zoom-out'){state.zoom=Core.clamp(state.zoom-.1,.65,1.35);schedule();}\n   else if(action==='pause'){state.paused=!state.paused;updateUI();schedule();}\n   else if(action==='compare')compare();\n   else if(action==='save')save();\n   else if(action==='export')exportPoster();\n });\n canvas.addEventListener('pointerdown',event=>{\n   if(!available||event.button!==0)return;\n   pointers.set(event.pointerId,{x:event.clientX,y:event.clientY});dragging=true;pinchDistance=0;\n   if(pointers.size===1)touchStart={x:event.clientX,y:event.clientY,moved:false};else if(touchStart)touchStart.moved=true;\n   canvas.focus({preventScroll:true});try{canvas.setPointerCapture(event.pointerId);}catch(_){}\n });\n canvas.addEventListener('pointermove',event=>{\n   const previous=pointers.get(event.pointerId);if(!previous)return;\n   const next={x:event.clientX,y:event.clientY};pointers.set(event.pointerId,next);\n   if(touchStart&&Math.hypot(event.clientX-touchStart.x,event.clientY-touchStart.y)>5)touchStart.moved=true;\n   if(pointers.size===1){state.yaw+=(next.x-previous.x)*.008;state.pitch=Core.clamp(state.pitch+(next.y-previous.y)*.008,-1.45,1.45);}\n   else{\n     const [a,b]=Array.from(pointers.values()),distance=Math.hypot(a.x-b.x,a.y-b.y);\n     if(pinchDistance>0)state.zoom=Core.clamp(state.zoom*distance\/pinchDistance,.65,1.35);pinchDistance=distance;\n   }\n   schedule();\n });\n function endPointer(event){\n   if(!pointers.has(event.pointerId))return;\n   const tap=event.type==='pointerup'&&pointers.size===1&&touchStart&&!touchStart.moved;\n   pointers.delete(event.pointerId);pinchDistance=0;dragging=pointers.size>0;\n   if(!dragging)touchStart=null;\n   if(tap)pulse();schedule();\n }\n for(const event of ['pointerup','pointercancel','lostpointercapture'])canvas.addEventListener(event,endPointer);\n canvas.addEventListener('keydown',event=>{\n   if(!available)return;\n   const key=event.key;\n   if(!['ArrowLeft','ArrowRight','ArrowUp','ArrowDown','+','=','-',' ','Home'].includes(key))return;\n   event.preventDefault();\n   if(key==='ArrowLeft')state.yaw-=.12;if(key==='ArrowRight')state.yaw+=.12;\n   if(key==='ArrowUp')state.pitch=Core.clamp(state.pitch-.12,-1.45,1.45);if(key==='ArrowDown')state.pitch=Core.clamp(state.pitch+.12,-1.45,1.45);\n   if(key==='+'||key==='=')state.zoom=Core.clamp(state.zoom+.1,.65,1.35);if(key==='-')state.zoom=Core.clamp(state.zoom-.1,.65,1.35);\n   if(key==='Home')camera();if(key===' ')pulse();schedule();\n });\n function fitLayout(){\n   layoutRAF=0;if(!root.isConnected)return;\n   const full=(document.fullscreenElement||document.webkitFullscreenElement)===root||root.classList.contains('is-app-screen');\n   const put=(key,value)=>{if(root.style.getPropertyValue(key)!==value)root.style.setProperty(key,value);};\n   if(!full){\n     const shift=parseFloat(root.style.getPropertyValue('--m-shift'))||0;\n     put('--m-shift',(Math.round((shift-root.getBoundingClientRect().left)*100)\/100)+'px');\n     put('--m-width',document.documentElement.clientWidth+'px');\n   }\n   const admin=document.getElementById('wpadminbar');\n   put('--okk-admin-height',(!full&&admin&&getComputedStyle(admin).position==='fixed'?admin.getBoundingClientRect().height:0)+'px');\n   resize();\n }\n function scheduleLayout(){if(!layoutRAF)layoutRAF=requestAnimationFrame(fitLayout);}\n const sizeObserver=typeof ResizeObserver==='function'?new ResizeObserver(scheduleLayout):null;\n if(sizeObserver){sizeObserver.observe(ui('stage'));sizeObserver.observe(document.documentElement);if(root.parentElement)sizeObserver.observe(root.parentElement);}\n const visibleObserver=typeof IntersectionObserver==='function'?new IntersectionObserver(entries=>{\n   visible=entries[0].isIntersecting;if(visible){lastFrame=0;schedule();}else if(raf){cancelAnimationFrame(raf);raf=0;}\n },{rootMargin:'160px'}):null;\n if(visibleObserver)visibleObserver.observe(ui('stage'));\n function visibilityChange(){lastFrame=0;if(document.hidden&&raf){cancelAnimationFrame(raf);raf=0;}else schedule();}\n function reduceChange(){if(reduce.matches){state.paused=true;updateUI();schedule();}}\n document.addEventListener('visibilitychange',visibilityChange);\n window.addEventListener('resize',scheduleLayout);\n window.addEventListener('orientationchange',scheduleLayout);\n if(window.visualViewport)window.visualViewport.addEventListener('resize',scheduleLayout);\n if(reduce.addEventListener)reduce.addEventListener('change',reduceChange);\n if(document.fonts)document.fonts.ready.then(()=>{if(root.isConnected)scheduleLayout();});\n function cleanup(){\n   if(raf)cancelAnimationFrame(raf);if(layoutRAF)cancelAnimationFrame(layoutRAF);clearTimeout(toastTimer);\n   if(sizeObserver)sizeObserver.disconnect();if(visibleObserver)visibleObserver.disconnect();\n   document.removeEventListener('visibilitychange',visibilityChange);window.removeEventListener('resize',scheduleLayout);window.removeEventListener('orientationchange',scheduleLayout);\n   if(window.visualViewport)window.visualViewport.removeEventListener('resize',scheduleLayout);\n   if(reduce.removeEventListener)reduce.removeEventListener('change',reduceChange);\n   if(renderer)renderer.dispose();if(targetRenderer)targetRenderer.dispose();\n }\n (function setupOK3DFullscreen(root) {\n  const doc = root.ownerDocument;\n  const win = doc.defaultView;\n  let appState = null;\n  const nativeFull = () => (doc.fullscreenElement || doc.webkitFullscreenElement) === root;\n  const active = () => nativeFull() || root.classList.contains('is-app-screen');\n  const attribute = (node, name, value) => {\n    if (node.getAttribute(name) !== value) node.setAttribute(name, value);\n  };\n  const sync = () => {\n    const full = active();\n    root.querySelectorAll('[data-okk-fullscreen]').forEach(button => {\n      const label = full ? '\u2922 Quitter le plein \u00e9cran' : '\u26f6 Plein \u00e9cran';\n      if (button.textContent !== label) button.textContent = label;\n      button.hidden = false;\n      attribute(button, 'aria-pressed', String(full));\n      attribute(button, 'aria-label', full ? 'Quitter le plein \u00e9cran' : 'Afficher en plein \u00e9cran');\n      attribute(button, 'title', full ? 'Quitter le plein \u00e9cran (\u00c9chap)' : 'Afficher en plein \u00e9cran');\n    });\n  };\n  const changed = () => {\n    sync();\n    win.dispatchEvent(new Event('resize'));\n  };\n  const saveOverflow = node => [node.style.getPropertyValue('overflow'), node.style.getPropertyPriority('overflow')];\n  const restoreOverflow = (node, value) => {\n    if (value[0]) node.style.setProperty('overflow', value[0], value[1]);\n    else node.style.removeProperty('overflow');\n  };\n  const focusButton = () => {\n    const button = root.querySelector('[data-okk-fullscreen]');\n    if (button) button.focus({ preventScroll: true });\n  };\n  const enterApp = () => {\n    if (appState) return;\n    const marker = doc.createComment('OK3D \u2014 retour du mode app');\n    appState = {\n      marker, parent: root.parentNode, next: root.nextSibling,\n      bodyOverflow: saveOverflow(doc.body), htmlOverflow: saveOverflow(doc.documentElement),\n      x: win.scrollX, y: win.scrollY\n    };\n    root.before(marker);\n    \/\/ Sortir des conteneurs transform\u00e9s d'Elementor pour couvrir toute la fen\u00eatre.\n    doc.body.appendChild(root);\n    root.classList.add('is-app-screen');\n    doc.body.style.setProperty('overflow', 'hidden', 'important');\n    doc.documentElement.style.setProperty('overflow', 'hidden', 'important');\n    root.scrollTop = 0;\n    changed();\n    focusButton();\n  };\n  const leaveApp = () => {\n    if (!appState) return;\n    const saved = appState;\n    appState = null;\n    root.classList.remove('is-app-screen');\n    if (saved.marker.isConnected) saved.marker.replaceWith(root);\n    else if (saved.parent && saved.parent.isConnected) {\n      saved.parent.insertBefore(root, saved.next && saved.next.parentNode === saved.parent ? saved.next : null);\n    }\n    restoreOverflow(doc.body, saved.bodyOverflow);\n    restoreOverflow(doc.documentElement, saved.htmlOverflow);\n    changed();\n    win.scrollTo(saved.x, saved.y);\n    focusButton();\n  };\n  root.addEventListener('click', async event => {\n    const button = event.target.closest && event.target.closest('[data-okk-fullscreen]');\n    if (!button || !root.contains(button)) return;\n    event.preventDefault();\n    if (appState) { leaveApp(); return; }\n    if (nativeFull()) {\n      const exit = doc.exitFullscreen || doc.webkitExitFullscreen;\n      if (exit) {\n        try { await exit.call(doc); } catch (_) { attribute(button, 'title', 'Appuie sur \u00c9chap pour quitter le plein \u00e9cran'); }\n      }\n      return;\n    }\n    const request = root.requestFullscreen || root.webkitRequestFullscreen;\n    if (request) {\n      try { await request.call(root); sync(); return; } catch (_) { \/* Aper\u00e7u ou appareil sans API : mode app. *\/ }\n    }\n    enterApp();\n  });\n  doc.addEventListener('keydown', event => {\n    if (event.key === 'Escape' && appState) {\n      event.preventDefault();\n      event.stopPropagation();\n      leaveApp();\n      return;\n    }\n    if (event.key !== 'Tab' || !active()) return;\n    const items = Array.from(root.querySelectorAll('button:not(:disabled),a[href],input:not(:disabled),select:not(:disabled),textarea:not(:disabled),[tabindex=\"0\"]'))\n      .filter(node => !node.hidden && node.getClientRects().length);\n    if (!items.length) return;\n    const first = items[0], last = items[items.length - 1];\n    if (event.shiftKey && (doc.activeElement === first || !root.contains(doc.activeElement))) {\n      event.preventDefault(); last.focus();\n    } else if (!event.shiftKey && (doc.activeElement === last || !root.contains(doc.activeElement))) {\n      event.preventDefault(); first.focus();\n    }\n  }, true);\n  doc.addEventListener('fullscreenchange', changed);\n  doc.addEventListener('webkitfullscreenchange', changed);\n  \/\/ Les barres de jeu (Keystroke notamment) sont reconstruites apr\u00e8s chaque s\u00e9rie.\n  if (typeof MutationObserver !== 'undefined') new MutationObserver(sync).observe(root, { childList: true, subtree: true });\n  sync();\n  return { active };\n})(root);\n renderGallery();init();fitLayout();\n})();\n<\/script>\n\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>\u26f6 Plein \u00e9cran \u2733 Ton record \u2014\/ 100 JOUE AVEC LA MATI\u00c8RE. Sculpt Lab. La beaut\u00e9 n\u2019a pas de mode d\u2019emploi. Tourne. D\u00e9forme. Laisse-toi surprendre.La prochaine pi\u00e8ce de collection, c\u2019est la tienne. Cr\u00e9ation libreLe d\u00e9fi du jour \u2197 Aucun mauvais geste. Juste des possibilit\u00e9s. Sculpture interactive : utilise les commandes de forme, mati\u00e8re et couleur. [&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-15721","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15721","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=15721"}],"version-history":[{"count":4,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15721\/revisions"}],"predecessor-version":[{"id":15725,"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/pages\/15721\/revisions\/15725"}],"wp:attachment":[{"href":"https:\/\/www.ok3d.fr\/en\/wp-json\/wp\/v2\/media?parent=15721"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}