{"id":4839,"date":"2025-09-14T16:20:34","date_gmt":"2025-09-14T14:20:34","guid":{"rendered":"https:\/\/www.ecopalettee2se.fr\/?page_id=4839"},"modified":"2026-07-30T08:35:17","modified_gmt":"2026-07-30T06:35:17","slug":"simulateur-palette-bois","status":"publish","type":"page","link":"https:\/\/www.ecopalettee2se.fr\/de\/simulateur-palette-bois\/","title":{"rendered":"Configurateur Solutions Bois Pro"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"4839\" class=\"elementor elementor-4839\" data-elementor-post-type=\"page\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-72fd40a elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"72fd40a\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-9b9393b\" data-id=\"9b9393b\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-6ef639d elementor-widget elementor-widget-heading\" data-id=\"6ef639d\" data-element_type=\"widget\" data-widget_type=\"heading.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<h1 class=\"elementor-heading-title elementor-size-default\">Configurateur de solutions bois sur mesure en ligne<\/h1>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-3fa33e5 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"3fa33e5\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-0054254\" data-id=\"0054254\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-393f1a5 elementor-widget elementor-widget-text-editor\" data-id=\"393f1a5\" data-element_type=\"widget\" data-widget_type=\"text-editor.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t\t\t\t\t<p>Concevez votre emballage bois directement en ligne. Choisissez votre produit, palette, rehausse, plateau pour rack ou caisse de transport, renseignez vos dimensions et vos options, et visualisez le r\u00e9sultat en 3D au fur et \u00e0 mesure. L&rsquo;outil calcule aussi le nombre de palettes que vous pouvez charger dans un camion ou un conteneur selon votre format. Une fois votre configuration termin\u00e9e, envoyez-la en un clic \u00e0 notre bureau d&rsquo;\u00e9tude. Nous validons la structure, l&rsquo;essence de bois et le traitement adapt\u00e9s \u00e0 votre usage, puis nous vous adressons un devis gratuit sous 24h. Fabrication fran\u00e7aise \u00e0 Firminy, dans la Loire.<\/p>\t\t\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-bd7b785 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"bd7b785\" data-element_type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-73bc1ea\" data-id=\"73bc1ea\" data-element_type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-5aaa13e elementor-widget elementor-widget-html\" data-id=\"5aaa13e\" data-element_type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!-- Configurateur Solutions Bois Pro, Ecopalette E2SE.\r\n     A coller dans un widget HTML Elementor. Necessite un compte administrateur\r\n     (WordPress bloque les scripts pour les autres roles).\r\n     Version alignee sur les sept fiches produit 07\/2026. -->\r\n<div id=\"ecopalette-configurateur\" style=\"min-height:600px\"><\/div>\r\n<script crossorigin src=\"https:\/\/unpkg.com\/react@18\/umd\/react.production.min.js\"><\/script>\r\n<script crossorigin src=\"https:\/\/unpkg.com\/react-dom@18\/umd\/react-dom.production.min.js\"><\/script>\r\n<script src=\"https:\/\/unpkg.com\/@babel\/standalone\/babel.min.js\"><\/script>\r\n<script type=\"text\/babel\">\r\nconst { useState, useMemo, useEffect } = React;\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 BRAND TOKENS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst ACCENT = \"#76B82A\";\r\nconst ACCENT_TEXT = \"#5A8F1F\";\r\nconst DARK = \"#01173C\";\r\nconst FONT_TITLE = \"'Teko', sans-serif\";\r\nconst FONT_BODY = \"'Poppins', sans-serif\";\r\nconst FONT_MONO = \"'DM Mono', monospace\";\r\nconst PHONE = \"04 77 61 39 39\";\r\nconst CONTACT_URL = \"https:\/\/www.ecopalettee2se.fr\/contact\/\";\r\nconst EMAIL = \"contact@ecopalette-e2se.fr\";\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 ICONS \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nfunction Icon({ name, size = 20, color = ACCENT, stroke = 1.7, style }) {\r\n  const p = { width: size, height: size, viewBox: \"0 0 24 24\", fill: \"none\", stroke: color, strokeWidth: stroke, strokeLinecap: \"round\", strokeLinejoin: \"round\", style: { flexShrink: 0, ...style } };\r\n  const s = {\r\n    pallet: <><rect x=\"3\" y=\"7\" width=\"18\" height=\"3\" rx=\"0.5\" \/><rect x=\"3\" y=\"14\" width=\"18\" height=\"3\" rx=\"0.5\" \/><path d=\"M6 10v4M12 10v4M18 10v4\" \/><\/>,\r\n    rehausse: <><path d=\"M4 9h16v9H4z\" \/><path d=\"M4 13.5h16\" \/><circle cx=\"8\" cy=\"13.5\" r=\"0.9\" \/><circle cx=\"16\" cy=\"13.5\" r=\"0.9\" \/><\/>,\r\n    rack: <><path d=\"M4 5v14M20 5v14\" \/><path d=\"M4 9.5h16M4 14.5h16\" \/><path d=\"M7 5v3M17 5v3\" \/><\/>,\r\n    crate: <><path d=\"M12 3l8 4.5v9L12 21l-8-4.5v-9z\" \/><path d=\"M4 7.5l8 4.5 8-4.5M12 12v9\" \/><\/>,\r\n    truck: <><rect x=\"2\" y=\"7\" width=\"12\" height=\"9\" rx=\"1\" \/><path d=\"M14 10h3.5l2.5 3v3h-6z\" \/><circle cx=\"6\" cy=\"18\" r=\"1.7\" \/><circle cx=\"17\" cy=\"18\" r=\"1.7\" \/><\/>,\r\n    ruler: <><rect x=\"2\" y=\"8\" width=\"20\" height=\"8\" rx=\"1\" \/><path d=\"M6 8v3M10 8v4M14 8v3M18 8v4\" \/><\/>,\r\n    stack: <><path d=\"M12 3l9 5-9 5-9-5z\" \/><path d=\"M3 12l9 5 9-5\" \/><path d=\"M3 16l9 5 9-5\" \/><\/>,\r\n    box: <><path d=\"M12 3l8 4.5v9L12 21l-8-4.5v-9z\" \/><path d=\"M4 7.5l8 4.5 8-4.5M12 12v9\" \/><\/>,\r\n    grid: <><rect x=\"4\" y=\"4\" width=\"16\" height=\"16\" rx=\"1.5\" \/><path d=\"M4 10h16M4 15h16\" \/><\/>,\r\n    arrowLR: <path d=\"M3 12h18M3 12l3-3M3 12l3 3M21 12l-3-3M21 12l-3 3\" \/>,\r\n    arrowUD: <path d=\"M12 3v18M12 3l-3 3M12 3l3 3M12 21l-3-3M12 21l3 3\" \/>,\r\n    layers: <><path d=\"M12 3l9 5-9 5-9-5z\" \/><path d=\"M3 12l9 5 9-5\" \/><\/>,\r\n    tag: <><path d=\"M3 12V4h8l10 10-8 8L3 12z\" \/><circle cx=\"7.5\" cy=\"7.5\" r=\"1.2\" \/><\/>,\r\n    scissors: <><circle cx=\"6\" cy=\"7\" r=\"2\" \/><circle cx=\"6\" cy=\"17\" r=\"2\" \/><path d=\"M8 8.5L20 17M8 15.5L20 7\" \/><\/>,\r\n    hash: <path d=\"M9 3.5v17M15 3.5v17M3.5 9h17M3.5 15h17\" \/>,\r\n    note: <><path d=\"M4 20.5h16\" \/><path d=\"M14 4l6 6-9 9H5v-6z\" \/><path d=\"M12.5 5.5l6 6\" \/><\/>,\r\n    check: <path d=\"M20 6.5L9.2 17.3 4 12.1\" \/>,\r\n    info: <><circle cx=\"12\" cy=\"12\" r=\"9\" \/><path d=\"M12 11v5M12 7.5h.01\" \/><\/>,\r\n    warning: <><path d=\"M12 3.5l9 16H3z\" \/><path d=\"M12 10v4M12 17h.01\" \/><\/>,\r\n    shield: <><path d=\"M12 3l8 3v6c0 5-3.4 8-8 9-4.6-1-8-4-8-9V6z\" \/><\/>,\r\n    beam: <><path d=\"M3 9h18v6H3z\" \/><path d=\"M7 9v6M17 9v6\" \/><\/>,\r\n    cradle: <><path d=\"M3 8v8h18V8\" \/><path d=\"M7 8a5 5 0 0 0 10 0\" \/><\/>,\r\n    send: <><path d=\"M21 3L3 10.5l7 2.5 2.5 7z\" \/><path d=\"M21 3l-11 10\" \/><\/>,\r\n  };\r\n  return <svg {...p}>{s[name]}<\/svg>;\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MOTEUR 3D ISOM\u00c9TRIQUE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst COS30 = Math.cos(Math.PI \/ 6);\r\nconst SIN30 = 0.5;\r\nconst WOOD = \"#C9A85C\";\r\nconst WOOD_DK = \"#B8934A\";\r\nconst ORANGE = \"#F5821F\";\r\n\r\nfunction adjustColor(hex, amount) {\r\n  const num = parseInt(hex.replace(\"#\", \"\"), 16);\r\n  const r = Math.min(255, Math.max(0, ((num >> 16) & 0xff) + amount));\r\n  const g = Math.min(255, Math.max(0, ((num >> 8) & 0xff) + amount));\r\n  const b = Math.min(255, Math.max(0, (num & 0xff) + amount));\r\n  return `#${(r << 16 | g << 8 | b).toString(16).padStart(6, \"0\")}`;\r\n}\r\nfunction proj(x, y, z, cx, cy) { return [cx + (x - y) * COS30, cy + (x + y) * SIN30 - z]; }\r\n\r\nfunction IsoBox({ x, y, z, w, h, d, color, cx, cy, edge = 0.6, opacity = 1 }) {\r\n  const P = (X, Y, Z) => proj(X, Y, Z, cx, cy).join(\",\");\r\n  const top = [P(x, y, z + d), P(x + w, y, z + d), P(x + w, y + h, z + d), P(x, y + h, z + d)].join(\" \");\r\n  const right = [P(x + w, y, z), P(x + w, y + h, z), P(x + w, y + h, z + d), P(x + w, y, z + d)].join(\" \");\r\n  const front = [P(x, y + h, z), P(x + w, y + h, z), P(x + w, y + h, z + d), P(x, y + h, z + d)].join(\" \");\r\n  return (\r\n    <g opacity={opacity}>\r\n      <polygon points={front} fill={adjustColor(color, -34)} stroke={adjustColor(color, -52)} strokeWidth={edge} strokeLinejoin=\"round\" \/>\r\n      <polygon points={right} fill={adjustColor(color, -18)} stroke={adjustColor(color, -40)} strokeWidth={edge} strokeLinejoin=\"round\" \/>\r\n      <polygon points={top} fill={color} stroke={adjustColor(color, -14)} strokeWidth={edge} strokeLinejoin=\"round\" \/>\r\n    <\/g>\r\n  );\r\n}\r\n\r\nfunction IsoScene({ cx, cy, footprintX, footprintY, children }) {\r\n  const fid = useMemo(() => \"ds3d-\" + Math.random().toString(36).slice(2, 9), []);\r\n  const sx = proj(footprintX \/ 2, footprintY \/ 2, 0, cx, cy)[0];\r\n  const sy = proj(footprintX \/ 2, footprintY \/ 2, 0, cx, cy)[1] + 8;\r\n  return (\r\n    <svg width=\"100%\" viewBox=\"0 0 460 300\" style={{ display: \"block\", maxWidth: 640, margin: \"0 auto\", overflow: \"visible\" }}>\r\n      <defs><filter id={fid}><feDropShadow dx=\"3\" dy=\"6\" stdDeviation=\"6\" floodColor=\"rgba(1,23,60,0.14)\" \/><\/filter><\/defs>\r\n      <ellipse cx={sx} cy={sy} rx={footprintX * COS30 * 0.62} ry={footprintY * 0.24} fill=\"rgba(1,23,60,0.07)\" \/>\r\n      <g filter={`url(#${fid})`}>{children}<\/g>\r\n    <\/svg>\r\n  );\r\n}\r\n\r\nfunction fitScale(a, b) { return Math.min(0.42, 330 \/ ((a + b) * COS30)); }\r\nfunction centerOf(L, D, S, extraTop = 0) {\r\n  return { cx: 230 - ((L - D) \/ 2) * COS30 * S, cy: 172 - ((L + D) \/ 2) * SIN30 * S - extraTop };\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 CONSTANTES COMMUNES \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\/\/ Options de traitement des fiches produit. Une seule liste pour tous les modules.\r\nconst TRAITEMENTS = [\r\n  { id: \"aucun\", name: \"Aucun\", desc: \"Usage national\" },\r\n  { id: \"nimp15\", name: \"NIMP 15 (HT)\", desc: \"Export hors Union europ\u00e9enne\" },\r\n  { id: \"sechage\", name: \"S\u00e9chage\", desc: \"Stabilisation de l'humidit\u00e9\" },\r\n  { id: \"rabotage\", name: \"Rabotage 4 faces\", desc: \"Finition soign\u00e9e, ar\u00eates nettes\" },\r\n  { id: \"m1\", name: \"Traitement M1\", desc: \"Exigence de s\u00e9curit\u00e9 incendie\" },\r\n  { id: \"autoclave\", name: \"Autoclave\", desc: \"Usage ext\u00e9rieur ou humide\" },\r\n];\r\nconst MARGE_CHARGEMENT = 30; \/\/ mm, jeu de chargement sur la longueur\r\n\r\n\/\/ \u2500\u2500 RACK : plateau bois sur lisses \u2500\u2500\r\nfunction Preview3DRack({ L, P, nb, gap, ep = 27, travT = 27, renforce, finition, traitement }) {\r\n  \/\/ Cadrage exact : montants sous le plateau, planches au-dessus du plan de pose\r\n  const PAD = 22;\r\n  const postM = Math.max(P * 0.42, 260);            \/\/ hauteur de montant, en mm\r\n  const thkM = ep * 2, travM = travT * 2 * (renforce ? 1.4 : 1);\r\n  const S = Math.min(0.42,\r\n    (460 - 2 * PAD) \/ ((L + P) * COS30),\r\n    (300 - 2 * PAD) \/ ((L + P) * SIN30 + postM + thkM));\r\n  const postH = postM * S;\r\n  const thk = thkM * S;\r\n  const travH = travM * S;\r\n  const pw = Math.max(75 * S, 4);\r\n  const sL = L * S, sP = P * S;\r\n  const pitch = sL \/ nb;\r\n  const sGap = Math.max(gap * S, 0.6);\r\n  const plankW = Math.max(pitch - sGap, 2);\r\n  const cx = PAD + P * COS30 * S;\r\n  const cy = PAD + thk;\r\n  \/\/ Teinte selon la finition, autoclave verd\u00e2tre\r\n  const plankC = finition === \"autoclave\" ? \"#A8A06A\"\r\n    : finition === \"lourdes\" ? adjustColor(WOOD, -14)\r\n    : traitement === \"rabotage\" ? adjustColor(WOOD, 16)\r\n    : WOOD;\r\n\r\n  const M = (x, y, k) => <IsoBox key={k} x={x} y={y} z={-postH} w={pw} h={pw} d={postH} color={DARK} cx={cx} cy={cy} \/>;\r\n  const els = [];\r\n  els.push(M(0, 0, \"m00\"));\r\n  els.push(<IsoBox key=\"tb\" x={0} y={0} z={-travH} w={sL} h={pw} d={travH} color={ORANGE} cx={cx} cy={cy} \/>);\r\n  els.push(M(sL - pw, 0, \"m10\"));\r\n  els.push(M(0, sP - pw, \"m01\"));\r\n  for (let i = 0; i < nb; i++) {\r\n    els.push(<IsoBox key={`pl${i}`} x={i * pitch} y={0} z={0} w={plankW} h={sP} d={thk} color={plankC} cx={cx} cy={cy} \/>);\r\n  }\r\n  els.push(<IsoBox key=\"tf\" x={0} y={sP - pw} z={-travH} w={sL} h={pw} d={travH} color={ORANGE} cx={cx} cy={cy} \/>);\r\n  els.push(M(sL - pw, sP - pw, \"m11\"));\r\n  if (traitement === \"nimp15\") {\r\n    const [nx, ny] = proj(sL * 0.5, sP * 0.5, thk, cx, cy);\r\n    els.push(<g key=\"nimp\" transform={`matrix(${COS30} ${SIN30} ${-COS30} ${SIN30} ${nx} ${ny})`} opacity={0.7}>\r\n      <rect x={-14} y={-8} width={28} height={16} rx={2} fill=\"none\" stroke={DARK} strokeWidth={0.9} \/>\r\n      <text x={0} y={-0.5} textAnchor=\"middle\" fontSize={6} fontFamily={FONT_MONO} fontWeight=\"700\" fill={DARK}>IPPC<\/text>\r\n      <text x={0} y={6} textAnchor=\"middle\" fontSize={5.5} fontFamily={FONT_MONO} fill={DARK}>HT<\/text>\r\n    <\/g>);\r\n  }\r\n  return <IsoScene cx={cx} cy={cy} footprintX={sL} footprintY={sP}>{els}<\/IsoScene>;\r\n}\r\n\r\n\/\/ \u2500\u2500 REHAUSSE : cadre bois pliable sur palette, empilable \u2500\u2500\r\nfunction Preview3DRehausse({ L, W, H, stack = 1, sepL = 0, sepT = 0, couvercle, coins, pliable, charnieres = 4,\r\n  grillage, marqTexte, marqColor, nimp, roulettes, etiquettes, distance, magasin }) {\r\n  \/\/ Cadrage exact : on calcule l echelle qui garantit que la scene tient dans le viewBox,\r\n  \/\/ puis on centre sur les extremes reels plutot que sur une approximation.\r\n  const EXAG = 1.4;                                  \/\/ exageration verticale\r\n  const PAD = 22;                                     \/\/ marge, absorbe aussi l ombre au sol\r\n  const hautM = (H * stack + (couvercle ? 68 : 0)) * EXAG;   \/\/ au-dessus du plan de pose\r\n  const basM = (100 + (roulettes ? 70 : 0)) * EXAG;          \/\/ palette et roulettes\r\n  const scale = Math.min(0.5,\r\n    (460 - 2 * PAD) \/ ((L + W) * COS30),\r\n    (300 - 2 * PAD) \/ ((L + W) * SIN30 + hautM + basM));\r\n  const sL = L * scale, sW = W * scale;\r\n  const sH = H * scale * EXAG;\r\n  const t = Math.max(38 * scale, 1.6);\r\n  const baseH = 100 * scale * EXAG;\r\n  const totalH = sH * stack;\r\n  const cx = PAD + W * COS30 * scale;\r\n  const cy = PAD + hautM * scale;\r\n  const P = (x, y, z) => proj(x, y, z, cx, cy);\r\n\r\n  const els = [];\r\n  els.push(<IsoBox key=\"base\" x={0} y={0} z={-baseH} w={sL} h={sW} d={baseH} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n  \/\/ roulettes sous la palette\r\n  if (roulettes) {\r\n    const rr = Math.max(baseH * 0.42, 3);\r\n    [[rr, rr], [sL - rr, rr], [rr, sW - rr], [sL - rr, sW - rr]].forEach(([x, y], i) => {\r\n      const [wx, wy] = P(x, y, -baseH);\r\n      els.push(<circle key={`roul${i}`} cx={wx} cy={wy + rr * 0.8} r={rr} fill=\"#3A4048\" stroke=\"#20252B\" strokeWidth={0.7} \/>);\r\n    });\r\n  }\r\n\r\n  const METAL = \"#2E3338\";\r\n  const MESH = \"#3D4349\";\r\n\r\n  for (let lv = 0; lv < stack; lv++) {\r\n    const z0 = lv * sH;\r\n    const wc = adjustColor(WOOD, lv % 2 === 0 ? 0 : 5);\r\n\r\n    if (grillage && lv === stack - 1) {\r\n      const ft = Math.max(t * 0.5, 2.5);\r\n      const railH = Math.max(sH * 0.09, 2.5);\r\n      const meshFace = (face, opacity) => {\r\n        const lines = [];\r\n        const nV = Math.max(5, Math.round((face === \"b\" || face === \"f\" ? sL : sW) \/ 16));\r\n        const nH2 = Math.max(3, Math.round(sH \/ 13));\r\n        for (let i = 1; i < nV; i++) {\r\n          const f = i \/ nV;\r\n          const p1 = face === \"b\" ? P(sL * f, 0, z0 + railH) : face === \"f\" ? P(sL * f, sW, z0 + railH)\r\n            : face === \"l\" ? P(0, sW * f, z0 + railH) : P(sL, sW * f, z0 + railH);\r\n          const p2 = face === \"b\" ? P(sL * f, 0, z0 + sH - railH) : face === \"f\" ? P(sL * f, sW, z0 + sH - railH)\r\n            : face === \"l\" ? P(0, sW * f, z0 + sH - railH) : P(sL, sW * f, z0 + sH - railH);\r\n          lines.push(<line key={`v${face}${lv}-${i}`} x1={p1[0]} y1={p1[1]} x2={p2[0]} y2={p2[1]} stroke={MESH} strokeWidth={0.9} \/>);\r\n        }\r\n        for (let i = 1; i < nH2; i++) {\r\n          const z = z0 + railH + ((sH - 2 * railH) \/ nH2) * i;\r\n          const p1 = face === \"b\" ? P(0, 0, z) : face === \"f\" ? P(0, sW, z) : face === \"l\" ? P(0, 0, z) : P(sL, 0, z);\r\n          const p2 = face === \"b\" ? P(sL, 0, z) : face === \"f\" ? P(sL, sW, z) : face === \"l\" ? P(0, sW, z) : P(sL, sW, z);\r\n          lines.push(<line key={`h${face}${lv}-${i}`} x1={p1[0]} y1={p1[1]} x2={p2[0]} y2={p2[1]} stroke={MESH} strokeWidth={0.9} \/>);\r\n        }\r\n        return <g key={`mesh${face}${lv}`} opacity={opacity}>{lines}<\/g>;\r\n      };\r\n      els.push(meshFace(\"b\", 0.45));\r\n      els.push(meshFace(\"l\", 0.4));\r\n      els.push(<IsoBox key={`rb0${lv}`} x={0} y={0} z={z0} w={sL} h={ft} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rl0${lv}`} x={0} y={0} z={z0} w={ft} h={sW} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      if (lv === 0) {\r\n        for (let i = 1; i <= sepL; i++) {\r\n          const yy = (sW \/ (sepL + 1)) * i - t \/ 2;\r\n          els.push(<IsoBox key={`sl${i}`} x={t} y={yy} z={z0} w={sL - 2 * t} h={t} d={sH * 0.88} color={adjustColor(WOOD, 8)} cx={cx} cy={cy} opacity={0.95} \/>);\r\n        }\r\n        for (let i = 1; i <= sepT; i++) {\r\n          const xx = (sL \/ (sepT + 1)) * i - t \/ 2;\r\n          els.push(<IsoBox key={`st${i}`} x={xx} y={t} z={z0} w={t} h={sW - 2 * t} d={sH * 0.88} color={adjustColor(WOOD, 8)} cx={cx} cy={cy} opacity={0.95} \/>);\r\n        }\r\n      }\r\n      [[0, 0], [sL - ft, 0], [0, sW - ft], [sL - ft, sW - ft]].forEach(([x, y], i) => {\r\n        els.push(<IsoBox key={`gp${lv}-${i}`} x={x} y={y} z={z0} w={ft} h={ft} d={sH} color={METAL} cx={cx} cy={cy} \/>);\r\n      });\r\n      els.push(meshFace(\"r\", 0.9));\r\n      els.push(meshFace(\"f\", 0.95));\r\n      els.push(<IsoBox key={`rr0${lv}`} x={sL - ft} y={0} z={z0} w={ft} h={sW} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rf0${lv}`} x={0} y={sW - ft} z={z0} w={sL} h={ft} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rb1${lv}`} x={0} y={0} z={z0 + sH - railH} w={sL} h={ft} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rl1${lv}`} x={0} y={0} z={z0 + sH - railH} w={ft} h={sW} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rr1${lv}`} x={sL - ft} y={0} z={z0 + sH - railH} w={ft} h={sW} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`rf1${lv}`} x={0} y={sW - ft} z={z0 + sH - railH} w={sL} h={ft} d={railH} color={METAL} cx={cx} cy={cy} \/>);\r\n    } else {\r\n      els.push(<IsoBox key={`wb${lv}`} x={0} y={0} z={z0} w={sL} h={t} d={sH} color={wc} cx={cx} cy={cy} opacity={0.75} \/>);\r\n      els.push(<IsoBox key={`wl${lv}`} x={0} y={0} z={z0} w={t} h={sW} d={sH} color={wc} cx={cx} cy={cy} opacity={0.72} \/>);\r\n      if (lv === 0) {\r\n        for (let i = 1; i <= sepL; i++) {\r\n          const yy = (sW \/ (sepL + 1)) * i - t \/ 2;\r\n          els.push(<IsoBox key={`sl${i}`} x={t} y={yy} z={z0} w={sL - 2 * t} h={t} d={sH * 0.88} color={adjustColor(WOOD, 8)} cx={cx} cy={cy} opacity={0.95} \/>);\r\n        }\r\n        for (let i = 1; i <= sepT; i++) {\r\n          const xx = (sL \/ (sepT + 1)) * i - t \/ 2;\r\n          els.push(<IsoBox key={`st${i}`} x={xx} y={t} z={z0} w={t} h={sW - 2 * t} d={sH * 0.88} color={adjustColor(WOOD, 8)} cx={cx} cy={cy} opacity={0.95} \/>);\r\n        }\r\n      }\r\n      els.push(<IsoBox key={`wr${lv}`} x={sL - t} y={0} z={z0} w={t} h={sW} d={sH} color={wc} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`wf${lv}`} x={0} y={sW - t} z={z0} w={sL} h={t} d={sH} color={wc} cx={cx} cy={cy} \/>);\r\n    }\r\n\r\n    \/\/ Magasin : produits mis en avant dans le bac, comme en surface de vente\r\n    if (magasin && lv === stack - 1) {\r\n      const teintes = [\"#5B7FB9\", \"#C7643F\", \"#7CA45A\", \"#B9585F\"];\r\n      const marge = t * 1.6;\r\n      const pw2 = (sL - 2 * marge) \/ 3.4, ph2 = (sW - 2 * marge) \/ 2.4;\r\n      for (let k = 0; k < 6; k++) {\r\n        const cxp = marge + (k % 3) * pw2 * 1.15;\r\n        const cyp = marge + Math.floor(k \/ 3) * ph2 * 1.3;\r\n        els.push(<IsoBox key={`prod${lv}-${k}`} x={cxp} y={cyp} z={z0 + sH * 0.12}\r\n          w={pw2} h={ph2} d={sH * (0.5 + 0.1 * (k % 3))} color={teintes[k % 4]} cx={cx} cy={cy} edge={0.5} \/>);\r\n      }\r\n    }\r\n    if (pliable) {\r\n      const hgH = sH * 0.28;\r\n      const positions = charnieres === 6 ? [0.06, 0.5, 0.94] : [0.06, 0.94];\r\n      positions.forEach((f, i) => {\r\n        const [hx, hy] = P(sL * f, sW, z0 + sH * 0.36);\r\n        els.push(<rect key={`hg${lv}-${i}`} x={hx - 3} y={hy - hgH \/ 2} width={6} height={hgH} rx={1.2} fill=\"#9AA0A6\" stroke=\"#6B7076\" strokeWidth={0.6} \/>);\r\n        els.push(<circle key={`hp${lv}-${i}`} cx={hx} cy={hy} r={1.6} fill=\"#C6CBD1\" stroke=\"#6B7076\" strokeWidth={0.5} \/>);\r\n      });\r\n    }\r\n    \/\/ coins plastique : protections d'angle sur les deux angles visibles\r\n    if (coins) {\r\n      const cs = Math.max(6 * scale * 8, 5);\r\n      els.push(<IsoBox key={`cg1-${lv}`} x={sL - cs} y={sW - cs} z={z0 + sH - cs * 0.6} w={cs} h={cs} d={cs * 0.6} color=\"#1F2933\" cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`cg2-${lv}`} x={0} y={sW - cs} z={z0 + sH - cs * 0.6} w={cs} h={cs} d={cs * 0.6} color=\"#1F2933\" cx={cx} cy={cy} \/>);\r\n    }\r\n    \/\/ gestionnaire de distance : cales entre deux niveaux empil\u00e9s\r\n    if (distance && lv < stack - 1) {\r\n      const ds = Math.max(t * 0.8, 3.5), dh = Math.max(sH * 0.14, 3);\r\n      [[t, t], [sL - t - ds, t], [t, sW - t - ds], [sL - t - ds, sW - t - ds]].forEach(([x, y], i) => {\r\n        els.push(<IsoBox key={`dist${lv}-${i}`} x={x} y={y} z={z0 + sH} w={ds} h={ds} d={dh} color=\"#5C6773\" cx={cx} cy={cy} \/>);\r\n      });\r\n    }\r\n  }\r\n\r\n  if (etiquettes) {\r\n    const [ex, ey] = P(sL * 0.16, sW, sH * 0.55);\r\n    els.push(<g key=\"etiq\" transform={`translate(${ex} ${ey}) skewY(30)`}>\r\n      <rect x={-11} y={-8} width={22} height={16} rx={1.5} fill=\"#FFFEF5\" stroke=\"#B9BFC6\" strokeWidth={0.9} \/>\r\n      <line x1={-7} y1={-3} x2={7} y2={-3} stroke=\"#C9CED4\" strokeWidth={1.2} \/>\r\n      <line x1={-7} y1={1} x2={7} y2={1} stroke=\"#C9CED4\" strokeWidth={1.2} \/>\r\n      <line x1={-7} y1={5} x2={3} y2={5} stroke=\"#C9CED4\" strokeWidth={1.2} \/>\r\n    <\/g>);\r\n  }\r\n  if (marqTexte) {\r\n    const z1 = sH * 0.30, z2 = sH * 0.52;\r\n    const band = [P(t, sW, z1), P(sL - t, sW, z1), P(sL - t, sW, z2), P(t, sW, z2)].map(p => p.join(\",\")).join(\" \");\r\n    const [tx, ty] = P(sL \/ 2, sW, z1 + (z2 - z1) * 0.32);\r\n    els.push(<g key=\"marq\">\r\n      <polygon points={band} fill={marqColor} opacity={0.18} \/>\r\n      <text x={0} y={0} textAnchor=\"middle\" fontSize={Math.max(sH * 0.16, 8)} fontFamily={FONT_MONO} fontWeight=\"700\"\r\n        fill={marqColor === \"#F1F0EA\" ? \"#8A8A85\" : marqColor} opacity={0.92}\r\n        transform={`translate(${tx} ${ty}) skewY(30)`}>{marqTexte}<\/text>\r\n    <\/g>);\r\n  }\r\n  if (nimp) {\r\n    const [nx, ny] = P(sL * 0.84, sW, sH * 0.68);\r\n    els.push(<g key=\"nimp\" transform={`translate(${nx} ${ny}) skewY(30)`} opacity={0.7}>\r\n      <rect x={-14} y={-9} width={28} height={18} rx={2} fill=\"none\" stroke={DARK} strokeWidth={0.9} \/>\r\n      <text x={0} y={-1} textAnchor=\"middle\" fontSize={6.5} fontFamily={FONT_MONO} fontWeight=\"700\" fill={DARK}>IPPC<\/text>\r\n      <text x={0} y={6.5} textAnchor=\"middle\" fontSize={6} fontFamily={FONT_MONO} fill={DARK}>HT<\/text>\r\n    <\/g>);\r\n  }\r\n  if (couvercle) {\r\n    const o = Math.max(3, 4 * scale * 6), lid = 40 * scale * EXAG;\r\n    els.push(<IsoBox key=\"lid\" x={-o} y={-o} z={totalH} w={sL + 2 * o} h={sW + 2 * o} d={lid} color={adjustColor(WOOD, 12)} cx={cx} cy={cy} \/>);\r\n    const rw = Math.max(30 * scale, 4);\r\n    [0.12, 0.85].forEach((f, i) => {\r\n      els.push(<IsoBox key={`lr${i}`} x={sL * f - rw \/ 2} y={-o} z={totalH + lid} w={rw} h={sW + 2 * o} d={lid * 0.7} color={adjustColor(WOOD, -4)} cx={cx} cy={cy} \/>);\r\n    });\r\n  }\r\n\r\n  return <IsoScene cx={cx} cy={cy} footprintX={sL} footprintY={sW}>{els}<\/IsoScene>;\r\n}\r\n\r\n\/\/ \u2500\u2500 CAISSE : caisse ouverte, couvercle lev\u00e9, am\u00e9nagements int\u00e9rieurs \u2500\u2500\r\nfunction Preview3DCaisse({ L, W, H, type, base, nbTrav = 3, amenag = [] }) {\r\n  const baseHR = base === \"palette\" ? 144 : base === \"patins\" ? 90 : 70;\r\n  const lidR = 30, renfR = 14;\r\n  const totR = H * 1.55 + baseHR + lidR + renfR;\r\n  const S = Math.min(0.42, 380 \/ ((L + W) * COS30), 230 \/ ((L + W) * SIN30 + totR));\r\n  const sL = L * S, sW = W * S, sH = Math.max(H * S, 18);\r\n  const baseH = Math.max(baseHR * S, 5);\r\n  const lid = Math.max(lidR * S, 3.5), renf = Math.max(renfR * S, 2.5);\r\n  const lift = Math.max(sH * 0.45, 12);\r\n  const totalH = sH + lift + lid + renf;\r\n  const cx = 230 - (sL - sW) * COS30 \/ 2;\r\n  const cy = 150 - ((sL + sW) * SIN30 - totalH) \/ 2;\r\n  const P = (x, y, z) => proj(x, y, z, cx, cy).join(\",\");\r\n  const bodyC = WOOD;\r\n  const tw = Math.max(sW * 0.055, 3);\r\n  const ft = Math.max(sH * 0.07, 2.5);\r\n  const has = id => amenag.includes(id);\r\n\r\n  const els = [];\r\n\r\n  if (base === \"palette\") {\r\n    const semH = baseH * 0.24, desH = baseH * 0.5, platH = baseH * 0.26;\r\n    const semW = Math.max(sW * 0.14, 4);\r\n    [0, 0.5, 1].forEach((f, i) => {\r\n      els.push(<IsoBox key={`sem${i}`} x={0} y={f * (sW - semW)} z={-baseH} w={sL} h={semW} d={semH} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n    });\r\n    const deS = Math.max(Math.min(sL, sW) * 0.14, 5);\r\n    [[0.06, 0.06], [0.5, 0.06], [0.94, 0.06], [0.06, 0.94], [0.5, 0.94], [0.94, 0.94]].forEach(([fx, fy], i) => {\r\n      els.push(<IsoBox key={`de${i}`} x={fx * (sL - deS)} y={fy * (sW - deS)} z={-baseH + semH} w={deS} h={deS} d={desH} color={adjustColor(WOOD_DK, -10)} cx={cx} cy={cy} \/>);\r\n    });\r\n    els.push(<IsoBox key=\"plat\" x={0} y={0} z={-platH} w={sL} h={sW} d={platH} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n  } else if (base === \"patins\") {\r\n    const patW = Math.max(sW * 0.16, 5);\r\n    [0.06, 0.94].forEach((f, i) => {\r\n      els.push(<IsoBox key={`pat${i}`} x={0} y={f * (sW - patW)} z={-baseH} w={sL} h={patW} d={baseH} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n    });\r\n  } else {\r\n    const tvW = Math.max(50 * S, 4);\r\n    const n = Math.max(2, Math.min(8, nbTrav));\r\n    for (let i = 0; i < n; i++) {\r\n      const f = n === 1 ? 0.5 : i \/ (n - 1);\r\n      els.push(<IsoBox key={`tv${i}`} x={f * (sL - tvW)} y={0} z={-baseH} w={tvW} h={sW} d={baseH} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n    }\r\n  }\r\n\r\n  const nLat = Math.max(3, Math.round(sH \/ 16));\r\n  function wallOrSlats(kind, key) {\r\n    const geo = kind === \"b\" ? { x: 0, y: 0, w: sL, h: tw } : kind === \"f\" ? { x: 0, y: sW - tw, w: sL, h: tw }\r\n      : kind === \"l\" ? { x: 0, y: 0, w: tw, h: sW } : { x: sL - tw, y: 0, w: tw, h: sW };\r\n    const op = (kind === \"b\" || kind === \"l\") ? 0.8 : 1;\r\n    if (type === \"clairevoie\") {\r\n      const out = [];\r\n      const latH = (sH \/ nLat) * 0.62, jour = (sH \/ nLat) * 0.38;\r\n      for (let i = 0; i < nLat; i++) {\r\n        out.push(<IsoBox key={`${key}${i}`} x={geo.x} y={geo.y} z={i * (latH + jour)} w={geo.w} h={geo.h} d={latH} color={bodyC} cx={cx} cy={cy} opacity={op} \/>);\r\n      }\r\n      return out;\r\n    }\r\n    return [<IsoBox key={key} x={geo.x} y={geo.y} z={0} w={geo.w} h={geo.h} d={sH} color={bodyC} cx={cx} cy={cy} opacity={op} \/>];\r\n  }\r\n\r\n  els.push(...wallOrSlats(\"b\", \"wb\"));\r\n  els.push(...wallOrSlats(\"l\", \"wl\"));\r\n  els.push(<IsoBox key=\"floor\" x={tw} y={tw} z={0} w={sL - 2 * tw} h={sW - 2 * tw} d={ft} color={adjustColor(bodyC, -8)} cx={cx} cy={cy} \/>);\r\n\r\n  const ix0 = tw, iy0 = tw, iw = sL - 2 * tw, ih = sW - 2 * tw;\r\n  \/\/ Berceaux \u00e0 la forme : blocs usin\u00e9s au profil de la pi\u00e8ce\r\n  if (has(\"berceaux\")) {\r\n    const bw = Math.max(45 * S, 6);\r\n    [0.28, 0.72].forEach((f, i) => {\r\n      const bx = ix0 + iw * f - bw \/ 2;\r\n      els.push(<IsoBox key={`bc${i}a`} x={bx} y={iy0} z={ft} w={bw} h={ih * 0.42} d={sH * 0.36} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n      els.push(<IsoBox key={`bc${i}b`} x={bx} y={iy0 + ih * 0.58} z={ft} w={bw} h={ih * 0.42} d={sH * 0.36} color={WOOD_DK} cx={cx} cy={cy} \/>);\r\n    });\r\n  }\r\n  \/\/ Diviseurs et plateaux : compartimentage int\u00e9rieur\r\n  if (has(\"diviseurs\")) {\r\n    els.push(<IsoBox key=\"sepl\" x={ix0} y={iy0 + ih \/ 2 - tw \/ 2} z={ft} w={iw} h={tw} d={sH * 0.62} color={adjustColor(WOOD, 10)} cx={cx} cy={cy} \/>);\r\n    els.push(<IsoBox key=\"sept\" x={ix0 + iw \/ 2 - tw \/ 2} y={iy0} z={ft} w={tw} h={ih} d={sH * 0.62} color={adjustColor(WOOD, 10)} cx={cx} cy={cy} \/>);\r\n  }\r\n\r\n  els.push(...wallOrSlats(\"r\", \"wr\"));\r\n  els.push(...wallOrSlats(\"f\", \"wf\"));\r\n\r\n  \/\/ Fixation et arrimage : cales de blocage et points de bridage\r\n  if (has(\"arrimage\")) {\r\n    const sc = \"#4A5568\";\r\n    [0.3, 0.7].forEach((f, i) => {\r\n      const [fx1, fy1] = proj(sL * f, sW, 0, cx, cy);\r\n      const [fx2, fy2] = proj(sL * f, sW, sH, cx, cy);\r\n      els.push(<line key={`str-f${i}`} x1={fx1} y1={fy1} x2={fx2} y2={fy2} stroke={sc} strokeWidth={3.2} opacity={0.85} \/>);\r\n      const [rx1, ry1] = proj(sL, sW * f, 0, cx, cy);\r\n      const [rx2, ry2] = proj(sL, sW * f, sH, cx, cy);\r\n      els.push(<line key={`str-r${i}`} x1={rx1} y1={ry1} x2={rx2} y2={ry2} stroke={sc} strokeWidth={3.2} opacity={0.7} \/>);\r\n    });\r\n  }\r\n\r\n  const o = Math.max(4 * S, 2.5);\r\n  const zLid = sH + lift;\r\n  els.push(<IsoBox key=\"lid\" x={-o} y={-o} z={zLid} w={sL + 2 * o} h={sW + 2 * o} d={lid} color={adjustColor(bodyC, 10)} cx={cx} cy={cy} \/>);\r\n  const rW = Math.max(40 * S, 3.5);\r\n  [0.22, 0.78].forEach((f, i) => {\r\n    els.push(<IsoBox key={`renf${i}`} x={sL * f - rW \/ 2} y={-o} z={zLid + lid} w={rW} h={sW + 2 * o} d={renf} color={adjustColor(bodyC, -6)} cx={cx} cy={cy} \/>);\r\n  });\r\n  [[sL, sW], [0, sW]].forEach(([x, y], i) => {\r\n    const [x1, y1] = proj(x, y, sH, cx, cy);\r\n    const [x2, y2] = proj(x, y, zLid, cx, cy);\r\n    els.push(<line key={`lift${i}`} x1={x1} y1={y1} x2={x2} y2={y2} stroke=\"#9AA6B2\" strokeWidth={0.8} strokeDasharray=\"3,3\" opacity={0.7} \/>);\r\n  });\r\n\r\n  return <IsoScene cx={cx} cy={cy} footprintX={sL} footprintY={sW}>{els}<\/IsoScene>;\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 SHARED UI \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst card = { background: \"#fff\", borderRadius: 16, padding: \"20px\", boxShadow: \"0 1px 10px rgba(1,23,60,0.06)\", border: \"1px solid rgba(1,23,60,0.05)\" };\r\n\r\nfunction Label({ icon, children }) {\r\n  return <label style={{ display: \"flex\", alignItems: \"center\", gap: 6, fontSize: 12.5, fontWeight: 600, color: DARK, marginBottom: 7, fontFamily: FONT_BODY }}>{icon && <Icon name={icon} size={15} \/>}{children}<\/label>;\r\n}\r\n\r\nfunction NumInput({ value, onChange, unit, min, max, placeholder }) {\r\n  return (\r\n    <div style={{ position: \"relative\" }}>\r\n      <input type=\"number\" value={value} min={min} max={max} placeholder={placeholder} onChange={e => onChange(e.target.value)}\r\n        style={{ width: \"100%\", padding: \"10px 12px\", paddingRight: unit ? 42 : 12, border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 14, fontFamily: FONT_MONO, background: \"#FAFBFC\", color: DARK, outline: \"none\", boxSizing: \"border-box\" }}\r\n        onFocus={e => { e.target.style.borderColor = ACCENT; e.target.style.boxShadow = \"0 0 0 3px rgba(118,184,42,0.15)\"; }}\r\n        onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; e.target.style.boxShadow = \"none\"; }} \/>\r\n      {unit && <span style={{ position: \"absolute\", right: 12, top: \"50%\", transform: \"translateY(-50%)\", color: \"#90A4AE\", fontSize: 12, fontWeight: 600, fontFamily: FONT_MONO }}>{unit}<\/span>}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction Field({ label, icon, hint, children }) {\r\n  return (\r\n    <div style={{ marginBottom: 16 }}>\r\n      <Label icon={icon}>{label}<\/Label>\r\n      {children}\r\n      {hint && <div style={{ fontSize: 10, color: \"#9AA6B2\", marginTop: 4, fontStyle: \"italic\" }}>{hint}<\/div>}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction Chip({ active, onClick, title, sub }) {\r\n  return (\r\n    <button onClick={onClick} style={{\r\n      padding: \"9px 12px\", borderRadius: 10, cursor: \"pointer\", fontFamily: FONT_BODY, fontSize: 12.5, fontWeight: 600,\r\n      textAlign: \"left\", transition: \"all .18s\", border: `1.5px solid ${active ? ACCENT : \"#E4E8EE\"}`,\r\n      background: active ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\", color: DARK,\r\n      boxShadow: active ? \"0 2px 10px rgba(118,184,42,0.16)\" : \"none\", display: \"flex\", flexDirection: \"column\", gap: 1,\r\n    }}>\r\n      <span>{title}<\/span>\r\n      {sub && <span style={{ fontSize: 10, color: \"#8A96A3\", fontFamily: FONT_MONO }}>{sub}<\/span>}\r\n    <\/button>\r\n  );\r\n}\r\n\r\nfunction CardSelect({ items, value, onChange, columns = 2 }) {\r\n  return (\r\n    <div style={{ display: \"grid\", gridTemplateColumns: `repeat(${columns},1fr)`, gap: 10, marginBottom: 16 }}>\r\n      {items.map(it => {\r\n        const sel = value === it.id;\r\n        return (\r\n          <div key={it.id} onClick={() => onChange(it.id)} style={{\r\n            padding: \"14px\", borderRadius: 12, cursor: \"pointer\", position: \"relative\", transition: \"all .18s\",\r\n            border: `2px solid ${sel ? ACCENT : \"#E4E8EE\"}`, background: sel ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\",\r\n            boxShadow: sel ? \"0 2px 12px rgba(118,184,42,0.16)\" : \"none\",\r\n          }}>\r\n            {sel && <div style={{ position: \"absolute\", top: 8, right: 8, width: 18, height: 18, borderRadius: \"50%\", background: ACCENT, display: \"flex\", alignItems: \"center\", justifyContent: \"center\" }}><Icon name=\"check\" size={11} color=\"#fff\" stroke={2.4} \/><\/div>}\r\n            {it.icon && <div style={{ marginBottom: 8 }}><Icon name={it.icon} size={22} color={sel ? ACCENT : DARK} \/><\/div>}\r\n            <div style={{ fontSize: 13, fontWeight: 700, color: DARK, marginBottom: 2 }}>{it.name}<\/div>\r\n            {it.desc && <div style={{ fontSize: 11, color: \"#6B7684\", lineHeight: 1.4 }}>{it.desc}<\/div>}\r\n          <\/div>\r\n        );\r\n      })}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction Toggle({ options, value, onChange }) {\r\n  return (\r\n    <div style={{ display: \"flex\", gap: 6 }}>\r\n      {options.map(o => (\r\n        <button key={o.value} onClick={() => onChange(o.value)} style={{\r\n          flex: 1, padding: \"10px\", borderRadius: 9, cursor: \"pointer\", fontFamily: FONT_BODY, fontSize: 13, fontWeight: 600,\r\n          border: `1.5px solid ${value === o.value ? ACCENT : \"#E4E8EE\"}`,\r\n          background: value === o.value ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\", color: DARK,\r\n        }}>{o.label}<\/button>\r\n      ))}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction SelectSimple({ value, onChange, options, unit }) {\r\n  return (\r\n    <select value={value} onChange={e => onChange(e.target.value)}\r\n      style={{\r\n        width: \"100%\", padding: \"10px 12px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9,\r\n        fontSize: 13.5, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\",\r\n        cursor: \"pointer\", appearance: \"none\", boxSizing: \"border-box\",\r\n        backgroundImage: `url(\"data:image\/svg+xml,%3Csvg xmlns='http:\/\/www.w3.org\/2000\/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%2301173C' d='M6 8L1 3h10z'\/%3E%3C\/svg%3E\")`,\r\n        backgroundRepeat: \"no-repeat\", backgroundPosition: \"right 12px center\",\r\n      }}>\r\n      {options.map(o => <option key={o.value} value={o.value}>{o.label}{unit ? ` ${unit}` : \"\"}<\/option>)}\r\n    <\/select>\r\n  );\r\n}\r\n\r\nfunction SectionTitle({ icon, children }) {\r\n  return (\r\n    <h2 style={{ display: \"flex\", alignItems: \"center\", gap: 9, fontSize: 26, fontWeight: 600, color: DARK, margin: \"0 0 4px\", fontFamily: FONT_TITLE, lineHeight: 1, letterSpacing: \"0.3px\" }}>\r\n      <span style={{ display: \"inline-flex\", padding: 7, background: \"rgba(118,184,42,0.12)\", borderRadius: 9 }}><Icon name={icon} size={18} \/><\/span>\r\n      {children}\r\n    <\/h2>\r\n  );\r\n}\r\n\r\nfunction RecapCard({ rows, note }) {\r\n  return (\r\n    <div style={{ ...card, marginBottom: 16 }}>\r\n      <h3 style={{ display: \"flex\", alignItems: \"center\", gap: 8, fontSize: 22, fontWeight: 600, color: DARK, margin: \"0 0 12px\", fontFamily: FONT_TITLE, letterSpacing: \"0.3px\" }}>\r\n        <Icon name=\"note\" size={18} \/> R\u00e9capitulatif de votre configuration\r\n      <\/h3>\r\n      {rows.filter(Boolean).map(([k, v], i, arr) => (\r\n        <div key={i} style={{ display: \"flex\", justifyContent: \"space-between\", gap: 12, padding: \"8px 0\", borderBottom: i < arr.length - 1 ? \"1px solid #EDF1F5\" : \"none\" }}>\r\n          <span style={{ fontSize: 13, color: \"#6B7684\", fontFamily: FONT_BODY }}>{k}<\/span>\r\n          <span style={{ fontSize: 13, fontWeight: 600, color: DARK, fontFamily: FONT_MONO, textAlign: \"right\" }}>{v}<\/span>\r\n        <\/div>\r\n      ))}\r\n      {note && (\r\n        <div style={{ marginTop: 12, padding: \"10px 12px\", background: \"rgba(118,184,42,0.07)\", borderRadius: 8, border: \"1px solid rgba(118,184,42,0.2)\", display: \"flex\", gap: 7 }}>\r\n          <Icon name=\"info\" size={15} style={{ marginTop: 1 }} \/>\r\n          <div style={{ fontSize: 12, color: \"#41505F\", lineHeight: 1.55, fontFamily: FONT_BODY }}>{note}<\/div>\r\n        <\/div>\r\n      )}\r\n    <\/div>\r\n  );\r\n}\r\n\r\nfunction buildMailto(produit, specs) {\r\n  const lines = specs.split(\"\\n\");\r\n  const items = lines[0].split(\", \").map(s => \"- \" + s.charAt(0).toUpperCase() + s.slice(1)).join(\"\\n\");\r\n  const notes = lines.length > 1 ? \"\\n\\n\" + lines.slice(1).join(\"\\n\") : \"\";\r\n  const body = `Bonjour,\\n\\nDemande de devis : ${produit}\\n\\n${items}${notes}\\n\\nMerci de me recommander la solution adapt\u00e9e.\\n`;\r\n  return `mailto:${EMAIL}?subject=${encodeURIComponent(\"Demande de devis - \" + produit)}&body=${encodeURIComponent(body)}`;\r\n}\r\n\r\nfunction generatePDF(produit, specs) {\r\n  const w = window.open(\"\", \"_blank\");\r\n  if (!w) return;\r\n  const parts = specs.split(\"\\n\");\r\n  const items = parts[0].split(\", \").map(s => `<tr><td class=\"dot\"><\/td><td>${s.charAt(0).toUpperCase() + s.slice(1)}<\/td><\/tr>`).join(\"\");\r\n  const notes = parts.slice(1).join(\"<br>\");\r\n  const today = new Date().toLocaleDateString(\"fr-FR\", { day: \"numeric\", month: \"long\", year: \"numeric\" });\r\n  w.document.write(`<!DOCTYPE html><html lang=\"fr\"><head><meta charset=\"utf-8\"><title>R\\u00e9capitulatif ${produit}<\/title>\r\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Teko:wght@600&family=Poppins:wght@400;600;700&family=DM+Mono&display=swap\" rel=\"stylesheet\">\r\n<style>\r\n  body { font-family: 'Poppins', sans-serif; color: ${DARK}; margin: 40px 48px; }\r\n  .head { display: flex; justify-content: space-between; align-items: flex-end; border-bottom: 3px solid ${ACCENT}; padding-bottom: 14px; margin-bottom: 24px; }\r\n  .brand { font-family: 'Teko', sans-serif; font-size: 30px; line-height: 1; }\r\n  .brand span { color: ${ACCENT}; }\r\n  .date { font-family: 'DM Mono', monospace; font-size: 12px; color: #6B7684; }\r\n  h1 { font-family: 'Teko', sans-serif; font-size: 26px; font-weight: 600; margin: 0 0 16px; letter-spacing: 0.3px; }\r\n  table { border-collapse: collapse; width: 100%; }\r\n  td { padding: 7px 0; font-size: 13px; border-bottom: 1px solid #EDF1F5; }\r\n  td.dot { width: 16px; } td.dot::before { content: \"\"; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: ${ACCENT}; }\r\n  .notes { margin-top: 18px; padding: 12px 14px; background: #F5F8EF; border: 1px solid #D9E8C4; border-radius: 8px; font-size: 12px; }\r\n  .disc { margin-top: 26px; font-size: 11px; color: #6B7684; font-style: italic; }\r\n  .foot { margin-top: 30px; padding-top: 12px; border-top: 1px solid #E4E8EE; font-family: 'DM Mono', monospace; font-size: 11px; color: #6B7684; }\r\n<\/style><\/head><body>\r\n<div class=\"head\"><div class=\"brand\">ECOPALETTE <span>E2SE<\/span><\/div><div class=\"date\">${today}<\/div><\/div>\r\n<h1>R\\u00e9capitulatif ${produit}<\/h1>\r\n<table>${items}<\/table>\r\n${notes ? `<div class=\"notes\">${notes}<\/div>` : \"\"}\r\n<div class=\"disc\">Estimations g\\u00e9om\\u00e9triques indicatives. Les sp\\u00e9cifications exactes (essence, charge admissible, renforts) sont valid\\u00e9es par notre bureau d'\\u00e9tudes.<\/div>\r\n<div class=\"foot\">Ecopalette E2SE, Firminy (42) | 04 77 61 39 39 | contact@ecopalette-e2se.fr | ecopalettee2se.fr<\/div>\r\n<\/body><\/html>`);\r\n  w.document.close();\r\n  setTimeout(() => w.print(), 500);\r\n}\r\n\r\nfunction BriefCTA({ produit, specs }) {\r\n  return (\r\n    <>\r\n      <div style={{ ...card, marginBottom: 16, background: \"linear-gradient(135deg,#F7FAF3,#EEF6E4)\", border: \"1px solid rgba(118,184,42,0.25)\" }}>\r\n        <div style={{ display: \"flex\", gap: 8, alignItems: \"flex-start\" }}>\r\n          <Icon name=\"check\" size={18} stroke={2.2} \/>\r\n          <div style={{ fontSize: 13, color: \"#41505F\", lineHeight: 1.55, fontFamily: FONT_BODY }}>\r\n            Votre besoin est pr\u00eat \u00e0 \u00eatre transmis. <strong style={{ color: DARK }}>Notre bureau d'\u00e9tudes vous recommande la solution exacte sous 24h<\/strong>, dimensions, essence et traitement compris.\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ padding: \"22px 20px\", borderRadius: 14, background: \"linear-gradient(135deg,#01173C,#0A2A5C)\", boxShadow: \"0 6px 22px rgba(1,23,60,0.26)\", textAlign: \"center\" }}>\r\n        <div style={{ fontSize: 17, fontWeight: 700, color: \"#fff\", marginBottom: 3, fontFamily: FONT_BODY }}>Envoyer mon besoin au bureau d'\u00e9tudes<\/div>\r\n        <div style={{ fontSize: 12, color: \"rgba(255,255,255,0.7)\", marginBottom: 14, fontFamily: FONT_BODY }}>R\u00e9ponse sous 24h, devis gratuit<\/div>\r\n        <div style={{ display: \"flex\", gap: 10, justifyContent: \"center\", flexWrap: \"wrap\" }}>\r\n          <a href={buildMailto(produit, specs)}\r\n            style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, padding: \"12px 22px\", borderRadius: 9, background: ACCENT, color: \"#fff\", fontWeight: 700, fontSize: 14, fontFamily: FONT_BODY, textDecoration: \"none\" }}>\r\n            <Icon name=\"send\" size={15} color=\"#fff\" \/> Envoyer ma demande par email\r\n          <\/a>\r\n          <button onClick={() => generatePDF(produit, specs)}\r\n            style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, padding: \"12px 22px\", borderRadius: 9, background: \"transparent\", color: \"#fff\", fontWeight: 600, fontSize: 14, fontFamily: FONT_BODY, border: \"1.5px solid rgba(255,255,255,0.45)\", cursor: \"pointer\" }}>\r\n            <Icon name=\"note\" size={15} color=\"#fff\" \/> T\u00e9l\u00e9charger mon r\u00e9cap\r\n          <\/button>\r\n        <\/div>\r\n        <div style={{ marginTop: 12, fontSize: 11.5, color: \"rgba(255,255,255,0.55)\", fontFamily: FONT_MONO }}>\r\n          Le r\u00e9cap complet est pr\u00e9-rempli dans votre email. Sinon <a href={CONTACT_URL} target=\"_blank\" rel=\"noopener noreferrer\" style={{ color: \"rgba(255,255,255,0.85)\" }}>formulaire en ligne<\/a> ou {PHONE}\r\n        <\/div>\r\n      <\/div>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE CHARGEMENT \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst VEHICLES = [\r\n  { id: \"semi\", name: \"Semi 13,6 m\", L: 13600, W: 2450, H: 2700, payload: \"\u2248 24 t\" },\r\n  { id: \"porteur\", name: \"Porteur\", L: 7200, W: 2450, H: 2500, payload: \"\u2248 12 t\" },\r\n  { id: \"c20\", name: \"Conteneur 20'\", L: 5900, W: 2350, H: 2390, payload: \"\u2248 28 t\" },\r\n  { id: \"c40\", name: \"Conteneur 40'\", L: 12030, W: 2350, H: 2390, payload: \"\u2248 26 t\" },\r\n  { id: \"c40hc\", name: \"40' High Cube\", L: 12030, W: 2350, H: 2695, payload: \"\u2248 26 t\" },\r\n  { id: \"perso\", name: \"Personnalis\u00e9\", L: 13600, W: 2450, H: 2700, payload: null },\r\n];\r\nconst PAL_FORMATS = [\r\n  { id: \"eur1\", name: \"EUR \/ EPAL 1\", sub: \"1200 \u00d7 800\", L: 1200, W: 800 },\r\n  { id: \"eur2\", name: \"EUR 2\", sub: \"1200 \u00d7 1000\", L: 1200, W: 1000 },\r\n  { id: \"demi\", name: \"Demi-palette\", sub: \"800 \u00d7 600\", L: 800, W: 600 },\r\n  { id: \"quart\", name: \"Quart\", sub: \"600 \u00d7 400\", L: 600, W: 400 },\r\n  { id: \"perso\", name: \"Personnalis\u00e9\", sub: \"L \u00d7 l\", L: 1200, W: 800 },\r\n];\r\nconst CAPACITES_USUELLES = {\r\n  \"semi|eur1\": 33, \"semi|eur2\": 26,\r\n  \"c20|eur1\": 11, \"c40|eur1\": 25, \"c40hc|eur1\": 25,\r\n};\r\n\r\nfunction gridFor(FL, FW, along, across) {\r\n  const nL = Math.floor(FL \/ along), nW = Math.floor(FW \/ across);\r\n  return { nL, nW, count: nL * nW, along, across };\r\n}\r\n\r\nfunction TopViewCamion({ FL, FW, ori }) {\r\n  const pad = 14, drawW = 500;\r\n  const scale = (drawW - pad * 2) \/ FL;\r\n  const drawH = FW * scale + pad * 2, gap = 2;\r\n  const pw = ori.along * scale, ph = ori.across * scale;\r\n  const rects = [];\r\n  for (let r = 0; r < ori.nW; r++) for (let c = 0; c < ori.nL; c++)\r\n    rects.push(<rect key={`${r}-${c}`} x={pad + c * pw + gap \/ 2} y={pad + r * ph + gap \/ 2} width={pw - gap} height={ph - gap} rx={2} fill=\"rgba(118,184,42,0.22)\" stroke={ACCENT} strokeWidth={1} \/>);\r\n  return (\r\n    <svg width=\"100%\" viewBox={`0 0 ${drawW} ${drawH}`} style={{ display: \"block\", maxWidth: 660, margin: \"0 auto\" }}>\r\n      <rect x={pad - 3} y={pad - 3} width={FL * scale + 6} height={FW * scale + 6} rx={5} fill=\"#F7FAF3\" stroke={DARK} strokeWidth={2} \/>\r\n      {rects}\r\n      <text x={pad + FL * scale \/ 2} y={drawH - 2} textAnchor=\"middle\" fontSize={11} fontFamily={FONT_MONO} fill={DARK}>{FL} mm<\/text>\r\n      <text x={6} y={pad + FW * scale \/ 2} textAnchor=\"middle\" fontSize={11} fontFamily={FONT_MONO} fill={DARK} transform={`rotate(-90 6 ${pad + FW * scale \/ 2})`}>{FW} mm<\/text>\r\n    <\/svg>\r\n  );\r\n}\r\n\r\nfunction ModuleChargement({ configs = {} }) {\r\n  const [vehId, setVehId] = useState(\"semi\");\r\n  const [palId, setPalId] = useState(\"eur1\");\r\n  const clientFormats = Object.entries(configs)\r\n    .filter(([, v]) => v && v.L > 0 && v.W > 0)\r\n    .map(([k, v]) => ({ id: \"client-\" + k, name: v.label, sub: `votre config, ${v.L}\u00d7${v.W}`, L: v.L, W: v.W, hauteur: v.hauteur }));\r\n  const formats = [...clientFormats, ...PAL_FORMATS];\r\n  const [cVeh, setCVeh] = useState({ L: \"13600\", W: \"2450\", H: \"2700\" });\r\n  const [cPal, setCPal] = useState({ L: \"1200\", W: \"800\" });\r\n  const [hauteur, setHauteur] = useState(\"1200\");\r\n  const [gerbable, setGerbable] = useState(true);\r\n  const [sens, setSens] = useState(\"auto\");\r\n\r\n  const veh = useMemo(() => { const b = VEHICLES.find(v => v.id === vehId); return vehId === \"perso\" ? { ...b, L: +cVeh.L || 1, W: +cVeh.W || 1, H: +cVeh.H || 1 } : b; }, [vehId, cVeh]);\r\n  const pal = useMemo(() => { const b = formats.find(p => p.id === palId) || PAL_FORMATS[0]; return palId === \"perso\" ? { ...b, L: +cPal.L || 1, W: +cPal.W || 1 } : b; }, [palId, cPal, configs]);\r\n\r\n  const res = useMemo(() => {\r\n    const utileL = Math.max(veh.L - MARGE_CHARGEMENT, 1);\r\n    const long = gridFor(utileL, veh.W, pal.L, pal.W);\r\n    const travers = gridFor(utileL, veh.W, pal.W, pal.L);\r\n    const hCharge = Math.max(+hauteur || 0, 1);\r\n    const niveaux = gerbable ? Math.max(1, Math.floor(veh.H \/ hCharge)) : 1;\r\n    const dress = o => ({ ...o, total: o.count * niveaux, occSol: veh.L * veh.W ? (o.count * pal.L * pal.W) \/ (veh.L * veh.W) : 0 });\r\n    return { long: dress(long), travers: dress(travers), niveaux };\r\n  }, [veh, pal, hauteur, gerbable]);\r\n\r\n  const usuel = CAPACITES_USUELLES[`${vehId}|${palId}`];\r\n  const chosen = sens === \"long\" ? res.long : sens === \"travers\" ? res.travers : (res.long.count >= res.travers.count ? res.long : res.travers);\r\n  const chosenSens = chosen === res.long ? \"longueur\" : \"travers\";\r\n  const specsChargement = `${pal.name} ${pal.L}\u00d7${pal.W} mm, ${veh.name}, hauteur charg\u00e9e ${hauteur} mm, ${gerbable ? \"gerbable\" : \"non gerbable\"}, ${chosen.count} au sol \u00d7 ${res.niveaux} niveau${res.niveaux > 1 ? \"x\" : \"\"} = ${chosen.total} palettes${usuel ? `, capacit\u00e9 usuelle mixte ${usuel * res.niveaux}` : \"\"}, marge de chargement ${MARGE_CHARGEMENT} mm incluse`;\r\n\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"truck\">Combien de palettes par camion<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Remplissage au sol et en gerbage, pour tout format et tout v\u00e9hicule<\/p>\r\n\r\n        <Label icon=\"truck\">V\u00e9hicule ou conteneur<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 6 }}>\r\n          {VEHICLES.map(v => <Chip key={v.id} active={vehId === v.id} onClick={() => setVehId(v.id)} title={v.name} sub={v.id !== \"perso\" ? `${v.L \/ 1000}\u00d7${(v.W \/ 1000).toFixed(2)} m` : null} \/>)}\r\n        <\/div>\r\n        {vehId === \"perso\" && (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginTop: 8 }}>\r\n            <NumInput value={cVeh.L} onChange={v => setCVeh(s => ({ ...s, L: v }))} unit=\"mm\" \/>\r\n            <NumInput value={cVeh.W} onChange={v => setCVeh(s => ({ ...s, W: v }))} unit=\"mm\" \/>\r\n            <NumInput value={cVeh.H} onChange={v => setCVeh(s => ({ ...s, H: v }))} unit=\"mm\" \/>\r\n          <\/div>\r\n        )}\r\n\r\n        <div style={{ height: 14 }} \/>\r\n        <Label icon=\"grid\">Format de palette<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8 }}>\r\n          {formats.map(p => <Chip key={p.id} active={palId === p.id} onClick={() => { setPalId(p.id); if (p.hauteur) setHauteur(String(p.hauteur)); }} title={p.name} sub={p.sub} \/>)}\r\n        <\/div>\r\n        {palId === \"perso\" && (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8, marginTop: 8 }}>\r\n            <NumInput value={cPal.L} onChange={v => setCPal(s => ({ ...s, L: v }))} unit=\"mm\" \/>\r\n            <NumInput value={cPal.W} onChange={v => setCPal(s => ({ ...s, W: v }))} unit=\"mm\" \/>\r\n          <\/div>\r\n        )}\r\n\r\n        <div style={{ height: 14 }} \/>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 14 }}>\r\n          <Field label=\"Hauteur charg\u00e9e\" icon=\"box\" hint=\"Palette + marchandise\"><NumInput value={hauteur} onChange={setHauteur} unit=\"mm\" min={100} \/><\/Field>\r\n          <Field label=\"Gerbage\" icon=\"stack\" hint={`${res.niveaux} niveau${res.niveaux > 1 ? \"x\" : \"\"} possible${res.niveaux > 1 ? \"s\" : \"\"}`}>\r\n            <Toggle value={gerbable} onChange={setGerbable} options={[{ value: true, label: \"Gerbable\" }, { value: false, label: \"Non\" }]} \/>\r\n          <\/Field>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ background: \"linear-gradient(135deg,#01173C,#0A2A5C)\", borderRadius: 16, padding: \"22px 20px\", marginBottom: 16, textAlign: \"center\", boxShadow: \"0 6px 22px rgba(1,23,60,0.24)\" }}>\r\n        <div style={{ fontSize: 12, color: \"rgba(255,255,255,0.6)\", textTransform: \"uppercase\", letterSpacing: \"0.08em\", marginBottom: 4, fontFamily: FONT_BODY }}>Total charg\u00e9<\/div>\r\n        <div style={{ fontSize: 64, fontWeight: 600, color: \"#fff\", fontFamily: FONT_TITLE, lineHeight: 0.9 }}>{chosen.total}<span style={{ fontSize: 22, color: ACCENT, marginLeft: 8, fontFamily: FONT_BODY, fontWeight: 600 }}>palettes<\/span><\/div>\r\n        <div style={{ fontSize: 12.5, color: \"rgba(255,255,255,0.75)\", marginTop: 8, fontFamily: FONT_MONO }}>\r\n          {chosen.count} au sol \u00d7 {res.niveaux} niveau{res.niveaux > 1 ? \"x\" : \"\"}, marge de chargement {MARGE_CHARGEMENT} mm incluse\r\n        <\/div>\r\n        {usuel && usuel !== chosen.count && (\r\n          <div style={{ marginTop: 10, display: \"inline-flex\", alignItems: \"center\", gap: 7, padding: \"7px 14px\", borderRadius: 20, background: \"rgba(118,184,42,0.16)\", fontSize: 12, fontFamily: FONT_BODY, color: \"#DBEFC5\" }}>\r\n            Capacit\u00e9 usuelle en chargement mixte : <strong style={{ fontFamily: FONT_MONO, color: \"#fff\" }}>{usuel * res.niveaux}<\/strong> ({usuel} au sol, palettes crois\u00e9es)\r\n          <\/div>\r\n        )}\r\n      <\/div>\r\n\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ display: \"flex\", justifyContent: \"space-between\", alignItems: \"center\", marginBottom: 12, flexWrap: \"wrap\", gap: 8 }}>\r\n          <span style={{ fontSize: 13, fontWeight: 700, color: DARK, fontFamily: FONT_BODY }}>Vue de dessus, 1 niveau<\/span>\r\n          <div style={{ display: \"flex\", gap: 6 }}>\r\n            {[[\"auto\", \"Auto\"], [\"long\", \"Longueur\"], [\"travers\", \"Travers\"]].map(([id, lbl]) => (\r\n              <button key={id} onClick={() => setSens(id)} style={{ padding: \"6px 10px\", borderRadius: 8, cursor: \"pointer\", fontSize: 11.5, fontWeight: 600, fontFamily: FONT_BODY, border: `1.5px solid ${sens === id ? ACCENT : \"#E4E8EE\"}`, background: sens === id ? \"rgba(118,184,42,0.1)\" : \"#fff\", color: DARK }}>{lbl}<\/button>\r\n            ))}\r\n          <\/div>\r\n        <\/div>\r\n        <TopViewCamion FL={veh.L} FW={veh.W} ori={chosen} \/>\r\n        <div style={{ display: \"flex\", justifyContent: \"center\", gap: 20, marginTop: 12, fontSize: 11.5, color: \"#6B7684\", fontFamily: FONT_MONO, flexWrap: \"wrap\" }}>\r\n          <span>{chosen.nL} \u00d7 {chosen.nW} au sol<\/span><span>Occupation : {(chosen.occSol * 100).toFixed(0)} %<\/span><span>Sens : {chosenSens}<\/span>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      {veh.payload && (\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"12px 14px\", borderRadius: 12, background: \"rgba(118,184,42,0.07)\", border: \"1px solid rgba(118,184,42,0.2)\", marginBottom: 16 }}>\r\n          <Icon name=\"info\" size={16} style={{ marginTop: 1 }} \/>\r\n          <div style={{ fontSize: 12, color: \"#41505F\", lineHeight: 1.55, fontFamily: FONT_BODY }}>Calcul g\u00e9om\u00e9trique du remplissage. Pensez \u00e0 la <strong style={{ color: DARK }}>charge utile<\/strong> du v\u00e9hicule ({veh.payload}) : selon le poids des marchandises, la limite peut \u00eatre atteinte avant le remplissage complet.<\/div>\r\n        <\/div>\r\n      )}\r\n\r\n      <div style={{ padding: \"18px 20px\", borderRadius: 14, background: ACCENT, boxShadow: \"0 4px 18px rgba(118,184,42,0.3)\", textAlign: \"center\" }}>\r\n        <div style={{ fontSize: 15.5, fontWeight: 700, color: \"#fff\", fontFamily: FONT_BODY, marginBottom: 12 }}>Optimiser votre plan de chargement<\/div>\r\n        <div style={{ display: \"flex\", gap: 10, justifyContent: \"center\", flexWrap: \"wrap\" }}>\r\n          <a href={buildMailto(\"Chargement palettes\", specsChargement)}\r\n            style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, padding: \"11px 20px\", borderRadius: 9, background: \"#fff\", color: DARK, fontWeight: 700, fontSize: 13.5, fontFamily: FONT_BODY, textDecoration: \"none\" }}>\r\n            <Icon name=\"send\" size={14} color={DARK} \/> Envoyer ma demande par email\r\n          <\/a>\r\n          <button onClick={() => generatePDF(\"Chargement palettes\", specsChargement)}\r\n            style={{ display: \"inline-flex\", alignItems: \"center\", gap: 8, padding: \"11px 20px\", borderRadius: 9, background: \"transparent\", color: \"#fff\", fontWeight: 600, fontSize: 13.5, fontFamily: FONT_BODY, border: \"1.5px solid rgba(255,255,255,0.6)\", cursor: \"pointer\" }}>\r\n            <Icon name=\"note\" size={14} color=\"#fff\" \/> T\u00e9l\u00e9charger mon r\u00e9cap\r\n          <\/button>\r\n        <\/div>\r\n        <div style={{ marginTop: 10, fontSize: 11.5, color: \"rgba(255,255,255,0.75)\", fontFamily: FONT_MONO }}>Le r\u00e9cap est pr\u00e9-rempli dans votre email. Sinon <a href={CONTACT_URL} target=\"_blank\" rel=\"noopener noreferrer\" style={{ color: \"#fff\" }}>formulaire en ligne<\/a> ou {PHONE}<\/div>\r\n      <\/div>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE PALETTE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\/\/ Trois familles de la plaquette : 4 entr\u00e9es, chevrons, sp\u00e9ciales\r\nconst PAL_TYPES = [\r\n  { id: \"4entrees\", name: \"4 entr\u00e9es \/ 3 semelles\", desc: \"Prise sur les 4 c\u00f4t\u00e9s, usage standard\", icon: \"pallet\", semelles: 3 },\r\n  { id: \"4entrees5\", name: \"4 entr\u00e9es \/ 5 semelles\", desc: \"P\u00e9rim\u00e9trique, stabilit\u00e9 au gerbage\", icon: \"pallet\", semelles: 5 },\r\n  { id: \"chevrons\", name: \"Chevrons (2 entr\u00e9es)\", desc: \"Plus rigide qu'une 4 entr\u00e9es, souvent plus \u00e9conomique\", icon: \"beam\", semelles: 0 },\r\n  { id: \"speciale\", name: \"Sp\u00e9ciale (berceau ou chevalet)\", desc: \"Berceaux, chevalets et per\u00e7ages sur vos besoins\", icon: \"cradle\", semelles: 2 },\r\n];\r\nconst PAL_EP = [15, 18, 22, 25, 27, 30, 35];\r\nconst PAL_DES = [\"78\u00d778\", \"95\u00d795\", \"100\u00d7100\", \"120\u00d7100\", \"145\u00d795\", \"145\u00d7145\"];\r\nconst PAL_CHEV = [\"50\u00d750\", \"60\u00d760\", \"70\u00d770\", \"75\u00d775\", \"80\u00d780\", \"100\u00d775\", \"100\u00d7100\"];\r\n\/\/ Formats r\u00e9alis\u00e9s : de 600 \u00d7 400 mm jusqu'\u00e0 4 090 mm de longueur\r\nconst PAL_LMAX = 4090, PAL_LMIN = 400, PAL_WMAX = 2400, PAL_WMIN = 400;\r\n\r\nfunction Preview3DPalette({ L, W, H, type, nTop, nBot, ep, nDes, desSize, chevronSection }) {\r\n  const hEx = 2.6;\r\n  const berceau = type === \"speciale\";\r\n  const hBerceau = berceau ? Math.min(W * 0.42, 420) : 0;   \/\/ montants du chevalet\r\n  const totR = H * hEx + hBerceau;\r\n  const S = Math.min(0.42, 380 \/ ((L + W) * COS30), 210 \/ ((L + W) * SIN30 + totR));\r\n  const sL = L * S, sW = W * S;\r\n  const sEp = Math.max(ep * S * hEx, 3);\r\n  const sEpBot = Math.max(sEp * 0.85, 2.5);\r\n  const desH = Math.max((H - 2 * ep) * S * hEx, 7);\r\n  const totalH = sEpBot + desH + sEp;\r\n  const cx = 230 - (sL - sW) * COS30 \/ 2;\r\n  const cy = 150 - ((sL + sW) * SIN30 - totalH) \/ 2 - 8;\r\n\r\n  const els = [];\r\n  \/\/ le type fait foi sur le nombre de semelles, l aper\u00e7u ne peut donc pas etre ambigu\r\n  const tObj = PAL_TYPES.find(t => t.id === type);\r\n  const nSem = tObj && tObj.semelles > 0 ? tObj.semelles : nBot;\r\n  const bGap = Math.max(sW * 0.012, 1);\r\n  const bw = (sW - (nSem - 1) * bGap) \/ nSem;\r\n  for (let i = 0; i < nSem; i++) {\r\n    els.push(<IsoBox key={`b${i}`} x={0} y={i * (bw + bGap)} z={0} w={sL} h={bw} d={sEpBot} color={adjustColor(WOOD, -10)} cx={cx} cy={cy} \/>);\r\n  }\r\n  if (type === \"chevrons\") {\r\n    const sec = (chevronSection || \"75\u00d775\").split(\"\u00d7\").map(Number);\r\n    const cw = Math.max(sec[0] * S, 4);\r\n    [0.12, 0.5, 0.88].forEach((f, i) => {\r\n      els.push(<IsoBox key={`ch${i}`} x={sL * f - cw \/ 2} y={0} z={sEpBot} w={cw} h={sW} d={desH} color={adjustColor(WOOD, -22)} cx={cx} cy={cy} \/>);\r\n    });\r\n  } else {\r\n    const rows = nDes <= 4 ? 2 : 3;\r\n    const cols = Math.ceil(nDes \/ rows);\r\n    const sec = (desSize || \"95\u00d795\").split(\"\u00d7\").map(Number);\r\n    const dw = Math.max(sec[0] * S, 4.5), dd = Math.max(sec[1] * S, 4.5);\r\n    let count = 0;\r\n    for (let r = 0; r < rows && count < nDes; r++) {\r\n      for (let c = 0; c < cols && count < nDes; c++) {\r\n        const x = (sL \/ (rows + 1)) * (r + 1) - dw \/ 2;\r\n        const y = (sW \/ (cols + 1)) * (c + 1) - dd \/ 2;\r\n        els.push(<IsoBox key={`d${r}-${c}`} x={x} y={y} z={sEpBot} w={dw} h={dd} d={desH} color={adjustColor(WOOD, -16)} cx={cx} cy={cy} \/>);\r\n        count++;\r\n      }\r\n    }\r\n  }\r\n  const tGap = Math.max(sW * 0.018, 1.2);\r\n  const tw2 = (sW - (nTop - 1) * tGap) \/ nTop;\r\n  const zTop = sEpBot + desH;\r\n  for (let i = 0; i < nTop; i++) {\r\n    els.push(<IsoBox key={`t${i}`} x={0} y={i * (tw2 + tGap)} z={zTop} w={sL} h={tw2} d={sEp} color={WOOD} cx={cx} cy={cy} \/>);\r\n  }\r\n\r\n  \/\/ Speciale : deux montants a encoches demi-circulaires, le berceau de la plaquette\r\n  if (berceau) {\r\n    const zb = zTop + sEp;\r\n    const hb = hBerceau * S * hEx * 0.42;\r\n    const epM = Math.max(sL * 0.045, 5);\r\n    const rr = Math.min(sW * 0.2, 190 * S);\r\n    const p = (x, y, z) => proj(x, y, z, cx, cy).join(\",\");\r\n    [sL * 0.2, sL * 0.68].forEach((x0, k) => {\r\n      const arc = [];\r\n      for (let i = 0; i <= 12; i++) {\r\n        const a = Math.PI - Math.PI * (i \/ 12);\r\n        arc.push([sW \/ 2 + rr * Math.cos(a), zb + hb - rr * Math.sin(a) * 0.8]);\r\n      }\r\n      const face = [p(x0, 0, zb + hb)].concat(arc.map(q => p(x0, q[0], q[1])),\r\n        [p(x0, sW, zb + hb), p(x0, sW, zb), p(x0, 0, zb)]);\r\n      const avant = [p(x0 + epM, 0, zb + hb)].concat(arc.map(q => p(x0 + epM, q[0], q[1])),\r\n        [p(x0 + epM, sW, zb + hb), p(x0 + epM, sW, zb), p(x0 + epM, 0, zb)]);\r\n      const creux = arc.map(q => p(x0, q[0], q[1]))\r\n        .concat(arc.slice().reverse().map(q => p(x0 + epM, q[0], q[1])));\r\n      els.push(<polygon key={`bcf${k}`} points={face.join(\" \")} fill={adjustColor(WOOD, -26)} stroke={adjustColor(WOOD, -46)} strokeWidth={0.6} strokeLinejoin=\"round\" \/>);\r\n      els.push(<polygon key={`bcc${k}`} points={creux.join(\" \")} fill={adjustColor(WOOD, -6)} stroke={adjustColor(WOOD, -40)} strokeWidth={0.6} strokeLinejoin=\"round\" \/>);\r\n      els.push(<polygon key={`bca${k}`} points={avant.join(\" \")} fill={adjustColor(WOOD, -34)} stroke={adjustColor(WOOD, -50)} strokeWidth={0.6} strokeLinejoin=\"round\" \/>);\r\n    });\r\n  }\r\n\r\n  return <IsoScene cx={cx} cy={cy} footprintX={sL} footprintY={sW}>{els}<\/IsoScene>;\r\n}\r\n\r\nfunction ModulePalette({ onConfig }) {\r\n  const [L, setL] = useState(\"1200\"), [W, setW] = useState(\"800\"), [H, setH] = useState(\"145\");\r\n  const [type, setType] = useState(\"4entrees\");\r\n  const [nTop, setNTop] = useState(\"5\"), [nBot, setNBot] = useState(\"3\");\r\n  \/\/ le type impose le nombre de semelles, sinon 3 et 5 semelles donnent le meme dessin\r\n  function choisirType(id) {\r\n    setType(id);\r\n    const t = PAL_TYPES.find(x => x.id === id);\r\n    if (t && t.semelles > 0) setNBot(String(t.semelles));\r\n  }\r\n  const [ep, setEp] = useState(\"22\");\r\n  const [nDes, setNDes] = useState(\"9\"), [desSize, setDesSize] = useState(\"95\u00d795\");\r\n  const [chevronSection, setChevronSection] = useState(\"75\u00d775\");\r\n  const [trait, setTrait] = useState(\"aucun\");\r\n  const [qte, setQte] = useState(\"100\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  const iL = Math.max(+L || 0, 0), iW = Math.max(+W || 0, 0), iH = Math.max(+H || 0, 145);\r\n  const epN = +ep;\r\n  const top = Math.max(3, Math.min(12, parseInt(nTop) || 5));\r\n  const typeObj = PAL_TYPES.find(t => t.id === type);\r\n  const bot = typeObj && typeObj.semelles > 0 ? typeObj.semelles : Math.max(2, Math.min(7, parseInt(nBot) || 3));\r\n  const des = Math.max(4, Math.min(16, parseInt(nDes) || 9));\r\n\r\n  const lwTop = iW > 0 ? Math.round(iW \/ top) : 0;\r\n  const lwBot = iW > 0 ? Math.round(iW \/ bot) : 0;\r\n  let volStruct = 0, structDesc = \"\";\r\n  if (type === \"chevrons\") {\r\n    const s = chevronSection.split(\"\u00d7\").map(Number);\r\n    volStruct = 3 * iL * s[0] * s[1];\r\n    structDesc = `3 chevrons ${chevronSection} mm`;\r\n  } else {\r\n    const s = desSize.split(\"\u00d7\").map(Number);\r\n    volStruct = des * s[0] * s[1] * Math.max(iH - 2 * epN, 50);\r\n    structDesc = `${des} d\u00e9s de ${desSize} mm`;\r\n  }\r\n  const volBois = Math.round((top * iL * lwTop * epN + bot * iL * lwBot * epN + volStruct) \/ 1e9 * 1e6) \/ 1000;\r\n  const camU = 13600 - MARGE_CHARGEMENT, camW2 = 2450;\r\n  const palCamion = iL > 0 && iW > 0\r\n    ? Math.max(Math.floor(camU \/ iL) * Math.floor(camW2 \/ iW), Math.floor(camU \/ iW) * Math.floor(camW2 \/ iL))\r\n    : 0;\r\n  const horsPlage = iL > PAL_LMAX || iW > PAL_WMAX;\r\n\r\n  const typeName = typeObj?.name || \"\";\r\n  const traitName = TRAITEMENTS.find(t => t.id === trait)?.name || \"\";\r\n  const specs = `Palette ${typeName.toLowerCase()} ${iL}\u00d7${iW}\u00d7${iH} mm, ${top} planches dessus (lames ${lwTop} mm), ${bot} semelles (${lwBot} mm), \u00e9paisseur ${ep} mm, ${structDesc}, traitement ${traitName}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"palette\", { label: \"Votre palette\", L: iL, W: iW, hauteur: null }); }, [iL, iW]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"pallet\">Palette bois sur-mesure<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Formats r\u00e9alis\u00e9s de 600 \u00d7 400 mm jusqu'\u00e0 4 090 mm de longueur<\/p>\r\n\r\n        <Label icon=\"pallet\">Type de palette<\/Label>\r\n        <CardSelect items={PAL_TYPES} value={type} onChange={choisirType} columns={2} \/>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Longueur\" icon=\"arrowLR\" hint={`${PAL_LMIN} \u00e0 ${PAL_LMAX} mm`}><NumInput value={L} onChange={setL} unit=\"mm\" min={PAL_LMIN} max={PAL_LMAX} \/><\/Field>\r\n          <Field label=\"Largeur\" icon=\"arrowLR\" hint={`${PAL_WMIN} \u00e0 ${PAL_WMAX} mm`}><NumInput value={W} onChange={setW} unit=\"mm\" min={PAL_WMIN} max={PAL_WMAX} \/><\/Field>\r\n          <Field label=\"Hauteur totale\" icon=\"arrowUD\" hint=\"Standard 145 mm\"><NumInput value={H} onChange={setH} unit=\"mm\" min={80} max={300} \/><\/Field>\r\n        <\/div>\r\n\r\n        {horsPlage && (\r\n          <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 10, background: \"#FFF7ED\", border: \"1px solid #FDBA74\", marginBottom: 14 }}>\r\n            <Icon name=\"warning\" size={15} color=\"#C2660A\" style={{ marginTop: 1 }} \/>\r\n            <span style={{ fontSize: 11.5, color: \"#7C4A0A\", fontFamily: FONT_BODY, lineHeight: 1.5 }}>Au-del\u00e0 de nos formats r\u00e9alis\u00e9s courants. Envoyez-nous vos cotes, le bureau d'\u00e9tudes \u00e9tudie la faisabilit\u00e9.<\/span>\r\n          <\/div>\r\n        )}\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Planches dessus\" icon=\"grid\" hint={`Lames de ${lwTop} mm`}><NumInput value={nTop} onChange={setNTop} min={3} max={12} \/><\/Field>\r\n          {type === \"chevrons\" ? (\r\n            <Field label=\"Semelles dessous\" icon=\"layers\" hint={`Semelles de ${lwBot} mm`}><NumInput value={nBot} onChange={setNBot} min={2} max={7} \/><\/Field>\r\n          ) : (\r\n            <Field label=\"Semelles dessous\" icon=\"layers\" hint={`Semelles de ${lwBot} mm, impose par le type retenu`}>\r\n              <div style={{ padding: \"10px 12px\", border: \"1.5px solid #E4E8EE\", borderRadius: 9, fontSize: 14, fontFamily: FONT_MONO, background: \"#F5F7F9\", color: \"#6B7684\" }}>{bot}<\/div>\r\n            <\/Field>\r\n          )}\r\n        <\/div>\r\n\r\n        <Field label=\"\u00c9paisseur des planches\" icon=\"layers\" hint=\"22 mm = standard\">\r\n          <SelectSimple value={ep} onChange={setEp} options={PAL_EP.map(e => ({ value: String(e), label: `${e} mm${e === 22 ? \" (standard)\" : e === 15 ? \" (l\u00e9ger)\" : e >= 30 ? \" (renforc\u00e9)\" : \"\"}` }))} \/>\r\n        <\/Field>\r\n\r\n        {type !== \"chevrons\" ? (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n            <Field label=\"Nombre de d\u00e9s\" icon=\"grid\" hint=\"Plots de support\"><NumInput value={nDes} onChange={setNDes} min={4} max={16} \/><\/Field>\r\n            <Field label=\"Section des d\u00e9s\" icon=\"grid\"><SelectSimple value={desSize} onChange={setDesSize} options={PAL_DES.map(s => ({ value: s, label: `${s} mm` }))} \/><\/Field>\r\n          <\/div>\r\n        ) : (\r\n          <Field label=\"Section des chevrons\" icon=\"beam\">\r\n            <SelectSimple value={chevronSection} onChange={setChevronSection} options={PAL_CHEV.map(s => ({ value: s, label: `${s} mm${s === \"75\u00d775\" ? \" (standard)\" : \"\"}` }))} \/>\r\n          <\/Field>\r\n        )}\r\n\r\n        <Label icon=\"shield\">Traitement et finition<\/Label>\r\n        <CardSelect items={TRAITEMENTS} value={trait} onChange={setTrait} columns={3} \/>\r\n\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 9, background: \"rgba(118,184,42,0.07)\", border: \"1px solid rgba(118,184,42,0.2)\", marginBottom: 16, alignItems: \"flex-start\" }}>\r\n          <Icon name=\"info\" size={15} style={{ marginTop: 1 }} \/>\r\n          <span style={{ fontSize: 11.5, color: \"#41505F\", lineHeight: 1.5, fontFamily: FONT_BODY }}>Bois PEFC. L'essence sera recommand\u00e9e par notre bureau d'\u00e9tudes selon vos contraintes de charge, transport et budget.<\/span>\r\n        <\/div>\r\n\r\n        <Field label=\"Quantit\u00e9 souhait\u00e9e\" icon=\"hash\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} max={100000} \/><\/Field>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Notes et contraintes<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : palette devant supporter un moteur de 800 kg, passage en lave-palettes, gerbage 3 niveaux...\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D<\/div>\r\n          <Preview3DPalette L={iL || 1} W={iW || 1} H={iH} type={type} nTop={top} nBot={bot} ep={epN} nDes={des} desSize={desSize} chevronSection={chevronSection} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{iL}\u00d7{iW}\u00d7{iH} mm, {top} planches dessus \u00e9p. {ep} mm, {structDesc}, {bot} semelles<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{palCamion}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>palettes \/ camion (1 niveau)<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volBois}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>dm\u00b3 bois estim\u00e9 \/ palette<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Type\", typeName],\r\n        [\"Dimensions\", `${iL} \u00d7 ${iW} \u00d7 ${iH} mm`],\r\n        [\"Planches dessus\", `${top} lames de ${lwTop} mm`],\r\n        [\"Semelles\", `${bot} de ${lwBot} mm`],\r\n        [\"\u00c9paisseur\", `${ep} mm`],\r\n        [\"Structure\", structDesc],\r\n        [\"Essence\", \"Recommand\u00e9e par le bureau d'\u00e9tudes\"],\r\n        [\"Traitement\", traitName],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note=\"Bois PEFC. Les charges dynamique et statique sont valid\u00e9es par le bureau d'\u00e9tudes selon l'essence, l'\u00e9paisseur et l'usage, jamais estim\u00e9es par l'outil.\" \/>\r\n\r\n      <BriefCTA produit=\"Palette bois sur-mesure\" specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE REHAUSSE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst REHAUSSE_TYPES = [\r\n  { id: \"charnieres\", name: \"Charni\u00e8res bois\", desc: \"Pliable \u00e0 plat, la plus courante\", icon: \"rehausse\" },\r\n  { id: \"grillage\", name: \"Grillag\u00e9e pliante\", desc: \"Produit visible sur les quatre faces\", icon: \"grid\" },\r\n  { id: \"fixe\", name: \"Fixe\", desc: \"Non pliante, rigidit\u00e9 maximale\", icon: \"crate\" },\r\n  { id: \"plv\", name: \"Magasin \/ PLV\", desc: \"Bac de pr\u00e9sentation, marquage \u00e0 votre image\", icon: \"tag\" },\r\n];\r\n\/\/ Les six formats du catalogue, plus le sur-mesure\r\nconst REHAUSSE_FORMATS = [\r\n  { id: \"f400\", name: \"400\u00d7400\", sub: \"Bac d'impulsion\", L: 400, W: 400 },\r\n  { id: \"f600400\", name: \"600\u00d7400\", sub: \"Quart de palette\", L: 600, W: 400 },\r\n  { id: \"f800600\", name: \"600\u00d7800\", sub: \"Demi-palette\", L: 800, W: 600 },\r\n  { id: \"f800\", name: \"800\u00d7800\", sub: \"Multi-r\u00e9f\u00e9rences\", L: 800, W: 800 },\r\n  { id: \"eur\", name: \"800\u00d71200\", sub: \"Palette Europe\", L: 1200, W: 800 },\r\n  { id: \"indus\", name: \"1000\u00d71200\", sub: \"Palette industrielle\", L: 1200, W: 1000 },\r\n  { id: \"perso\", name: \"Sur mesure\", sub: \"L. 300 \u00e0 3500 mm\", L: 1200, W: 800 },\r\n];\r\nconst REHAUSSE_HAUTEURS = [100, 200, 300, 400];\r\nconst REHAUSSE_LMIN = 300, REHAUSSE_LMAX = 3500;\r\nconst REHAUSSE_HMIN = 100, REHAUSSE_HMAX = 400;\r\nconst REHAUSSE_EP = [15, 18, 19, 20, 22, 25];\r\nconst REHAUSSE_ACCESS = [\r\n  { id: \"couvercle\", name: \"Couvercle bois\" },\r\n  { id: \"diviseurs\", name: \"Diviseurs\" },\r\n  { id: \"distance\", name: \"Gestionnaire de distance\" },\r\n  { id: \"etiquettes\", name: \"Porte-\u00e9tiquettes\" },\r\n  { id: \"roulettes\", name: \"Roulettes\" },\r\n  { id: \"coins\", name: \"Coins plastique\" },\r\n];\r\nconst RAL_COLORS = [\r\n  { id: \"naturel\", name: \"Bois naturel\", hex: \"#C9A85C\" },\r\n  { id: \"9010\", name: \"Blanc 9010\", hex: \"#F1F0EA\" },\r\n  { id: \"9005\", name: \"Noir 9005\", hex: \"#0E0E10\" },\r\n  { id: \"3020\", name: \"Rouge 3020\", hex: \"#BF111B\" },\r\n  { id: \"5015\", name: \"Bleu 5015\", hex: \"#1761AB\" },\r\n  { id: \"6024\", name: \"Vert 6024\", hex: \"#1E8A44\" },\r\n  { id: \"1023\", name: \"Jaune 1023\", hex: \"#F7B500\" },\r\n];\r\n\/\/ Cas d'usage : pr\u00e9-remplissage, jamais l'essence\r\nconst REHAUSSE_USAGES = [\r\n  { id: \"none\", name: \"Libre\", desc: \"Configuration manuelle\" },\r\n  { id: \"transport\", name: \"Transport\", desc: \"Exp\u00e9dition, inter-sites\", set: { type: \"charnieres\", hauteur: 200, charnieres: \"6\", access: [\"couvercle\"], trait: \"aucun\", ep: \"20\" } },\r\n  { id: \"export\", name: \"Export\", desc: \"Hors UE, NIMP 15\", set: { type: \"charnieres\", hauteur: 200, charnieres: \"6\", access: [\"couvercle\", \"etiquettes\"], trait: \"nimp15\", ep: \"20\" } },\r\n  { id: \"magasin\", name: \"Magasin\", desc: \"Bac de pr\u00e9sentation\", set: { type: \"plv\", hauteur: 200, charnieres: \"4\", access: [\"etiquettes\", \"roulettes\", \"coins\"], trait: \"aucun\", ep: \"20\" } },\r\n  { id: \"picking\", name: \"Picking\", desc: \"Pr\u00e9paration de commandes\", set: { type: \"charnieres\", hauteur: 200, charnieres: \"4\", access: [\"diviseurs\", \"etiquettes\"], trait: \"aucun\", ep: \"20\" } },\r\n  { id: \"fragile\", name: \"Produit fragile\", desc: \"Deux niveaux sans \u00e9crasement\", set: { type: \"charnieres\", hauteur: 300, charnieres: \"6\", access: [\"couvercle\", \"distance\"], trait: \"aucun\", ep: \"22\" } },\r\n];\r\n\r\nfunction ModuleRehausse({ onConfig }) {\r\n  const [usage, setUsage] = useState(\"none\");\r\n  const [fmt, setFmt] = useState(\"eur\");\r\n  const [cL, setCL] = useState(\"1200\"), [cW, setCW] = useState(\"800\");\r\n  const [hauteur, setHauteur] = useState(200);\r\n  const [stack, setStack] = useState(1);\r\n  const [cible, setCible] = useState(\"\");\r\n  const [type, setType] = useState(\"charnieres\");\r\n  const [charnieres, setCharnieres] = useState(\"4\");\r\n  const [ep, setEp] = useState(\"20\");\r\n  const [access, setAccess] = useState([]);\r\n  const [sepL, setSepL] = useState(\"1\"), [sepT, setSepT] = useState(\"1\");\r\n  const [trait, setTrait] = useState(\"aucun\");\r\n  const [marqTexte, setMarqTexte] = useState(\"\");\r\n  const [ral, setRal] = useState(\"naturel\");\r\n  const [qte, setQte] = useState(\"100\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  function applyUsage(id) {\r\n    setUsage(id);\r\n    const u = REHAUSSE_USAGES.find(x => x.id === id);\r\n    if (!u || !u.set) return;\r\n    setType(u.set.type); setHauteur(u.set.hauteur);\r\n    setCharnieres(u.set.charnieres); setAccess(u.set.access); setTrait(u.set.trait); setEp(u.set.ep);\r\n  }\r\n  function toggleAccess(id) { setAccess(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]); }\r\n\r\n  const fmtObj = REHAUSSE_FORMATS.find(f => f.id === fmt);\r\n  const L = fmt === \"perso\" ? Math.max(Math.min(+cL || REHAUSSE_LMIN, REHAUSSE_LMAX), REHAUSSE_LMIN) : fmtObj.L;\r\n  const W = fmt === \"perso\" ? Math.max(Math.min(+cW || REHAUSSE_LMIN, REHAUSSE_LMAX), REHAUSSE_LMIN) : fmtObj.W;\r\n  const epN = +ep;\r\n  \/\/ la rehausse magasin est elle aussi un cadre pliable a charnieres\r\n  const pliable = type === \"charnieres\" || type === \"grillage\" || type === \"plv\";\r\n  const hasSep = access.includes(\"diviseurs\");\r\n  const nSepL = hasSep ? Math.max(0, Math.min(4, parseInt(sepL) || 0)) : 0;\r\n  const nSepT = hasSep ? Math.max(0, Math.min(4, parseInt(sepT) || 0)) : 0;\r\n  const compartiments = (nSepL + 1) * (nSepT + 1);\r\n\r\n  const volUtile = Math.round((L - 2 * epN) * (W - 2 * epN) * hauteur \/ 1e6);\r\n  const epPliee = charnieres === \"6\" ? epN * 2 + 10 : epN * 2 + 15;\r\n  const gainPliage = Math.round((1 - epPliee \/ hauteur) * 100);\r\n  const plieesParPile = Math.floor(1000 \/ epPliee);\r\n  const hauteurTotale = hauteur * stack + 144 + (access.includes(\"couvercle\") ? 18 : 0);\r\n  const camL = 13600 - MARGE_CHARGEMENT, camW = 2450, camH = 2700;\r\n  const solAssemble = Math.max(Math.floor(camL \/ L) * Math.floor(camW \/ W), Math.floor(camL \/ W) * Math.floor(camW \/ L));\r\n  const nivAssemble = Math.max(1, Math.floor(camH \/ Math.max(hauteurTotale, 1)));\r\n  const caissesCamion = solAssemble * nivAssemble;\r\n  const solEUR = Math.max(Math.floor(camL \/ 1200) * Math.floor(camW \/ 800), Math.floor(camL \/ 800) * Math.floor(camW \/ 1200));\r\n  const plieesCamion = pliable ? solEUR * Math.floor((camH - 144) \/ epPliee) : 0;\r\n  const nbNecessaires = cible ? Math.ceil((+cible || 0) \/ hauteur) : 0;\r\n\r\n  const typeName = REHAUSSE_TYPES.find(t => t.id === type)?.name || \"\";\r\n  const traitName = TRAITEMENTS.find(t => t.id === trait)?.name || \"\";\r\n  const accessNames = access.length ? access.map(a => REHAUSSE_ACCESS.find(x => x.id === a)?.name).join(\", \") : \"Aucun\";\r\n  const ralObj = RAL_COLORS.find(r => r.id === ral);\r\n  const isEUR = fmt === \"eur\";\r\n\r\n  const specs = `Rehausse ${typeName.toLowerCase()} ${L}\u00d7${W}\u00d7${hauteur} mm, ${stack} niveau${stack > 1 ? \"x\" : \"\"}, ${pliable ? `${charnieres} charni\u00e8res, ` : \"\"}\u00e9p. ${ep} mm, accessoires ${accessNames.toLowerCase()}${hasSep ? ` (${nSepL} long. + ${nSepT} transv., ${compartiments} compartiments)` : \"\"}, traitement ${traitName}${marqTexte ? `, marquage \"${marqTexte}\" ${ralObj.name}` : \"\"}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"rehausse\", { label: \"Votre rehausse\", L, W, hauteur: hauteurTotale }); }, [L, W, hauteurTotale]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"rehausse\">Rehausse bois sur-mesure<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Choisissez un cas d'usage ou configurez librement<\/p>\r\n\r\n        <Label icon=\"stack\">Cas d'usage (pr\u00e9-remplit la configuration)<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 16 }}>\r\n          {REHAUSSE_USAGES.map(u => <Chip key={u.id} active={usage === u.id} onClick={() => applyUsage(u.id)} title={u.name} sub={u.desc} \/>)}\r\n        <\/div>\r\n\r\n        <Label icon=\"pallet\">Format<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 6 }}>\r\n          {REHAUSSE_FORMATS.map(f => <Chip key={f.id} active={fmt === f.id} onClick={() => setFmt(f.id)} title={f.name} sub={f.sub} \/>)}\r\n        <\/div>\r\n        {fmt === \"perso\" && (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8, marginTop: 8, marginBottom: 8 }}>\r\n            <NumInput value={cL} onChange={setCL} unit=\"mm\" min={REHAUSSE_LMIN} max={REHAUSSE_LMAX} \/>\r\n            <NumInput value={cW} onChange={setCW} unit=\"mm\" min={REHAUSSE_LMIN} max={REHAUSSE_LMAX} \/>\r\n          <\/div>\r\n        )}\r\n\r\n        <div style={{ height: 12 }} \/>\r\n        <Label icon=\"arrowUD\">Hauteur de rehausse<\/Label>\r\n        <div style={{ display: \"flex\", gap: 6, flexWrap: \"wrap\", marginBottom: 6 }}>\r\n          {REHAUSSE_HAUTEURS.map(h => (\r\n            <button key={h} onClick={() => setHauteur(h)} style={{\r\n              padding: \"8px 14px\", borderRadius: 9, cursor: \"pointer\", fontFamily: FONT_MONO, fontSize: 13, fontWeight: 600,\r\n              border: `1.5px solid ${hauteur === h ? ACCENT : \"#E4E8EE\"}`, background: hauteur === h ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\", color: DARK,\r\n            }}>{h}<\/button>\r\n          ))}\r\n          <span style={{ alignSelf: \"center\", fontSize: 11, color: \"#9AA6B2\", fontFamily: FONT_BODY }}>mm<\/span>\r\n        <\/div>\r\n        <div style={{ fontSize: 10.5, color: \"#9AA6B2\", marginBottom: 12, fontStyle: \"italic\" }}>\r\n          Hauteurs standard. Toute hauteur interm\u00e9diaire de {REHAUSSE_HMIN} \u00e0 {REHAUSSE_HMAX} mm est r\u00e9alisable sur demande.\r\n        <\/div>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Empilements\" icon=\"stack\" hint=\"1 \u00e0 5 niveaux superpos\u00e9s\">\r\n            <div style={{ display: \"flex\", gap: 5 }}>\r\n              {[1, 2, 3, 4, 5].map(n => (\r\n                <button key={n} onClick={() => setStack(n)} style={{\r\n                  flex: 1, padding: \"9px 0\", borderRadius: 8, cursor: \"pointer\", fontFamily: FONT_MONO, fontSize: 13, fontWeight: 600,\r\n                  border: `1.5px solid ${stack === n ? ACCENT : \"#E4E8EE\"}`, background: stack === n ? \"rgba(118,184,42,0.1)\" : \"#fff\", color: DARK,\r\n                }}>{n}<\/button>\r\n              ))}\r\n            <\/div>\r\n          <\/Field>\r\n          <Field label=\"J'ai besoin de...\" icon=\"ruler\" hint={cible ? `Soit ${nbNecessaires} rehausse${nbNecessaires > 1 ? \"s\" : \"\"} de ${hauteur} mm` : \"Hauteur utile cible, on calcule le nombre\"}>\r\n            <NumInput value={cible} onChange={setCible} unit=\"mm\" placeholder=\"ex : 750\" \/>\r\n          <\/Field>\r\n        <\/div>\r\n        {nbNecessaires > 5 && (\r\n          <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 10, background: \"#FFF7ED\", border: \"1px solid #FDBA74\", marginBottom: 14 }}>\r\n            <Icon name=\"warning\" size={15} color=\"#C2660A\" style={{ marginTop: 1 }} \/>\r\n            <span style={{ fontSize: 11.5, color: \"#7C4A0A\", fontFamily: FONT_BODY, lineHeight: 1.5 }}>Plus de 5 empilements : pr\u00e9f\u00e9rez une hauteur de rehausse sup\u00e9rieure, le bureau d'\u00e9tudes valide la stabilit\u00e9.<\/span>\r\n          <\/div>\r\n        )}\r\n\r\n        <Label icon=\"rehausse\">Type de rehausse<\/Label>\r\n        <CardSelect items={REHAUSSE_TYPES} value={type} onChange={setType} columns={2} \/>\r\n\r\n        {pliable && (\r\n          <Field label=\"Charni\u00e8res\" icon=\"rehausse\" hint={charnieres === \"6\" ? \"Se replie compl\u00e8tement \u00e0 plat, rigidit\u00e9 conserv\u00e9e sur les grands formats\" : \"Version standard \u00e0 quatre points\"}>\r\n            <Toggle value={charnieres} onChange={setCharnieres} options={[{ value: \"4\", label: \"4 charni\u00e8res\" }, { value: \"6\", label: \"6 charni\u00e8res\" }]} \/>\r\n          <\/Field>\r\n        )}\r\n\r\n        <Field label=\"\u00c9paisseur des planches\" icon=\"layers\" hint=\"20 mm = standard march\u00e9\">\r\n          <SelectSimple value={ep} onChange={setEp} options={REHAUSSE_EP.map(e => ({ value: String(e), label: `${e} mm${e === 20 ? \" (standard)\" : \"\"}` }))} \/>\r\n        <\/Field>\r\n\r\n        <Label icon=\"grid\">Accessoires (plusieurs choix possibles)<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: hasSep ? 10 : 16 }}>\r\n          {REHAUSSE_ACCESS.map(a => <Chip key={a.id} active={access.includes(a.id)} onClick={() => toggleAccess(a.id)} title={a.name} \/>)}\r\n        <\/div>\r\n        {hasSep && (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n            <Field label=\"Diviseurs longitudinaux\" icon=\"grid\"><NumInput value={sepL} onChange={setSepL} min={0} max={4} \/><\/Field>\r\n            <Field label=\"Diviseurs transversaux\" icon=\"grid\" hint={`${compartiments} compartiment${compartiments > 1 ? \"s\" : \"\"}`}><NumInput value={sepT} onChange={setSepT} min={0} max={4} \/><\/Field>\r\n          <\/div>\r\n        )}\r\n\r\n        <Label icon=\"shield\">Traitement et finition<\/Label>\r\n        <CardSelect items={TRAITEMENTS} value={trait} onChange={setTrait} columns={3} \/>\r\n\r\n        <Field label=\"Marquage (logo, nom, r\u00e9f\u00e9rence)\" icon=\"tag\" hint=\"12 caract\u00e8res max, visible sur l'aper\u00e7u 3D\">\r\n          <input type=\"text\" value={marqTexte} maxLength={12} onChange={e => setMarqTexte(e.target.value)} placeholder=\"ex : ECOPALETTE\"\r\n            style={{ width: \"100%\", padding: \"10px 12px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 14, fontFamily: FONT_MONO, background: \"#FAFBFC\", color: DARK, outline: \"none\", boxSizing: \"border-box\", textTransform: \"uppercase\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/Field>\r\n        <div style={{ display: \"flex\", gap: 8, marginBottom: 16, flexWrap: \"wrap\" }}>\r\n          {RAL_COLORS.map(r => (\r\n            <button key={r.id} onClick={() => setRal(r.id)} title={r.name} style={{\r\n              width: 30, height: 30, borderRadius: \"50%\", cursor: \"pointer\", background: r.hex,\r\n              border: ral === r.id ? `3px solid ${ACCENT}` : \"2px solid #D6DBE2\",\r\n              boxShadow: ral === r.id ? \"0 0 0 3px rgba(118,184,42,0.2)\" : \"none\",\r\n            }} \/>\r\n          ))}\r\n          <span style={{ alignSelf: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_BODY }}>{ralObj.name}<\/span>\r\n        <\/div>\r\n\r\n        <Field label=\"Quantit\u00e9\" icon=\"hash\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} \/><\/Field>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Notes et contraintes<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : retour \u00e0 vide fr\u00e9quent, gerbage 3 niveaux charg\u00e9s, marquage aux couleurs de l'enseigne...\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D<\/div>\r\n          <Preview3DRehausse L={L} W={W} H={hauteur} stack={stack} sepL={nSepL} sepT={nSepT}\r\n            couvercle={access.includes(\"couvercle\")} coins={access.includes(\"coins\")} pliable={pliable}\r\n            charnieres={+charnieres} grillage={type === \"grillage\"} marqTexte={marqTexte} marqColor={ralObj.hex}\r\n            nimp={trait === \"nimp15\"} roulettes={access.includes(\"roulettes\")} etiquettes={access.includes(\"etiquettes\")}\r\n            distance={access.includes(\"distance\")} magasin={type === \"plv\"} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{L}\u00d7{W}\u00d7{hauteur} mm, {stack} niveau{stack > 1 ? \"x\" : \"\"}{hasSep ? `, ${compartiments} compartiments` : \"\"}<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volUtile * stack}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>L volume utile \u00d7{stack}<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{caissesCamion}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>caisses-palettes \/ camion<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{hauteurTotale}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>mm hauteur totale<\/div>\r\n        <\/div>\r\n      <\/div>\r\n\r\n      {pliable && (\r\n        <div style={{ ...card, marginBottom: 16, display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 10, textAlign: \"center\" }}>\r\n          <div>\r\n            <div style={{ fontSize: 20, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{gainPliage} %<\/div>\r\n            <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>gain de pliage<\/div>\r\n          <\/div>\r\n          <div>\r\n            <div style={{ fontSize: 20, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{plieesParPile}<\/div>\r\n            <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>pli\u00e9es \/ pile de 1 m<\/div>\r\n          <\/div>\r\n          <div>\r\n            <div style={{ fontSize: 20, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{plieesCamion}<\/div>\r\n            <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>pli\u00e9es \/ camion (retour)<\/div>\r\n          <\/div>\r\n        <\/div>\r\n      )}\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Dimensions\", `${L} \u00d7 ${W} \u00d7 ${hauteur} mm`],\r\n        [\"Empilements\", `${stack} niveau${stack > 1 ? \"x\" : \"\"} (h. totale ${hauteurTotale} mm)`],\r\n        [\"Type\", typeName],\r\n        pliable && [\"Charni\u00e8res\", `${charnieres} points (\u00e9p. pli\u00e9e ${epPliee} mm)`],\r\n        [\"\u00c9paisseur\", `${ep} mm`],\r\n        [\"Accessoires\", accessNames],\r\n        hasSep && [\"Diviseurs\", `${nSepL} long. + ${nSepT} transv. (${compartiments} compartiments)`],\r\n        [\"Essence\", \"Recommand\u00e9e par le bureau d'\u00e9tudes\"],\r\n        [\"Traitement\", traitName],\r\n        marqTexte && [\"Marquage\", `\"${marqTexte}\" ${ralObj.name}`],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note={isEUR\r\n        ? \"Bois PEFC. Base EUR \/ EPAL : charge dynamique normalis\u00e9e 1500 kg (norme EPAL publi\u00e9e). Le poids de la rehausse et la charge utile sont valid\u00e9s par le bureau d'\u00e9tudes.\"\r\n        : \"Bois PEFC. Le poids de la rehausse et la charge utile sont valid\u00e9s par le bureau d'\u00e9tudes selon l'essence retenue, jamais estim\u00e9s par l'outil.\"} \/>\r\n\r\n      <BriefCTA produit=\"Rehausse bois sur-mesure\" specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE PLATEAU RACK \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst RACK_PLANK = {\r\n  \"21\": { nom: \"21\u00d795\", trav: \"21\u00d795\", travT: 21, travW: 95 },\r\n  \"27\": { nom: \"27\u00d7100\", trav: \"27\u00d7100\", travT: 27, travW: 100 },\r\n  \"40\": { nom: \"40\u00d795\", trav: \"50\u00d795\", travT: 50, travW: 95 },\r\n};\r\nconst RACK_FORMATS = [\r\n  { id: \"890x1100\", L: 890, P: 1100, bay: \"Rack 2700, \u00d73\" },\r\n  { id: \"890x800\", L: 890, P: 800, bay: \"Rack 2700, \u00d73\" },\r\n  { id: \"1190x1100\", L: 1190, P: 1100, bay: \"Rack 3600, \u00d73\" },\r\n  { id: \"1090x1100\", L: 1090, P: 1100, bay: \"Rack 3300, \u00d73\" },\r\n  { id: \"perso\", L: 1090, P: 1100, bay: \"Sur mesure\" },\r\n];\r\n\/\/ Les trois finitions de la fiche\r\nconst RACK_FINITIONS = [\r\n  { id: \"brut\", name: \"Bois brut\", desc: \"Stockage courant en int\u00e9rieur\", icon: \"rack\" },\r\n  { id: \"lourdes\", name: \"Charges lourdes\", desc: \"Renforts et mat\u00e9riaux adapt\u00e9s\", icon: \"layers\" },\r\n  { id: \"autoclave\", name: \"Autoclave\", desc: \"Usage ext\u00e9rieur ou humide\", icon: \"crate\" },\r\n];\r\n\/\/ Les quatre mat\u00e9riaux de panneau de la fiche\r\nconst RACK_MATERIAUX = [\r\n  { id: \"osb\", name: \"OSB\", desc: \"Le standard, bon rapport r\u00e9sistance et prix\" },\r\n  { id: \"contreplaque\", name: \"Contreplaqu\u00e9\", desc: \"Plus rigide \u00e0 \u00e9paisseur \u00e9gale, charges concentr\u00e9es\" },\r\n  { id: \"agglomere\", name: \"Agglom\u00e9r\u00e9\", desc: \"Surface plane, charges l\u00e9g\u00e8res et r\u00e9parties\" },\r\n  { id: \"melamine\", name: \"M\u00e9lamin\u00e9\", desc: \"Surface lisse et lavable, environnements propres\" },\r\n];\r\n\r\nfunction ModuleRack({ onConfig }) {\r\n  const [fmt, setFmt] = useState(\"890x1100\");\r\n  const [cL, setCL] = useState(\"1090\"), [cP, setCP] = useState(\"1100\");\r\n  const [ep, setEp] = useState(\"27\"), [nb, setNb] = useState(\"7\");\r\n  const [finition, setFinition] = useState(\"brut\");\r\n  const [materiau, setMateriau] = useState(\"osb\");\r\n  const [traitement, setTraitement] = useState(\"aucun\");\r\n  const [qte, setQte] = useState(\"1\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  const fmtObj = RACK_FORMATS.find(f => f.id === fmt);\r\n  const L = fmt === \"perso\" ? Math.max(+cL || 1, 1) : fmtObj.L;\r\n  const P = fmt === \"perso\" ? Math.max(+cP || 1, 1) : fmtObj.P;\r\n  const plank = RACK_PLANK[ep];\r\n  const nbP = Math.max(5, Math.min(11, parseInt(nb) || 7));\r\n  const renforce = finition === \"lourdes\";\r\n\r\n  const largeur = L \/ nbP;\r\n  const gap = Math.round(Math.max(5, largeur * 0.03) * 10) \/ 10;\r\n  const utile = Math.max(largeur - gap, 0);\r\n  const volPlanches = nbP * utile * P * ep \/ 1e9;\r\n  const volTrav = 2 * L * plank.travT * plank.travW \/ 1e9;\r\n  const volBois = Math.round((volPlanches + volTrav) * (renforce ? 1.25 : 1) * 1e6) \/ 1000;\r\n  const camL = 13600 - MARGE_CHARGEMENT, camW = 2450;\r\n  const platCamion = Math.max(Math.floor(camL \/ L) * Math.floor(camW \/ P), Math.floor(camL \/ P) * Math.floor(camW \/ L));\r\n  const finName = RACK_FINITIONS.find(t => t.id === finition)?.name || \"\";\r\n  const matName = RACK_MATERIAUX.find(m => m.id === materiau)?.name || \"\";\r\n  const traitName = TRAITEMENTS.find(t => t.id === traitement)?.name || \"\";\r\n  const persoHint = fmt === \"perso\" ? \"Longueur 600 \u00e0 1650 mm, profondeur 400 \u00e0 2100 mm\" : `${fmtObj.bay}`;\r\n  const specs = `Plateau rack ${L}\u00d7${P} mm entre lisses int\u00e9rieures, mat\u00e9riau ${matName.toLowerCase()}, finition ${finName.toLowerCase()}, planches \u00e9p. ${ep} (${plank.nom}), ${nbP} planches, traverses 2 \u00d7 ${plank.trav}, clouage machine, traitement ${traitName}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"rack\", { label: \"Votre plateau rack\", L, W: P, hauteur: null }); }, [L, P]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"rack\">Plateau bois pour rack<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Cotes prises entre lisses int\u00e9rieures, jamais hors-tout. Clouage machine.<\/p>\r\n\r\n        <Label icon=\"ruler\">Format entre lisses int\u00e9rieures<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 6 }}>\r\n          {RACK_FORMATS.map(f => <Chip key={f.id} active={fmt === f.id} onClick={() => setFmt(f.id)} title={f.id === \"perso\" ? \"Sur mesure\" : `${f.L}\u00d7${f.P}`} sub={f.bay} \/>)}\r\n        <\/div>\r\n        {fmt === \"perso\" && (\r\n          <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8, marginTop: 8 }}>\r\n            <div><span style={{ fontSize: 10.5, color: \"#8A96A3\" }}>Longueur<\/span><NumInput value={cL} onChange={setCL} unit=\"mm\" min={600} max={1650} \/><\/div>\r\n            <div><span style={{ fontSize: 10.5, color: \"#8A96A3\" }}>Profondeur<\/span><NumInput value={cP} onChange={setCP} unit=\"mm\" min={400} max={2100} \/><\/div>\r\n          <\/div>\r\n        )}\r\n        <div style={{ fontSize: 10.5, color: \"#9AA6B2\", marginTop: 6, fontStyle: \"italic\" }}>{persoHint}<\/div>\r\n\r\n        <div style={{ height: 16 }} \/>\r\n        <Label icon=\"layers\">Mat\u00e9riau de panneau<\/Label>\r\n        <CardSelect items={RACK_MATERIAUX} value={materiau} onChange={setMateriau} columns={2} \/>\r\n\r\n        <Label icon=\"crate\">Finition selon le stockage<\/Label>\r\n        <CardSelect items={RACK_FINITIONS} value={finition} onChange={setFinition} columns={3} \/>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Nombre de planches\" icon=\"grid\" hint=\"5 \u00e0 11, selon la longueur\"><NumInput value={nb} onChange={setNb} min={5} max={11} \/><\/Field>\r\n          <Field label=\"\u00c9paisseur de planche\" icon=\"layers\" hint={`Traverse auto : ${plank.trav} mm`}>\r\n            <Toggle value={ep} onChange={setEp} options={[{ value: \"21\", label: \"21\" }, { value: \"27\", label: \"27\" }, { value: \"40\", label: \"40\" }]} \/>\r\n          <\/Field>\r\n        <\/div>\r\n\r\n        <div style={{ display: \"flex\", gap: 7, padding: \"9px 12px\", borderRadius: 9, background: \"#F5F7F9\", border: \"1px solid #E4E8EE\", marginBottom: 16, alignItems: \"center\" }}>\r\n          <Icon name=\"info\" size={14} color=\"#9AA6B2\" \/>\r\n          <span style={{ fontSize: 11.5, color: \"#6B7684\", fontFamily: FONT_BODY }}>Traverses fix\u00e9es \u00e0 2, section {plank.trav} mm. Bois clou\u00e9, une planche ab\u00eem\u00e9e se remplace.<\/span>\r\n        <\/div>\r\n\r\n        <Label icon=\"shield\">Traitement et finition<\/Label>\r\n        <CardSelect items={TRAITEMENTS} value={traitement} onChange={setTraitement} columns={3} \/>\r\n\r\n        <Field label=\"Quantit\u00e9 souhait\u00e9e\" icon=\"hash\" hint=\"Nombre de niveaux \u00e0 \u00e9quiper\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} max={1000} \/><\/Field>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Notes et contraintes<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : charge r\u00e9partie ou concentr\u00e9e, type de lisse, appuis interm\u00e9diaires, environnement humide...\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D, {nbP} planches sur traverses<\/div>\r\n          <Preview3DRack L={L} P={P} nb={nbP} gap={gap} ep={+ep} travT={plank.travT} renforce={renforce} finition={finition} traitement={traitement} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{matName}, {finName.toLowerCase()}, jeu {gap} mm<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"16px\" }}>\r\n          <div style={{ fontSize: 26, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{platCamion}<\/div>\r\n          <div style={{ fontSize: 10.5, color: \"#6B7684\", fontFamily: FONT_BODY }}>plateaux par camion<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"16px\" }}>\r\n          <div style={{ fontSize: 26, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volBois}<\/div>\r\n          <div style={{ fontSize: 10.5, color: \"#6B7684\", fontFamily: FONT_BODY }}>dm\u00b3 de bois \/ plateau<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Cotes entre lisses int\u00e9rieures\", `${L} \u00d7 ${P} mm`],\r\n        [\"Mat\u00e9riau\", matName],\r\n        [\"Finition\", finName],\r\n        [\"Planches\", `${nbP} \u00d7 \u00e9p. ${ep} mm`],\r\n        [\"Largeur utile \/ planche\", `\u2248 ${Math.round(utile)} mm`],\r\n        [\"Jeu entre planches\", `${gap} mm`],\r\n        [\"Traverses\", `2 \u00d7 ${plank.trav} mm, clouage machine`],\r\n        [\"Traitement\", traitName],\r\n        [\"Volume bois\", `${volBois} dm\u00b3`],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note=\"Bois PEFC. Cotes exprim\u00e9es entre lisses int\u00e9rieures, jamais hors-tout. Le poids et la charge admissible sont valid\u00e9s par le bureau d'\u00e9tudes selon le mat\u00e9riau et le renfort, jamais estim\u00e9s par l'outil.\" \/>\r\n\r\n      <BriefCTA produit=\"Plateau bois pour rack\" specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE CAISSE \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\/\/ Deux conceptions, comme la fiche produit\r\nconst CAISSE_TYPES = [\r\n  { id: \"pleine\", name: \"Pleine\", desc: \"Panneaux pleins sur toutes les faces, protection int\u00e9grale\", icon: \"crate\" },\r\n  { id: \"clairevoie\", name: \"Claire-voie\", desc: \"Faces ajour\u00e9es, plus l\u00e9g\u00e8re et plus \u00e9conomique \u00e0 volume \u00e9gal\", icon: \"rack\" },\r\n];\r\nconst CAISSE_BASES = [\r\n  { id: \"traverses\", name: \"Semelles \/ traverses\", desc: \"Base standard, prise de fourches\" },\r\n  { id: \"palette\", name: \"Palette int\u00e9gr\u00e9e\", desc: \"Manutention 4 entr\u00e9es\" },\r\n  { id: \"patins\", name: \"Patins \/ plots\", desc: \"Base l\u00e9g\u00e8re, petites caisses\" },\r\n];\r\n\/\/ Am\u00e9nagements r\u00e9ellement document\u00e9s\r\nconst CAISSE_AMENAG = [\r\n  { id: \"berceaux\", name: \"Berceaux \u00e0 la forme\" },\r\n  { id: \"diviseurs\", name: \"Diviseurs et plateaux\" },\r\n  { id: \"arrimage\", name: \"Fixation et arrimage\" },\r\n];\r\nconst CAISSE_EP = [12, 15, 18, 22, 25, 27, 30];\r\nconst CAISSE_TRANSPORT = [\r\n  { value: \"routier\", label: \"Routier\" }, { value: \"maritime\", label: \"Maritime\" },\r\n  { value: \"aerien\", label: \"A\u00e9rien\" }, { value: \"ferroviaire\", label: \"Ferroviaire\" },\r\n  { value: \"multimodal\", label: \"Multimodal\" },\r\n];\r\n\r\nfunction ModuleCaisse({ onConfig }) {\r\n  const [L, setL] = useState(\"800\"), [W, setW] = useState(\"600\"), [H, setH] = useState(\"600\");\r\n  const [poidsContenu, setPoidsContenu] = useState(\"\");\r\n  const [type, setType] = useState(\"pleine\");\r\n  const [base, setBase] = useState(\"traverses\");\r\n  const [epParois, setEpParois] = useState(\"18\"), [epFond, setEpFond] = useState(\"22\"), [epCouv, setEpCouv] = useState(\"18\");\r\n  const [montants, setMontants] = useState(\"50\u00d750\"), [traverses, setTraverses] = useState(\"50\u00d775\");\r\n  const [nbTrav, setNbTrav] = useState(\"3\");\r\n  const [traitement, setTraitement] = useState(\"aucun\");\r\n  const [amenag, setAmenag] = useState([]);\r\n  const [transport, setTransport] = useState(\"routier\");\r\n  const [qte, setQte] = useState(\"50\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  const iL = Math.max(+L || 0, 0), iW = Math.max(+W || 0, 0), iH = Math.max(+H || 0, 0);\r\n  const eP = +epParois, eF = +epFond, eC = +epCouv;\r\n\r\n  const extL = iL + 2 * eP, extW = iW + 2 * eP, extH = iH + eF + eC;\r\n  const volInt = Math.round(iL * iW * iH \/ 1e6);\r\n\r\n  const mSec = montants.split(\"\u00d7\").map(Number);\r\n  const tSec = traverses.split(\"\u00d7\").map(Number);\r\n  const nT = Math.max(2, Math.min(8, parseInt(nbTrav) || 3));\r\n  const facteurParois = type === \"clairevoie\" ? 0.55 : 1;\r\n  const volMontants = 4 * mSec[0] * mSec[1] * iH;\r\n  const volParois = (2 * iL * iH + 2 * iW * iH) * eP * facteurParois;\r\n  const volFond = extL * extW * eF;\r\n  const volCouv = extL * extW * eC;\r\n  const volTrav = nT * extW * tSec[0] * tSec[1];\r\n  const volBois = Math.round((volMontants + volParois + volFond + volCouv + volTrav) \/ 1e9 * 1e6) \/ 1000;\r\n\r\n  const camL = 13600 - MARGE_CHARGEMENT, camW = 2450, camH = 2700;\r\n  const solA = Math.floor(camL \/ extL) * Math.floor(camW \/ extW);\r\n  const solB = Math.floor(camL \/ extW) * Math.floor(camW \/ extL);\r\n  const auSol = Math.max(solA, solB);\r\n  const niveaux = Math.max(1, Math.floor(camH \/ Math.max(extH, 1)));\r\n  const parCamion = auSol * niveaux;\r\n\r\n  const nimp = traitement === \"nimp15\";\r\n  const typeName = CAISSE_TYPES.find(t => t.id === type)?.name || \"\";\r\n  const baseName = CAISSE_BASES.find(b => b.id === base)?.name || \"\";\r\n  const traitName = TRAITEMENTS.find(t => t.id === traitement)?.name || \"\";\r\n  const amenagNames = amenag.length ? amenag.map(a => CAISSE_AMENAG.find(x => x.id === a)?.name).join(\", \") : \"Aucun\";\r\n  const horsUE = transport === \"maritime\" || transport === \"aerien\" || transport === \"multimodal\";\r\n  const alerteExport = horsUE && !nimp;\r\n\r\n  function toggleAmenag(id) {\r\n    setAmenag(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]);\r\n  }\r\n\r\n  const specs = `Caisse ${typeName.toLowerCase()} ${iL}\u00d7${iW}\u00d7${iH} mm int. (ext. ${extL}\u00d7${extW}\u00d7${extH} mm), base ${baseName.toLowerCase()}, parois ${eP} \/ fond ${eF} \/ couvercle ${eC} mm, montants ${montants}, ${nT} traverses ${traverses}, traitement ${traitName}, am\u00e9nagements ${amenagNames.toLowerCase()}, transport ${transport}${poidsContenu ? `, contenu \u2248 ${poidsContenu} kg` : \"\"}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"caisse\", { label: \"Votre caisse\", L: extL, W: extW, hauteur: extH }); }, [extL, extW, extH]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"crate\">Caisse bois sur-mesure<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Cotes int\u00e9rieures cal\u00e9es sur la pi\u00e8ce, l'outil calcule l'ext\u00e9rieur<\/p>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Longueur int.\" icon=\"arrowLR\"><NumInput value={L} onChange={setL} unit=\"mm\" min={100} max={6000} \/><\/Field>\r\n          <Field label=\"Largeur int.\" icon=\"arrowLR\"><NumInput value={W} onChange={setW} unit=\"mm\" min={100} max={4000} \/><\/Field>\r\n          <Field label=\"Hauteur int.\" icon=\"arrowUD\"><NumInput value={H} onChange={setH} unit=\"mm\" min={50} max={3000} \/><\/Field>\r\n        <\/div>\r\n\r\n        <Field label=\"Poids du contenu\" icon=\"box\" hint=\"La structure est calcul\u00e9e pour le poids r\u00e9el par notre bureau d'\u00e9tudes\">\r\n          <NumInput value={poidsContenu} onChange={setPoidsContenu} unit=\"kg\" min={1} max={20000} placeholder=\"ex : 350\" \/>\r\n        <\/Field>\r\n\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 9, background: \"rgba(118,184,42,0.07)\", border: \"1px solid rgba(118,184,42,0.2)\", marginBottom: 16, alignItems: \"center\" }}>\r\n          <Icon name=\"ruler\" size={15} \/>\r\n          <span style={{ fontSize: 12, color: \"#41505F\", fontFamily: FONT_BODY }}>Dimensions ext\u00e9rieures : <strong style={{ color: DARK, fontFamily: FONT_MONO }}>{extL} \u00d7 {extW} \u00d7 {extH} mm<\/strong><\/span>\r\n        <\/div>\r\n\r\n        <Label icon=\"crate\">Conception<\/Label>\r\n        <CardSelect items={CAISSE_TYPES} value={type} onChange={setType} columns={2} \/>\r\n\r\n        <Label icon=\"pallet\">Type de base<\/Label>\r\n        <CardSelect items={CAISSE_BASES} value={base} onChange={setBase} columns={3} \/>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"\u00c9p. parois\" icon=\"layers\"><SelectSimple value={epParois} onChange={setEpParois} unit=\"mm\" options={CAISSE_EP.map(e => ({ value: String(e), label: `${e}${e === 18 ? \" (std)\" : \"\"}` }))} \/><\/Field>\r\n          <Field label=\"\u00c9p. fond\" icon=\"layers\"><SelectSimple value={epFond} onChange={setEpFond} unit=\"mm\" options={CAISSE_EP.map(e => ({ value: String(e), label: `${e}${e === 22 ? \" (std)\" : \"\"}` }))} \/><\/Field>\r\n          <Field label=\"\u00c9p. couvercle\" icon=\"layers\"><SelectSimple value={epCouv} onChange={setEpCouv} unit=\"mm\" options={CAISSE_EP.map(e => ({ value: String(e), label: `${e}${e === 18 ? \" (std)\" : \"\"}` }))} \/><\/Field>\r\n        <\/div>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Montants\" icon=\"grid\"><SelectSimple value={montants} onChange={setMontants} unit=\"mm\" options={[\"40\u00d740\", \"50\u00d750\", \"60\u00d760\", \"70\u00d770\", \"75\u00d775\", \"80\u00d780\"].map(s => ({ value: s, label: `${s}${s === \"50\u00d750\" ? \" (std)\" : \"\"}` }))} \/><\/Field>\r\n          <Field label=\"Traverses\" icon=\"grid\"><SelectSimple value={traverses} onChange={setTraverses} unit=\"mm\" options={[\"40\u00d760\", \"50\u00d775\", \"60\u00d780\", \"75\u00d7100\", \"80\u00d7100\"].map(s => ({ value: s, label: `${s}${s === \"50\u00d775\" ? \" (std)\" : \"\"}` }))} \/><\/Field>\r\n          <Field label=\"Nb traverses\" icon=\"hash\"><NumInput value={nbTrav} onChange={setNbTrav} min={2} max={8} \/><\/Field>\r\n        <\/div>\r\n\r\n        <Label icon=\"shield\">Traitement et finition<\/Label>\r\n        <CardSelect items={TRAITEMENTS} value={traitement} onChange={setTraitement} columns={3} \/>\r\n\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 9, background: \"rgba(118,184,42,0.07)\", border: \"1px solid rgba(118,184,42,0.2)\", marginBottom: 16, alignItems: \"flex-start\" }}>\r\n          <Icon name=\"info\" size={15} style={{ marginTop: 1 }} \/>\r\n          <span style={{ fontSize: 11.5, color: \"#41505F\", lineHeight: 1.5, fontFamily: FONT_BODY }}>Bois PEFC. L'essence sera recommand\u00e9e par notre bureau d'\u00e9tudes selon vos contraintes de charge, transport et budget.<\/span>\r\n        <\/div>\r\n\r\n        <Label icon=\"stack\">Am\u00e9nagements int\u00e9rieurs (plusieurs choix possibles)<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 16 }}>\r\n          {CAISSE_AMENAG.map(a => (\r\n            <Chip key={a.id} active={amenag.includes(a.id)} onClick={() => toggleAmenag(a.id)} title={a.name} \/>\r\n          ))}\r\n        <\/div>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Transport principal\" icon=\"truck\"><SelectSimple value={transport} onChange={setTransport} options={CAISSE_TRANSPORT} \/><\/Field>\r\n          <Field label=\"Quantit\u00e9\" icon=\"hash\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} max={100000} \/><\/Field>\r\n        <\/div>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Notes et contraintes<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : points d'appui et zones fragiles, reprise au pont, sens de levage impos\u00e9, s\u00e9rie \u00e0 rejouer \u00e0 l'identique...\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D<\/div>\r\n          <Preview3DCaisse L={extL || 1} W={extW || 1} H={extH || 1} type={type} base={base} nbTrav={nT} amenag={amenag} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{typeName}, ext. {extL}\u00d7{extW}\u00d7{extH} mm<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      {alerteExport && (\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"12px 14px\", borderRadius: 12, background: \"#FFF7ED\", border: \"1px solid #FDBA74\", marginBottom: 16 }}>\r\n          <Icon name=\"warning\" size={16} color=\"#C2660A\" style={{ marginTop: 1 }} \/>\r\n          <div style={{ fontSize: 12, color: \"#7C4A0A\", lineHeight: 1.55, fontFamily: FONT_BODY }}>Mode de transport souvent utilis\u00e9 \u00e0 l'export. Le marquage NIMP 15 est obligatoire pour toute exp\u00e9dition hors Union europ\u00e9enne.<\/div>\r\n        <\/div>\r\n      )}\r\n\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volInt}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>L volume int\u00e9rieur<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{parCamion}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>caisses \/ camion<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volBois}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>dm\u00b3 bois estim\u00e9<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Conception\", typeName],\r\n        [\"Dimensions int\u00e9rieures\", `${iL} \u00d7 ${iW} \u00d7 ${iH} mm`],\r\n        [\"Dimensions ext\u00e9rieures\", `${extL} \u00d7 ${extW} \u00d7 ${extH} mm`],\r\n        [\"Base\", baseName],\r\n        [\"\u00c9paisseurs\", `parois ${eP} \/ fond ${eF} \/ couvercle ${eC} mm`],\r\n        [\"Montants\", `${montants} mm`],\r\n        [\"Traverses\", `${nT} \u00d7 ${traverses} mm`],\r\n        [\"Essence\", \"Recommand\u00e9e par le bureau d'\u00e9tudes\"],\r\n        [\"Traitement\", traitName],\r\n        [\"Am\u00e9nagements\", amenagNames],\r\n        [\"Transport\", CAISSE_TRANSPORT.find(t => t.value === transport)?.label],\r\n        poidsContenu && [\"Poids du contenu\", `\u2248 ${poidsContenu} kg`],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note={nimp\r\n        ? \"Bois PEFC et marquage NIMP 15, conforme aux exp\u00e9ditions hors Union europ\u00e9enne. Le poids de la caisse et la charge admissible sont valid\u00e9s par le bureau d'\u00e9tudes.\"\r\n        : \"Bois PEFC. Le poids de la caisse et la charge admissible sont valid\u00e9s par le bureau d'\u00e9tudes selon l'essence et le renfort, jamais estim\u00e9s par l'outil.\"} \/>\r\n\r\n      <BriefCTA produit={`Caisse bois ${typeName.toLowerCase()}`} specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE PANNEAUX \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\n\/\/ Stock permanent, repris tel quel de la fiche Panneaux.\r\n\/\/ eps vide = la fiche ne publie pas d epaisseur pour cette matiere, saisie libre.\r\nconst PANNEAU_MATIERES = [\r\n  { id: \"isorel\", name: \"Isorel\", desc: \"Fonds de caisse, s\u00e9parateurs l\u00e9gers\", eps: [] },\r\n  { id: \"medium\", name: \"Medium (MDF)\", desc: \"Usinage fin, ar\u00eates nettes\", eps: [\"19\", \"25\"] },\r\n  { id: \"agglomere\", name: \"Agglom\u00e9r\u00e9\", desc: \"\u00c9conomique, charges r\u00e9parties\", eps: [\"19\"] },\r\n  { id: \"osb\", name: \"OSB\", desc: \"Le standard structurel\", eps: [\"9\", \"12\", \"18\"] },\r\n  { id: \"contreplaque\", name: \"Contreplaqu\u00e9\", desc: \"\u00c9videments et pi\u00e8ces sollicit\u00e9es\", eps: [] },\r\n  { id: \"autres\", name: \"Autres panneaux\", desc: \"Approvisionn\u00e9s \u00e0 la demande selon le volume et l'usage\", eps: [] },\r\n];\r\nconst PANNEAU_USINAGES = [\r\n  { id: \"debit\", name: \"D\u00e9bit au format\", desc: \"Mise aux cotes sur scie \u00e0 panneau horizontale\", icon: \"scissors\" },\r\n  { id: \"evidement\", name: \"\u00c9videment\", desc: \"Poches, poign\u00e9es et passages de main\", icon: \"grid\" },\r\n  { id: \"berceau\", name: \"Berceau\", desc: \"Usin\u00e9 au profil de la pi\u00e8ce\", icon: \"cradle\" },\r\n  { id: \"contour\", name: \"Trous et contours\", desc: \"Quart de rond, per\u00e7ages, plaques \u00e0 trous\", icon: \"hash\" },\r\n];\r\nconst PANNEAU_APPLICATIONS = [\r\n  { id: \"calage\", name: \"Calage et berceaux\" },\r\n  { id: \"fonds\", name: \"Fonds et couvercles\" },\r\n  { id: \"separateurs\", name: \"S\u00e9parateurs et intercalaires\" },\r\n];\r\n\r\nfunction Preview3DPanneau({ L, W, ep, usinage }) {\r\n  const S = Math.min(0.42, 360 \/ ((L + W) * COS30));\r\n  const sL = L * S, sW = W * S;\r\n  const sEp = Math.max(ep * S * 6, 3.5);\r\n  const cx = 230 - (sL - sW) * COS30 \/ 2;\r\n  const cy = 158 - ((sL + sW) * SIN30 - sEp) \/ 2;\r\n  const P = (x, y, z) => proj(x, y, z, cx, cy);\r\n  const els = [];\r\n\r\n  if (usinage === \"debit\") {\r\n    const trait = Math.max(sL * 0.035, 3);\r\n    const coupe = sL * 0.58;\r\n    els.push(<IsoBox key=\"p1\" x={0} y={0} z={0} w={coupe} h={sW} d={sEp} color={WOOD} cx={cx} cy={cy} \/>);\r\n    els.push(<IsoBox key=\"p2\" x={coupe + trait} y={0} z={0} w={sL - coupe - trait} h={sW} d={sEp} color={WOOD} cx={cx} cy={cy} \/>);\r\n    const a = P(coupe + trait \/ 2, -sW * 0.12, sEp), b = P(coupe + trait \/ 2, sW * 1.12, sEp);\r\n    els.push(<line key=\"cut\" x1={a[0]} y1={a[1]} x2={b[0]} y2={b[1]} stroke=\"#8A93A6\" strokeWidth={1.2} strokeDasharray=\"6,4\" opacity={0.85} \/>);\r\n  } else {\r\n    els.push(<IsoBox key=\"p\" x={0} y={0} z={0} w={sL} h={sW} d={sEp} color={WOOD} cx={cx} cy={cy} \/>);\r\n  }\r\n\r\n  if (usinage === \"evidement\") {\r\n    [[0.18, 0.24], [0.58, 0.24]].forEach(([fx, fy], i) => {\r\n      const px0 = sL * fx, py0 = sW * fy, pw = sL * 0.22, ph = sW * 0.16;\r\n      const pts = [P(px0, py0, sEp), P(px0 + pw, py0, sEp), P(px0 + pw, py0 + ph, sEp), P(px0, py0 + ph, sEp)].map(p => p.join(\",\")).join(\" \");\r\n      els.push(<polygon key={`ev${i}`} points={pts} fill={adjustColor(WOOD, -46)} stroke={adjustColor(WOOD, -60)} strokeWidth={0.7} \/>);\r\n    });\r\n  }\r\n  if (usinage === \"berceau\") {\r\n    const n = 4, rr = sW * 0.2;\r\n    for (let i = 0; i < n; i++) {\r\n      const ccx = sL * ((i + 0.5) \/ n);\r\n      const pts = [];\r\n      for (let k = 0; k <= 12; k++) {\r\n        const ang = Math.PI - Math.PI * (k \/ 12);\r\n        pts.push(P(ccx + rr * Math.cos(ang), sW * 0.5 + rr * Math.sin(ang) * 0.55, sEp).join(\",\"));\r\n      }\r\n      els.push(<polygon key={`bc${i}`} points={pts.join(\" \")} fill={adjustColor(WOOD, -46)} stroke={adjustColor(WOOD, -60)} strokeWidth={0.7} \/>);\r\n    }\r\n  }\r\n  if (usinage === \"contour\") {\r\n    [0.2, 0.4, 0.6, 0.8].forEach(fx => {\r\n      [0.28, 0.62].forEach(fy => {\r\n        const [hx, hy] = P(sL * fx, sW * fy, sEp);\r\n        els.push(<ellipse key={`h${fx}-${fy}`} cx={hx} cy={hy} rx={Math.max(sL * 0.022, 2.2)} ry={Math.max(sL * 0.012, 1.3)}\r\n          fill={adjustColor(WOOD, -50)} stroke={adjustColor(WOOD, -62)} strokeWidth={0.6} \/>);\r\n      });\r\n    });\r\n  }\r\n  return <IsoScene cx={cx} cy={cy} footprintX={sL} footprintY={sW}>{els}<\/IsoScene>;\r\n}\r\n\r\nfunction ModulePanneau({ onConfig }) {\r\n  const [L, setL] = useState(\"1200\"), [W, setW] = useState(\"800\");\r\n  const [matiere, setMatiere] = useState(\"osb\");\r\n  const [ep, setEp] = useState(\"18\");\r\n  const [usinage, setUsinage] = useState(\"debit\");\r\n  const [applications, setApplications] = useState([]);\r\n  const [qte, setQte] = useState(\"50\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  const matObj = PANNEAU_MATIERES.find(m => m.id === matiere);\r\n  const epLibre = matObj.eps.length === 0;   \/\/ la fiche ne publie pas d epaisseur pour cette matiere\r\n  useEffect(() => {\r\n    if (matObj.eps.length && !matObj.eps.includes(ep)) setEp(matObj.eps[matObj.eps.length - 1]);\r\n  }, [matiere]);\r\n\r\n  const iL = Math.max(+L || 0, 0), iW = Math.max(+W || 0, 0);\r\n  const epN = +ep;\r\n  const surface = Math.round(iL * iW \/ 1e4) \/ 100;\r\n  const volume = Math.round(iL * iW * epN \/ 1e6) \/ 1000;\r\n  const usinObj = PANNEAU_USINAGES.find(u => u.id === usinage);\r\n  const appNames = applications.length ? applications.map(a => PANNEAU_APPLICATIONS.find(x => x.id === a)?.name).join(\", \") : \"Non pr\u00e9cis\u00e9\";\r\n\r\n  function toggleApp(id) { setApplications(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]); }\r\n\r\n  const specs = `Panneau ${matObj.name.toLowerCase()} ${iL}\u00d7${iW} mm \u00e9p. ${ep} mm, usinage ${usinObj.name.toLowerCase()}, application ${appNames.toLowerCase()}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"panneau\", { label: \"Votre panneau\", L: iL, W: iW, hauteur: null }); }, [iL, iW]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"scissors\">Panneau bois, d\u00e9bit et usinage<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Stock permanent, d\u00e9bit\u00e9 et usin\u00e9 \u00e0 vos plans, de la pi\u00e8ce unitaire \u00e0 la s\u00e9rie<\/p>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Longueur\" icon=\"arrowLR\"><NumInput value={L} onChange={setL} unit=\"mm\" min={50} max={5000} \/><\/Field>\r\n          <Field label=\"Largeur\" icon=\"arrowLR\"><NumInput value={W} onChange={setW} unit=\"mm\" min={50} max={2500} \/><\/Field>\r\n        <\/div>\r\n\r\n        <Label icon=\"layers\">Mati\u00e8re<\/Label>\r\n        <CardSelect items={PANNEAU_MATIERES} value={matiere} onChange={setMatiere} columns={3} \/>\r\n\r\n        {epLibre ? (\r\n          <Field label=\"\u00c9paisseur\" icon=\"layers\" hint=\"\u00c9paisseur \u00e0 pr\u00e9ciser selon l'usage, valid\u00e9e par notre bureau d'\u00e9tudes\">\r\n            <NumInput value={ep} onChange={setEp} unit=\"mm\" min={1} max={80} placeholder=\"ex : 18\" \/>\r\n          <\/Field>\r\n        ) : (\r\n          <Field label=\"\u00c9paisseur\" icon=\"layers\" hint=\"\u00c9paisseurs courantes en stock pour cette mati\u00e8re\">\r\n            <SelectSimple value={ep} onChange={setEp} options={matObj.eps.map(e => ({ value: e, label: `${e} mm` }))} \/>\r\n          <\/Field>\r\n        )}\r\n\r\n        <Label icon=\"scissors\">Usinage<\/Label>\r\n        <CardSelect items={PANNEAU_USINAGES} value={usinage} onChange={setUsinage} columns={2} \/>\r\n\r\n        <Label icon=\"grid\">Application (plusieurs choix possibles)<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr 1fr\", gap: 8, marginBottom: 16 }}>\r\n          {PANNEAU_APPLICATIONS.map(a => <Chip key={a.id} active={applications.includes(a.id)} onClick={() => toggleApp(a.id)} title={a.name} \/>)}\r\n        <\/div>\r\n\r\n        <Field label=\"Quantit\u00e9\" icon=\"hash\" hint=\"Pi\u00e8ce unitaire, lot ponctuel ou s\u00e9rie r\u00e9currente\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} max={100000} \/><\/Field>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Plan et tol\u00e9rances<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : contours, \u00e9videments, per\u00e7ages et tol\u00e9rances. Un fichier de d\u00e9coupe ou un croquis cot\u00e9 suffit.\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D<\/div>\r\n          <Preview3DPanneau L={iL || 1} W={iW || 1} ep={epN} usinage={usinage} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{matObj.name}, {iL}\u00d7{iW} mm \u00e9p. {ep} mm, {usinObj.name.toLowerCase()}<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{surface}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>m\u00b2 par pi\u00e8ce<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"14px 10px\" }}>\r\n          <div style={{ fontSize: 24, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{volume}<\/div>\r\n          <div style={{ fontSize: 10, color: \"#6B7684\", fontFamily: FONT_BODY }}>dm\u00b3 par pi\u00e8ce<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Mati\u00e8re\", matObj.name],\r\n        [\"Dimensions\", `${iL} \u00d7 ${iW} mm`],\r\n        [\"\u00c9paisseur\", `${ep} mm`],\r\n        [\"Usinage\", usinObj.name],\r\n        [\"Application\", appNames],\r\n        [\"Surface\", `${surface} m\u00b2 par pi\u00e8ce`],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note=\"D\u00e9bit sur scie \u00e0 panneau horizontale \u00e0 chargement automatique, usinage sur centre \u00e0 commande num\u00e9rique. Le prototype et la s\u00e9rie passent sur la m\u00eame machine, \u00e0 partir du m\u00eame fichier. Les panneaux hors stock sont approvisionn\u00e9s \u00e0 la demande selon le volume et l'usage.\" \/>\r\n\r\n      <BriefCTA produit=\"Panneau bois d\u00e9coup\u00e9 et usin\u00e9\" specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 MODULE COMPOSTEUR \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst COMPOST_GAMMES = [\r\n  { id: \"particulier\", name: \"Particuliers\", desc: \"Bac domestique, montage sans outil\", icon: \"crate\" },\r\n  { id: \"collectif\", name: \"Collectif\", desc: \"Grands volumes, modules juxtaposables\", icon: \"stack\" },\r\n];\r\nconst COMPOST_ACCESS = [\r\n  { id: \"couvercle\", name: \"Couvercle amovible\" },\r\n  { id: \"trappe\", name: \"Trappe d'extraction\" },\r\n  { id: \"separateurs\", name: \"S\u00e9parateurs internes\" },\r\n  { id: \"marquage\", name: \"Marquage collectivit\u00e9\" },\r\n];\r\n\/\/ Cadres de rehausse empil\u00e9s. Volumes annonc\u00e9s de 400 L \u00e0 1 500 L.\r\nconst COMPOST_CADRES = [2, 3, 4];\r\nconst COMPOST_BASE = { L: 1200, W: 1000, H: 200, ep: 22 };\r\n\r\nfunction ModuleComposteur({ onConfig }) {\r\n  const [gamme, setGamme] = useState(\"particulier\");\r\n  const [cadres, setCadres] = useState(3);\r\n  const [bacs, setBacs] = useState(1);\r\n  const [access, setAccess] = useState([\"couvercle\"]);\r\n  const [usagers, setUsagers] = useState(\"\");\r\n  const [qte, setQte] = useState(\"1\");\r\n  const [notes, setNotes] = useState(\"\");\r\n\r\n  function toggleAccess(id) { setAccess(s => s.includes(id) ? s.filter(x => x !== id) : [...s, id]); }\r\n  useEffect(() => { if (gamme === \"particulier\" && bacs > 1) setBacs(1); }, [gamme]);\r\n\r\n  const { L, W, H, ep } = COMPOST_BASE;\r\n  const nBacs = gamme === \"particulier\" ? 1 : bacs;\r\n  const volUnit = Math.round((L - 2 * ep) * (W - 2 * ep) * (cadres * H) \/ 1e6 \/ 10) * 10;\r\n  const volTotal = volUnit * nBacs;\r\n  const hauteurTotale = cadres * H;\r\n  const gammeName = COMPOST_GAMMES.find(g => g.id === gamme)?.name || \"\";\r\n  const accessNames = access.length ? access.map(a => COMPOST_ACCESS.find(x => x.id === a)?.name).join(\", \") : \"Aucun\";\r\n\r\n  const specs = `Composteur ${gammeName.toLowerCase()}, ${nBacs} bac${nBacs > 1 ? \"s\" : \"\"} de ${cadres} cadres, ${L}\u00d7${W} mm, hauteur ${hauteurTotale} mm, volume utile ${volTotal} L, accessoires ${accessNames.toLowerCase()}${usagers ? `, ${usagers} apporteurs` : \"\"}, quantit\u00e9 ${qte}${notes ? `\\nNotes : ${notes}` : \"\"}`;\r\n\r\n  useEffect(() => { onConfig && onConfig(\"composteur\", { label: \"Votre composteur\", L, W, hauteur: hauteurTotale }); }, [hauteurTotale]);\r\n  return (\r\n    <>\r\n      <div className=\"cfg-grid\">\r\n      <div>\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <SectionTitle icon=\"stack\">Composteur bois rehaussable<\/SectionTitle>\r\n        <p style={{ fontSize: 12, color: \"#9AA6B2\", margin: \"0 0 16px\", fontFamily: FONT_BODY }}>Des cadres bois assembl\u00e9s par charni\u00e8res m\u00e9talliques, mont\u00e9s sans outil<\/p>\r\n\r\n        <Label icon=\"crate\">Gamme<\/Label>\r\n        <CardSelect items={COMPOST_GAMMES} value={gamme} onChange={setGamme} columns={2} \/>\r\n\r\n        <Label icon=\"arrowUD\">Nombre de cadres empil\u00e9s<\/Label>\r\n        <div style={{ display: \"flex\", gap: 6, flexWrap: \"wrap\", marginBottom: 16 }}>\r\n          {COMPOST_CADRES.map(c => (\r\n            <button key={c} onClick={() => setCadres(c)} style={{\r\n              padding: \"9px 18px\", borderRadius: 9, cursor: \"pointer\", fontFamily: FONT_MONO, fontSize: 13, fontWeight: 600,\r\n              border: `1.5px solid ${cadres === c ? ACCENT : \"#E4E8EE\"}`, background: cadres === c ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\", color: DARK,\r\n            }}>{c} cadres<\/button>\r\n          ))}\r\n        <\/div>\r\n\r\n        {gamme === \"collectif\" && (\r\n          <Field label=\"Nombre de bacs juxtapos\u00e9s\" icon=\"stack\" hint=\"Apport, maturation, r\u00e9serve de broyat\">\r\n            <div style={{ display: \"flex\", gap: 5 }}>\r\n              {[1, 2, 3, 4].map(n => (\r\n                <button key={n} onClick={() => setBacs(n)} style={{\r\n                  flex: 1, padding: \"9px 0\", borderRadius: 8, cursor: \"pointer\", fontFamily: FONT_MONO, fontSize: 13, fontWeight: 600,\r\n                  border: `1.5px solid ${bacs === n ? ACCENT : \"#E4E8EE\"}`, background: bacs === n ? \"rgba(118,184,42,0.1)\" : \"#fff\", color: DARK,\r\n                }}>{n}<\/button>\r\n              ))}\r\n            <\/div>\r\n          <\/Field>\r\n        )}\r\n\r\n        <Label icon=\"grid\">Accessoires pratiques<\/Label>\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 8, marginBottom: 16 }}>\r\n          {COMPOST_ACCESS.map(a => <Chip key={a.id} active={access.includes(a.id)} onClick={() => toggleAccess(a.id)} title={a.name} \/>)}\r\n        <\/div>\r\n\r\n        <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 12 }}>\r\n          <Field label=\"Nombre d'apporteurs\" icon=\"hash\" hint=\"Un foyer, un immeuble, une \u00e9cole\"><NumInput value={usagers} onChange={setUsagers} placeholder=\"ex : 40\" min={1} max={5000} \/><\/Field>\r\n          <Field label=\"Quantit\u00e9\" icon=\"hash\" hint=\"Bac unique ou d\u00e9ploiement multi-sites\"><NumInput value={qte} onChange={setQte} unit=\"pcs\" min={1} max={500} \/><\/Field>\r\n        <\/div>\r\n\r\n        <div style={{ display: \"flex\", gap: 8, padding: \"10px 12px\", borderRadius: 9, background: \"rgba(118,184,42,0.07)\", border: \"1px solid rgba(118,184,42,0.2)\", marginBottom: 16, alignItems: \"flex-start\" }}>\r\n          <Icon name=\"info\" size={15} style={{ marginTop: 1 }} \/>\r\n          <span style={{ fontSize: 11.5, color: \"#41505F\", lineHeight: 1.5, fontFamily: FONT_BODY }}>Bois brut non trait\u00e9, sans aucun traitement chimique au contact des biod\u00e9chets. Bois PEFC. Livr\u00e9 \u00e0 plat en kit.<\/span>\r\n        <\/div>\r\n\r\n        <div style={{ marginBottom: 16 }}>\r\n          <Label icon=\"note\">Emplacement et contraintes<\/Label>\r\n          <textarea value={notes} onChange={e => setNotes(e.target.value)} placeholder=\"Ex : surface disponible, acc\u00e8s, nature du sol, quantit\u00e9 de biod\u00e9chets par semaine, d\u00e9chets verts disponibles...\"\r\n            style={{ width: \"100%\", minHeight: 64, padding: \"11px 13px\", border: \"1.5px solid #D6DBE2\", borderRadius: 9, fontSize: 13, fontFamily: FONT_BODY, background: \"#FAFBFC\", color: DARK, outline: \"none\", resize: \"vertical\", boxSizing: \"border-box\" }}\r\n            onFocus={e => { e.target.style.borderColor = ACCENT; }} onBlur={e => { e.target.style.borderColor = \"#D6DBE2\"; }} \/>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div>\r\n\r\n      <div><div className=\"cfg-sticky\">\r\n      <div style={{ ...card, marginBottom: 16 }}>\r\n        <div style={{ padding: \"16px\", background: \"#F7FAF3\", borderRadius: 12, border: \"1px dashed #C9D8B4\" }}>\r\n          <div style={{ fontSize: 11.5, fontWeight: 600, color: DARK, marginBottom: 4, fontFamily: FONT_BODY }}>Aper\u00e7u 3D<\/div>\r\n          <Preview3DRehausse L={L} W={W} H={H} stack={cadres} sepL={0} sepT={0}\r\n            couvercle={access.includes(\"couvercle\")} coins={false} pliable={true} charnieres={4}\r\n            grillage={false} marqTexte={access.includes(\"marquage\") ? \"COMPOST\" : \"\"} marqColor=\"#1E8A44\"\r\n            nimp={false} roulettes={false} etiquettes={false} distance={false} \/>\r\n          <div style={{ textAlign: \"center\", fontSize: 11, color: \"#6B7684\", fontFamily: FONT_MONO, marginTop: 4 }}>{cadres} cadres, hauteur {hauteurTotale} mm, {volUnit} L par bac<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <div style={{ display: \"grid\", gridTemplateColumns: \"1fr 1fr\", gap: 10, marginBottom: 16 }}>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"16px\" }}>\r\n          <div style={{ fontSize: 26, fontWeight: 500, color: ACCENT_TEXT, fontFamily: FONT_MONO }}>{volTotal}<\/div>\r\n          <div style={{ fontSize: 10.5, color: \"#6B7684\", fontFamily: FONT_BODY }}>litres de volume utile<\/div>\r\n        <\/div>\r\n        <div style={{ ...card, textAlign: \"center\", padding: \"16px\" }}>\r\n          <div style={{ fontSize: 26, fontWeight: 500, color: DARK, fontFamily: FONT_MONO }}>{hauteurTotale}<\/div>\r\n          <div style={{ fontSize: 10.5, color: \"#6B7684\", fontFamily: FONT_BODY }}>mm de hauteur<\/div>\r\n        <\/div>\r\n      <\/div>\r\n      <\/div><\/div>\r\n      <\/div>\r\n\r\n      <RecapCard rows={[\r\n        [\"Gamme\", gammeName],\r\n        [\"Bacs\", `${nBacs} bac${nBacs > 1 ? \"s\" : \"\"}`],\r\n        [\"Cadres empil\u00e9s\", `${cadres} par bac`],\r\n        [\"Emprise au sol\", `${L} \u00d7 ${W} mm par bac`],\r\n        [\"Hauteur\", `${hauteurTotale} mm`],\r\n        [\"Volume utile\", `${volTotal} L au total`],\r\n        [\"Accessoires\", accessNames],\r\n        usagers && [\"Apporteurs\", `${usagers} personnes`],\r\n        [\"Quantit\u00e9\", `${qte} unit\u00e9s`],\r\n      ]} note=\"Volumes de 400 L \u00e0 1 500 L selon la configuration. Bois brut non trait\u00e9, PEFC, livr\u00e9 \u00e0 plat en kit avec instructions de montage. Le volume affich\u00e9 est purement g\u00e9om\u00e9trique.\" \/>\r\n\r\n      <BriefCTA produit=\"Composteur bois rehaussable\" specs={specs} \/>\r\n    <\/>\r\n  );\r\n}\r\n\r\n\/\/ \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 SHELL \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\r\nconst PRODUCTS = [\r\n  { id: \"palette\", name: \"Palette\", icon: \"pallet\", h1: \"Configurateur de palette bois sur-mesure\", sub: \"Trois familles, formats de 600 \u00d7 400 mm jusqu'\u00e0 4 090 mm de longueur\" },\r\n  { id: \"rehausse\", name: \"Rehausse\", icon: \"rehausse\", h1: \"Configurateur de rehausse bois sur-mesure\", sub: \"Six formats standard, hauteurs 100 \u00e0 400 mm, accessoires magasin et transport\" },\r\n  { id: \"rack\", name: \"Plateau rack\", icon: \"rack\", h1: \"Configurateur de plateau bois pour rack\", sub: \"Cotes entre lisses int\u00e9rieures, quatre mat\u00e9riaux, trois finitions\" },\r\n  { id: \"caisse\", name: \"Caisse\", icon: \"crate\", h1: \"Configurateur de caisse bois sur-mesure\", sub: \"Cotes int\u00e9rieures cal\u00e9es sur la pi\u00e8ce, am\u00e9nagements et marquage NIMP 15\" },\r\n  { id: \"panneau\", name: \"Panneau\", icon: \"scissors\", h1: \"Configurateur de panneau bois\", sub: \"Stock permanent, d\u00e9bit sur scie \u00e0 panneau et usinage \u00e0 commande num\u00e9rique\" },\r\n  { id: \"composteur\", name: \"Composteur\", icon: \"stack\", h1: \"Configurateur de composteur bois\", sub: \"Cadres rehaussables mont\u00e9s sans outil, du bac domestique au site collectif\" },\r\n  { id: \"chargement\", name: \"Chargement\", icon: \"truck\", h1: \"Combien de palettes par camion ?\", sub: \"Calcul du remplissage au sol et en gerbage, pour tout format et tout v\u00e9hicule\" },\r\n];\r\n\r\nfunction getDefaultProduct() {\r\n  try {\r\n    const p = new URLSearchParams(window.location.search).get(\"produit\");\r\n    if (PRODUCTS.some(x => x.id === p)) return p;\r\n  } catch (e) {}\r\n  return \"palette\";\r\n}\r\n\r\nfunction ConfigurateurEcopalette() {\r\n  const [produit, setProduit] = useState(getDefaultProduct);\r\n  const [configs, setConfigs] = useState({});\r\n  const registerConfig = (source, data) => setConfigs(s => ({ ...s, [source]: data }));\r\n  const actif = PRODUCTS.find(p => p.id === produit);\r\n\r\n  return (\r\n    <div style={{ minHeight: \"100vh\", background: \"linear-gradient(160deg,#F7F8F5 0%,#F1F6EA 45%,#F7F8F5 100%)\", borderRadius: 16, fontFamily: FONT_BODY, padding: \"20px 16px 60px\" }}>\r\n      <link href=\"https:\/\/fonts.googleapis.com\/css2?family=Teko:wght@500;600;700&family=Poppins:wght@400;500;600;700&family=DM+Mono:wght@400;500&display=swap\" rel=\"stylesheet\" \/>\r\n      <style>{`*{box-sizing:border-box}@media (prefers-reduced-motion:reduce){*{transition:none!important}}\r\n.cfg-grid{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:16px;align-items:start}\r\n.cfg-sticky{position:sticky;top:14px}\r\n.cfg-nav{display:grid;grid-template-columns:repeat(7,1fr);gap:8px;margin-bottom:18px}\r\n@media (max-width:980px){.cfg-grid{grid-template-columns:1fr}.cfg-sticky{position:static}.cfg-nav{grid-template-columns:repeat(4,1fr)}}\r\n@media (max-width:560px){.cfg-nav{grid-template-columns:repeat(2,1fr)}}`}<\/style>\r\n\r\n      <div style={{ maxWidth: 1100, margin: \"0 auto\" }}>\r\n        <div style={{ textAlign: \"center\", marginBottom: 18 }}>\r\n          <h1 style={{ fontSize: 40, fontWeight: 600, color: DARK, margin: \"0 0 4px\", fontFamily: FONT_TITLE, letterSpacing: \"0.5px\", lineHeight: 0.95 }}>{actif?.h1}<\/h1>\r\n          <p style={{ fontSize: 13, color: \"#6B7684\", margin: 0 }}>{actif?.sub}<\/p>\r\n        <\/div>\r\n\r\n        <div className=\"cfg-nav\">\r\n          {PRODUCTS.map(p => {\r\n            const on = produit === p.id;\r\n            return (\r\n              <button key={p.id} onClick={() => setProduit(p.id)} style={{\r\n                padding: \"12px 6px\", borderRadius: 12, cursor: \"pointer\", fontFamily: FONT_BODY, fontSize: 12, fontWeight: 700,\r\n                border: `2px solid ${on ? ACCENT : \"#E4E8EE\"}`, background: on ? \"linear-gradient(135deg,#F1F8E9,#F7FCEE)\" : \"#fff\",\r\n                color: DARK, display: \"flex\", flexDirection: \"column\", alignItems: \"center\", gap: 6, transition: \"all .18s\",\r\n                boxShadow: on ? \"0 2px 12px rgba(118,184,42,0.18)\" : \"none\",\r\n              }}>\r\n                <Icon name={p.icon} size={24} color={on ? ACCENT : DARK} \/>\r\n                {p.name}\r\n              <\/button>\r\n            );\r\n          })}\r\n        <\/div>\r\n\r\n        <div style={{ display: produit === \"palette\" ? \"block\" : \"none\" }}><ModulePalette onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"rehausse\" ? \"block\" : \"none\" }}><ModuleRehausse onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"rack\" ? \"block\" : \"none\" }}><ModuleRack onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"caisse\" ? \"block\" : \"none\" }}><ModuleCaisse onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"panneau\" ? \"block\" : \"none\" }}><ModulePanneau onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"composteur\" ? \"block\" : \"none\" }}><ModuleComposteur onConfig={registerConfig} \/><\/div>\r\n        <div style={{ display: produit === \"chargement\" ? \"block\" : \"none\" }}><ModuleChargement configs={configs} \/><\/div>\r\n\r\n        <div style={{ textAlign: \"center\", marginTop: 24, fontSize: 10.5, color: \"#9AAAB8\", fontFamily: FONT_MONO }}>\r\n          Configurateur Solutions Bois Pro \u00a9 Ecopalette E2SE, Firminy (42)\r\n          <br \/>Estimations g\u00e9om\u00e9triques indicatives, validation technique par notre bureau d'\u00e9tudes.\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n  );\r\n}\r\n\r\nReactDOM.createRoot(document.getElementById(\"ecopalette-configurateur\")).render(<ConfigurateurEcopalette \/>);\r\n\r\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Configurateur de solutions bois sur mesure en ligne Concevez votre emballage bois directement en ligne. Choisissez votre produit, palette, rehausse, plateau pour rack ou caisse de transport, renseignez vos dimensions et vos options, et visualisez le r\u00e9sultat en 3D au fur et \u00e0 mesure. L&rsquo;outil calcule aussi le nombre de palettes que vous pouvez charger [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"footnotes":""},"class_list":["post-4839","page","type-page","status-publish","hentry"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.5 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Configurateur Solutions Bois Pro | Ecopalette E2SE<\/title>\n<meta name=\"description\" content=\"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.ecopalettee2se.fr\/de\/simulateur-palette-bois\/\" \/>\n<meta property=\"og:locale\" content=\"de_DE\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Configurateur Solutions Bois Pro | Ecopalette E2SE\" \/>\n<meta property=\"og:description\" content=\"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.ecopalettee2se.fr\/de\/simulateur-palette-bois\/\" \/>\n<meta property=\"og:site_name\" content=\"Ecopalette E2SE\" \/>\n<meta property=\"article:modified_time\" content=\"2026-07-30T06:35:17+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/www.ecopalettee2se.fr\/wp-content\/uploads\/2026\/08\/Trou-2.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1200\" \/>\n\t<meta property=\"og:image:height\" content=\"1200\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data1\" content=\"1\u00a0Minute\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/simulateur-palette-bois\\\/\",\"url\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/simulateur-palette-bois\\\/\",\"name\":\"Configurateur Solutions Bois Pro | Ecopalette E2SE\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#website\"},\"datePublished\":\"2025-09-14T14:20:34+00:00\",\"dateModified\":\"2026-07-30T06:35:17+00:00\",\"description\":\"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/simulateur-palette-bois\\\/#breadcrumb\"},\"inLanguage\":\"de\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/www.ecopalettee2se.fr\\\/simulateur-palette-bois\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/simulateur-palette-bois\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Configurateur Solutions Bois Pro\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#website\",\"url\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/\",\"name\":\"Ecopalette E2SE\",\"description\":\"Fabricant et fournisseur palette bois, r\u00e9hausse sur-mesure\",\"publisher\":{\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"de\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#organization\",\"name\":\"Ecopalette E2SE\",\"url\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"de\",\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/wp-content\\\/uploads\\\/2022\\\/05\\\/logo-eco.svg\",\"contentUrl\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/wp-content\\\/uploads\\\/2022\\\/05\\\/logo-eco.svg\",\"width\":190,\"height\":59,\"caption\":\"Ecopalette E2SE\"},\"image\":{\"@id\":\"https:\\\/\\\/www.ecopalettee2se.fr\\\/#\\\/schema\\\/logo\\\/image\\\/\"},\"sameAs\":[\"https:\\\/\\\/www.linkedin.com\\\/company\\\/ecopalette-e2se\\\/\"]}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Configurateur Solutions Bois Pro | Ecopalette E2SE","description":"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.ecopalettee2se.fr\/de\/simulateur-palette-bois\/","og_locale":"de_DE","og_type":"article","og_title":"Configurateur Solutions Bois Pro | Ecopalette E2SE","og_description":"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.","og_url":"https:\/\/www.ecopalettee2se.fr\/de\/simulateur-palette-bois\/","og_site_name":"Ecopalette E2SE","article_modified_time":"2026-07-30T06:35:17+00:00","og_image":[{"width":1200,"height":1200,"url":"https:\/\/www.ecopalettee2se.fr\/wp-content\/uploads\/2026\/08\/Trou-2.png","type":"image\/png"}],"twitter_card":"summary_large_image","twitter_misc":{"Est. reading time":"1\u00a0Minute"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.ecopalettee2se.fr\/simulateur-palette-bois\/","url":"https:\/\/www.ecopalettee2se.fr\/simulateur-palette-bois\/","name":"Configurateur Solutions Bois Pro | Ecopalette E2SE","isPartOf":{"@id":"https:\/\/www.ecopalettee2se.fr\/#website"},"datePublished":"2025-09-14T14:20:34+00:00","dateModified":"2026-07-30T06:35:17+00:00","description":"Configurez en ligne votre palette, rehausse, plateau rack ou caisse bois sur mesure. Aper\u00e7u 3D, calcul de chargement et devis gratuit.","breadcrumb":{"@id":"https:\/\/www.ecopalettee2se.fr\/simulateur-palette-bois\/#breadcrumb"},"inLanguage":"de","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.ecopalettee2se.fr\/simulateur-palette-bois\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.ecopalettee2se.fr\/simulateur-palette-bois\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/www.ecopalettee2se.fr\/"},{"@type":"ListItem","position":2,"name":"Configurateur Solutions Bois Pro"}]},{"@type":"WebSite","@id":"https:\/\/www.ecopalettee2se.fr\/#website","url":"https:\/\/www.ecopalettee2se.fr\/","name":"Ecopalette E2SE","description":"Hersteller und Lieferant von Holzpaletten und Palettenaufsatzrahmen nach Ma\u00df","publisher":{"@id":"https:\/\/www.ecopalettee2se.fr\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.ecopalettee2se.fr\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"de"},{"@type":"Organization","@id":"https:\/\/www.ecopalettee2se.fr\/#organization","name":"Ecopalette E2SE","url":"https:\/\/www.ecopalettee2se.fr\/","logo":{"@type":"ImageObject","inLanguage":"de","@id":"https:\/\/www.ecopalettee2se.fr\/#\/schema\/logo\/image\/","url":"https:\/\/www.ecopalettee2se.fr\/wp-content\/uploads\/2022\/05\/logo-eco.svg","contentUrl":"https:\/\/www.ecopalettee2se.fr\/wp-content\/uploads\/2022\/05\/logo-eco.svg","width":190,"height":59,"caption":"Ecopalette E2SE"},"image":{"@id":"https:\/\/www.ecopalettee2se.fr\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/www.linkedin.com\/company\/ecopalette-e2se\/"]}]}},"_links":{"self":[{"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/pages\/4839","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/comments?post=4839"}],"version-history":[{"count":40,"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/pages\/4839\/revisions"}],"predecessor-version":[{"id":5802,"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/pages\/4839\/revisions\/5802"}],"wp:attachment":[{"href":"https:\/\/www.ecopalettee2se.fr\/de\/wp-json\/wp\/v2\/media?parent=4839"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}