{"id":7055,"date":"2026-05-04T16:25:18","date_gmt":"2026-05-04T15:25:18","guid":{"rendered":"https:\/\/ikonera.art\/?page_id=7055"},"modified":"2026-05-11T16:09:41","modified_gmt":"2026-05-11T15:09:41","slug":"%f0%9f%94%92","status":"publish","type":"page","link":"https:\/\/ikonera.art\/it\/%f0%9f%94%92\/","title":{"rendered":"\ud83d\udd12"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"7055\" class=\"elementor elementor-7055\" data-elementor-post-type=\"page\">\n\t\t\t\t<div data-particle_enable=\"false\" data-particle-mobile-disabled=\"false\" class=\"elementor-element elementor-element-fd78a74 e-flex e-con-boxed e-con e-parent\" data-id=\"fd78a74\" data-element_type=\"container\" data-e-type=\"container\" data-settings=\"{&quot;ekit_has_onepagescroll_dot&quot;:&quot;yes&quot;}\">\n\t\t\t\t\t<div class=\"e-con-inner\">\n\t\t\t\t<div class=\"elementor-element elementor-element-1be749c elementor-widget elementor-widget-html\" data-id=\"1be749c\" data-element_type=\"widget\" data-e-type=\"widget\" data-settings=\"{&quot;ekit_we_effect_on&quot;:&quot;none&quot;}\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!--\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n  IKONERA \u00b7 CALCULATEUR S\u00c9CURIS\u00c9 \u2014 WIDGET ELEMENTOR (FR \u00b7 EN \u00b7 DE \u00b7 IT)\n\n  \u00c0 COLLER dans un widget HTML d'Elementor.\n  - 4 langues avec s\u00e9lecteur en haut \u00e0 droite (FR \/ EN \/ DE \/ IT)\n  - D\u00e9tection auto de la langue navigateur, persistance localStorage\n  - Tout scop\u00e9 sous .ikonera-app \u2192 ne pollue pas ton site\n  - Responsive jusqu'\u00e0 380px\n\n  MOT DE PASSE : ik22\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n-->\n\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"><\/script>\n\n<style>\n\n  .ikonera-app {\n    --bg-dark:        #0E0907;\n    --card-cream:     #F2EBDD;\n    --card-cream-2:   #EAE2D2;\n    --header-brown:   #2C1F15;\n    --header-brown-2: #3A2A1E;\n    --text-dark:      #1F1611;\n    --text-mid:       #6B5C4D;\n    --text-soft:      #9C8E7E;\n    --or:             #C9A14C;\n    --or-deep:        #A0772E;\n    --or-soft:        #D4B560;\n    --or-line:        rgba(201, 161, 76, 0.35);\n    --border-soft:    rgba(31, 22, 17, 0.10);\n    --border-mid:     rgba(31, 22, 17, 0.20);\n\n    --m-bianco-carrara-1:  #F0E8DA;  --m-bianco-carrara-2:  #D8CFBE;\n    --m-bianco-venatino-1: #D0CDC6;  --m-bianco-venatino-2: #A8A6A0;\n    --m-statuario-1:       #F4ECDD;  --m-statuario-2:       #C9C0AE;\n    --m-calacatta-1:       #EAD9A8;  --m-calacatta-2:       #C0A66E;\n    --m-arabescato-1:      #DDD0BC;  --m-arabescato-2:      #B5A687;\n    --m-bardiglio-1:       #B0AEB4;  --m-bardiglio-2:       #6E6E78;\n    --m-cipollino-1:       #95A18C;  --m-cipollino-2:       #4F5E48;\n    --m-nero-marquinia-1:  #2C2823;  --m-nero-marquinia-2:  #0E0B08;\n    --m-rosso-levanto-1:   #8E342E;  --m-rosso-levanto-2:   #4A1814;\n    --m-portoro-1:         #2E2A1E;  --m-portoro-2:         #786230;\n    --m-verde-alpi-1:      #4A6048;  --m-verde-alpi-2:      #1E2C1C;\n    --m-giallo-siena-1:    #D4AB52;  --m-giallo-siena-2:    #8E6818;\n    --m-paonazzo-1:        #9C8AA6;  --m-paonazzo-2:        #5C4A66;\n  }\n\n  .ikonera-app * { box-sizing: border-box; margin: 0; padding: 0; }\n\n  .ikonera-app, .ikonera-app {\n    background: var(--bg-dark);\n    color: var(--text-dark);\n    font-family: 'Inter', system-ui, -apple-system, sans-serif;\n    line-height: 1.5;\n    -webkit-font-smoothing: antialiased;\n  }\n  .ikonera-app { padding: 0; min-height: 100vh; }\n\n  \/* \u2550\u2550\u2550 S\u00c9LECTEUR DE LANGUE \u2550\u2550\u2550 *\/\n  .ikonera-app .lang-switcher {\n    position: absolute; top: 18px; right: 18px;\n    display: flex; gap: 4px;\n    background: rgba(0,0,0,0.45);\n    border: 1px solid rgba(201,161,76,0.25);\n    border-radius: 100px;\n    padding: 3px;\n    z-index: 100;\n    backdrop-filter: blur(8px);\n    -webkit-backdrop-filter: blur(8px);\n  }\n  .ikonera-app .lang-btn {\n    background: transparent; border: none;\n    color: rgba(245,240,232,0.55);\n    font-family: inherit; font-size: 10px; font-weight: 600;\n    padding: 6px 9px; border-radius: 100px;\n    letter-spacing: 0.12em; text-transform: uppercase;\n    cursor: pointer; transition: all 0.2s ease;\n    line-height: 1;\n  }\n  .ikonera-app .lang-btn:hover { color: var(--or); }\n  .ikonera-app .lang-btn.active {\n    background: var(--or);\n    color: #0E0907;\n  }\n  \/* Sur mobile : plus discret *\/\n  @media (max-width: 500px) {\n    .ikonera-app .lang-switcher { top: 12px; right: 12px; padding: 2px; }\n    .ikonera-app .lang-btn { font-size: 9px; padding: 5px 7px; letter-spacing: 0.08em; }\n  }\n\n  \/* \u2550\u2550\u2550 HEADER PLEINE PAGE (visible apr\u00e8s login) \u2550\u2550\u2550 *\/\n  .ikonera-app .secure-header {\n    display: none;\n    position: relative; z-index: 5;\n    padding: 26px 36px 0;\n    max-width: 1100px; margin: 0 auto;\n    justify-content: space-between; align-items: center;\n    flex-wrap: wrap; gap: 12px;\n  }\n  .ikonera-app.authed .secure-header { display: flex; }\n  .ikonera-app .secure-header h1 {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 22px; font-weight: 400;\n    letter-spacing: 0.18em; text-transform: uppercase;\n    color: #E8DCC8;\n    margin: 0;\n  }\n  .ikonera-app .logout-btn {\n    background: transparent; color: rgba(232,220,200,0.7);\n    border: 1px solid rgba(201,161,76,0.35);\n    padding: 8px 18px; border-radius: 4px;\n    font-family: inherit; font-size: 11px; font-weight: 500;\n    text-transform: uppercase; letter-spacing: 0.18em;\n    cursor: pointer; transition: all 0.25s ease;\n  }\n  .ikonera-app .logout-btn:hover { border-color: var(--or); color: var(--or); }\n\n  .ikonera-app:not(.authed) .calc-wrap { display: none; }\n  .ikonera-app.authed .login-overlay { display: none; }\n  .ikonera-app .calc-wrap { padding: 24px 18px; }\n\n  \/* \u2550\u2550\u2550 LOGIN OVERLAY \u2014 design IKONERA \u2550\u2550\u2550 *\/\n  .ikonera-app .login-overlay {\n    min-height: 100vh;\n    display: flex; align-items: center; justify-content: center;\n    padding: 60px 24px;\n    position: relative; overflow: hidden;\n  }\n  .ikonera-app .login-overlay::before {\n    content: ''; position: absolute; inset: 0;\n    background:\n      radial-gradient(ellipse at top, rgba(201,161,76,0.10), transparent 65%),\n      radial-gradient(ellipse at bottom, rgba(201,161,76,0.05), transparent 60%);\n    pointer-events: none;\n  }\n  .ikonera-app .login-card {\n    width: 100%; max-width: 500px; position: relative; z-index: 1;\n    background: #110A06;\n    border: 1px solid rgba(201,161,76,0.25);\n    border-radius: 6px;\n    padding: 56px 50px 48px;\n    box-shadow:\n      0 30px 80px rgba(0,0,0,0.7),\n      0 0 100px rgba(201,161,76,0.04),\n      inset 0 1px 0 rgba(255,255,255,0.04);\n    color: #F5F0E8;\n    text-align: center;\n  }\n  \/* Coins dor\u00e9s d\u00e9coratifs *\/\n  .ikonera-app .login-card::before, .ikonera-app .login-card::after {\n    content: ''; position: absolute; width: 22px; height: 22px;\n    border: 1px solid rgba(201,161,76,0.6);\n  }\n  .ikonera-app .login-card::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }\n  .ikonera-app .login-card::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }\n\n  \/* Logo IKONERA en haut *\/\n  .ikonera-app .login-brand-logo {\n    display: block;\n    width: 250px;\n    max-width: 70%;\n    height: auto;\n    margin: 0 auto 14px;\n    filter: drop-shadow(0 0 18px rgba(201,161,76,0.10));\n  }\n  .ikonera-app .login-tagline {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-style: italic;\n    font-size: 14px;\n    color: var(--or);\n    letter-spacing: 0.04em;\n    margin-bottom: 28px;\n  }\n  \/* S\u00e9parateur or *\/\n  .ikonera-app .login-separator {\n    width: 60%;\n    margin: 0 auto 28px;\n    height: 1px;\n    background: linear-gradient(90deg, transparent, rgba(201,161,76,0.4), transparent);\n  }\n\n  \/* Cadenas *\/\n  .ikonera-app .login-lock {\n    color: var(--or);\n    margin: 0 auto 22px;\n    width: 32px; height: 32px;\n  }\n  .ikonera-app .login-lock svg { width: 100%; height: 100%; }\n\n  .ikonera-app .login-title {\n    font-family: 'Inter', sans-serif;\n    font-size: 18px; font-weight: 500; text-align: center;\n    color: #F5F0E8; margin: 0 0 10px;\n    letter-spacing: 0.18em;\n    text-transform: uppercase;\n  }\n  .ikonera-app .login-subtitle {\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.32em;\n    text-align: center; color: var(--or);\n    margin-bottom: 26px; font-weight: 500;\n  }\n  .ikonera-app .login-text {\n    font-size: 14px; line-height: 1.7;\n    color: rgba(245,240,232,0.65); text-align: center;\n    margin-bottom: 26px;\n    padding: 0 8px;\n  }\n\n  \/* Box \"Pour demander un acc\u00e8s\" *\/\n  .ikonera-app .request-section {\n    background: rgba(201,161,76,0.05);\n    border-left: 2px solid var(--or);\n    padding: 18px 22px;\n    margin-bottom: 22px;\n    border-radius: 0 6px 6px 0;\n    text-align: left;\n  }\n  .ikonera-app .request-title {\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.22em;\n    color: var(--or); margin-bottom: 14px; font-weight: 500;\n  }\n  .ikonera-app .request-list { list-style: none; padding: 0; margin: 0; }\n  .ikonera-app .request-list li {\n    font-size: 13.5px; color: rgba(245,240,232,0.75);\n    padding: 5px 0 5px 18px; position: relative; line-height: 1.5;\n  }\n  .ikonera-app .request-list li::before {\n    content: '\u2014'; color: var(--or); position: absolute; left: 0;\n    font-weight: 400; font-size: 11px; line-height: 1.5; top: 7px;\n  }\n\n  \/* CTA principal *\/\n  .ikonera-app .cta-contact {\n    display: block; width: 100%;\n    padding: 16px 14px;\n    background: var(--or); color: #110A06;\n    border: none; border-radius: 4px;\n    font-family: inherit; font-size: 13px; font-weight: 600;\n    text-transform: uppercase; letter-spacing: 0.22em;\n    text-align: center; text-decoration: none;\n    cursor: pointer; transition: all 0.25s ease;\n    margin-bottom: 4px;\n  }\n  .ikonera-app .cta-contact:hover {\n    background: #d4b560;\n    transform: translateY(-1px);\n    box-shadow: 0 10px 30px rgba(201,161,76,0.30);\n  }\n\n  \/* Section \"j'ai d\u00e9j\u00e0 un code\" \u2014 repli\u00e9e par d\u00e9faut *\/\n  .ikonera-app .pwd-toggle {\n    display: block;\n    margin: 22px auto 0;\n    background: transparent;\n    border: none;\n    color: rgba(245,240,232,0.5);\n    font-family: inherit;\n    font-size: 11px;\n    letter-spacing: 0.22em;\n    text-transform: uppercase;\n    cursor: pointer;\n    padding: 6px 12px;\n    transition: color 0.2s ease;\n  }\n  .ikonera-app .pwd-toggle:hover { color: var(--or); }\n  .ikonera-app .pwd-toggle::before { content: '+ '; }\n  .ikonera-app .pwd-toggle.open::before { content: '\u2212 '; }\n\n  .ikonera-app .pwd-collapse {\n    max-height: 0;\n    overflow: hidden;\n    transition: max-height 0.35s ease, margin 0.35s ease, padding 0.35s ease;\n  }\n  .ikonera-app .pwd-collapse.open {\n    max-height: 220px;\n    margin-top: 14px;\n  }\n  .ikonera-app .pwd-input-wrap { display: flex; gap: 8px; }\n  .ikonera-app .pwd-input {\n    flex: 1; padding: 13px 16px;\n    background: rgba(0,0,0,0.4);\n    border: 1px solid rgba(201,161,76,0.25);\n    color: #F5F0E8;\n    -webkit-text-fill-color: #F5F0E8;\n    caret-color: var(--or);\n    font-family: inherit; font-size: 16px;\n    border-radius: 4px; outline: none;\n    transition: all 0.25s ease;\n    color-scheme: dark;\n    -webkit-appearance: none; appearance: none;\n    text-align: center;\n    letter-spacing: 0.1em;\n  }\n  .ikonera-app .pwd-input:focus {\n    border-color: var(--or); background: rgba(0,0,0,0.6);\n    box-shadow: 0 0 0 3px rgba(201,161,76,0.12);\n  }\n  .ikonera-app .pwd-input::placeholder { color: rgba(245,240,232,0.3); letter-spacing: 0.05em; }\n  .ikonera-app .pwd-input:-webkit-autofill, .ikonera-app .pwd-input:-webkit-autofill:focus {\n    -webkit-text-fill-color: #F5F0E8;\n    -webkit-box-shadow: 0 0 0 1000px rgba(0,0,0,0.4) inset;\n    transition: background-color 9999s ease-in-out 0s;\n  }\n  .ikonera-app .pwd-submit {\n    padding: 13px 22px;\n    background: transparent; color: var(--or);\n    border: 1px solid var(--or);\n    border-radius: 4px; font-family: inherit;\n    font-size: 11px; font-weight: 600;\n    text-transform: uppercase; letter-spacing: 0.18em;\n    cursor: pointer; transition: all 0.25s ease;\n    white-space: nowrap;\n  }\n  .ikonera-app .pwd-submit:hover { background: var(--or); color: #110A06; }\n  .ikonera-app .error-msg {\n    color: #d4716a; font-size: 12px;\n    margin-top: 12px; min-height: 18px; text-align: center;\n  }\n\n  @keyframes shake {\n    0%,100% { transform: translateX(0); }\n    25%     { transform: translateX(-6px); }\n    75%     { transform: translateX(6px); }\n  }\n  .ikonera-app .pwd-input.shake { animation: shake 0.3s; }\n\n  @media (max-width: 700px) {\n    .ikonera-app .secure-header { padding: 18px 18px 0; }\n    .ikonera-app .secure-header h1 { font-size: 18px; }\n    .ikonera-app .login-card { padding: 40px 28px 32px; }\n    .ikonera-app .login-brand-logo { width: 200px; }\n    .ikonera-app .login-text { font-size: 13px; }\n  }\n\n  \/* \u2550\u2550\u2550 CONTENEUR \u2550\u2550\u2550 *\/\n  .ikonera-app .calc {\n    max-width: 920px;\n    margin: 0 auto;\n    background: var(--card-cream);\n    border-radius: 14px;\n    overflow: hidden;\n    box-shadow: 0 24px 60px rgba(0,0,0,0.5), 0 0 0 1px var(--or-line);\n    animation: rise 0.6s ease-out;\n  }\n  @keyframes rise { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: translateY(0); } }\n\n  \/* \u2550\u2550\u2550 HEADER SOMBRE \u2550\u2550\u2550 *\/\n  .ikonera-app .calc-header {\n    background: linear-gradient(135deg, var(--header-brown) 0%, var(--header-brown-2) 100%);\n    padding: 26px 36px 30px;\n    color: #E8DCC8;\n    position: relative;\n    border-bottom: 1px solid var(--or-line);\n  }\n  .ikonera-app .calc-header::after {\n    content: \"\"; position: absolute; bottom: 0; left: 0; right: 0; height: 1px;\n    background: linear-gradient(90deg, transparent, var(--or) 50%, transparent);\n  }\n  .ikonera-app .calc-header-top {\n    display: flex; justify-content: space-between; align-items: flex-start;\n    gap: 16px; flex-wrap: wrap;\n  }\n  .ikonera-app .calc-header-text { color: rgba(232,220,200,0.85); font-size: 13px; padding-top: 6px; }\n  .ikonera-app .calc-header-text .dot { color: var(--or); margin: 0 6px; }\n  .ikonera-app .badge-online {\n    display: inline-flex; align-items: center; gap: 8px;\n    padding: 8px 16px;\n    border: 1px solid var(--or); border-radius: 100px;\n    color: var(--or-soft); font-size: 11px; font-weight: 500;\n    letter-spacing: 0.18em; text-transform: uppercase;\n    background: rgba(201,161,76,0.05);\n  }\n  .ikonera-app .badge-online::before {\n    content: \"\"; width: 6px; height: 6px;\n    background: var(--or-soft); border-radius: 50%;\n    box-shadow: 0 0 8px var(--or-soft);\n    animation: pulse 2s ease-in-out infinite;\n  }\n  @keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }\n\n  \/* \u2550\u2550\u2550 STEPPER \u2550\u2550\u2550 *\/\n  .ikonera-app .stepper {\n    display: grid; grid-template-columns: repeat(5, 1fr);\n    background: var(--card-cream); border-bottom: 1px solid var(--border-soft);\n  }\n  .ikonera-app .step {\n    padding: 18px 14px; display: flex; align-items: center; gap: 12px;\n    cursor: pointer; position: relative; transition: background 0.2s;\n    border-right: 1px solid var(--border-soft);\n  }\n  .ikonera-app .step:last-child { border-right: none; }\n  .ikonera-app .step:hover:not(.disabled) { background: var(--card-cream-2); }\n  .ikonera-app .step.active::before {\n    content: \"\"; position: absolute; top: 0; left: 0; right: 0; height: 2px;\n    background: var(--or);\n  }\n  .ikonera-app .step.disabled { cursor: not-allowed; opacity: 0.5; }\n  .ikonera-app .step-num {\n    width: 28px; height: 28px; border-radius: 50%;\n    background: rgba(201,161,76,0.18); color: var(--or-deep);\n    display: flex; align-items: center; justify-content: center;\n    font-size: 13px; font-weight: 600; flex-shrink: 0;\n  }\n  .ikonera-app .step.active .step-num { background: var(--or); color: var(--header-brown); }\n  .ikonera-app .step.complete .step-num { background: var(--or-deep); color: var(--card-cream); }\n  .ikonera-app .step.complete .step-num::after { content: \"\u2713\"; }\n  .ikonera-app .step.complete .step-num span { display: none; }\n  .ikonera-app .step-info { min-width: 0; }\n  .ikonera-app .step-info-title { font-size: 13px; font-weight: 500; color: var(--text-dark); line-height: 1.2; }\n  .ikonera-app .step-info-sub { font-size: 11px; color: var(--text-soft); margin-top: 2px; }\n\n  @media (max-width: 700px) {\n    .ikonera-app .stepper { grid-template-columns: repeat(5, auto); overflow-x: auto; -webkit-overflow-scrolling: touch; }\n    .ikonera-app .step { padding: 14px 12px; min-width: 130px; }\n    .ikonera-app .step-info-title { font-size: 12px; }\n    .ikonera-app .step-info-sub { font-size: 10px; }\n  }\n\n  \/* \u2550\u2550\u2550 PANELS \u2550\u2550\u2550 *\/\n  .ikonera-app .panels { padding: 32px 36px 24px; }\n  .ikonera-app .panel { display: none; animation: fade 0.35s ease-out; }\n  .ikonera-app .panel.active { display: block; }\n  @keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }\n  .ikonera-app .panel-head { margin-bottom: 22px; }\n  .ikonera-app .panel-head h2 {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 26px; font-weight: 400; color: var(--text-dark);\n    letter-spacing: 0.01em; margin-bottom: 6px;\n  }\n  .ikonera-app .panel-head p { color: var(--text-mid); font-size: 14px; }\n\n  \/* \u2550\u2550\u2550 \u00c9TAPE 1 \u2014 MARBRES \u2550\u2550\u2550 *\/\n  .ikonera-app .marbles {\n    display: grid; grid-template-columns: repeat(6, 1fr); gap: 12px;\n  }\n  @media (max-width: 900px) { .ikonera-app .marbles { grid-template-columns: repeat(4, 1fr); } }\n  @media (max-width: 600px) { .ikonera-app .marbles { grid-template-columns: repeat(2, 1fr); } }\n\n  .ikonera-app .marble-card {\n    background: var(--card-cream); border: 1px solid var(--border-soft);\n    border-radius: 8px; padding: 16px 10px 14px;\n    text-align: center; cursor: pointer; transition: all 0.2s;\n    position: relative; user-select: none;\n  }\n  .ikonera-app .marble-card:hover {\n    border-color: var(--or-line); transform: translateY(-2px);\n    box-shadow: 0 6px 20px rgba(31,22,17,0.08);\n  }\n  .ikonera-app .marble-card.selected {\n    border-color: var(--or); background: var(--card-cream-2);\n    box-shadow: 0 6px 24px rgba(201,161,76,0.20);\n  }\n  .ikonera-app .marble-card.selected::after {\n    content: \"\u2713\"; position: absolute; top: 8px; right: 10px;\n    color: var(--or-deep); font-size: 14px; font-weight: 700;\n  }\n  .ikonera-app .sphere {\n    width: 56px; height: 56px; border-radius: 50%; margin: 0 auto 10px;\n    background: radial-gradient(circle at 32% 28%, var(--c1) 0%, var(--c1) 25%, var(--c2) 95%);\n    box-shadow: inset 0 -4px 8px rgba(0,0,0,0.18),\n                inset 2px 4px 6px rgba(255,255,255,0.25),\n                0 4px 10px rgba(0,0,0,0.10);\n    position: relative;\n  }\n  .ikonera-app .sphere::after {\n    content: \"\"; position: absolute; top: 14%; left: 22%;\n    width: 22%; height: 18%;\n    background: radial-gradient(ellipse, rgba(255,255,255,0.55), transparent 70%);\n    border-radius: 50%; pointer-events: none;\n  }\n  .ikonera-app .marble-name { font-size: 13px; color: var(--text-dark); font-weight: 500; line-height: 1.2; margin-bottom: 4px; }\n  .ikonera-app .marble-meta { font-size: 10px; color: var(--text-mid); line-height: 1.4; }\n  .ikonera-app .marble-tier { display: inline-block; margin-right: 4px; font-weight: 500; }\n  .ikonera-app .marble-tier::before {\n    content: \"\"; display: inline-block; width: 6px; height: 6px;\n    border-radius: 50%; background: var(--or-soft);\n    margin-right: 4px; vertical-align: middle; transform: translateY(-1px);\n  }\n  .ikonera-app .marble-tier.standard::before { background: #B0A48E; }\n  .ikonera-app .marble-tier.standardp::before { background: #C9A14C; }\n  .ikonera-app .marble-tier.premium::before { background: var(--or); }\n  .ikonera-app .marble-tier.premiump::before { background: var(--or); }\n  .ikonera-app .marble-tier.luxe::before { background: var(--or-deep); }\n  .ikonera-app .marble-tier.luxep::before { background: var(--or-deep); }\n\n  \/* \u2550\u2550\u2550 \u00c9TAPE 2 \u2014 FINITIONS (R + H) \u2550\u2550\u2550 *\/\n  .ikonera-app .finish-section {\n    margin-bottom: 22px;\n  }\n  .ikonera-app .finish-section:last-child { margin-bottom: 0; }\n  .ikonera-app .finish-section-head {\n    display: flex; align-items: baseline; gap: 12px;\n    margin-bottom: 12px;\n    padding-bottom: 8px;\n    border-bottom: 1px solid var(--or-line);\n  }\n  .ikonera-app .finish-section-icon {\n    font-size: 16px;\n  }\n  .ikonera-app .finish-section-title {\n    font-size: 11px;\n    text-transform: uppercase;\n    letter-spacing: 0.22em;\n    color: var(--or-deep);\n    font-weight: 600;\n  }\n  .ikonera-app .finish-section-sub {\n    font-size: 11px;\n    color: var(--text-soft);\n    font-weight: 400;\n    letter-spacing: 0.05em;\n  }\n  .ikonera-app .finish-row {\n    display: grid;\n    grid-template-columns: repeat(3, 1fr);\n    gap: 10px;\n  }\n  .ikonera-app .finish-row.h4 { grid-template-columns: repeat(4, 1fr); }\n  @media (max-width: 700px) {\n    .ikonera-app .finish-row, .ikonera-app .finish-row.h4 { grid-template-columns: repeat(2, 1fr); }\n  }\n  @media (max-width: 420px) {\n    .ikonera-app .finish-row, .ikonera-app .finish-row.h4 { grid-template-columns: 1fr; }\n  }\n\n  .ikonera-app .finish-mini {\n    background: var(--card-cream);\n    border: 1px solid var(--border-soft);\n    border-radius: 8px;\n    padding: 12px 14px;\n    cursor: pointer;\n    transition: all 0.2s;\n    position: relative;\n  }\n  .ikonera-app .finish-mini:hover {\n    border-color: var(--or-line);\n    transform: translateY(-1px);\n    box-shadow: 0 4px 14px rgba(31,22,17,0.06);\n  }\n  .ikonera-app .finish-mini.selected {\n    border-color: var(--or);\n    background: var(--card-cream-2);\n    box-shadow: 0 4px 18px rgba(201,161,76,0.18);\n  }\n  .ikonera-app .finish-mini.selected::after {\n    content: \"\u2713\";\n    position: absolute; top: 8px; right: 12px;\n    color: var(--or-deep); font-weight: 700;\n    font-size: 13px;\n  }\n  .ikonera-app .finish-mini-code {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 13px;\n    color: var(--or-deep);\n    font-weight: 500;\n    letter-spacing: 0.05em;\n  }\n  .ikonera-app .finish-mini-name {\n    font-size: 14px;\n    color: var(--text-dark);\n    font-weight: 500;\n    margin: 2px 0 4px;\n  }\n  .ikonera-app .finish-mini-desc {\n    font-size: 11px;\n    color: var(--text-mid);\n    line-height: 1.45;\n  }\n\n  .ikonera-app .finish-tip {\n    background: rgba(201,161,76,0.08);\n    border-left: 3px solid var(--or);\n    padding: 10px 16px;\n    margin-top: 16px;\n    border-radius: 0 6px 6px 0;\n    font-size: 12px;\n    color: var(--text-dark);\n    line-height: 1.5;\n    display: flex;\n    align-items: flex-start;\n    gap: 10px;\n  }\n  .ikonera-app .finish-tip-icon { flex-shrink: 0; font-size: 14px; }\n\n  \/* \u2550\u2550\u2550 TYPES D'OBJET (\u00e9tape 3) \u2550\u2550\u2550 *\/\n  .ikonera-app .shape-types {\n    display: grid;\n    grid-template-columns: repeat(4, 1fr);\n    gap: 10px;\n    margin-bottom: 18px;\n  }\n  @media (max-width: 700px) { .ikonera-app .shape-types { grid-template-columns: repeat(2, 1fr); } }\n\n  .ikonera-app .shape-card {\n    background: var(--card-cream);\n    border: 1px solid var(--border-soft);\n    border-radius: 10px;\n    padding: 14px 10px;\n    text-align: center;\n    cursor: pointer;\n    transition: all 0.2s;\n    position: relative;\n  }\n  .ikonera-app .shape-card:hover {\n    border-color: var(--or-line);\n    transform: translateY(-2px);\n  }\n  .ikonera-app .shape-card.selected {\n    border-color: var(--or);\n    background: var(--card-cream-2);\n    box-shadow: 0 4px 18px rgba(201,161,76,0.18);\n  }\n  .ikonera-app .shape-card.selected::after {\n    content: \"\u2713\";\n    position: absolute; top: 8px; right: 10px;\n    color: var(--or-deep); font-weight: 700; font-size: 13px;\n  }\n  .ikonera-app .shape-icon {\n    font-size: 28px;\n    margin-bottom: 6px;\n    color: var(--or-deep);\n  }\n  .ikonera-app .shape-icon svg { width: 32px; height: 32px; margin: 0 auto; }\n  .ikonera-app .shape-name {\n    font-size: 13px;\n    color: var(--text-dark);\n    font-weight: 500;\n    margin-bottom: 2px;\n  }\n  .ikonera-app .shape-coef {\n    font-size: 10px;\n    color: var(--text-soft);\n    letter-spacing: 0.05em;\n  }\n\n  \/* \u2550\u2550\u2550 ROTATION MANUELLE 3D \u2550\u2550\u2550 *\/\n  .ikonera-app .viz-canvas-wrap { cursor: grab; touch-action: none; }\n  .ikonera-app .viz-canvas-wrap:active { cursor: grabbing; }\n  .ikonera-app .viz-hint {\n    position: absolute;\n    bottom: 12px; left: 50%; transform: translateX(-50%);\n    color: rgba(232,220,200,0.55);\n    font-size: 10px; text-transform: uppercase; letter-spacing: 0.18em;\n    pointer-events: none;\n    opacity: 1;\n    transition: opacity 0.4s ease-out;\n  }\n  .ikonera-app .viz-hint.fade { opacity: 0; }\n\n  \/* \u2550\u2550\u2550 \u00c9TAPE 3 \u2014 DIMENSIONS \u2550\u2550\u2550 *\/\n  .ikonera-app .dim-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin-bottom: 24px; }\n  @media (max-width: 700px) { .ikonera-app .dim-grid { grid-template-columns: 1fr; } }\n  .ikonera-app .dim-field {\n    background: var(--card-cream); border: 1px solid var(--border-soft);\n    border-radius: 10px; padding: 16px 18px;\n  }\n  .ikonera-app .dim-row-top {\n    display: flex; justify-content: space-between; align-items: baseline;\n    margin-bottom: 10px;\n  }\n  .ikonera-app .dim-row-top .dim-label { margin-bottom: 0; }\n  .ikonera-app .dim-row-top .dim-input-wrap { flex: 0 0 auto; max-width: 50%; }\n  .ikonera-app .dim-label {\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.2em;\n    color: var(--or-deep); margin-bottom: 8px; font-weight: 500;\n  }\n  .ikonera-app .dim-input-wrap { display: flex; align-items: baseline; gap: 6px; }\n  .ikonera-app .dim-input {\n    width: 100%; background: transparent; border: none;\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 26px; color: var(--text-dark); font-weight: 400;\n    outline: none; -webkit-appearance: none; appearance: none;\n    -moz-appearance: textfield; text-align: right;\n  }\n  .ikonera-app .dim-input::-webkit-outer-spin-button, .ikonera-app .dim-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }\n  .ikonera-app .dim-unit { font-size: 13px; color: var(--text-mid); font-weight: 500; }\n\n  .ikonera-app .dim-slider {\n    width: 100%; -webkit-appearance: none; appearance: none;\n    height: 4px;\n    background: linear-gradient(to right,\n      var(--or-deep) 0%, var(--or-deep) var(--pct, 0%),\n      rgba(31,22,17,0.12) var(--pct, 0%), rgba(31,22,17,0.12) 100%);\n    border-radius: 2px; outline: none;\n    cursor: pointer;\n  }\n  .ikonera-app .dim-slider::-webkit-slider-thumb {\n    -webkit-appearance: none; width: 18px; height: 18px;\n    background: var(--or); border-radius: 50%; cursor: pointer;\n    border: 2px solid var(--card-cream);\n    box-shadow: 0 2px 6px rgba(31,22,17,0.25);\n    transition: transform 0.15s;\n  }\n  .ikonera-app .dim-slider::-webkit-slider-thumb:hover { transform: scale(1.15); }\n  .ikonera-app .dim-slider::-moz-range-thumb {\n    width: 18px; height: 18px; background: var(--or); border-radius: 50%;\n    cursor: pointer; border: 2px solid var(--card-cream);\n    box-shadow: 0 2px 6px rgba(31,22,17,0.25);\n  }\n\n  .ikonera-app .fill-block { display: none; } \/* Remplac\u00e9 par les shape-types *\/\n\n  .ikonera-app .dim-summary {\n    background: linear-gradient(135deg, rgba(201,161,76,0.08), rgba(201,161,76,0.04));\n    border: 1px solid var(--or-line); border-radius: 10px;\n    padding: 16px 22px;\n    display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px;\n  }\n  @media (max-width: 600px) { .ikonera-app .dim-summary { grid-template-columns: repeat(2, 1fr); } }\n  .ikonera-app .dim-sum-item .dim-label { color: var(--or-deep); }\n  .ikonera-app .dim-sum-value {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 22px; color: var(--text-dark); font-weight: 500; margin-top: 4px;\n  }\n\n  \/* \u2550\u2550\u2550 \u00c9TAPE 4 \u2014 VISUEL 3D \u2550\u2550\u2550 *\/\n  .ikonera-app .viz-3d {\n    background: linear-gradient(135deg, var(--header-brown) 0%, var(--header-brown-2) 100%);\n    border-radius: 14px; padding: 40px 24px; min-height: 380px;\n    display: flex; align-items: center; justify-content: center;\n    position: relative; overflow: hidden;\n  }\n  .ikonera-app .viz-3d::before {\n    content: \"\"; position: absolute; inset: 0;\n    background: radial-gradient(ellipse at 30% 20%, rgba(201,161,76,0.12), transparent 60%),\n                radial-gradient(ellipse at 70% 80%, rgba(201,161,76,0.06), transparent 60%);\n    pointer-events: none;\n  }\n  .ikonera-app .stage { perspective: 1200px; width: 100%; max-width: 320px; height: 280px; position: relative; }\n  .ikonera-app .cube {\n    position: absolute; top: 50%; left: 50%;\n    transform-style: preserve-3d;\n    transform: translate(-50%, -50%) rotateX(-22deg) rotateY(35deg);\n    animation: cube-spin 18s linear infinite;\n  }\n  @keyframes cube-spin {\n    from { transform: translate(-50%, -50%) rotateX(-22deg) rotateY(0deg); }\n    to   { transform: translate(-50%, -50%) rotateX(-22deg) rotateY(360deg); }\n  }\n  .ikonera-app .face {\n    position: absolute;\n    background: linear-gradient(135deg, var(--c1, #E8DCC8), var(--c2, #B8A878));\n    border: 1px solid rgba(201,161,76,0.4);\n    box-shadow: inset 0 0 30px rgba(255,255,255,0.08), inset 0 0 0 1px rgba(255,255,255,0.05);\n    opacity: 0.92;\n  }\n  .ikonera-app .viz-info-overlay {\n    position: absolute; top: 16px; left: 20px;\n    color: rgba(232,220,200,0.85);\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.22em;\n    z-index: 5;\n  }\n  .ikonera-app .viz-info-overlay strong { color: var(--or-soft); font-weight: 500; }\n\n  \/* \u2500\u2500\u2500 Upload 3D zone \u2500\u2500\u2500 *\/\n  .ikonera-app .upload-zone {\n    margin-bottom: 18px;\n    background: var(--card-cream);\n    border: 2px dashed var(--or-line);\n    border-radius: 12px;\n    padding: 22px;\n    text-align: center;\n    transition: all 0.2s;\n    cursor: pointer;\n    position: relative;\n  }\n  .ikonera-app .upload-zone:hover, .ikonera-app .upload-zone.dragover {\n    border-color: var(--or);\n    background: var(--card-cream-2);\n    transform: translateY(-1px);\n  }\n  .ikonera-app .upload-zone.has-file {\n    border-style: solid;\n    border-color: var(--or);\n    background: linear-gradient(135deg, rgba(201,161,76,0.10), rgba(201,161,76,0.04));\n    text-align: left;\n  }\n  .ikonera-app .upload-icon {\n    width: 44px; height: 44px; margin: 0 auto 10px;\n    color: var(--or-deep);\n  }\n  .ikonera-app .upload-icon svg { width: 100%; height: 100%; }\n  .ikonera-app .upload-title {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 18px; color: var(--text-dark);\n    font-weight: 500; margin-bottom: 4px;\n  }\n  .ikonera-app .upload-hint { font-size: 12px; color: var(--text-mid); line-height: 1.5; }\n  .ikonera-app .upload-formats { display: inline-block; margin-top: 6px; padding: 4px 10px;\n    background: rgba(201,161,76,0.15); border-radius: 100px;\n    font-size: 10px; letter-spacing: 0.2em; text-transform: uppercase;\n    color: var(--or-deep); font-weight: 500;\n  }\n  .ikonera-app .upload-input { display: none; }\n\n  .ikonera-app .file-info {\n    display: flex; align-items: center; gap: 14px;\n    flex-wrap: wrap;\n  }\n  .ikonera-app .file-info-icon {\n    width: 40px; height: 40px; flex-shrink: 0;\n    background: rgba(201,161,76,0.18); border-radius: 8px;\n    display: flex; align-items: center; justify-content: center;\n    color: var(--or-deep);\n  }\n  .ikonera-app .file-info-icon svg { width: 22px; height: 22px; }\n  .ikonera-app .file-info-meta { flex: 1; min-width: 0; }\n  .ikonera-app .file-info-name {\n    font-size: 14px; color: var(--text-dark); font-weight: 500;\n    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;\n  }\n  .ikonera-app .file-info-stats { font-size: 11px; color: var(--text-mid); margin-top: 2px; }\n  .ikonera-app .file-info-remove {\n    background: transparent; border: 1px solid var(--border-mid);\n    color: var(--text-dark); padding: 6px 12px;\n    border-radius: 6px; font-size: 11px;\n    text-transform: uppercase; letter-spacing: 0.15em;\n    cursor: pointer; transition: all 0.2s;\n    font-family: inherit;\n  }\n  .ikonera-app .file-info-remove:hover { border-color: var(--text-dark); background: rgba(31,22,17,0.04); }\n\n  .ikonera-app .upload-progress {\n    margin-top: 10px;\n    width: 100%; height: 4px;\n    background: rgba(201,161,76,0.15);\n    border-radius: 2px; overflow: hidden;\n    display: none;\n  }\n  .ikonera-app .upload-progress.show { display: block; }\n  .ikonera-app .upload-progress-bar {\n    height: 100%; background: var(--or);\n    width: 0%; transition: width 0.3s;\n  }\n\n  \/* \u2500\u2500\u2500 Canvas Three.js \u2500\u2500\u2500 *\/\n  .ikonera-app #viz-canvas {\n    width: 100%;\n    max-width: 100%;\n    height: 100%;\n    display: block;\n    background: transparent;\n  }\n  .ikonera-app .viz-canvas-wrap {\n    position: relative;\n    width: 100%;\n    height: 280px;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n  }\n\n  .ikonera-app .viz-mode-tag {\n    position: absolute;\n    top: 16px; right: 20px;\n    padding: 5px 11px;\n    background: rgba(201,161,76,0.18);\n    border: 1px solid var(--or-line);\n    border-radius: 100px;\n    font-size: 9px; text-transform: uppercase; letter-spacing: 0.22em;\n    color: var(--or-soft);\n    font-weight: 500;\n    z-index: 5;\n  }\n\n  .ikonera-app .viz-stats {\n    margin-top: 22px; display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px;\n  }\n  @media (max-width: 600px) { .ikonera-app .viz-stats { grid-template-columns: repeat(2, 1fr); } }\n  .ikonera-app .viz-stat {\n    background: var(--card-cream); border: 1px solid var(--border-soft);\n    border-radius: 10px; padding: 14px 16px; text-align: center;\n  }\n  .ikonera-app .viz-stat-label {\n    font-size: 10px; text-transform: uppercase; letter-spacing: 0.2em;\n    color: var(--or-deep); margin-bottom: 6px; font-weight: 500;\n  }\n  .ikonera-app .viz-stat-value {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: 22px; color: var(--text-dark); font-weight: 500;\n  }\n  .ikonera-app .viz-stat-value small { font-size: 13px; color: var(--text-mid); }\n\n  \/* \u2550\u2550\u2550 \u00c9TAPE 5 \u2014 PRIX \u2550\u2550\u2550 *\/\n  .ikonera-app .price-hero {\n    background: linear-gradient(135deg, var(--header-brown) 0%, var(--header-brown-2) 100%);\n    border-radius: 14px; padding: 40px 36px; text-align: center;\n    color: var(--card-cream); position: relative; overflow: hidden;\n  }\n  .ikonera-app .price-hero::before {\n    content: \"\"; position: absolute; top: 0; left: 0; right: 0; height: 1px;\n    background: linear-gradient(90deg, transparent, var(--or), transparent);\n  }\n  .ikonera-app .price-eyebrow {\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.32em;\n    color: var(--or-soft); margin-bottom: 10px; font-weight: 500;\n  }\n  .ikonera-app .price-amount {\n    font-family: 'Cormorant Garamond', Georgia, serif;\n    font-size: clamp(46px, 9vw, 72px); font-weight: 300;\n    line-height: 1; margin-bottom: 6px; letter-spacing: -0.01em;\n  }\n  .ikonera-app .price-amount .price-cur {\n    font-size: 0.4em; color: var(--or-soft); margin-left: 8px;\n    letter-spacing: 0.1em; vertical-align: top;\n    position: relative; top: 0.45em;\n  }\n  .ikonera-app .price-range { font-size: 13px; color: rgba(232,220,200,0.7); }\n\n  .ikonera-app .recap-grid {\n    margin-top: 24px; display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px;\n    background: var(--card-cream); padding: 22px; border-radius: 10px;\n  }\n  @media (max-width: 600px) { .ikonera-app .recap-grid { grid-template-columns: 1fr; } }\n  .ikonera-app .recap-row {\n    display: flex; justify-content: space-between; padding: 8px 0;\n    font-size: 14px; border-bottom: 1px solid var(--border-soft);\n    color: var(--text-mid);\n  }\n  .ikonera-app .recap-row:last-child { border-bottom: none; }\n  .ikonera-app .recap-row b { color: var(--text-dark); font-weight: 500; }\n\n  .ikonera-app .breakdown {\n    margin-top: 22px; background: var(--card-cream); padding: 22px;\n    border-radius: 10px; border: 1px solid var(--border-soft);\n  }\n  .ikonera-app .breakdown-title {\n    font-size: 11px; text-transform: uppercase; letter-spacing: 0.22em;\n    color: var(--or-deep); margin-bottom: 12px; font-weight: 500;\n  }\n  .ikonera-app .breakdown-row {\n    display: flex; justify-content: space-between; padding: 9px 0;\n    font-size: 14px; color: var(--text-mid);\n    border-bottom: 1px solid var(--border-soft);\n  }\n  .ikonera-app .breakdown-row:last-child { border-bottom: none; }\n  .ikonera-app .breakdown-row.total {\n    color: var(--or-deep); font-weight: 600;\n    border-top: 1px solid var(--or-line); border-bottom: none;\n    padding-top: 12px; margin-top: 6px;\n  }\n\n  .ikonera-app .cta-final { margin-top: 24px; text-align: center; }\n  .ikonera-app .btn-cta {\n    display: inline-block; padding: 16px 36px;\n    background: var(--or); color: var(--header-brown);\n    border: none; border-radius: 8px;\n    font-family: inherit; font-size: 12px; font-weight: 600;\n    text-transform: uppercase; letter-spacing: 0.22em;\n    cursor: pointer; transition: all 0.2s; text-decoration: none;\n  }\n  .ikonera-app .btn-cta:hover {\n    background: var(--or-soft); transform: translateY(-1px);\n    box-shadow: 0 10px 28px rgba(201,161,76,0.35);\n  }\n  .ikonera-app .cta-hint { margin-top: 14px; font-size: 12px; color: var(--text-soft); line-height: 1.6; }\n\n  .ikonera-app .monumental-msg {\n    margin-top: 18px; padding: 14px 18px;\n    background: rgba(160,119,46,0.08);\n    border-left: 3px solid var(--or-deep);\n    border-radius: 0 8px 8px 0;\n    font-size: 13px; color: var(--text-dark); line-height: 1.55;\n    display: none;\n  }\n  .ikonera-app .monumental-msg.show { display: block; }\n  .ikonera-app .monumental-msg b { color: var(--or-deep); }\n\n  \/* \u2550\u2550\u2550 FOOTER NAV \u2550\u2550\u2550 *\/\n  .ikonera-app .calc-footer {\n    border-top: 1px solid var(--border-soft);\n    padding: 18px 36px;\n    display: flex; justify-content: space-between; align-items: center;\n    gap: 12px; flex-wrap: wrap;\n  }\n  .ikonera-app .footer-status {\n    font-size: 13px; color: var(--text-mid);\n    display: flex; align-items: center; gap: 8px;\n  }\n  .ikonera-app .footer-status .indicator {\n    width: 8px; height: 8px; border-radius: 50%; background: rgba(31,22,17,0.20);\n  }\n  .ikonera-app .footer-status.ok .indicator { background: var(--or-deep); }\n  .ikonera-app .nav-actions { display: flex; gap: 10px; }\n  .ikonera-app .btn-nav {\n    padding: 12px 24px; border-radius: 6px;\n    font-family: inherit; font-size: 12px; font-weight: 500;\n    text-transform: uppercase; letter-spacing: 0.18em;\n    cursor: pointer; transition: all 0.2s;\n    border: 1px solid var(--border-mid);\n    background: transparent; color: var(--text-dark);\n  }\n  .ikonera-app .btn-nav:hover { border-color: var(--text-dark); }\n  .ikonera-app .btn-nav.primary {\n    background: var(--header-brown); color: var(--card-cream);\n    border-color: var(--header-brown);\n  }\n  .ikonera-app .btn-nav.primary:hover { background: var(--header-brown-2); transform: translateX(2px); }\n  .ikonera-app .btn-nav:disabled { opacity: 0.4; cursor: not-allowed; transform: none !important; }\n\n  .ikonera-app .calc-bottom {\n    padding: 14px 36px; border-top: 1px solid var(--border-soft);\n    text-align: right; font-size: 11px; color: var(--text-soft); letter-spacing: 0.05em;\n  }\n  .ikonera-app .calc-bottom b { color: var(--text-mid); font-weight: 600; letter-spacing: 0.1em; }\n\n  @media (max-width: 700px) {\n    .ikonera-app { padding: 16px 8px; }\n    .ikonera-app .calc-header { padding: 22px 22px 24px; }\n    .ikonera-app .panels { padding: 24px 22px 20px; }\n    .ikonera-app .calc-footer { padding: 16px 22px; flex-direction: column; align-items: stretch; }\n    .ikonera-app .nav-actions { justify-content: space-between; }\n    .ikonera-app .calc-bottom { padding: 12px 22px; }\n  }\n\n\n@media (max-width: 700px) {\n  .ikonera-app .calc-wrap { padding: 16px 10px; }\n  .ikonera-app .calc-header { padding: 18px 18px 22px; }\n  .ikonera-app .panels { padding: 22px 16px 18px; }\n  .ikonera-app .panel-head h2 { font-size: 22px; }\n  .ikonera-app .calc-footer { padding: 14px 16px; flex-direction: column; align-items: stretch; }\n  .ikonera-app .nav-actions { justify-content: space-between; width: 100%; }\n  .ikonera-app .stepper { display: flex; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }\n  .ikonera-app .stepper::-webkit-scrollbar { display: none; }\n  .ikonera-app .step { min-width: 130px; flex-shrink: 0; padding: 12px; }\n  .ikonera-app .marbles { grid-template-columns: repeat(2, 1fr); gap: 8px; }\n  .ikonera-app .sphere { width: 48px; height: 48px; }\n  .ikonera-app .finish-row, .ikonera-app .finish-row.h4 { grid-template-columns: repeat(2, 1fr); gap: 8px; }\n  .ikonera-app .shape-types { grid-template-columns: repeat(2, 1fr); gap: 8px; }\n  .ikonera-app .dim-grid { grid-template-columns: 1fr; gap: 10px; }\n  .ikonera-app .dim-input { font-size: 22px; }\n  .ikonera-app .dim-summary { grid-template-columns: repeat(3, 1fr); gap: 10px; padding: 14px; }\n  .ikonera-app .dim-sum-value { font-size: 18px; }\n  .ikonera-app .viz-3d { padding: 24px 14px; min-height: 320px; }\n  .ikonera-app .stage { max-width: 240px; height: 220px; }\n  .ikonera-app .viz-stats { grid-template-columns: repeat(2, 1fr); gap: 8px; }\n  .ikonera-app .viz-stat-value { font-size: 18px; }\n  .ikonera-app .price-hero { padding: 30px 22px; }\n  .ikonera-app .recap-grid { grid-template-columns: 1fr; padding: 18px; }\n  .ikonera-app .breakdown { padding: 18px; }\n}\n@media (max-width: 500px) {\n  .ikonera-app .calc { border-radius: 8px; }\n  .ikonera-app .calc-header-top { gap: 8px; flex-direction: column-reverse; align-items: flex-start; }\n  .ikonera-app .panel-head h2 { font-size: 20px; }\n  .ikonera-app .marbles { grid-template-columns: repeat(2, 1fr); gap: 6px; }\n  .ikonera-app .marble-card { padding: 10px 6px 8px; }\n  .ikonera-app .sphere { width: 42px; height: 42px; margin-bottom: 6px; }\n  .ikonera-app .finish-row, .ikonera-app .finish-row.h4 { grid-template-columns: 1fr; }\n  .ikonera-app .dim-summary { grid-template-columns: 1fr 1fr; }\n  .ikonera-app .stage { max-width: 200px; height: 180px; }\n  .ikonera-app .price-amount { font-size: 50px; }\n  .ikonera-app .btn-cta { padding: 14px 22px; font-size: 11px; letter-spacing: 0.18em; }\n  .ikonera-app .btn-nav { padding: 10px 16px; font-size: 11px; letter-spacing: 0.14em; }\n  .ikonera-app .login-overlay { padding: 30px 14px; }\n  .ikonera-app .login-card { padding: 32px 22px 26px; }\n  .ikonera-app .login-brand-logo { width: 180px; }\n  .ikonera-app .login-tagline { font-size: 12px; }\n  .ikonera-app .login-title { font-size: 16px; letter-spacing: 0.14em; }\n  .ikonera-app .request-list li { font-size: 12.5px; }\n  .ikonera-app .cta-contact { padding: 14px 12px; font-size: 12px; letter-spacing: 0.18em; }\n  .ikonera-app .secure-header { padding: 14px 14px 0; }\n  .ikonera-app .secure-header h1 { font-size: 15px; letter-spacing: 0.12em; }\n  .ikonera-app .logout-btn { padding: 6px 12px; font-size: 10px; letter-spacing: 0.14em; }\n}\n@media (max-width: 380px) {\n  .ikonera-app .login-brand-logo { width: 150px; }\n  .ikonera-app .login-card { padding: 28px 18px 22px; }\n  .ikonera-app .login-title { font-size: 14px; }\n  .ikonera-app .price-amount { font-size: 42px; }\n  .ikonera-app .panel-head h2 { font-size: 18px; }\n  .ikonera-app .step-info-title { font-size: 11px; }\n  .ikonera-app .step-info-sub { font-size: 9px; }\n  .ikonera-app .step-num { width: 24px; height: 24px; font-size: 11px; }\n}\n\n<\/style>\n\n<div class=\"ikonera-app\">\n<!-- \u2550\u2550\u2550 HEADER (visible apr\u00e8s login) \u2550\u2550\u2550 -->\n<div class=\"secure-header\">\n  <h1 data-i18n=\"header.title\"><\/h1>\n  <div style=\"display: flex; align-items: center; gap: 12px; flex-wrap: wrap;\">\n    <div class=\"lang-switcher\" style=\"position: static;\">\n      <button class=\"lang-btn\" type=\"button\" data-lang=\"fr\">FR<\/button>\n      <button class=\"lang-btn\" type=\"button\" data-lang=\"en\">EN<\/button>\n      <button class=\"lang-btn\" type=\"button\" data-lang=\"de\">DE<\/button>\n      <button class=\"lang-btn\" type=\"button\" data-lang=\"it\">IT<\/button>\n    <\/div>\n    <button class=\"logout-btn\" type=\"button\" id=\"logout-btn\" data-i18n=\"header.logout\"><\/button>\n  <\/div>\n<\/div>\n\n<!-- \u2550\u2550\u2550 LOGIN OVERLAY \u2550\u2550\u2550 -->\n<div class=\"login-overlay\" id=\"login-overlay\">\n\n  <!-- S\u00e9lecteur de langue (en haut \u00e0 droite de l'overlay) -->\n  <div class=\"lang-switcher\">\n    <button class=\"lang-btn\" type=\"button\" data-lang=\"fr\">FR<\/button>\n    <button class=\"lang-btn\" type=\"button\" data-lang=\"en\">EN<\/button>\n    <button class=\"lang-btn\" type=\"button\" data-lang=\"de\">DE<\/button>\n    <button class=\"lang-btn\" type=\"button\" data-lang=\"it\">IT<\/button>\n  <\/div>\n\n  <div class=\"login-card\">\n\n    <!-- Logo IKONERA -->\n    <img decoding=\"async\" src=\"data:image\/png;base64,iVBORw0KGgoAAAANSUhEUgAAA7EAAAC3CAYAAADEktnZAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyZpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw\/eHBhY2tldCBiZWdpbj0i77u\/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDkuMS1jMDAyIDc5LmI3YzY0Y2NmOSwgMjAyNC8wNy8xNi0xMjozOTowNCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OEI1RkVFMDExRDU4MTFGMUFGMTlEM0U1OEExNzEyNzAiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OEI1RkVFMDAxRDU4MTFGMUFGMTlEM0U1OEExNzEyNzAiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDI2LjAgKFdpbmRvd3MpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NUYwRDFCNjUxOTQ2MTFGMTkyNDlCNTM4MEY2NTgyNDUiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NUYwRDFCNjYxOTQ2MTFGMTkyNDlCNTM4MEY2NTgyNDUiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw\/eHBhY2tldCBlbmQ9InIiPz6MLcxCAAAkCklEQVR42uyd\/VUcudKHte\/Z\/y8bwR1HcHEEHiJYiICZCIAIYCIAIph2BLAR0I7AOAL3RmA2Ar9doPFizMdIrdLn85zTB6\/X3cNU66N+qlLpt+\/fvxsAAAAAAACAEvg\/TAAAAAAAAACIWAAAAAAAAABELAAAAAAAACBiAQAAAAAAABCxAAAAAAAAAIhYAAAAAAAAQMQCAAAAAAAAIGIBAAAAAAAAELEAAAAAAACAiAUAAAAAAABAxAIAAAAAAAAgYgEAAAAAAAARCwAAAAAAAICIBQAAAAAAAEDEAgAAAAAAACIWAAAAAAAAABELAAAAAAAAgIgFAAAAAAAARCwAAAAAAAAAIhYAAAAAAADgRX5v+Lt\/Ha9ZxM\/rf\/vtt70cvvj3799vxh\/zwI9djd\/vrKUGNNpxZ\/zx2bEdZdMOCrT3zNpa2u5\/xmvX\/i\/5ubPFIwZ7CZ8270P+bnwnAxaOOna8w+a\/2FbGz9PC5pJtf2fYkvHd\/qb0rr5jXcaexPP3V8WP6Mb3usTSL9ofEVsZq\/Fa07RhAmtHAXs3XgeYbesFgrkVqB8chOprzB69r40QO300wIugvbUC9xZH58Vxcx6o77CYAwCxOWcejo\/Mp+M8241\/XCh9xGJ8\/op5uy3+r+EOJZ2Jxg6+Iut4\/LHveNtybHd3WO9Fm+5KZGe8JLr9bbyurMicBxCw2yCfc2w\/9+v4e8h1Pl77vJ0f42Zvxf5kW2NXAEjA\/jj2zDFDElbKzycrBBFLhwJ4S2yZh9VcFy5GAXCN9Z4VriIUJc3os52EdjP59WYbUTv+ft\/GS34ueGvBxs1zG3EHAIgJYicBNkraKX7EwqYtAyK2iQ4lnWmgGYCD6BKn2zUNXVJTT7DevzaUSLaNuH62QjH3iUfeu0QO11bQrltdzQ8Yjd0sEgAAxGRuF6MhPkRjARFbUIeCupAIrMvkxz7Yf8WrRF1lAeCbhx1zE7SL8bqxKcfHDUYUQ42bp6ycA0ACjjBBfIjGAiI2bIeSzjTQFGALEbYw7kUJTlovNCARS1vV9rPRK+qQipkV5F9tdLaJyTNgNFagwB4AxGbBdoZkEI0FRGxBHQrKF2IbseJCZxdJWhevGkc65cYmOtuSmA01blLkCQCSCFlMEB+isYCIDduhpDMNWAJe4cq4VciVo1qa3Adr04ZbEa8vOUbVi9nA0ViKPAFAbA4xQTK0g0ekiyNim+ISE8ALosxn\/2Zzx+nYgk2SGvq5UfH6kpg9q1ighTpcXsQ+RZ4AICa7FHhKQ6RoLAujiNhmkM7EGZ7wVJjtezjXsg\/2tjE7iY2+GtKznuPUitnqbBPYEaHIEwir3xqmsP4\/5Xv+MV575mEh7MI8ZC+lYE6XS8ZHxWfvGBZGEbGtYKNmRGPhsTDzOU7nemxLFw3ZaJM6fG7c0q1b474tia0qFGoh08Io8gTQiM8lWxJkO5ccQTde762wXUYWtKQUp2sDvQm3JeU5jojGImJbQsQH0VjY4LoPdjDh0itLELCyyhkzdfjOTnivXbn3X7HVfYpxRY6ItPsulH0o8gTQtLDtrKDdUxY4G3YROknR3BtLNLZyfscEPw+g42Am0VjKczeOFRmu4qyJfbB2wr9SFK+39vpifw6uxxTZaOfM\/o7\/Mw97mmcZmVFSZ\/8cfx5UcgSTOCKLQM+SIk99a3vKAeAnf0wEbG8XtdZGN9NH5olrrJ7mPct4r+hPSDT2gvmkTojE\/grRWASsDKauCxkrO+m2YJuvgScc6W+dCLrx+kNW4cdLFgQubLqZs8iTe+y9Z+MlQvHd+NdyLU0++99FWH+uYa9s4GisLDaweg4AMraIuJTIrGaKMcWd0kI0FhCxgQbMO6NbMQ3yFmk++2DvxVIDtpHveGPCrIhv+pkI1j+saL3WXC21wrazn\/WHFc2pBe1mr+y6gpS2kEU6KPIEAI\/H7veKvtkHrJz0\/faGvbGAiA3GP5igWUTAujjPd1YMVS3sx0vSh0Ok2ctqukRD31kxmayKsxXNG0G7NHH2X73EQhYIOO7hl74IALAZs5dKQnaGdZNDNBYQsQATxJoMcq5FZareB2ujYTcednmKCMQ9myrc5WYz+ztJIRHN1f632LVCdk5vvIciTwAQQ8giYtO\/194QjQVELICXWBMB4RppvLD7dWq2yWczbb\/QRrzulbBnWCLD1kl6l0jM7lghu6BX3nOO4wEATzgxgffIsniYBURjAREL4Dh5bfbBujjLInZOKraJTOhT9r8O5qHy7l6JBa\/sHqyNmE2xUCF7ZM\/pnRR5AoBfxufNNh6KcNb1XsVX0NxiRDQWEQtQHSIWXKKNVe+DtVHAKQJWKny\/ryFKbcWsvOs9K8xjciwFn+ieFHkCgF\/HZhM2ckc9gjy4VHw20VhELEBVgk323C0cbzup5GzPlwSsr3AarHg9qW2fsD2u553RTXd6jgVC9h5sAABPx+ULE25xkQhdHu+0M7oLxkRjEbEAVQi2mYdz3NlBFgH7xC5WwN7W3GbsUUrvTdyoLEKWIk8A8DwrTMA7dYBoLCIWoAqujOM+WPNQUAIB+zNLe1RNE\/uTrFCPXcUYIUuRJwD4dTyWcXjAErxTBw6xMiIWoGTR5roPdiPW7iq0xb6ngBVbvK81Mv3GJHtnCz\/FXNRoXcjODCvoAPArIeov\/BczVvdOX5xLOAEAEQtQqmibezjDJzWmytpjdHyEkdhir\/b04S3ErOzJ2jPxqmS2LmQp8gQATwlRDIhxJS+0t4+cYmJELEBpok3SEa8cb7u2YqVGAetThRgB+7OQ7RMI2UXDJqfIEwA8HoMHMz39lPksH99E5reZ8scQjUXEAhSH6z5YmRiXlYp517NxHwtYzuf72Ym6tUI2liO0brjQEUWeAOAp\/cT7\/8GE2XBa2ecAIhZgsnA7EwfY8bZaCxaJmHfdE4yAzU\/Itnq2IUWeAOAxnzBBFX7awsRL7SYai4gFKGJgFGffddVtZVNFa7PFuYeYv0PAbiVk7yIK2R0rZFsUc+LkUOQJADYMmKAKTiv\/PEDEAjiJNp99sL09E7Q2W+x7OP8IWD8hG8NevoW5qnB2KPIEAHbc7Sc+gvktvX8yN\/ELbBGNRcQCZM3acWCUyeygwgli5il4lhRxylrI7jc8CVPkCQBCwByXntPGPhcQsQCvCjeJOroWgal1H6xPISdJqb6mJXkJ2duIQva80agkRZ4AIAREYtP6anPjvs0pFERjEbEA2Q2KPvtgL2oUbZ5FrapMqU4gZE8ifNSm2nSLUOQJACYJUbKNknPa+OcDIhbgh2jzOULmdpzITiq0hY+YF2dgSUsKImS78ccqwkfNbeZBa8wMRZ4AwD3T6Mfcj+mS+yjz1PMI0VhELEAuSAVel+NHqtwHa\/GJ0K3sAfIQRsiejT9iRPhbLXZEkScA8AURm5Yjh3+r6ZcQjUXEAqTF7pFbON52UqNos2nErmeJShrxBS0pOMsIzlLLacUUeQIAH75ggmQ+yszRX9tTFLJEYxGxAMkHRFdntrMpnzXa4sjjVtKIFbDFwsS22gVE5o1OxBR5Amh37t+dcHuPBZPhEv3sbLBhlcnvA4hYgKDIebBO+2BNnMI7KTg3ftWIB5qRmpCN1d5OMy521Bu9iPSaIk8ATTLzvG+gqFMaPKKwKzuPdkY3Gjvn7SBiAWIPiK77YIUqj9Oxg7BrVErsQBqxvpCVCVh7f6w4BzkXO9IS8iJgWUkHaA\/fSGyP6ZLhkinWPVlgJxoLiFioSrS5Ou0nFa\/A+uwPvKz0fNwckbTiQdtByDUqObYzcRw7pccfT0wtBIDy+OB531+YLonPJnPTwuGWj0\/mkE5xDp0TjUXEAsQcDK8cb7uutXiR3Q85c7xt4EzYqCIuxhFG0i9yj8ZqLZqc08oAmvIBfETHXY3nwhfCsdl+u1NvFz6fQjQWEQtQPK77YAdTd\/Ein8H3I80oupCVSVl7ISXnaOydohMyp8okQDP4FnTrMF187Jzkkkq8emEO6QzRWEQsQMGD4ZlxX4Fd1po26xmFZS9sOlZGN60462iszYbQSuk\/p8gTQBMcet7H4m0aQkRhY7xDorGIWAA1wbbrMcis3hgQS8dn0O3YC5tMxMVIKz7K3AwUeQIAXz9gZvxSiXuqEifDZdHhLZEqC6Fa\/gvRWEQsgMrE5bMPtq9536dnFFa4pEUlFbK90U1r28k5tZYiTwAwAd+FqhWmy95PGWzK8Gvzx52yD8NCKCIWIDhrR8EmA90Bk\/kv3HIubBZoFjkSDhv+\/hR5AqhTEIkPsPC4dag8I6sWP2XbhQaisYhYgGImLtlP4VrIYVlzyuxok33jF4VlT1AGKBc52kzEs0a\/P0WeAOpk7XnfCaZL4qcsTMAo7JP5g2gsIhYg+0HQZx\/sRQNl9H0jbRwvkI+Qk9XkQfEj9gv4\/hR5AoBtfAEZz+Yet\/Ycq5MMjSjsDz\/PEI1FxAJkPGmJEyorry7OqKTLnlRul5mnQCGVOD802+phw9+fIk8Adc15RGHLemciAmdb\/nMRo04LDURjEbEAuSN721yKtLSwD1bwrT7b06TywkYItN7Lbs4pxfb7y3fvlB5PkSeA8sXQpqijT2bFBRWJk+EiAi89t38RjUXEAmQ5cUmkceF420kjkUbfNNG\/aFlZssqwrcSEIk8A8Fof9lmMGgwViVP5byL+thWA3ufWE41FxALkOADOjHvqULdtUYAKxP3M83ZWpDPERiN7pcd\/KOD7U+QJAJ6b78QP8O2\/S85DT0aMKOwG7Wgs2TyIWAAnXFOHRJy1su\/lT18By4SeNVqrySVEYinyBACPxevORAG74kidZO9ORN\/c4ZZu4tyhHY094q0iYgG2HQDPjHvqUEsrrr6ihChs3iJO9sYOSn1qXogZKPIEgA8wG3\/cTBCw1+N4eoYlk+Ei+rpAW8A6xe+zyL22BCIWII\/Ja+7hbJ60UrjB2sc3ovQ3LSx7tFJqi0iHosgTQPM+gCzSfp4wZokvsMSSyd6fiL1F7DnPCmFNIcsiKCIW4NXBb1OB0IVrm4bYCn9OuJdIbP5INFYjo+BDQTagyBNAg\/P\/eF0Z\/yrEggiZPbbNJMVF7HWBC3FqFvEiGouIBXgV18lLBr\/WVlyn7G9kYs8c63xdKzx6VpgNKPIE0I54PRv\/+DXA\/HaAgE36LmWecRlfg+5jJRqLiAVINfgdG7dCAEJTlQftBDFFjBCJLQONAhVFpdFS5Amg+vls3xZu+mbFwZQ+KeJlj\/Ngk+OyF7ZXel9EYxGxAFEnM3GwXdP8Wqw8OJ9yMyvUxQi4W6NQ4KnA\/aAUeQKoY46fST0HibhKyvB4iXCVzKtFgMfLWPkeAZv8He+YBHthn5k\/pT10il+VuQMRC\/DTwOe6D7ZvtPLgB1pMM2ikFBcVfaTIE0Cx8\/qNve4xD6nCN1YA7Acei2amoO0SFXPs8F575SAE0VhELEAU1o4T0P2+l0ZtNcXpJgpbFh8Vnjkv0A4UeQIoj3nk8eaGRal02GCESyqxpsgkGouIBYgy8C2MeyGHZcNpsVMmaVKtCkIrpbhAO1DkCQDeYgchm5SF2T4KO0TaCkY0FhELoCZgffbBXoyD33Wj9prTapoj9ERfZDo6RZ4AACGbNdlEYR\/NG4MhGouIBVAQZDLZrI3bvpjbcVA6adhsTMzt8Rcm+AFFngBgm\/68ZmEqqj+3MNtvCZMobBfx1yMai4gFCM6poyhreR\/shv\/SbJqjxwQPUOQJoChCZU4MHvdIX75ByEb153IQlc\/NG4PyPMoCKCIWWkLOhjMPVexcOLGDUctMdbLntL7ihNudCZtGW7pQo8gTQBlM7aciPOTc13eeIkTGujWvQd2fW5jto7B3kaOwMYQz0VhELDQ04M08JpYu0cCXGwyUbdIHfFbRkQmKPAFUzTBesv\/93djX9x4V\/1l6Pm9\/7NMIWV1c9sJeJpo3eqMbjT2iGcTld0wAibhydKQlCnWC2RCxDfMFE\/zkkFyMjumh0YkqS5Gn64arn6fkcLR9SYXHTmwFcZjGrRUYH1+yp2RhjW1DFq98UjclUvaJhfDw2GKT247DMqZeJPx1pf3MlZ4tbWzFvIGIhboHvDMPx3PJwPCjEFaI58xIyy7SyYMnAmK8bhSeuynyxMJZfGamrIU69lu601sx88X++dZhfhcBdOjZRqTQU6xjXVrCZVHhMqUvJ+9+bAO9kpCVsUC2yJ3RJOJAOjHEFmFz476Kykr3v4SKOs0wZVnQB553SIzeqj5FngCmIwtBkhL8GPnvg\/E6kz7sImrsv52yuHTF3sXgPt22gjB1FHaD5t7YIwqJIWKhzsFOOvaV423X9mxICAuTeJn0mOBZh4QiTwAZIotvoSOf9ox432fuWCGL0AiDS1Aiiy0ayntjN9FYQMRCZbjugxXmREMQsfCDARP84pBMjcy8Nf4ssDJAdiwn3Cs+BQtUE7ER7bnDLauMfn2isYhYgK0Hu2PjtweBA8t\/nXxD8AFTFsnfmOBZIdsZvZX1c8YfgOz6\/GCmpaYuWKCajEsUtsupDgfRWEQswLYCduqqJweW\/zw4hmCGKYtkyOw5OXGi2Oc4yB4gP6ZuJTgn08vbrxMfYuH4rnJsP1oQjUXEQgUDnc8+WISsPjNsiYitCVv4iiJPAI0Q4LxoMr38cd0LO2TYfnpDNBYRC\/AKcsD4LNCzELJhwSkvD86fex2KPAG0JWRl4WqKQNo1ZFo44RGFvcz463xUfDbRWEQsFDzQySC3ryC8ELJhmGOC4hy2UMfs3FVqH4o8AbTH1LRQybTYx4xb4zIO9jmfy2vrKQxKjycaq8zvmACUBKxm9b+NkN3LoVx7ZEJ+X4o7tcuXioV+N44Nh0ZnkUb20F03OO7ERJzKjwX9vpzfXEefl7Tinr79pm8nwuwo4gJDDOR3XCs9W6KxF7QrRCyUNcjJgKAZLRUhK3tt9xozb0iHaU5rbZbaJ1SJxn5WeO6myNMJTUiNv3OO3EDWQmQ+sW+L33KAKV\/l2MG3uy2hL9tFEBnXZ0pzhtjsjKYTHtKJQYNTE2e\/paT3rTG3P6RQNUvV0SOKPAG0RaAiPfvMia\/6C65R2MuCvh6VihGxwCB3PwHE3AOwQMhOgpTiNhka+I4UeQJoixBChGrFr\/hbZvso7GD3mxYBe2MRsQAb5zh2qmJLQjZ0BI1V5wbJ8bgDhe9IkSeAtsa13kyPxm7SiuFXatsLG\/N3PqT5IGIh\/0lERNYeQlbVMQ\/JjNTI5uhb+aJ2dV3r+54TsQGoUohIWvEcU\/6LXbSbbfnPi4rCPpkvBqXHz1j4RMQCQrZ5IaswyLJC2BatVVPVisZuijwBQD7+R2\/CLFyRVvwzLmPdZcHfc5WJDQERC40K2dr3q4UWsaQUt8WXlr4sRZ4AmiOEEJkZ9jHe4xiFFZ+vK3i+6AzRWEQsQEIhe1z5QBE6kjYjdaoYZ2I3w\/ZTilNLkSeANnyPPpAQOR3H3BkWdatIXMGZqERjEbEASYXsumIh+7fCM0kpLoOp6W13tk+2Ng5R5AmgLUIJkaaLPNkF7m0XT2WcvSj9O9torJbPSjQWEQsFCtkUB4jXKmQ1RMiC\/T9FMJt4f9\/wONQZijwBtNTfhwCPmjeeqeQSOewqiMJuuMzEpoCIhQwmFHEelwjZYLbUgP0\/9YvYT43bjyJPAO3wMZQf0aLxrHh3EfCXFX19iSgTjUXEAvwQXx1CNhga0VhSivPnvxPv7xsfgyjyBNAOoYRIq6LDNQo7VDRX3BmisYhYAISsChpihNXB\/JkikoYW98M+A0WeANrwN6SfX4fq2y1tGbAFreaO42ptEI1FxAJkJWRriZRopYWyOliviL3GfBR5AmiMUOJKBGxLW26ajcI+mSuIxiJiAbIRsjeVCNle6bmsDmZKgOIin7DiT+OPVh+iyBNAPn19CNjXj1ro2zYK6+IHfKzYHERjEbEA2QjZnRqErF0h1HLCWR3MkyltVo7WIRL7MxR5AmiDjwH7dgvRWJfxq1csNpmLr0U0FhEL8KKQjb2XogohO\/KX0nNldfCM1pkdHybci4D9deyhyBNAO35GqGha1dFYjyjsqoEmRDQWEQvw4gQjgqlDyGYlTI5IicyO+YR7LzHfiw4YRZ4A6ieUj1F7NNZFUFUdhX3koxKNRcQCvDpILBGyzjYbjF5K8Q5OeD7Y\/bC+iwpUJX7dOaHIE0D9hBQhVS7y2u90lMimuaMdjZ3TRRGxgJD1FbKlTkiaBRUWDKzZ8GcmzluN446MOb3S4ynyBJBHPx9MuPPVa43GHpvtF0uHluos2AVPTf+UaCwiFhCyzQlZmUTuFJ9PNDYP9ifc22G+N6HIE0D9hFz0PazJMB5R2FWD7UdzQXhO0AARCwhZX3ZLFLKBD3N\/1i4UeUruXMjENvMVsLaNwOv9iCJPAPUT0q+orSCPfBeXKGzXWuOx0XzN782CJyIWELJtCVmjvyJ6ihOelMOM20ZNUOQJoG6\/IvSi71FF5iEKm\/67E41FxEJlQrZHyL5ppyGCnda0yPjYduibStzbtgHbO7gUeQKom5BH0+3WIDrs2DTb8p9rZ3+V4G91ih9BNBYRCxVxYMIVY6hWyBr9lVHSitMgzsVOpm2iRgdFnJNe6fEUeQJIT2gBVkM01kU4XbJFhWgsIhZgO6dSBss9hOybduqNfjT2lME1Or4OUhPn9ylBkSeAun2KkEJ2f5wXZ6XawyMKe0EbIhqLiAUoQ8iWtJctRuTtimhSls5FirZQ63hDkSeAuvkr8PMWBdvC6VxYorBR5liisYhYQMiGmZzGwWRdiI16o79XRQTsFS0yCr6rsURhwzgoFHkCqBNSis2Pyvcui2oXNJ0f\/tZgiMYiYgEQskE5ifAZc\/bHqjsXC+MfhV1iwSBjDUWeAOrt3yGF7E6hfdpFKHFc268QjUXEAiBkA9pnMHFSSWV\/7D4tUkXASrTbN1rXUZE4WF\/qDEWeAGoldErxYUlf3gokF5HEFpXn\/a0uk0UGRCxAYUI29qpgKRFZSfmJIWTWJRe0yJhj41eRWDN62CoUeQKokz7w8+aFzYeuUdiBJhNd3BONRcQCQrYtIaucCvnUEafQU0Bs0R9fcbMi3St4X6LIE0CdfVtEWeiMriL2xlqx7SKOiMK+3o76TBYbELEAhTmYqYTscea2kf0+MQ4kL62Cc+74LpBIMSeKbuhAkSeAOgktPhaFfG8XYdQThU0q8ucsdiJiASEb3PksoJDDMpJdFhR6mo61oe9kRRqx3hhDkSeAOgm9L3Yn91oRNgrrMuYQhX17juiNbjT2CCsjYgEhG5p1zg6odb5jVao9xRmf5FhMSSM+sX0A9PpSZyjyBFCj+AjtN+Re4Mk1CtvTUpKL\/QX1RxCxgJBtUchKSvFFRFuQ9uIuYKecvUsacTwo8gRQH6FF2n6ui1JEYVV9rd6wNxYRC4CQDY5MRLEidTcIWff2Y\/zOhI0ZaWd8US7yNF7\/w8oA0fmk8Mxc\/QGX32sgCuvla6m9O6KxiFhow9E8SCFEchWyj9KKY4j7HYTs9ox2ksI+vnuolhTcSOKkaPUjzl0GiI+GUMsupdhGh132VhKFdfe1ekM0FhELEGAgSRGhWud6ppcV97GK\/yBkt3MqFuYhAuclpmyqOMTtR5zFC1BXn5a5MfTC1G6GUTOX88cHWwcAPOZmxWcTjUXEQiMTU5dIyF7lKt6sTWLtn0TIvi1gfY\/TuR7f5RlWTNqPeiwBUA0a\/TmbzAqisFHnh94QjUXEAhQqZLMWb6NNJIp0jS2KFbC3hn2wOUA0FqAeNPbF5pRSvDBEYWNCNBYRC4CQVWJp4hV6QsiGE7CS8rZnU1oh7biiWeQJAOKiMR\/mlFLsEoX9SHOYPD\/0hmgsIhYAIatij3sxZOJVcRZbfG79HFkEbHWsTPxK6ACgIzo0SJ5SbOedbcW0jGcszoVBczGAaCwiFhCyCNnITvjaVuNtUcCeBxCwt\/Tk7PoQacUAdaAhZHNIKXaJ2l2yUBrU5xwyea+IWIBKBpXYBQtyFrIpztU9Hm1xk+th8AridWe8rox\/FWIEbP5jSo8lAIqnupRiorDJYW8sIhYgqNN5Nv7oEgjZdY7CLZGQnY\/X11yPIwroQMjCxWfjn1KGgC0DorEA5fNF6bkpU4pd9sJeE4UN7l+JrzkofkTz0VhELLQ4sCwTCFkRNDcI2Z+EvdjjvMao7Pidjq2AnSFgqx9PKPIEUD5aY22SlGK7SOySAcaxOjoQjUXEAiBkKxSywr3YG22yX0O7kuirpEuPf5yy9\/cWAVuko0IUA6Bcn0BrvN1NNOe7ROm68fsPtAKVdiV+pqZtj1q2LyIWELII2dRCdjZeV3avbJFH8di9r2fmIfo6R8A2N5ZQ5AmgfHql50ZdpLVRWJd5iCisLtrR2J1WDYuIBYQsQvapkH1n4p0j+xiZdCUquy4pRcYWz\/hqpu9PkdXw9+xLKnYs6QxFngBKRmve+xD5e7hE54jCxpkbtGwsfuRxq7ZFxAIDDEL2qT02x+9cJ\/oV7kWhFbPznMXreIl4XduJZApL2w6hbIjGApTL30rPjRaJtQvALp\/3kdceBc1o7FGr0VhELMC\/QrZPIGTXmdrjbrwOTNqCNQsr9G9stDMH4Sppw8ePxOts4iOH8XpvV2qh\/HEkRpGnHSwNoIJWJHYn4oKsS0ZQP45ZPa89ytzQGaKxiNiCmXOmU\/YcmPhptPsSccx44D2xdkmZ4iqTv0Rlv9nobPQiUPKZ9j19Mw9Fm0L05WsrYNn\/WhfaRZ52az+eCqAyEbuZx7TnKZmXFo5jFcSdG7RoMhqLiI3LKSbIl0dptLFFxSJzIXsvtkyafbKP2bET9NUjQatSYl6eaZ99L57lMx2dg9eQdnYgkW72v1Y7jminFTOXAOj0Xa0x+c\/MfEyisPG5VmxfTUZjEbHxxcoMMyBkCxSygxQdMvms3G4ErdhM9s\/KJeL2TKJULpGqzb+3917ZVOFNuvDChE3fFKfhvV0YgHrHkc7obk+YE40FUKHIo3bssxcOt7AXNo1\/ean4Ec1FYxGx8WEFHSFbpJC1tjkzeURlnzIzD8UspH\/JOa2yj3bDN7uv9vH1g82\/t\/fumzCpwk\/ZRF\/3qATZDERjARCxj5krPtslCjdQhyEZF4ZoLCK2YIjGImRLF7K3Nip7YtLulXUZ2OdPrtiT1juir82NIdpFnojGAoTnH8Vnqxy1Y6NvLsfqsBc2rW9JNBYRWzSsoCNk3xKy5wXY516cmfjHE5VCbx5Sh0\/Y+9os2kWeapxLTr83DF0mi3Fbi7nScyX6tq1wIQqbHqKxiNiiIRpbnpCNLUKOczlW5i372OOJ3pn4RxTl7ATt2dRhKg8zfmimFRONBSiH4PtiPaKwl7yGLOYForGI2KIhGouQfYt1CULW2khWd\/esnVoVsyJYl1a8Iuhh0zc65T7BXAIQrr9qj93zwM+TOg7bChbxYTrechYQjUXEFg3R2LImtluE7HYOgBWz7xuaLMXpEeH6njQteAGisQAghN4X67KIdcnWlmx8JaKxiNjiYQUdIVudkN3Y6lGa8cqUUQDKhc2K9jsir7Dl2KFZTIW5BCAcmuP5PNSDrF8wc5izLni1WUE0FhFbNERjEbLVCllrL0kzPhuvP8b\/PDAPh32XjPz+SytelxyXA44Oi1Z7IRoLUAa7AZ9FFLZsf1I7GnuIiAVtWEFHyFYtZB\/Z7Xq8RMj+YYVgKYJ2I1z\/kN9fUoZxBsDTYdFMK2YuAQiDakG+EAtOjlFYoeO1ZolmNHZWss+IiC0DorEIWRfOx\/ayW7jt7qwQ3AjaAzvBDpn8ioOdWA4QrhC47cuCSK\/0eKKxAGH4R\/n5IfqpS5StI2soX3\/I6C7oV724iYjNA1bQyxWyB5E\/VvY53JQuZJ8I2mubmiv7Z99Zm66ss68tHO\/s56weidZ39mzXa4QrKLBkLgHIGu1x\/39TbraLVS5CeMUrzRrN91N1NPZ32k4WSDR2xUpZkSKsH9+dOKXrBEL2fW1txn4fua4fTdjyfUW0z+z1H\/PzvqLN3z\/l9okz8unx31OMCVK1cRnvlQSnRGN3OZ8YYBLa\/WfqIrTLubBEYcuYEzrRAkofIXNNV6XtRsPRggAAAAAAAKAISCcGAAAAAAAARCwAAAAAAAAAIhYAAAAAAAAQsQAAAAAAAACIWAAAAAAAAABELAAAAAAAACBiAQAAAAAAABCxAAAAAAAAAIhYAAAAAAAAQMQCAAAAAAAAIGIBAAAAAAAAELEAAAAAAACAiAUAAAAAAABAxAIAAAAAAAAgYgEAAAAAAKBK\/l+AAQDKmSg8FY+wNQAAAABJRU5ErkJggg==\" alt=\"IKONERA\" class=\"login-brand-logo\">\n    <div class=\"login-tagline\" data-i18n=\"login.tagline\"><\/div>\n    <div class=\"login-separator\"><\/div>\n\n    <!-- Cadenas -->\n    <div class=\"login-lock\" aria-hidden=\"true\">\n      <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\">\n        <rect x=\"5\" y=\"11\" width=\"14\" height=\"9\" rx=\"1.5\"\/>\n        <path d=\"M8 11V8a4 4 0 0 1 8 0v3\"\/>\n      <\/svg>\n    <\/div>\n\n    <h1 class=\"login-title\" data-i18n=\"login.title\"><\/h1>\n    <div class=\"login-subtitle\" data-i18n=\"login.subtitle\"><\/div>\n\n    <p class=\"login-text\" data-i18n=\"login.text\"><\/p>\n\n    <div class=\"request-section\">\n      <div class=\"request-title\" data-i18n=\"login.request_title\"><\/div>\n      <ul class=\"request-list\">\n        <li data-i18n=\"login.request_1\"><\/li>\n        <li data-i18n=\"login.request_2\"><\/li>\n        <li data-i18n=\"login.request_3\"><\/li>\n      <\/ul>\n    <\/div>\n\n    <a href=\"mailto:contact@ikonera.art\" class=\"cta-contact\" data-i18n=\"login.cta\" data-i18n-mailto=\"login\"><\/a>\n\n    <button class=\"pwd-toggle\" type=\"button\" id=\"pwd-toggle\" data-i18n=\"login.toggle_code\"><\/button>\n    <div class=\"pwd-collapse\" id=\"pwd-collapse\">\n      <div class=\"pwd-input-wrap\">\n        <input type=\"password\" id=\"pwd-input\" class=\"pwd-input\"\n               placeholder=\"Code d'acc\u00e8s\" data-i18n-placeholder=\"login.placeholder\" autocomplete=\"off\" inputmode=\"text\">\n        <button class=\"pwd-submit\" type=\"button\" id=\"pwd-submit\" data-i18n=\"login.submit\"><\/button>\n      <\/div>\n      <div class=\"error-msg\" id=\"error-msg\" role=\"alert\" aria-live=\"polite\"><\/div>\n    <\/div>\n\n  <\/div>\n<\/div>\n\n<!-- \u2550\u2550\u2550 CALCULATEUR (visible apr\u00e8s login) \u2550\u2550\u2550 -->\n<div class=\"calc-wrap\">\n\n<div class=\"calc\">\n\n  <!-- HEADER -->\n  <div class=\"calc-header\">\n    <div class=\"calc-header-top\">\n      <div class=\"calc-header-text\">\n        <span data-i18n=\"header.subtitle\"><\/span> <span class=\"dot\">\u00b7<\/span> <span data-i18n=\"header.subtitle_2\"><\/span>\n      <\/div>\n      <div class=\"badge-online\" data-i18n=\"header.badge\"><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- STEPPER -->\n  <div class=\"stepper\">\n    <div class=\"step active\" data-step=\"1\">\n      <div class=\"step-num\"><span>1<\/span><\/div>\n      <div class=\"step-info\"><div class=\"step-info-title\" data-i18n=\"step1.label\"><\/div><div class=\"step-info-sub\" data-i18n=\"step1.sub\"><\/div><\/div>\n    <\/div>\n    <div class=\"step disabled\" data-step=\"2\">\n      <div class=\"step-num\"><span>2<\/span><\/div>\n      <div class=\"step-info\"><div class=\"step-info-title\" data-i18n=\"step2.label\"><\/div><div class=\"step-info-sub\" data-i18n=\"step2.sub\"><\/div><\/div>\n    <\/div>\n    <div class=\"step disabled\" data-step=\"3\">\n      <div class=\"step-num\"><span>3<\/span><\/div>\n      <div class=\"step-info\"><div class=\"step-info-title\" data-i18n=\"step3.label\"><\/div><div class=\"step-info-sub\" data-i18n=\"step3.sub\"><\/div><\/div>\n    <\/div>\n    <div class=\"step disabled\" data-step=\"4\">\n      <div class=\"step-num\"><span>4<\/span><\/div>\n      <div class=\"step-info\"><div class=\"step-info-title\" data-i18n=\"step4.label\"><\/div><div class=\"step-info-sub\" data-i18n=\"step4.sub\"><\/div><\/div>\n    <\/div>\n    <div class=\"step disabled\" data-step=\"5\">\n      <div class=\"step-num\"><span>5<\/span><\/div>\n      <div class=\"step-info\"><div class=\"step-info-title\" data-i18n=\"step5.label\"><\/div><div class=\"step-info-sub\" data-i18n=\"step5.sub\"><\/div><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- PANELS -->\n  <div class=\"panels\">\n\n    <div class=\"panel active\" id=\"panel-1\">\n      <div class=\"panel-head\">\n        <h2 data-i18n=\"step1.title\"><\/h2>\n        <p data-i18n=\"step1.desc\"><\/p>\n      <\/div>\n      <div class=\"marbles\" id=\"marbles-grid\"><\/div>\n    <\/div>\n\n    <div class=\"panel\" id=\"panel-2\">\n      <div class=\"panel-head\">\n        <h2 data-i18n=\"step2.title\"><\/h2>\n        <p data-i18n=\"step2.desc\"><\/p>\n      <\/div>\n\n      <!-- FINITION ROBOTIQUE (R) -->\n      <div class=\"finish-section\">\n        <div class=\"finish-section-head\">\n          <span class=\"finish-section-icon\">\u2699\ufe0f<\/span>\n          <span class=\"finish-section-title\" data-i18n=\"step2.r_title\"><\/span>\n          <span class=\"finish-section-sub\" data-i18n=\"step2.r_sub\"><\/span>\n        <\/div>\n        <div class=\"finish-row\" id=\"finish-r-grid\"><\/div>\n      <\/div>\n\n      <!-- FINITION MAIN (H) -->\n      <div class=\"finish-section\">\n        <div class=\"finish-section-head\">\n          <span class=\"finish-section-icon\">\u270b<\/span>\n          <span class=\"finish-section-title\" data-i18n=\"step2.h_title\"><\/span>\n          <span class=\"finish-section-sub\" data-i18n=\"step2.h_sub\"><\/span>\n        <\/div>\n        <div class=\"finish-row h4\" id=\"finish-h-grid\"><\/div>\n      <\/div>\n\n      <div class=\"finish-tip\">\n        <span class=\"finish-tip-icon\">\ud83d\udca1<\/span>\n        <span data-i18n=\"step2.tip\"><\/span>\n      <\/div>\n    <\/div>\n\n    <div class=\"panel\" id=\"panel-3\">\n      <div class=\"panel-head\">\n        <h2 data-i18n=\"step3.title\"><\/h2>\n        <p data-i18n=\"step3.desc\"><\/p>\n      <\/div>\n\n      <div class=\"dim-grid\">\n        <div class=\"dim-field\">\n          <div class=\"dim-row-top\">\n            <div class=\"dim-label\" data-i18n=\"step3.width\"><\/div>\n            <div class=\"dim-input-wrap\">\n              <input type=\"number\" class=\"dim-input\" id=\"dim-w\" value=\"0.50\" step=\"0.05\" min=\"0.05\" max=\"6\" inputmode=\"decimal\">\n              <span class=\"dim-unit\">m<\/span>\n            <\/div>\n          <\/div>\n          <input type=\"range\" class=\"dim-slider\" id=\"dim-w-slider\" min=\"0.05\" max=\"5\" step=\"0.05\" value=\"0.50\">\n        <\/div>\n        <div class=\"dim-field\">\n          <div class=\"dim-row-top\">\n            <div class=\"dim-label\" data-i18n=\"step3.height\"><\/div>\n            <div class=\"dim-input-wrap\">\n              <input type=\"number\" class=\"dim-input\" id=\"dim-h\" value=\"0.80\" step=\"0.05\" min=\"0.05\" max=\"6\" inputmode=\"decimal\">\n              <span class=\"dim-unit\">m<\/span>\n            <\/div>\n          <\/div>\n          <input type=\"range\" class=\"dim-slider\" id=\"dim-h-slider\" min=\"0.05\" max=\"5\" step=\"0.05\" value=\"0.80\">\n        <\/div>\n        <div class=\"dim-field\">\n          <div class=\"dim-row-top\">\n            <div class=\"dim-label\" data-i18n=\"step3.depth\"><\/div>\n            <div class=\"dim-input-wrap\">\n              <input type=\"number\" class=\"dim-input\" id=\"dim-d\" value=\"0.50\" step=\"0.05\" min=\"0.05\" max=\"6\" inputmode=\"decimal\">\n              <span class=\"dim-unit\">m<\/span>\n            <\/div>\n          <\/div>\n          <input type=\"range\" class=\"dim-slider\" id=\"dim-d-slider\" min=\"0.05\" max=\"5\" step=\"0.05\" value=\"0.50\">\n        <\/div>\n      <\/div>\n\n      <div class=\"dim-label\" style=\"margin-bottom: 10px;\" data-i18n=\"step3.shape_label\"><\/div>\n      <div class=\"shape-types\" id=\"shape-types\"><\/div>\n\n      <div class=\"dim-summary\">\n        <div class=\"dim-sum-item\">\n          <div class=\"dim-label\" data-i18n=\"step3.bbox\"><\/div>\n          <div class=\"dim-sum-value\" id=\"sum-bbox\">\u2014<\/div>\n        <\/div>\n        <div class=\"dim-sum-item\">\n          <div class=\"dim-label\" data-i18n=\"step3.real_vol\"><\/div>\n          <div class=\"dim-sum-value\" id=\"sum-real\">\u2014<\/div>\n        <\/div>\n        <div class=\"dim-sum-item\">\n          <div class=\"dim-label\" data-i18n=\"step3.weight\"><\/div>\n          <div class=\"dim-sum-value\" id=\"sum-weight\">\u2014<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"panel\" id=\"panel-4\">\n      <div class=\"panel-head\">\n        <h2 data-i18n=\"step4.title\"><\/h2>\n        <p data-i18n=\"step4.desc\"><\/p>\n      <\/div>\n\n      <!-- Zone d'upload 3D -->\n      <div class=\"upload-zone\" id=\"upload-zone\">\n        <input type=\"file\" class=\"upload-input\" id=\"upload-input\" accept=\".stl,.obj\">\n\n        <!-- \u00c9tat vide -->\n        <div id=\"upload-empty\">\n          <div class=\"upload-icon\">\n            <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.4\">\n              <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4\"\/>\n              <polyline points=\"17 8 12 3 7 8\"\/>\n              <line x1=\"12\" y1=\"3\" x2=\"12\" y2=\"15\"\/>\n            <\/svg>\n          <\/div>\n          <div class=\"upload-title\"><span data-i18n=\"upload.title\"><\/span> <span style=\"font-size: 11px; font-weight: 400; color: var(--text-soft);\" data-i18n=\"upload.optional\"><\/span><\/div>\n          <div class=\"upload-hint\">\n            <span data-i18n=\"upload.hint\"><\/span><br>\n            <em style=\"font-style: normal; color: var(--text-soft);\" data-i18n=\"upload.hint_2\"><\/em>\n          <\/div>\n          <div class=\"upload-formats\" data-i18n=\"upload.formats\"><\/div>\n        <\/div>\n\n        <!-- \u00c9tat avec fichier -->\n        <div id=\"upload-loaded\" style=\"display: none;\">\n          <div class=\"file-info\">\n            <div class=\"file-info-icon\">\n              <svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\">\n                <path d=\"M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z\"\/>\n                <polyline points=\"14 2 14 8 20 8\"\/>\n              <\/svg>\n            <\/div>\n            <div class=\"file-info-meta\">\n              <div class=\"file-info-name\" id=\"file-name\">\u2014<\/div>\n              <div class=\"file-info-stats\" id=\"file-stats\">\u2014<\/div>\n            <\/div>\n            <button class=\"file-info-remove\" type=\"button\" id=\"file-remove\" data-i18n=\"upload.remove\"><\/button>\n          <\/div>\n          <div class=\"upload-progress\" id=\"upload-progress\">\n            <div class=\"upload-progress-bar\" id=\"upload-progress-bar\"><\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <!-- Visualisation -->\n      <div class=\"viz-3d\">\n        <div class=\"viz-info-overlay\" id=\"viz-overlay\"><strong>\u2014<\/strong><\/div>\n        <div class=\"viz-mode-tag\" id=\"viz-mode-tag\" data-i18n=\"viz.mode_preview\"><\/div>\n\n        <!-- Mode 1 : cube CSS (par d\u00e9faut) -->\n        <div class=\"stage\" id=\"stage-cube\">\n          <div class=\"cube\" id=\"cube\"><\/div>\n        <\/div>\n\n        <!-- Mode 2 : canvas Three.js (si fichier upload\u00e9) -->\n        <div class=\"viz-canvas-wrap\" id=\"stage-canvas\" style=\"display: none;\">\n          <canvas id=\"viz-canvas\"><\/canvas>\n          <div class=\"viz-hint\" id=\"viz-hint\" data-i18n=\"viz.hint\"><\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"viz-stats\">\n        <div class=\"viz-stat\">\n          <div class=\"viz-stat-label\" data-i18n=\"viz.dims_label\"><\/div>\n          <div class=\"viz-stat-value\" id=\"viz-dims\">\u2014 <small>m<\/small><\/div>\n        <\/div>\n        <div class=\"viz-stat\">\n          <div class=\"viz-stat-label\" data-i18n=\"viz.volume_label\"><\/div>\n          <div class=\"viz-stat-value\" id=\"viz-volume\">\u2014 <small>m\u00b3<\/small><\/div>\n        <\/div>\n        <div class=\"viz-stat\">\n          <div class=\"viz-stat-label\" data-i18n=\"viz.weight_label\"><\/div>\n          <div class=\"viz-stat-value\" id=\"viz-weight\">\u2014 <small>kg<\/small><\/div>\n        <\/div>\n        <div class=\"viz-stat\">\n          <div class=\"viz-stat-label\" data-i18n=\"viz.marble_label\"><\/div>\n          <div class=\"viz-stat-value\" id=\"viz-marble\" style=\"font-size: 16px;\">\u2014<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <div class=\"panel\" id=\"panel-5\">\n      <div class=\"panel-head\">\n        <h2 data-i18n=\"step5.title\"><\/h2>\n        <p data-i18n=\"step5.desc\"><\/p>\n      <\/div>\n\n      <div class=\"price-hero\">\n        <div class=\"price-eyebrow\" data-i18n=\"price.eyebrow\"><\/div>\n        <div class=\"price-amount\">\n          <span id=\"price-value\">\u2014<\/span><span class=\"price-cur\">CHF<\/span>\n        <\/div>\n        <div class=\"price-range\" id=\"price-range\">Fourchette : \u2014<\/div>\n      <\/div>\n\n      <div class=\"recap-grid\">\n        <div class=\"recap-row\"><span data-i18n=\"recap.marble\"><\/span><b id=\"recap-marble\">\u2014<\/b><\/div>\n        <div class=\"recap-row\"><span data-i18n=\"recap.finish\"><\/span><b id=\"recap-finish\">\u2014<\/b><\/div>\n        <div class=\"recap-row\"><span data-i18n=\"recap.dim\"><\/span><b id=\"recap-dim\">\u2014<\/b><\/div>\n        <div class=\"recap-row\"><span data-i18n=\"recap.vol\"><\/span><b id=\"recap-vol\">\u2014<\/b><\/div>\n        <div class=\"recap-row\"><span data-i18n=\"recap.weight\"><\/span><b id=\"recap-weight\">\u2014<\/b><\/div>\n        <div class=\"recap-row\"><span data-i18n=\"recap.delay\"><\/span><b id=\"recap-delay\">\u2014<\/b><\/div>\n      <\/div>\n\n      <div class=\"breakdown\">\n        <div class=\"breakdown-title\" data-i18n=\"breakdown.title\"><\/div>\n        <div class=\"breakdown-row\"><span data-i18n=\"breakdown.matiere\"><\/span><span id=\"bd-matiere\">\u2014<\/span><\/div>\n        <div class=\"breakdown-row\"><span data-i18n=\"breakdown.finition\"><\/span><span id=\"bd-finition\">\u2014<\/span><\/div>\n        <div class=\"breakdown-row\"><span data-i18n=\"breakdown.setup\"><\/span><span id=\"bd-setup\">\u2014<\/span><\/div>\n        <div class=\"breakdown-row total\"><span data-i18n=\"breakdown.total\"><\/span><span id=\"bd-total\">\u2014<\/span><\/div>\n      <\/div>\n\n      <div class=\"monumental-msg\" id=\"monumental-msg\" data-i18n=\"monumental.text\" data-i18n-html=\"true\"><\/div>\n\n      <div class=\"cta-final\">\n        <a href=\"#\" class=\"btn-cta\" id=\"cta-final-link\" data-i18n=\"cta.devis\"><\/a>\n        <div class=\"cta-hint\" data-i18n=\"cta.hint\"><\/div>\n      <\/div>\n    <\/div>\n\n  <\/div>\n\n  <!-- FOOTER NAV -->\n  <div class=\"calc-footer\">\n    <div class=\"footer-status\" id=\"footer-status\">\n      <div class=\"indicator\"><\/div>\n      <span id=\"footer-status-text\" data-i18n=\"footer.no_marble\"><\/span>\n    <\/div>\n    <div class=\"nav-actions\">\n      <button class=\"btn-nav\" id=\"btn-prev\" style=\"display: none;\" data-i18n=\"btn.back\"><\/button>\n      <button class=\"btn-nav primary\" id=\"btn-next\" disabled data-i18n=\"btn.next_finish\"><\/button>\n    <\/div>\n  <\/div>\n\n  <div class=\"calc-bottom\">\n    <span data-i18n=\"powered_by\"><\/span> <b>IKONERA<\/b> \u00b7 ikonera.art\n  <\/div>\n\n<\/div>\n\n<\/div><!-- \/calc-wrap -->\n<\/div>\n\n<script>\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   LOGIN \u2014 ex\u00e9cut\u00e9 avant le calculateur\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n(function () {\n  'use strict';\n  console.log('[IKONERA] Login script charg\u00e9');\n\n  const PWD_OBF = 'MjJraQ==';\n  const STORAGE_KEY = 'ikonera_calc_auth_v3';\n\n  function getPwd() {\n    try { return atob(PWD_OBF).split('').reverse().join(''); }\n    catch(e) { return null; }\n  }\n  function authenticate() {\n    document.querySelector('.ikonera-app').classList.add('authed');\n    try { sessionStorage.setItem(STORAGE_KEY, 'ok'); } catch(e) {}\n  }\n  function logout() {\n    document.querySelector('.ikonera-app').classList.remove('authed');\n    try { sessionStorage.removeItem(STORAGE_KEY); } catch(e) {}\n    const inp = document.getElementById('pwd-input');\n    const err = document.getElementById('error-msg');\n    if (inp) inp.value = '';\n    if (err) err.textContent = '';\n  }\n  function checkAndAuth() {\n    const inp = document.getElementById('pwd-input');\n    const err = document.getElementById('error-msg');\n    if (!inp || !err) return;\n    const v = inp.value.trim();\n    if (!v) { err.textContent = (typeof t === 'function' ? t('login.err_empty') : 'Veuillez saisir un code.'); return; }\n    if (v === getPwd()) {\n      authenticate();\n      console.log('[IKONERA] Authentifi\u00e9');\n    } else {\n      err.textContent = (typeof t === 'function' ? t('login.err_wrong') : 'Code incorrect.');\n      inp.value = '';\n      inp.classList.remove('shake');\n      void inp.offsetWidth;\n      inp.classList.add('shake');\n      inp.focus();\n    }\n  }\n  function init() {\n    try {\n      if (sessionStorage.getItem(STORAGE_KEY) === 'ok') {\n        document.querySelector('.ikonera-app').classList.add('authed');\n        console.log('[IKONERA] Auto-login (session)');\n      }\n    } catch(e) {}\n\n    const submitBtn = document.getElementById('pwd-submit');\n    const input = document.getElementById('pwd-input');\n    const logoutBtn = document.getElementById('logout-btn');\n    const pwdToggle = document.getElementById('pwd-toggle');\n    const pwdCollapse = document.getElementById('pwd-collapse');\n\n    if (submitBtn && !submitBtn.dataset.wired) {\n      submitBtn.addEventListener('click', (e) => { e.preventDefault(); checkAndAuth(); });\n      submitBtn.dataset.wired = '1';\n    }\n    if (input && !input.dataset.wired) {\n      input.addEventListener('keydown', (e) => {\n        if (e.key === 'Enter' || e.keyCode === 13) {\n          e.preventDefault();\n          checkAndAuth();\n        }\n      });\n      input.dataset.wired = '1';\n    }\n    if (logoutBtn && !logoutBtn.dataset.wired) {\n      logoutBtn.addEventListener('click', logout);\n      logoutBtn.dataset.wired = '1';\n    }\n    if (pwdToggle && pwdCollapse && !pwdToggle.dataset.wired) {\n      pwdToggle.addEventListener('click', () => {\n        const open = pwdCollapse.classList.toggle('open');\n        pwdToggle.classList.toggle('open', open);\n        if (open && input) setTimeout(() => input.focus(), 350);\n      });\n      pwdToggle.dataset.wired = '1';\n    }\n  }\n\n  if (document.readyState === 'loading') {\n    document.addEventListener('DOMContentLoaded', init);\n  } else {\n    init();\n  }\n\n  try {\n    const np = new URLSearchParams(location.search).get('genhash');\n    if (np) {\n      const obf = btoa(np.split('').reverse().join(''));\n      alert('Mot de passe : ' + np + '\\nValeur PWD_OBF :\\n' + obf);\n    }\n  } catch(e) {}\n\n  window.IKONERA_LOGIN = { auth: authenticate, logout: logout };\n})();\n\n<\/script>\n\n<script>\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   IKONERA \u00b7 I18N \u2014 4 langues\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\nconst I18N = {\n  fr: {\n    \/\/ \u2500\u2500\u2500 LOGIN \u2500\u2500\u2500\n    'login.tagline': 'La sculpture, r\u00e9invent\u00e9e par l\\'intelligence',\n    'login.title': 'Calculateur de devis',\n    'login.subtitle': 'Acc\u00e8s sur demande',\n    'login.text': 'Cet outil est r\u00e9serv\u00e9 \u00e0 nos partenaires architectes, designers et clients qualifi\u00e9s. Pour pr\u00e9server l\\'exclusivit\u00e9 de notre offre, l\\'acc\u00e8s est d\u00e9livr\u00e9 au cas par cas.',\n    'login.request_title': 'Pour demander un acc\u00e8s',\n    'login.request_1': 'Votre nom et entit\u00e9',\n    'login.request_2': 'Nature de votre projet',\n    'login.request_3': 'Pourquoi vous avez besoin du calculateur',\n    'login.cta': 'Demander l\\'acc\u00e8s',\n    'login.toggle_code': 'J\\'ai d\u00e9j\u00e0 un code d\\'acc\u00e8s',\n    'login.placeholder': 'Code d\\'acc\u00e8s',\n    'login.submit': 'Entrer',\n    'login.err_empty': 'Veuillez saisir un code.',\n    'login.err_wrong': 'Code incorrect.',\n    'login.mail_subject': 'Demande d\\'acc\u00e8s au calculateur',\n    'login.mail_body': 'Bonjour,\\n\\nNom \/ Entit\u00e9 : \\nProjet : \\nBesoin : \\n\\nMerci.',\n    \/\/ \u2500\u2500\u2500 HEADER \u2500\u2500\u2500\n    'header.title': 'Calculateur de devis',\n    'header.logout': 'D\u00e9connexion',\n    'header.subtitle': 'Configurez votre sculpture en marbre',\n    'header.subtitle_2': 'Estimation instantan\u00e9e en CHF',\n    'header.badge': 'Devis en ligne',\n    \/\/ \u2500\u2500\u2500 STEPS \u2500\u2500\u2500\n    'step1.label': 'Marbre', 'step1.sub': 'Choisir',\n    'step2.label': 'Finition', 'step2.sub': 'Robot+Main',\n    'step3.label': 'Dimensions', 'step3.sub': 'Taille en m',\n    'step4.label': 'Visuel 3D', 'step4.sub': 'Pr\u00e9visualiser',\n    'step5.label': 'Prix', 'step5.sub': 'Estimation',\n    \/\/ \u2500\u2500\u2500 STEP 1 \u2500\u2500\u2500\n    'step1.title': 'Choisissez votre marbre',\n    'step1.desc': 'Chaque marbre poss\u00e8de un caract\u00e8re unique et un tarif distinct.',\n    'tier.standard': 'Standard', 'tier.standardp': 'Standard+',\n    'tier.premium': 'Premium', 'tier.premiump': 'Premium+',\n    'tier.luxe': 'Luxe', 'tier.luxep': 'Luxe+',\n    \/\/ \u2500\u2500\u2500 STEP 2 \u2500\u2500\u2500\n    'step2.title': 'Niveaux de finition',\n    'step2.desc': 'Le robot fraise toujours la totalit\u00e9. La finition main s\\'ajoute en compl\u00e9ment.',\n    'step2.r_title': 'Finition robotique',\n    'step2.r_sub': '\u2014 base (toujours incluse)',\n    'step2.h_title': 'Finition main',\n    'step2.h_sub': '\u2014 additive en compl\u00e9ment',\n    'step2.tip': 'La finition main <b>s\\'ajoute<\/b> au robot. Les artisans d\\'Alle (Jura) compl\u00e8tent les d\u00e9tails.',\n    'r1.name': '\u00c9bauche', 'r1.desc': 'Fraisage CNC brut \u00b7 traces d\\'outil visibles',\n    'r2.name': 'Semi-finie', 'r2.desc': 'Pon\u00e7age robot grain 60-120 \u00b7 surface mate lisse',\n    'r3.name': 'Polie', 'r3.desc': 'Polissage robot jet d\\'eau \u00b7 semi-brillant',\n    'h0.name': 'Sans main', 'h0.desc': 'Aucune intervention manuelle \u00b7 robot seul',\n    'h1.name': 'L\u00e9g\u00e8re', 'h1.desc': 'Pon\u00e7age main grain 120-240 \u00b7 d\u00e9tails de surface',\n    'h2.name': 'Standard', 'h2.desc': 'Finition main 240-800 \u00b7 visages, drap\u00e9s, reliefs',\n    'h3.name': 'Compl\u00e8te', 'h3.desc': 'Polissage miroir grain 3000 \u00b7 qualit\u00e9 galerie',\n    \/\/ \u2500\u2500\u2500 STEP 3 \u2500\u2500\u2500\n    'step3.title': 'Dimensions de la sculpture',\n    'step3.desc': 'Saisissez la bo\u00eete englobante en m\u00e8tres et choisissez le type d\\'objet le plus proche.',\n    'step3.width': 'Largeur', 'step3.height': 'Hauteur', 'step3.depth': 'Profondeur',\n    'step3.shape_label': 'Type d\\'objet',\n    'step3.bbox': 'Volume bbox',\n    'step3.real_vol': 'Volume sculpture',\n    'step3.weight': 'Poids estim\u00e9',\n    'shape.figure': 'Figure humaine',\n    'shape.buste': 'Buste \/ portrait',\n    'shape.organique': 'Animal \/ organique',\n    'shape.bloc': 'Bloc \/ g\u00e9om\u00e9trique',\n    \/\/ \u2500\u2500\u2500 STEP 4 \u2500\u2500\u2500\n    'step4.title': 'Pr\u00e9visualisation',\n    'step4.desc': 'Importez votre fichier 3D pour le visualiser en rotation, ou consultez l\\'aper\u00e7u sch\u00e9matique de l\\'encombrement.',\n    'upload.title': 'Importez votre fichier 3D',\n    'upload.optional': '(optionnel)',\n    'upload.hint': 'Pour visualiser votre sculpture en 3D dans la teinte du marbre choisi.',\n    'upload.hint_2': 'Le calcul reste bas\u00e9 sur les dimensions saisies \u00e0 l\\'\u00e9tape pr\u00e9c\u00e9dente.',\n    'upload.formats': 'STL \u00b7 OBJ',\n    'upload.remove': 'Retirer',\n    'upload.err_format': 'Format non support\u00e9. Utilisez un fichier STL ou OBJ.',\n    'upload.err_size': 'Fichier trop volumineux (max 100 MB).',\n    'upload.err_invalid': 'Impossible de lire ce fichier. V\u00e9rifiez qu\\'il s\\'agit d\\'un STL ou OBJ valide.',\n    'upload.stats_suffix': 'pr\u00e9visualisation seule (le calcul reste bas\u00e9 sur vos dimensions)',\n    'viz.hint': 'Glissez pour faire tourner',\n    'viz.mode_preview': 'Aper\u00e7u sch\u00e9matique',\n    'viz.mode_3d': 'Pr\u00e9visualisation 3D',\n    'viz.dims_label': 'L \u00d7 H \u00d7 P',\n    'viz.volume_label': 'Volume',\n    'viz.weight_label': 'Poids',\n    'viz.marble_label': 'Marbre',\n    'viz.bloc_default': 'Bloc g\u00e9n\u00e9rique',\n    \/\/ \u2500\u2500\u2500 STEP 5 \u2500\u2500\u2500\n    'step5.title': 'Votre estimation',\n    'step5.desc': 'Tarif transparent calcul\u00e9 sur le volume r\u00e9el de la sculpture.',\n    'price.eyebrow': 'Estimation indicative',\n    'price.range': 'Fourchette',\n    'recap.marble': 'Marbre',\n    'recap.finish': 'Finition',\n    'recap.dim': 'Dimensions (L\u00d7H\u00d7P)',\n    'recap.vol': 'Volume sculpture',\n    'recap.weight': 'Poids estim\u00e9',\n    'recap.delay': 'D\u00e9lai estim\u00e9',\n    'breakdown.title': 'D\u00e9composition',\n    'breakdown.matiere': 'Mati\u00e8re (volume \u00d7 tarif marbre)',\n    'breakdown.finition': 'Finition (Robot + Main)',\n    'breakdown.setup': 'Setup, design IA, logistique',\n    'breakdown.total': 'Total HT',\n    'monumental.text': '<b>Pi\u00e8ce monumentale.<\/b> Pour les projets d\u00e9passant 400 000 CHF, l\\'estimation ci-dessus est purement indicative. Le prix final est n\u00e9goci\u00e9 au cas par cas selon le contexte (sponsoring, exposition publique, partenariat institutionnel, planning de production).',\n    'cta.devis': 'Demander un devis ferme',\n    'cta.hint': 'Cette estimation est indicative. Un devis ferme inclut TVA, livraison et installation.',\n    'devis.mail_subject': 'Devis sculpture',\n    'devis.mail_intro': 'Bonjour,\\n\\nJe souhaite un devis ferme pour la configuration suivante :',\n    'devis.mail_marble': 'Marbre',\n    'devis.mail_finish': 'Finition',\n    'devis.mail_dim': 'Dimensions',\n    'devis.mail_vol': 'Volume sculpture',\n    'devis.mail_est': 'Estimation',\n    'devis.mail_contact': 'Mes coordonn\u00e9es',\n    'devis.mail_name': 'Nom',\n    'devis.mail_company': 'Entit\u00e9',\n    'devis.mail_phone': 'T\u00e9l\u00e9phone',\n    'devis.mail_thanks': 'Merci.',\n    \/\/ \u2500\u2500\u2500 FOOTER \u2500\u2500\u2500\n    'footer.no_marble': 'Aucun marbre s\u00e9lectionn\u00e9',\n    'footer.marble_chosen': 'Marbre choisi : ',\n    'footer.no_finish': 'Aucune finition s\u00e9lectionn\u00e9e',\n    'footer.partial_r': ' robotique \u2014 choisissez un niveau main',\n    'footer.partial_h': ' main \u2014 choisissez un niveau robotique',\n    'footer.choose_both': 'Choisissez un niveau robotique et un niveau main',\n    'footer.finish_set': 'Finition : ',\n    'footer.vol_sculpt': 'Volume sculpture : ',\n    'footer.preview_ready': 'Aper\u00e7u pr\u00eat',\n    'footer.total_est': 'Total estim\u00e9 : ',\n    'btn.back': '\u2190 Retour',\n    'btn.next_finish': 'Finition \u2192',\n    'btn.next_dim': 'Dimensions \u2192',\n    'btn.next_viz': 'Visuel 3D \u2192',\n    'btn.next_price': 'Voir le prix \u2192',\n    'btn.next_devis': 'Demander un devis',\n    'powered_by': 'Propuls\u00e9 par',\n    \/\/ \u2500\u2500\u2500 UNITS \u2500\u2500\u2500\n    'unit.days': 'jours',\n    'unit.weeks': 'semaines',\n    'unit.years': 'ans',\n    'unit.kg': 'kg', 'unit.t': 't',\n  },\n\n  en: {\n    'login.tagline': 'Sculpture, reinvented by intelligence',\n    'login.title': 'Quote calculator',\n    'login.subtitle': 'Access on request',\n    'login.text': 'This tool is reserved for our architect partners, designers and qualified clients. To preserve the exclusivity of our offering, access is granted on a case-by-case basis.',\n    'login.request_title': 'To request access',\n    'login.request_1': 'Your name and organisation',\n    'login.request_2': 'Nature of your project',\n    'login.request_3': 'Why you need the calculator',\n    'login.cta': 'Request access',\n    'login.toggle_code': 'I already have an access code',\n    'login.placeholder': 'Access code',\n    'login.submit': 'Enter',\n    'login.err_empty': 'Please enter a code.',\n    'login.err_wrong': 'Incorrect code.',\n    'login.mail_subject': 'Calculator access request',\n    'login.mail_body': 'Hello,\\n\\nName \/ Organisation: \\nProject: \\nNeed: \\n\\nThank you.',\n    'header.title': 'Quote calculator',\n    'header.logout': 'Logout',\n    'header.subtitle': 'Configure your marble sculpture',\n    'header.subtitle_2': 'Instant CHF estimate',\n    'header.badge': 'Online quote',\n    'step1.label': 'Marble', 'step1.sub': 'Choose',\n    'step2.label': 'Finish', 'step2.sub': 'Robot+Hand',\n    'step3.label': 'Dimensions', 'step3.sub': 'Size in m',\n    'step4.label': '3D View', 'step4.sub': 'Preview',\n    'step5.label': 'Price', 'step5.sub': 'Estimate',\n    'step1.title': 'Choose your marble',\n    'step1.desc': 'Each marble has a unique character and distinct pricing.',\n    'tier.standard': 'Standard', 'tier.standardp': 'Standard+',\n    'tier.premium': 'Premium', 'tier.premiump': 'Premium+',\n    'tier.luxe': 'Luxury', 'tier.luxep': 'Luxury+',\n    'step2.title': 'Finish levels',\n    'step2.desc': 'The robot mills the entire piece. Hand finishing is added on top.',\n    'step2.r_title': 'Robotic finish',\n    'step2.r_sub': '\u2014 base (always included)',\n    'step2.h_title': 'Hand finish',\n    'step2.h_sub': '\u2014 additive complement',\n    'step2.tip': 'Hand finishing <b>adds<\/b> to the robot work. Our craftsmen in Alle (Jura) complete the details.',\n    'r1.name': 'Roughed', 'r1.desc': 'Raw CNC milling \u00b7 visible tool marks',\n    'r2.name': 'Semi-finished', 'r2.desc': 'Robot sanding 60-120 grit \u00b7 matte smooth surface',\n    'r3.name': 'Polished', 'r3.desc': 'Robot waterjet polishing \u00b7 semi-glossy',\n    'h0.name': 'No hand', 'h0.desc': 'No manual intervention \u00b7 robot only',\n    'h1.name': 'Light', 'h1.desc': 'Hand sanding 120-240 grit \u00b7 surface details',\n    'h2.name': 'Standard', 'h2.desc': 'Hand finish 240-800 \u00b7 faces, drapery, reliefs',\n    'h3.name': 'Complete', 'h3.desc': 'Mirror polish grit 3000 \u00b7 gallery quality',\n    'step3.title': 'Sculpture dimensions',\n    'step3.desc': 'Enter the bounding box in metres and choose the closest object type.',\n    'step3.width': 'Width', 'step3.height': 'Height', 'step3.depth': 'Depth',\n    'step3.shape_label': 'Object type',\n    'step3.bbox': 'Bounding volume',\n    'step3.real_vol': 'Sculpture volume',\n    'step3.weight': 'Estimated weight',\n    'shape.figure': 'Standing figure',\n    'shape.buste': 'Bust \/ portrait',\n    'shape.organique': 'Animal \/ organic',\n    'shape.bloc': 'Block \/ geometric',\n    'step4.title': 'Preview',\n    'step4.desc': 'Import your 3D file to view it in rotation, or check the schematic preview of the bounding box.',\n    'upload.title': 'Import your 3D file',\n    'upload.optional': '(optional)',\n    'upload.hint': 'To visualise your sculpture in 3D with the chosen marble tone.',\n    'upload.hint_2': 'Pricing remains based on the dimensions entered in the previous step.',\n    'upload.formats': 'STL \u00b7 OBJ',\n    'upload.remove': 'Remove',\n    'upload.err_format': 'Unsupported format. Please use an STL or OBJ file.',\n    'upload.err_size': 'File too large (max 100 MB).',\n    'upload.err_invalid': 'Cannot read this file. Make sure it is a valid STL or OBJ.',\n    'upload.stats_suffix': 'preview only (pricing stays based on your dimensions)',\n    'viz.hint': 'Drag to rotate',\n    'viz.mode_preview': 'Schematic preview',\n    'viz.mode_3d': '3D Preview',\n    'viz.dims_label': 'W \u00d7 H \u00d7 D',\n    'viz.volume_label': 'Volume',\n    'viz.weight_label': 'Weight',\n    'viz.marble_label': 'Marble',\n    'viz.bloc_default': 'Generic block',\n    'step5.title': 'Your estimate',\n    'step5.desc': 'Transparent pricing calculated on the actual sculpture volume.',\n    'price.eyebrow': 'Indicative estimate',\n    'price.range': 'Range',\n    'recap.marble': 'Marble',\n    'recap.finish': 'Finish',\n    'recap.dim': 'Dimensions (W\u00d7H\u00d7D)',\n    'recap.vol': 'Sculpture volume',\n    'recap.weight': 'Estimated weight',\n    'recap.delay': 'Estimated lead time',\n    'breakdown.title': 'Breakdown',\n    'breakdown.matiere': 'Material (volume \u00d7 marble rate)',\n    'breakdown.finition': 'Finish (Robot + Hand)',\n    'breakdown.setup': 'Setup, AI design, logistics',\n    'breakdown.total': 'Total excl. VAT',\n    'monumental.text': '<b>Monumental piece.<\/b> For projects above 400,000 CHF, the estimate above is purely indicative. The final price is negotiated case by case depending on context (sponsorship, public exhibition, institutional partnership, production schedule).',\n    'cta.devis': 'Request firm quote',\n    'cta.hint': 'This estimate is indicative. A firm quote includes VAT, delivery and installation.',\n    'devis.mail_subject': 'Sculpture quote',\n    'devis.mail_intro': 'Hello,\\n\\nI would like a firm quote for the following configuration:',\n    'devis.mail_marble': 'Marble',\n    'devis.mail_finish': 'Finish',\n    'devis.mail_dim': 'Dimensions',\n    'devis.mail_vol': 'Sculpture volume',\n    'devis.mail_est': 'Estimate',\n    'devis.mail_contact': 'My contact details',\n    'devis.mail_name': 'Name',\n    'devis.mail_company': 'Organisation',\n    'devis.mail_phone': 'Phone',\n    'devis.mail_thanks': 'Thank you.',\n    'footer.no_marble': 'No marble selected',\n    'footer.marble_chosen': 'Marble: ',\n    'footer.no_finish': 'No finish selected',\n    'footer.partial_r': ' robotic \u2014 choose a hand level',\n    'footer.partial_h': ' hand \u2014 choose a robotic level',\n    'footer.choose_both': 'Choose a robotic and a hand level',\n    'footer.finish_set': 'Finish: ',\n    'footer.vol_sculpt': 'Sculpture volume: ',\n    'footer.preview_ready': 'Preview ready',\n    'footer.total_est': 'Estimated total: ',\n    'btn.back': '\u2190 Back',\n    'btn.next_finish': 'Finish \u2192',\n    'btn.next_dim': 'Dimensions \u2192',\n    'btn.next_viz': '3D View \u2192',\n    'btn.next_price': 'See price \u2192',\n    'btn.next_devis': 'Request quote',\n    'powered_by': 'Powered by',\n    'unit.days': 'days', 'unit.weeks': 'weeks', 'unit.years': 'years',\n    'unit.kg': 'kg', 'unit.t': 't',\n  },\n\n  de: {\n    'login.tagline': 'Skulptur, neu erfunden durch Intelligenz',\n    'login.title': 'Angebotsrechner',\n    'login.subtitle': 'Zugang auf Anfrage',\n    'login.text': 'Dieses Tool ist unseren Architekturpartnern, Designern und qualifizierten Kunden vorbehalten. Um die Exklusivit\u00e4t unseres Angebots zu wahren, wird der Zugang individuell vergeben.',\n    'login.request_title': 'Zugang anfragen',\n    'login.request_1': 'Ihr Name und Unternehmen',\n    'login.request_2': 'Art Ihres Projekts',\n    'login.request_3': 'Warum Sie den Rechner ben\u00f6tigen',\n    'login.cta': 'Zugang anfragen',\n    'login.toggle_code': 'Ich habe bereits einen Zugangscode',\n    'login.placeholder': 'Zugangscode',\n    'login.submit': 'Eingeben',\n    'login.err_empty': 'Bitte geben Sie einen Code ein.',\n    'login.err_wrong': 'Falscher Code.',\n    'login.mail_subject': 'Anfrage Zugang Angebotsrechner',\n    'login.mail_body': 'Guten Tag,\\n\\nName \/ Unternehmen: \\nProjekt: \\nBedarf: \\n\\nVielen Dank.',\n    'header.title': 'Angebotsrechner',\n    'header.logout': 'Abmelden',\n    'header.subtitle': 'Konfigurieren Sie Ihre Marmorskulptur',\n    'header.subtitle_2': 'Sofortige CHF-Sch\u00e4tzung',\n    'header.badge': 'Online-Angebot',\n    'step1.label': 'Marmor', 'step1.sub': 'W\u00e4hlen',\n    'step2.label': 'Finish', 'step2.sub': 'Robot+Hand',\n    'step3.label': 'Masse', 'step3.sub': 'Gr\u00f6sse in m',\n    'step4.label': '3D-Ansicht', 'step4.sub': 'Vorschau',\n    'step5.label': 'Preis', 'step5.sub': 'Sch\u00e4tzung',\n    'step1.title': 'W\u00e4hlen Sie Ihren Marmor',\n    'step1.desc': 'Jeder Marmor hat einen eigenen Charakter und einen eigenen Preis.',\n    'tier.standard': 'Standard', 'tier.standardp': 'Standard+',\n    'tier.premium': 'Premium', 'tier.premiump': 'Premium+',\n    'tier.luxe': 'Luxus', 'tier.luxep': 'Luxus+',\n    'step2.title': 'Finish-Stufen',\n    'step2.desc': 'Der Roboter fr\u00e4st die gesamte Skulptur. Das Handfinish kommt zus\u00e4tzlich dazu.',\n    'step2.r_title': 'Roboter-Finish',\n    'step2.r_sub': '\u2014 Basis (immer inbegriffen)',\n    'step2.h_title': 'Handfinish',\n    'step2.h_sub': '\u2014 additiv erg\u00e4nzend',\n    'step2.tip': 'Das Handfinish <b>kommt<\/b> zur Roboterarbeit hinzu. Unsere Handwerker in Alle (Jura) vollenden die Details.',\n    'r1.name': 'Rohbau', 'r1.desc': 'Roher CNC-Fr\u00e4sung \u00b7 sichtbare Werkzeugspuren',\n    'r2.name': 'Halbfertig', 'r2.desc': 'Roboter-Schliff K\u00f6rnung 60-120 \u00b7 matte glatte Oberfl\u00e4che',\n    'r3.name': 'Poliert', 'r3.desc': 'Roboter-Wasserstrahlpolitur \u00b7 halbgl\u00e4nzend',\n    'h0.name': 'Ohne Hand', 'h0.desc': 'Keine manuelle Bearbeitung \u00b7 nur Roboter',\n    'h1.name': 'Leicht', 'h1.desc': 'Handschliff K\u00f6rnung 120-240 \u00b7 Oberfl\u00e4chendetails',\n    'h2.name': 'Standard', 'h2.desc': 'Handfinish 240-800 \u00b7 Gesichter, Faltenwurf, Reliefs',\n    'h3.name': 'Komplett', 'h3.desc': 'Spiegelpolitur K\u00f6rnung 3000 \u00b7 Galeriequalit\u00e4t',\n    'step3.title': 'Skulpturmasse',\n    'step3.desc': 'Geben Sie die Bounding-Box in Metern ein und w\u00e4hlen Sie den passenden Objekttyp.',\n    'step3.width': 'Breite', 'step3.height': 'H\u00f6he', 'step3.depth': 'Tiefe',\n    'step3.shape_label': 'Objekttyp',\n    'step3.bbox': 'Bounding-Volumen',\n    'step3.real_vol': 'Skulpturvolumen',\n    'step3.weight': 'Gesch\u00e4tztes Gewicht',\n    'shape.figure': 'Stehende Figur',\n    'shape.buste': 'B\u00fcste \/ Portr\u00e4t',\n    'shape.organique': 'Tier \/ organisch',\n    'shape.bloc': 'Block \/ geometrisch',\n    'step4.title': 'Vorschau',\n    'step4.desc': 'Importieren Sie Ihre 3D-Datei zur rotierenden Anzeige oder schauen Sie die schematische Vorschau an.',\n    'upload.title': '3D-Datei importieren',\n    'upload.optional': '(optional)',\n    'upload.hint': 'Um Ihre Skulptur in 3D mit dem gew\u00e4hlten Marmor zu visualisieren.',\n    'upload.hint_2': 'Die Berechnung basiert weiterhin auf den im vorigen Schritt eingegebenen Massen.',\n    'upload.formats': 'STL \u00b7 OBJ',\n    'upload.remove': 'Entfernen',\n    'upload.err_format': 'Format nicht unterst\u00fctzt. Bitte STL- oder OBJ-Datei verwenden.',\n    'upload.err_size': 'Datei zu gross (max. 100 MB).',\n    'upload.err_invalid': 'Datei kann nicht gelesen werden. Bitte g\u00fcltige STL- oder OBJ-Datei pr\u00fcfen.',\n    'upload.stats_suffix': 'nur Vorschau (Berechnung basiert auf Ihren Massen)',\n    'viz.hint': 'Ziehen zum Drehen',\n    'viz.mode_preview': 'Schematische Vorschau',\n    'viz.mode_3d': '3D-Vorschau',\n    'viz.dims_label': 'B \u00d7 H \u00d7 T',\n    'viz.volume_label': 'Volumen',\n    'viz.weight_label': 'Gewicht',\n    'viz.marble_label': 'Marmor',\n    'viz.bloc_default': 'Generischer Block',\n    'step5.title': 'Ihre Sch\u00e4tzung',\n    'step5.desc': 'Transparenter Preis berechnet auf dem tats\u00e4chlichen Skulpturvolumen.',\n    'price.eyebrow': 'Richtwert',\n    'price.range': 'Spanne',\n    'recap.marble': 'Marmor',\n    'recap.finish': 'Finish',\n    'recap.dim': 'Masse (B\u00d7H\u00d7T)',\n    'recap.vol': 'Skulpturvolumen',\n    'recap.weight': 'Gesch\u00e4tztes Gewicht',\n    'recap.delay': 'Gesch\u00e4tzte Lieferzeit',\n    'breakdown.title': 'Aufschl\u00fcsselung',\n    'breakdown.matiere': 'Material (Volumen \u00d7 Marmortarif)',\n    'breakdown.finition': 'Finish (Roboter + Hand)',\n    'breakdown.setup': 'Setup, KI-Design, Logistik',\n    'breakdown.total': 'Total exkl. MwSt.',\n    'monumental.text': '<b>Monumentales Werk.<\/b> Bei Projekten \u00fcber 400 000 CHF ist die obige Sch\u00e4tzung rein indikativ. Der Endpreis wird je nach Kontext (Sponsoring, \u00f6ffentliche Ausstellung, institutionelle Partnerschaft, Produktionsplan) individuell verhandelt.',\n    'cta.devis': 'Festes Angebot anfragen',\n    'cta.hint': 'Diese Sch\u00e4tzung ist indikativ. Ein festes Angebot enth\u00e4lt MwSt., Lieferung und Aufstellung.',\n    'devis.mail_subject': 'Skulptur-Angebot',\n    'devis.mail_intro': 'Guten Tag,\\n\\nich m\u00f6chte ein festes Angebot f\u00fcr folgende Konfiguration:',\n    'devis.mail_marble': 'Marmor',\n    'devis.mail_finish': 'Finish',\n    'devis.mail_dim': 'Masse',\n    'devis.mail_vol': 'Skulpturvolumen',\n    'devis.mail_est': 'Sch\u00e4tzung',\n    'devis.mail_contact': 'Meine Kontaktdaten',\n    'devis.mail_name': 'Name',\n    'devis.mail_company': 'Unternehmen',\n    'devis.mail_phone': 'Telefon',\n    'devis.mail_thanks': 'Vielen Dank.',\n    'footer.no_marble': 'Kein Marmor ausgew\u00e4hlt',\n    'footer.marble_chosen': 'Marmor: ',\n    'footer.no_finish': 'Kein Finish ausgew\u00e4hlt',\n    'footer.partial_r': ' Roboter \u2014 bitte Hand-Stufe w\u00e4hlen',\n    'footer.partial_h': ' Hand \u2014 bitte Roboter-Stufe w\u00e4hlen',\n    'footer.choose_both': 'W\u00e4hlen Sie eine Roboter- und eine Hand-Stufe',\n    'footer.finish_set': 'Finish: ',\n    'footer.vol_sculpt': 'Skulpturvolumen: ',\n    'footer.preview_ready': 'Vorschau bereit',\n    'footer.total_est': 'Gesch\u00e4tztes Total: ',\n    'btn.back': '\u2190 Zur\u00fcck',\n    'btn.next_finish': 'Finish \u2192',\n    'btn.next_dim': 'Masse \u2192',\n    'btn.next_viz': '3D-Ansicht \u2192',\n    'btn.next_price': 'Preis ansehen \u2192',\n    'btn.next_devis': 'Angebot anfragen',\n    'powered_by': 'Bereitgestellt von',\n    'unit.days': 'Tage', 'unit.weeks': 'Wochen', 'unit.years': 'Jahre',\n    'unit.kg': 'kg', 'unit.t': 't',\n  },\n\n  it: {\n    'login.tagline': 'La scultura, reinventata dall\\'intelligenza',\n    'login.title': 'Calcolatore di preventivi',\n    'login.subtitle': 'Accesso su richiesta',\n    'login.text': 'Questo strumento \u00e8 riservato ai nostri partner architetti, designer e clienti qualificati. Per preservare l\\'esclusivit\u00e0 della nostra offerta, l\\'accesso viene concesso caso per caso.',\n    'login.request_title': 'Per richiedere l\\'accesso',\n    'login.request_1': 'Il vostro nome e ente',\n    'login.request_2': 'Natura del vostro progetto',\n    'login.request_3': 'Perch\u00e9 vi serve il calcolatore',\n    'login.cta': 'Richiedere l\\'accesso',\n    'login.toggle_code': 'Ho gi\u00e0 un codice di accesso',\n    'login.placeholder': 'Codice di accesso',\n    'login.submit': 'Entrare',\n    'login.err_empty': 'Inserire un codice.',\n    'login.err_wrong': 'Codice errato.',\n    'login.mail_subject': 'Richiesta accesso al calcolatore',\n    'login.mail_body': 'Buongiorno,\\n\\nNome \/ Ente: \\nProgetto: \\nNecessit\u00e0: \\n\\nGrazie.',\n    'header.title': 'Calcolatore di preventivi',\n    'header.logout': 'Disconnessione',\n    'header.subtitle': 'Configurate la vostra scultura in marmo',\n    'header.subtitle_2': 'Stima istantanea in CHF',\n    'header.badge': 'Preventivo online',\n    'step1.label': 'Marmo', 'step1.sub': 'Scegliere',\n    'step2.label': 'Finitura', 'step2.sub': 'Robot+Mano',\n    'step3.label': 'Dimensioni', 'step3.sub': 'Misura in m',\n    'step4.label': 'Vista 3D', 'step4.sub': 'Anteprima',\n    'step5.label': 'Prezzo', 'step5.sub': 'Stima',\n    'step1.title': 'Scegliete il vostro marmo',\n    'step1.desc': 'Ogni marmo ha un carattere unico e una tariffa distinta.',\n    'tier.standard': 'Standard', 'tier.standardp': 'Standard+',\n    'tier.premium': 'Premium', 'tier.premiump': 'Premium+',\n    'tier.luxe': 'Lusso', 'tier.luxep': 'Lusso+',\n    'step2.title': 'Livelli di finitura',\n    'step2.desc': 'Il robot fresa sempre la totalit\u00e0. La finitura a mano si aggiunge come complemento.',\n    'step2.r_title': 'Finitura robotica',\n    'step2.r_sub': '\u2014 base (sempre inclusa)',\n    'step2.h_title': 'Finitura a mano',\n    'step2.h_sub': '\u2014 aggiuntiva',\n    'step2.tip': 'La finitura a mano <b>si aggiunge<\/b> al robot. I nostri artigiani di Alle (Giura) completano i dettagli.',\n    'r1.name': 'Sbozzo', 'r1.desc': 'Fresatura CNC grezza \u00b7 tracce d\\'utensile visibili',\n    'r2.name': 'Semi-finita', 'r2.desc': 'Levigatura robot grana 60-120 \u00b7 superficie opaca liscia',\n    'r3.name': 'Levigata', 'r3.desc': 'Lucidatura robot getto d\\'acqua \u00b7 semi-lucida',\n    'h0.name': 'Senza mano', 'h0.desc': 'Nessun intervento manuale \u00b7 solo robot',\n    'h1.name': 'Leggera', 'h1.desc': 'Levigatura a mano grana 120-240 \u00b7 dettagli di superficie',\n    'h2.name': 'Standard', 'h2.desc': 'Finitura a mano 240-800 \u00b7 volti, drappeggi, rilievi',\n    'h3.name': 'Completa', 'h3.desc': 'Lucidatura a specchio grana 3000 \u00b7 qualit\u00e0 galleria',\n    'step3.title': 'Dimensioni della scultura',\n    'step3.desc': 'Inserite la scatola di ingombro in metri e scegliete il tipo di oggetto pi\u00f9 vicino.',\n    'step3.width': 'Larghezza', 'step3.height': 'Altezza', 'step3.depth': 'Profondit\u00e0',\n    'step3.shape_label': 'Tipo di oggetto',\n    'step3.bbox': 'Volume bbox',\n    'step3.real_vol': 'Volume scultura',\n    'step3.weight': 'Peso stimato',\n    'shape.figure': 'Figura umana',\n    'shape.buste': 'Busto \/ ritratto',\n    'shape.organique': 'Animale \/ organico',\n    'shape.bloc': 'Blocco \/ geometrico',\n    'step4.title': 'Anteprima',\n    'step4.desc': 'Importate il vostro file 3D per visualizzarlo in rotazione, o consultate l\\'anteprima schematica dell\\'ingombro.',\n    'upload.title': 'Importate il vostro file 3D',\n    'upload.optional': '(opzionale)',\n    'upload.hint': 'Per visualizzare la vostra scultura in 3D nel tono del marmo scelto.',\n    'upload.hint_2': 'Il calcolo resta basato sulle dimensioni inserite nel passaggio precedente.',\n    'upload.formats': 'STL \u00b7 OBJ',\n    'upload.remove': 'Rimuovere',\n    'upload.err_format': 'Formato non supportato. Usate un file STL o OBJ.',\n    'upload.err_size': 'File troppo grande (max 100 MB).',\n    'upload.err_invalid': 'Impossibile leggere il file. Verificate che sia un STL o OBJ valido.',\n    'upload.stats_suffix': 'solo anteprima (il calcolo resta basato sulle vostre dimensioni)',\n    'viz.hint': 'Trascinate per ruotare',\n    'viz.mode_preview': 'Anteprima schematica',\n    'viz.mode_3d': 'Anteprima 3D',\n    'viz.dims_label': 'L \u00d7 A \u00d7 P',\n    'viz.volume_label': 'Volume',\n    'viz.weight_label': 'Peso',\n    'viz.marble_label': 'Marmo',\n    'viz.bloc_default': 'Blocco generico',\n    'step5.title': 'La vostra stima',\n    'step5.desc': 'Tariffa trasparente calcolata sul volume reale della scultura.',\n    'price.eyebrow': 'Stima indicativa',\n    'price.range': 'Forbice',\n    'recap.marble': 'Marmo',\n    'recap.finish': 'Finitura',\n    'recap.dim': 'Dimensioni (L\u00d7A\u00d7P)',\n    'recap.vol': 'Volume scultura',\n    'recap.weight': 'Peso stimato',\n    'recap.delay': 'Tempi stimati',\n    'breakdown.title': 'Scomposizione',\n    'breakdown.matiere': 'Materia (volume \u00d7 tariffa marmo)',\n    'breakdown.finition': 'Finitura (Robot + Mano)',\n    'breakdown.setup': 'Setup, design IA, logistica',\n    'breakdown.total': 'Totale IVA escl.',\n    'monumental.text': '<b>Pezzo monumentale.<\/b> Per progetti superiori a 400 000 CHF, la stima sopra \u00e8 puramente indicativa. Il prezzo finale \u00e8 negoziato caso per caso secondo il contesto (sponsorizzazione, esposizione pubblica, partenariato istituzionale, pianificazione di produzione).',\n    'cta.devis': 'Richiedere preventivo fermo',\n    'cta.hint': 'Questa stima \u00e8 indicativa. Un preventivo fermo include IVA, consegna e installazione.',\n    'devis.mail_subject': 'Preventivo scultura',\n    'devis.mail_intro': 'Buongiorno,\\n\\nDesidero un preventivo fermo per la seguente configurazione:',\n    'devis.mail_marble': 'Marmo',\n    'devis.mail_finish': 'Finitura',\n    'devis.mail_dim': 'Dimensioni',\n    'devis.mail_vol': 'Volume scultura',\n    'devis.mail_est': 'Stima',\n    'devis.mail_contact': 'I miei dati di contatto',\n    'devis.mail_name': 'Nome',\n    'devis.mail_company': 'Ente',\n    'devis.mail_phone': 'Telefono',\n    'devis.mail_thanks': 'Grazie.',\n    'footer.no_marble': 'Nessun marmo selezionato',\n    'footer.marble_chosen': 'Marmo: ',\n    'footer.no_finish': 'Nessuna finitura selezionata',\n    'footer.partial_r': ' robotica \u2014 scegliere un livello a mano',\n    'footer.partial_h': ' a mano \u2014 scegliere un livello robotico',\n    'footer.choose_both': 'Scegliere un livello robotico e uno a mano',\n    'footer.finish_set': 'Finitura: ',\n    'footer.vol_sculpt': 'Volume scultura: ',\n    'footer.preview_ready': 'Anteprima pronta',\n    'footer.total_est': 'Totale stimato: ',\n    'btn.back': '\u2190 Indietro',\n    'btn.next_finish': 'Finitura \u2192',\n    'btn.next_dim': 'Dimensioni \u2192',\n    'btn.next_viz': 'Vista 3D \u2192',\n    'btn.next_price': 'Vedere il prezzo \u2192',\n    'btn.next_devis': 'Richiedere preventivo',\n    'powered_by': 'Realizzato da',\n    'unit.days': 'giorni', 'unit.weeks': 'settimane', 'unit.years': 'anni',\n    'unit.kg': 'kg', 'unit.t': 't',\n  }\n};\n\nlet CURRENT_LANG = 'fr';\nfunction t(key) {\n  return (I18N[CURRENT_LANG] && I18N[CURRENT_LANG][key]) || (I18N.fr[key]) || key;\n}\nfunction setLang(lang) {\n  if (!I18N[lang]) return;\n  CURRENT_LANG = lang;\n  try { localStorage.setItem('ikonera_lang', lang); } catch(e) {}\n  applyI18n();\n  \/\/ Re-rendre les composants dynamiques\n  if (typeof renderMarbles === 'function') renderMarbles();\n  if (typeof renderFinishR === 'function') renderFinishR();\n  if (typeof renderFinishH === 'function') renderFinishH();\n  if (typeof renderShapeTypes === 'function') renderShapeTypes();\n  if (typeof updateFooter === 'function') updateFooter();\n  if (STATE && STATE.step === 5) renderPrice();\n  \/\/ Update Locale CHF formatting\n  if (typeof renderViz === 'function') renderViz();\n  \/\/ Update mailto links\n  document.querySelectorAll('[data-i18n-mailto]').forEach(a => {\n    const tpl = a.dataset.i18nMailto;\n    if (tpl === 'login') {\n      a.href = 'mailto:contact@ikonera.art?subject=' +\n        encodeURIComponent(t('login.mail_subject')) +\n        '&body=' + encodeURIComponent(t('login.mail_body'));\n    }\n  });\n  \/\/ Highlight active button\n  document.querySelectorAll('.lang-btn').forEach(b => {\n    b.classList.toggle('active', b.dataset.lang === lang);\n  });\n}\nfunction detectLang() {\n  try {\n    const saved = localStorage.getItem('ikonera_lang');\n    if (saved && I18N[saved]) return saved;\n    const nav = (navigator.language || 'fr').slice(0, 2).toLowerCase();\n    if (I18N[nav]) return nav;\n  } catch(e) {}\n  return 'fr';\n}\nfunction applyI18n() {\n  document.documentElement.lang = CURRENT_LANG;\n  document.querySelectorAll('[data-i18n]').forEach(el => {\n    const key = el.dataset.i18n;\n    const val = t(key);\n    if (el.dataset.i18nHtml === 'true' || val.indexOf('<') !== -1) {\n      el.innerHTML = val;\n    } else {\n      el.textContent = val;\n    }\n  });\n  document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {\n    el.placeholder = t(el.dataset.i18nPlaceholder);\n  });\n  document.querySelectorAll('[data-i18n-aria]').forEach(el => {\n    el.setAttribute('aria-label', t(el.dataset.i18nAria));\n  });\n}\n\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   IKONERA \u00b7 CALCULATEUR\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n\nconst MARBLES = [\n  { id: 'bianco_carrara',  name: 'Bianco Carrara',  tier: 'Standard',  tierClass: 'standard',  price: 38,  density: 2700, c1: 'var(--m-bianco-carrara-1)',  c2: 'var(--m-bianco-carrara-2)' },\n  { id: 'bianco_venatino', name: 'Bianco Venatino', tier: 'Standard+', tierClass: 'standardp', price: 47,  density: 2720, c1: 'var(--m-bianco-venatino-1)', c2: 'var(--m-bianco-venatino-2)' },\n  { id: 'statuario',       name: 'Statuario Extra', tier: 'Luxe',      tierClass: 'luxe',      price: 85,  density: 2700, c1: 'var(--m-statuario-1)',       c2: 'var(--m-statuario-2)' },\n  { id: 'calacatta',       name: 'Calacatta Gold',  tier: 'Luxe+',     tierClass: 'luxep',     price: 113, density: 2710, c1: 'var(--m-calacatta-1)',       c2: 'var(--m-calacatta-2)' },\n  { id: 'arabescato',      name: 'Arabescato Vagli',tier: 'Premium',   tierClass: 'premium',   price: 52,  density: 2700, c1: 'var(--m-arabescato-1)',      c2: 'var(--m-arabescato-2)' },\n  { id: 'bardiglio',       name: 'Bardiglio Imp.',  tier: 'Standard+', tierClass: 'standardp', price: 42,  density: 2750, c1: 'var(--m-bardiglio-1)',       c2: 'var(--m-bardiglio-2)' },\n  { id: 'cipollino',       name: 'Cipollino Verde', tier: 'Premium',   tierClass: 'premium',   price: 61,  density: 2780, c1: 'var(--m-cipollino-1)',       c2: 'var(--m-cipollino-2)' },\n  { id: 'nero_marquinia',  name: 'Nero Marquinia',  tier: 'Premium',   tierClass: 'premium',   price: 56,  density: 2780, c1: 'var(--m-nero-marquinia-1)',  c2: 'var(--m-nero-marquinia-2)' },\n  { id: 'rosso_levanto',   name: 'Rosso Levanto',   tier: 'Premium+',  tierClass: 'premiump',  price: 71,  density: 2800, c1: 'var(--m-rosso-levanto-1)',   c2: 'var(--m-rosso-levanto-2)' },\n  { id: 'portoro',         name: 'Portoro Oro',     tier: 'Luxe',      tierClass: 'luxe',      price: 103, density: 2780, c1: 'var(--m-portoro-1)',         c2: 'var(--m-portoro-2)' },\n  { id: 'verde_alpi',      name: 'Verde Alpi',      tier: 'Premium',   tierClass: 'premium',   price: 66,  density: 2810, c1: 'var(--m-verde-alpi-1)',      c2: 'var(--m-verde-alpi-2)' },\n  { id: 'giallo_siena',    name: 'Giallo Siena',    tier: 'Premium',   tierClass: 'premium',   price: 75,  density: 2750, c1: 'var(--m-giallo-siena-1)',    c2: 'var(--m-giallo-siena-2)' },\n  { id: 'paonazzo',        name: 'Paonazzo',        tier: 'Luxe',      tierClass: 'luxe',      price: 89,  density: 2720, c1: 'var(--m-paonazzo-1)',        c2: 'var(--m-paonazzo-2)' },\n];\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   FINITION ROBOTIQUE (R) \u2014 toujours appliqu\u00e9e, multiplicateur sur mati\u00e8re\n   FINITION MAIN (H) \u2014 additive, en heures \u00d7 tarif main\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst FINISH_R = [\n  { id: 'R1', name: '\u00c9bauche',    desc: 'Fraisage CNC brut \u00b7 traces d\\'outil visibles',           mult: 0.85 },\n  { id: 'R2', name: 'Semi-finie', desc: 'Pon\u00e7age robot grain 60-120 \u00b7 surface mate lisse',        mult: 1.00 },\n  { id: 'R3', name: 'Polie',      desc: 'Polissage robot jet d\\'eau \u00b7 semi-brillant',             mult: 1.10 }\n];\n\nconst FINISH_H = [\n  { id: 'H0', name: 'Sans main', desc: 'Aucune intervention manuelle \u00b7 robot seul',                hPow: 0   },\n  { id: 'H1', name: 'L\u00e9g\u00e8re',    desc: 'Pon\u00e7age main grain 120-240 \u00b7 d\u00e9tails de surface',          hPow: 80  },\n  { id: 'H2', name: 'Standard',  desc: 'Finition main 240-800 \u00b7 visages, drap\u00e9s, reliefs',         hPow: 200 },\n  { id: 'H3', name: 'Compl\u00e8te',  desc: 'Polissage miroir grain 3000 \u00b7 qualit\u00e9 galerie',            hPow: 450 }\n];\n\n\/* \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n   TYPES D'OBJET \u2014 coefficient mati\u00e8re + dimensions par d\u00e9faut\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 *\/\nconst SHAPE_TYPES = [\n  {\n    id: 'figure', name: 'Figure humaine', sub: '\u2248 35 %', coef: 0.35,\n    defaultDim: { w: 0.50, h: 1.70, d: 0.40 },\n    icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"12\" cy=\"5\" r=\"2.2\"\/><path d=\"M12 7.5v6m-3 0h6m-5 0L8 22m6-8.5L16 22\"\/><\/svg>'\n  },\n  {\n    id: 'buste', name: 'Buste \/ portrait', sub: '\u2248 55 %', coef: 0.55,\n    defaultDim: { w: 0.45, h: 0.65, d: 0.40 },\n    icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><circle cx=\"12\" cy=\"8\" r=\"4\"\/><path d=\"M5 22c0-4 3-7 7-7s7 3 7 7\"\/><\/svg>'\n  },\n  {\n    id: 'organique', name: 'Animal \/ organique', sub: '\u2248 45 %', coef: 0.45,\n    defaultDim: { w: 0.80, h: 0.60, d: 0.50 },\n    icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M5 14c0-3.5 3-6 7-6s7 2.5 7 6c0 2-1 3.5-2.5 4.5L14 22h-4l-2.5-3.5C6 17.5 5 16 5 14z\"\/><circle cx=\"9\" cy=\"13\" r=\"0.7\" fill=\"currentColor\"\/><circle cx=\"15\" cy=\"13\" r=\"0.7\" fill=\"currentColor\"\/><\/svg>'\n  },\n  {\n    id: 'bloc', name: 'Bloc \/ g\u00e9om\u00e9trique', sub: '\u2248 80 %', coef: 0.80,\n    defaultDim: { w: 0.60, h: 1.00, d: 0.40 },\n    icon: '<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.5\"><path d=\"M3 7l9-4 9 4-9 4-9-4z\"\/><path d=\"M3 7v10l9 4 9-4V7\"\/><path d=\"M12 11v10\"\/><\/svg>'\n  }\n];\n\nconst CONFIG = {\n  setupBase: 2400,            \/\/ CHF\n  setupPerM3: 1500,           \/\/ CHF\/m\u00b3 bbox\n  rateHand:  240,             \/\/ CHF\/h finition main\n  rangeLow:  0.85,\n  rangeHigh: 1.18,\n  baseDays:  20,\n  daysPerM3: 25,\n  monumentalThreshold: 400000,\n  minPrice: 3500\n};\n\nconst STATE = {\n  step: 1,\n  marble: null,\n  finishR: null,                                         \/\/ { id, name, mult, \u2026 }\n  finishH: null,                                         \/\/ { id, name, hPow, \u2026 }\n  dim: { w: 0.5, h: 0.8, d: 0.5 },\n  shapeType: SHAPE_TYPES[0],                             \/\/ par d\u00e9faut \"Figure humaine\"\n  mesh: null\n};\n\n\/* \u2500\u2500\u2500 Format helpers \u2500\u2500\u2500 *\/\nconst fmt = (n, dec = 0) => isFinite(n)\n  ? Number(n).toLocaleString('fr-CH', { minimumFractionDigits: dec, maximumFractionDigits: dec }) : '\u2014';\nconst fmtCHF = (n) => fmt(Math.round(n \/ 10) * 10) + ' CHF';\nconst fmtVol = (m3) => {\n  if (m3 < 0.001) return fmt(m3 * 1e6, 0) + ' cm\u00b3';\n  if (m3 < 1)     return fmt(m3, 3).replace(\/0+$\/, '').replace(\/\\.$\/, '') + ' m\u00b3';\n  return fmt(m3, 2) + ' m\u00b3';\n};\nconst fmtKg = (kg) => kg < 1000 ? fmt(kg, 0) + ' ' + t('unit.kg') : fmt(kg \/ 1000, 2) + ' ' + t('unit.t');\nconst fmtDays = (d) => d < 30 ? d + ' ' + t('unit.days') : d < 365 ? Math.round(d \/ 7) + ' ' + t('unit.weeks') : (d \/ 365).toFixed(1) + ' ' + t('unit.years');\n\n\/* \u2500\u2500\u2500 Compute \u2500\u2500\u2500 *\/\nfunction compute() {\n  const m = STATE.marble, fr = STATE.finishR, fh = STATE.finishH;\n  const d = STATE.dim;\n  \/\/ Le calcul est TOUJOURS bas\u00e9 sur les dimensions saisies + type d'objet\n  \/\/ Le mesh upload\u00e9 ne sert qu'\u00e0 la pr\u00e9visualisation visuelle 3D\n  const bbox = d.w * d.h * d.d;\n  const real = bbox * STATE.shapeType.coef;\n  const weight = m ? real * m.density : 0;\n\n  \/\/ Mati\u00e8re : volume r\u00e9el \u00d7 tarif marbre (k CHF\/m\u00b3 \u2192 \u00d71000)\n  const matiere = m ? real * (m.price * 1000) : 0;\n\n  \/\/ Finition robotique (R) : multiplicateur sur mati\u00e8re\n  const prixR = matiere * (fr ? fr.mult : 1.0);\n  const surcoutR = prixR - matiere;\n\n  \/\/ Finition main (H) : heures = base \u00d7 vol_r\u00e9el^(2\/3) \u00d7 tarif main\n  const heuresMain = fh ? fh.hPow * Math.pow(Math.max(real, 0.001), 2\/3) : 0;\n  const prixH = heuresMain * CONFIG.rateHand;\n\n  \/\/ Setup forfaitaire + variable\n  const setup = CONFIG.setupBase + bbox * CONFIG.setupPerM3;\n\n  let total = prixR + prixH + setup;\n  total = Math.max(total, CONFIG.minPrice);\n\n  \/\/ D\u00e9lai\n  const daysMain = heuresMain \/ 8;\n  const daysRobot = bbox * 12;\n  const days = Math.ceil(CONFIG.baseDays + daysRobot + daysMain * 0.7);\n\n  return {\n    bbox, real, weight,\n    matiere, surcoutR, heuresMain, prixH, prixR, setup,\n    total,\n    totalLow:  total * CONFIG.rangeLow,\n    totalHigh: total * CONFIG.rangeHigh,\n    days,\n    isMonumental: total > CONFIG.monumentalThreshold,\n    hasMesh: !!STATE.mesh\n  };\n}\n\n\/* \u2500\u2500\u2500 Rendu marbres \u2500\u2500\u2500 *\/\nfunction renderMarbles() {\n  const grid = document.getElementById('marbles-grid');\n  grid.innerHTML = MARBLES.map(m => `\n    <div class=\"marble-card ${STATE.marble && STATE.marble.id === m.id ? 'selected' : ''}\" data-id=\"${m.id}\">\n      <div class=\"sphere\" style=\"--c1: ${m.c1}; --c2: ${m.c2};\"><\/div>\n      <div class=\"marble-name\">${m.name}<\/div>\n      <div class=\"marble-meta\">\n        <span class=\"marble-tier ${m.tierClass}\">${t('tier.' + m.tierClass)}<\/span> \u00b7 ${m.price}k\/m\u00b3\n      <\/div>\n    <\/div>\n  `).join('');\n  grid.querySelectorAll('.marble-card').forEach(card => {\n    card.addEventListener('click', () => {\n      STATE.marble = MARBLES.find(x => x.id === card.dataset.id);\n      renderMarbles();\n      updateFooter();\n    });\n  });\n}\n\n\/* \u2500\u2500\u2500 Rendu finitions R + H \u2500\u2500\u2500 *\/\nfunction renderFinishR() {\n  const grid = document.getElementById('finish-r-grid');\n  grid.innerHTML = FINISH_R.map(f => `\n    <div class=\"finish-mini ${STATE.finishR && STATE.finishR.id === f.id ? 'selected' : ''}\" data-id=\"${f.id}\">\n      <div class=\"finish-mini-code\">${f.id}<\/div>\n      <div class=\"finish-mini-name\">${t(f.id.toLowerCase() + '.name')}<\/div>\n      <div class=\"finish-mini-desc\">${t(f.id.toLowerCase() + '.desc')}<\/div>\n    <\/div>\n  `).join('');\n  grid.querySelectorAll('.finish-mini').forEach(card => {\n    card.addEventListener('click', () => {\n      STATE.finishR = FINISH_R.find(x => x.id === card.dataset.id);\n      renderFinishR();\n      updateFooter();\n    });\n  });\n}\nfunction renderFinishH() {\n  const grid = document.getElementById('finish-h-grid');\n  grid.innerHTML = FINISH_H.map(f => `\n    <div class=\"finish-mini ${STATE.finishH && STATE.finishH.id === f.id ? 'selected' : ''}\" data-id=\"${f.id}\">\n      <div class=\"finish-mini-code\">${f.id}<\/div>\n      <div class=\"finish-mini-name\">${t(f.id.toLowerCase() + '.name')}<\/div>\n      <div class=\"finish-mini-desc\">${t(f.id.toLowerCase() + '.desc')}<\/div>\n    <\/div>\n  `).join('');\n  grid.querySelectorAll('.finish-mini').forEach(card => {\n    card.addEventListener('click', () => {\n      STATE.finishH = FINISH_H.find(x => x.id === card.dataset.id);\n      renderFinishH();\n      updateFooter();\n    });\n  });\n}\n\n\/* \u2500\u2500\u2500 Rendu types d'objet \u2500\u2500\u2500 *\/\nfunction renderShapeTypes() {\n  const grid = document.getElementById('shape-types');\n  if (!grid) return;\n  grid.innerHTML = SHAPE_TYPES.map(s => `\n    <div class=\"shape-card ${STATE.shapeType.id === s.id ? 'selected' : ''}\" data-id=\"${s.id}\">\n      <div class=\"shape-icon\">${s.icon}<\/div>\n      <div class=\"shape-name\">${t('shape.' + s.id)}<\/div>\n      <div class=\"shape-coef\">${s.sub}<\/div>\n    <\/div>\n  `).join('');\n  grid.querySelectorAll('.shape-card').forEach(card => {\n    card.addEventListener('click', () => {\n      STATE.shapeType = SHAPE_TYPES.find(x => x.id === card.dataset.id);\n      applyShapeDefaults(STATE.shapeType);\n      renderShapeTypes();\n      updateDimSummary();\n      updateFooter();\n    });\n  });\n}\n\n\/* \u2500\u2500\u2500 Dimensions \u2500\u2500\u2500 *\/\nfunction renderDimensions() {\n  setDimInputs();\n  renderShapeTypes();\n  updateDimSummary();\n}\n\n\/* Met \u00e0 jour input + slider + barre de progression visuelle *\/\nfunction setDimInputs() {\n  ['w', 'h', 'd'].forEach(key => {\n    const v = STATE.dim[key];\n    const inp = document.getElementById('dim-' + key);\n    const sl  = document.getElementById('dim-' + key + '-slider');\n    if (inp) inp.value = v.toFixed(2);\n    if (sl) {\n      sl.value = Math.min(parseFloat(sl.max), v);\n      const pct = ((v - parseFloat(sl.min)) \/ (parseFloat(sl.max) - parseFloat(sl.min))) * 100;\n      sl.style.setProperty('--pct', Math.min(100, Math.max(0, pct)) + '%');\n    }\n  });\n}\n\n\/* Applique les dimensions par d\u00e9faut d'un type d'objet *\/\nfunction applyShapeDefaults(shape) {\n  if (!shape || !shape.defaultDim) return;\n  STATE.dim = {\n    w: shape.defaultDim.w,\n    h: shape.defaultDim.h,\n    d: shape.defaultDim.d\n  };\n  setDimInputs();\n}\nfunction updateDimSummary() {\n  const r = compute();\n  document.getElementById('sum-bbox').textContent   = fmtVol(r.bbox);\n  document.getElementById('sum-real').textContent   = fmtVol(r.real);\n  document.getElementById('sum-weight').textContent = fmtKg(r.weight);\n}\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   PARSING 3D \u00b7 STL (binaire + ASCII) \u00b7 OBJ\n   Calcule bbox + volume r\u00e9el \u00e0 partir des triangles\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n\nfunction parseSTL(buffer) {\n  const view = new DataView(buffer);\n  \/\/ D\u00e9tection ASCII vs binaire : un STL ASCII commence par \"solid\"\n  \/\/ Mais certains binaires commencent aussi par \"solid\", donc on v\u00e9rifie la taille attendue\n  const hdr = new TextDecoder().decode(buffer.slice(0, 5));\n  const isAsciiStart = hdr.toLowerCase() === 'solid';\n  if (isAsciiStart) {\n    if (buffer.byteLength < 84) return parseAsciiSTL(buffer);\n    const triCount = view.getUint32(80, true);\n    const expectedBinarySize = 84 + triCount * 50;\n    if (buffer.byteLength === expectedBinarySize) return parseBinarySTL(view, triCount);\n    return parseAsciiSTL(buffer);\n  }\n  return parseBinarySTL(view, view.getUint32(80, true));\n}\n\nfunction parseBinarySTL(view, triCount) {\n  const triangles = new Float32Array(triCount * 9);\n  let off = 84;\n  for (let i = 0; i < triCount; i++) {\n    off += 12; \/\/ skip normal\n    for (let j = 0; j < 9; j++) {\n      triangles[i * 9 + j] = view.getFloat32(off, true);\n      off += 4;\n    }\n    off += 2; \/\/ skip attribute byte count\n  }\n  return triangles;\n}\n\nfunction parseAsciiSTL(buffer) {\n  const text = new TextDecoder().decode(buffer);\n  const verts = [];\n  const re = \/vertex\\s+([-\\d.eE+]+)\\s+([-\\d.eE+]+)\\s+([-\\d.eE+]+)\/g;\n  let m;\n  while ((m = re.exec(text)) !== null) {\n    verts.push(parseFloat(m[1]), parseFloat(m[2]), parseFloat(m[3]));\n  }\n  return new Float32Array(verts);\n}\n\nfunction parseOBJ(buffer) {\n  const text = new TextDecoder().decode(buffer);\n  const vertices = []; \/\/ tous les vertex d\u00e9clar\u00e9s (1-index\u00e9)\n  const triangles = [];\n  const lines = text.split(\/\\r?\\n\/);\n  for (const line of lines) {\n    if (line.startsWith('v ')) {\n      const p = line.split(\/\\s+\/);\n      vertices.push([parseFloat(p[1]), parseFloat(p[2]), parseFloat(p[3])]);\n    } else if (line.startsWith('f ')) {\n      const parts = line.split(\/\\s+\/).slice(1).map(t => parseInt(t.split('\/')[0], 10) - 1);\n      \/\/ Triangulation des faces n-gonales (fan)\n      for (let i = 1; i < parts.length - 1; i++) {\n        const a = vertices[parts[0]], b = vertices[parts[i]], c = vertices[parts[i + 1]];\n        if (a && b && c) {\n          triangles.push(a[0], a[1], a[2], b[0], b[1], b[2], c[0], c[1], c[2]);\n        }\n      }\n    }\n  }\n  return new Float32Array(triangles);\n}\n\n\/* Calcule bbox et volume sign\u00e9 d'un mesh (triangles flat array, 9 floats par triangle) *\/\nfunction meshAnalyze(triangles) {\n  let minX=Infinity, minY=Infinity, minZ=Infinity;\n  let maxX=-Infinity, maxY=-Infinity, maxZ=-Infinity;\n  let vol = 0;\n  for (let i = 0; i < triangles.length; i += 9) {\n    const ax = triangles[i],   ay = triangles[i+1], az = triangles[i+2];\n    const bx = triangles[i+3], by = triangles[i+4], bz = triangles[i+5];\n    const cx = triangles[i+6], cy = triangles[i+7], cz = triangles[i+8];\n    if (ax<minX) minX=ax; if (ax>maxX) maxX=ax;\n    if (ay<minY) minY=ay; if (ay>maxY) maxY=ay;\n    if (az<minZ) minZ=az; if (az>maxZ) maxZ=az;\n    if (bx<minX) minX=bx; if (bx>maxX) maxX=bx;\n    if (by<minY) minY=by; if (by>maxY) maxY=by;\n    if (bz<minZ) minZ=bz; if (bz>maxZ) maxZ=bz;\n    if (cx<minX) minX=cx; if (cx>maxX) maxX=cx;\n    if (cy<minY) minY=cy; if (cy>maxY) maxY=cy;\n    if (cz<minZ) minZ=cz; if (cz>maxZ) maxZ=cz;\n    \/\/ Volume sign\u00e9 = sum of signed tetrahedron volumes from origin\n    vol += (ax*(by*cz - cy*bz) - ay*(bx*cz - cx*bz) + az*(bx*cy - cx*by)) \/ 6;\n  }\n  return {\n    bbox: {\n      min: [minX, minY, minZ], max: [maxX, maxY, maxZ],\n      w: maxX - minX, h: maxY - minY, d: maxZ - minZ\n    },\n    volume: Math.abs(vol),\n    triangleCount: triangles.length \/ 9\n  };\n}\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   UNIT DETECTION\n   Les fichiers STL\/OBJ n'ont pas d'unit\u00e9. Heuristique :\n   - Si bbox max < 100 \u2192 unit\u00e9 = m\u00e8tre (d\u00e9j\u00e0 OK)\n   - Si bbox max entre 100 et 5000 \u2192 unit\u00e9 = millim\u00e8tre (diviser par 1000)\n   - Si bbox max > 5000 \u2192 erreur ou \u00e9chelle exotique (divise par 1000 par d\u00e9faut)\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\nfunction normalizeMeshScale(analysis) {\n  const max = Math.max(analysis.bbox.w, analysis.bbox.h, analysis.bbox.d);\n  let scale = 1;\n  let unit = 'm';\n  if (max > 100) { scale = 0.001; unit = 'mm'; }\n  else if (max < 0.01) { scale = 100; unit = 'cm'; }\n  \/\/ Re-scale bbox + volume\n  return {\n    bbox: {\n      w: analysis.bbox.w * scale,\n      h: analysis.bbox.h * scale,\n      d: analysis.bbox.d * scale\n    },\n    volume: analysis.volume * Math.pow(scale, 3),\n    triangleCount: analysis.triangleCount,\n    unit, scale\n  };\n}\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   THREE.JS \u00b7 Rendu mesh upload\u00e9\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n\nconst THREE_ENV = {\n  scene: null, camera: null, renderer: null,\n  mesh: null, controls: null,\n  rafId: null, ready: false\n};\n\nfunction initThree() {\n  if (THREE_ENV.ready || typeof THREE === 'undefined') return false;\n  const canvas = document.getElementById('viz-canvas');\n  if (!canvas) return false;\n\n  const wrap = canvas.parentElement;\n  const W = wrap.clientWidth || 320;\n  const H = wrap.clientHeight || 280;\n\n  THREE_ENV.scene = new THREE.Scene();\n  THREE_ENV.camera = new THREE.PerspectiveCamera(40, W \/ H, 0.01, 1000);\n  THREE_ENV.camera.position.set(2.2, 1.6, 2.6);\n  THREE_ENV.camera.lookAt(0, 0, 0);\n\n  THREE_ENV.renderer = new THREE.WebGLRenderer({\n    canvas: canvas,\n    alpha: true,\n    antialias: true\n  });\n  THREE_ENV.renderer.setPixelRatio(window.devicePixelRatio || 1);\n  THREE_ENV.renderer.setSize(W, H, false);\n  THREE_ENV.renderer.setClearColor(0x000000, 0);\n\n  \/\/ Lights\n  THREE_ENV.scene.add(new THREE.AmbientLight(0xfff5e0, 0.55));\n  const key = new THREE.DirectionalLight(0xffe9c4, 1.0);\n  key.position.set(3, 5, 4);\n  THREE_ENV.scene.add(key);\n  const fill = new THREE.DirectionalLight(0xc9a14c, 0.4);\n  fill.position.set(-3, 2, -2);\n  THREE_ENV.scene.add(fill);\n  const rim = new THREE.DirectionalLight(0xffffff, 0.3);\n  rim.position.set(0, -3, 5);\n  THREE_ENV.scene.add(rim);\n\n  THREE_ENV.ready = true;\n  return true;\n}\n\n\/* Convertit un hex string ou var(...) CSS en hex number Three.js *\/\nfunction resolveColorHex(c1) {\n  \/\/ Si var(--xxx), on lit la valeur computed\n  if (c1.startsWith('var(')) {\n    const name = c1.match(\/var\\(([^)]+)\\)\/)[1];\n    const val = getComputedStyle(document.documentElement).getPropertyValue(name).trim();\n    return parseInt(val.replace('#', ''), 16);\n  }\n  return parseInt(c1.replace('#', ''), 16);\n}\n\nfunction loadMeshIntoThree(triangles) {\n  if (!initThree()) return;\n  const { scene } = THREE_ENV;\n\n  \/\/ Cleanup ancien mesh\n  if (THREE_ENV.mesh) {\n    scene.remove(THREE_ENV.mesh);\n    THREE_ENV.mesh.geometry.dispose();\n    THREE_ENV.mesh.material.dispose();\n  }\n\n  const geom = new THREE.BufferGeometry();\n  geom.setAttribute('position', new THREE.BufferAttribute(triangles, 3));\n  geom.computeVertexNormals();\n\n  \/\/ Centrer + normaliser \u00e0 une taille raisonnable\n  geom.computeBoundingBox();\n  const bb = geom.boundingBox;\n  const size = new THREE.Vector3();\n  bb.getSize(size);\n  const center = new THREE.Vector3();\n  bb.getCenter(center);\n  const maxDim = Math.max(size.x, size.y, size.z);\n  const targetSize = 2.0;\n  const scale = targetSize \/ maxDim;\n\n  geom.translate(-center.x, -center.y, -center.z);\n  geom.scale(scale, scale, scale);\n\n  \/\/ Couleur du marbre\n  const m = STATE.marble;\n  const color = m ? resolveColorHex(m.c1) : 0xE8DCC8;\n\n  const mat = new THREE.MeshStandardMaterial({\n    color: color, roughness: 0.45, metalness: 0.05, flatShading: false\n  });\n\n  \/\/ Pivot pour rotation manuelle (group qui contient le mesh)\n  const pivot = new THREE.Group();\n  const mesh = new THREE.Mesh(geom, mat);\n\n  \/\/ Z-up STL convention \u2192 bascule en Y-up\n  if (size.z > size.y && size.z > size.x) {\n    mesh.rotation.x = -Math.PI \/ 2;\n  }\n  pivot.add(mesh);\n  pivot.rotation.x = -0.25;\n  pivot.rotation.y = 0.6;\n  scene.add(pivot);\n  THREE_ENV.mesh = pivot;\n  THREE_ENV.innerMesh = mesh;\n  THREE_ENV.material = mat;\n\n  \/\/ Setup interactions drag\n  setupDragRotation(pivot);\n\n  \/\/ Loop de rendu (sans auto-rotate, le mesh suit le drag uniquement)\n  if (THREE_ENV.rafId) cancelAnimationFrame(THREE_ENV.rafId);\n  function animate() {\n    \/\/ Si l'utilisateur n'a pas encore interagi, petit auto-rotate doux\n    if (!THREE_ENV.userInteracted) {\n      pivot.rotation.y += 0.004;\n    }\n    THREE_ENV.renderer.render(THREE_ENV.scene, THREE_ENV.camera);\n    THREE_ENV.rafId = requestAnimationFrame(animate);\n  }\n  THREE_ENV.rafId = requestAnimationFrame(animate);\n\n  \/\/ Resize\n  if (!THREE_ENV._resizeBound) {\n    window.addEventListener('resize', () => {\n      const wrap = document.getElementById('viz-canvas')?.parentElement;\n      if (!wrap || !THREE_ENV.renderer) return;\n      const W = wrap.clientWidth, H = wrap.clientHeight;\n      if (W && H) {\n        THREE_ENV.camera.aspect = W \/ H;\n        THREE_ENV.camera.updateProjectionMatrix();\n        THREE_ENV.renderer.setSize(W, H, false);\n      }\n    });\n    THREE_ENV._resizeBound = true;\n  }\n}\n\n\/* \u2500\u2500\u2500 Rotation manuelle au drag (souris + tactile) \u2500\u2500\u2500 *\/\nfunction setupDragRotation(pivot) {\n  const wrap = document.getElementById('stage-canvas');\n  if (!wrap || wrap.dataset.dragWired) return;\n  wrap.dataset.dragWired = '1';\n\n  let isDragging = false;\n  let lastX = 0, lastY = 0;\n  const SENS = 0.008;\n\n  function onStart(x, y) {\n    isDragging = true;\n    lastX = x; lastY = y;\n    THREE_ENV.userInteracted = true;\n    const hint = document.getElementById('viz-hint');\n    if (hint) hint.classList.add('fade');\n  }\n  function onMove(x, y) {\n    if (!isDragging || !THREE_ENV.mesh) return;\n    const dx = x - lastX;\n    const dy = y - lastY;\n    THREE_ENV.mesh.rotation.y += dx * SENS;\n    THREE_ENV.mesh.rotation.x += dy * SENS;\n    \/\/ Limiter la rotation X pour pas se retrouver \u00e0 l'envers\n    const maxX = Math.PI \/ 2.2;\n    THREE_ENV.mesh.rotation.x = Math.max(-maxX, Math.min(maxX, THREE_ENV.mesh.rotation.x));\n    lastX = x; lastY = y;\n  }\n  function onEnd() {\n    isDragging = false;\n  }\n\n  \/\/ Souris\n  wrap.addEventListener('mousedown', (e) => { e.preventDefault(); onStart(e.clientX, e.clientY); });\n  window.addEventListener('mousemove', (e) => onMove(e.clientX, e.clientY));\n  window.addEventListener('mouseup', onEnd);\n\n  \/\/ Tactile\n  wrap.addEventListener('touchstart', (e) => {\n    if (e.touches.length === 1) {\n      e.preventDefault();\n      onStart(e.touches[0].clientX, e.touches[0].clientY);\n    }\n  }, { passive: false });\n  wrap.addEventListener('touchmove', (e) => {\n    if (e.touches.length === 1) {\n      e.preventDefault();\n      onMove(e.touches[0].clientX, e.touches[0].clientY);\n    }\n  }, { passive: false });\n  wrap.addEventListener('touchend', onEnd);\n  wrap.addEventListener('touchcancel', onEnd);\n}\n\nfunction updateMeshColor() {\n  if (!THREE_ENV.material || !STATE.marble) return;\n  const color = resolveColorHex(STATE.marble.c1);\n  THREE_ENV.material.color.setHex(color);\n}\n\n\/* \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n   GESTION UPLOAD\n\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 *\/\n\nfunction handleFile(file) {\n  if (!file) return;\n  const ext = file.name.split('.').pop().toLowerCase();\n  if (!['stl', 'obj'].includes(ext)) {\n    alert(t('upload.err_format'));\n    return;\n  }\n  if (file.size > 100 * 1024 * 1024) {\n    alert(t('upload.err_size'));\n    return;\n  }\n\n  \/\/ Show progress\n  const progress = document.getElementById('upload-progress');\n  const progressBar = document.getElementById('upload-progress-bar');\n  progress.classList.add('show');\n  progressBar.style.width = '20%';\n\n  const reader = new FileReader();\n  reader.onprogress = (e) => {\n    if (e.lengthComputable) {\n      progressBar.style.width = (20 + (e.loaded \/ e.total) * 60) + '%';\n    }\n  };\n  reader.onload = (e) => {\n    progressBar.style.width = '90%';\n    try {\n      const buffer = e.target.result;\n      const triangles = ext === 'stl' ? parseSTL(buffer) : parseOBJ(buffer);\n      if (!triangles.length) throw new Error('Fichier vide ou invalide');\n\n      \/\/ On stocke juste le mesh pour la pr\u00e9visualisation visuelle\n      \/\/ On NE modifie PAS les dimensions saisies ni le type d'objet\n      STATE.mesh = {\n        name: file.name,\n        fileSize: file.size,\n        triangles: triangles,\n        triangleCount: triangles.length \/ 9\n      };\n\n      \/\/ Update UI\n      progressBar.style.width = '100%';\n      setTimeout(() => {\n        progress.classList.remove('show');\n        progressBar.style.width = '0%';\n      }, 600);\n\n      document.getElementById('upload-empty').style.display = 'none';\n      document.getElementById('upload-loaded').style.display = 'block';\n      document.getElementById('upload-zone').classList.add('has-file');\n      document.getElementById('file-name').textContent = file.name;\n      document.getElementById('file-stats').textContent =\n        `${STATE.mesh.triangleCount.toLocaleString('fr-CH')} triangles \u00b7 ${(file.size \/ 1024).toFixed(0)} KB \u00b7 ${t('upload.stats_suffix')}`;\n\n      \/\/ Switch viz mode\n      document.getElementById('stage-cube').style.display = 'none';\n      document.getElementById('stage-canvas').style.display = 'flex';\n      document.getElementById('viz-mode-tag').textContent = t('viz.mode_3d');\n      THREE_ENV.userInteracted = false;\n      const hint = document.getElementById('viz-hint');\n      if (hint) hint.classList.remove('fade');\n\n      loadMeshIntoThree(triangles);\n      renderViz();\n      updateFooter();\n    } catch (err) {\n      console.error('Erreur parsing 3D:', err);\n      alert(t('upload.err_invalid'));\n      progress.classList.remove('show');\n      progressBar.style.width = '0%';\n    }\n  };\n  reader.onerror = () => {\n    progress.classList.remove('show');\n    alert(t('upload.err_invalid'));\n  };\n\n  if (ext === 'stl') reader.readAsArrayBuffer(file);\n  else reader.readAsArrayBuffer(file); \/\/ OBJ aussi en buffer (TextDecoder le lira)\n}\n\nfunction removeFile() {\n  STATE.mesh = null;\n  document.getElementById('upload-empty').style.display = 'block';\n  document.getElementById('upload-loaded').style.display = 'none';\n  document.getElementById('upload-zone').classList.remove('has-file');\n  document.getElementById('upload-input').value = '';\n  document.getElementById('stage-cube').style.display = 'block';\n  document.getElementById('stage-canvas').style.display = 'none';\n  document.getElementById('viz-mode-tag').textContent = t('viz.mode_preview');\n  const hint = document.getElementById('viz-hint');\n  if (hint) hint.classList.remove('fade');\n\n  if (THREE_ENV.rafId) {\n    cancelAnimationFrame(THREE_ENV.rafId);\n    THREE_ENV.rafId = null;\n  }\n  if (THREE_ENV.mesh) {\n    THREE_ENV.scene.remove(THREE_ENV.mesh);\n    if (THREE_ENV.innerMesh) {\n      THREE_ENV.innerMesh.geometry.dispose();\n    }\n    if (THREE_ENV.material) {\n      THREE_ENV.material.dispose();\n    }\n    THREE_ENV.mesh = null;\n    THREE_ENV.innerMesh = null;\n    THREE_ENV.material = null;\n  }\n  THREE_ENV.userInteracted = false;\n  renderViz();\n  updateFooter();\n}\n\n\n\/* \u2500\u2500\u2500 Visuel 3D \u2014 cube CSS si pas de mesh, Three.js sinon \u2500\u2500\u2500 *\/\nfunction renderViz() {\n  const m = STATE.marble;\n  const d = STATE.dim;\n  const r = compute();\n\n  \/\/ Si mesh upload\u00e9 : Three.js prend le relais (mais on met \u00e0 jour la couleur)\n  if (STATE.mesh) {\n    updateMeshColor();\n    document.getElementById('viz-overlay').innerHTML =\n      `<strong>${m ? m.name : t('viz.bloc_default')}<\/strong>`;\n    \/\/ Stats = toujours bas\u00e9es sur les dimensions saisies par le user\n    document.getElementById('viz-dims').innerHTML =\n      `${d.w.toFixed(2)} \u00d7 ${d.h.toFixed(2)} \u00d7 ${d.d.toFixed(2)} <small>m<\/small>`;\n    document.getElementById('viz-volume').innerHTML =\n      `${fmt(r.real, 3).replace(\/0+$\/,'').replace(\/\\.$\/,'')} <small>m\u00b3<\/small>`;\n    document.getElementById('viz-weight').innerHTML = fmtKg(r.weight);\n    document.getElementById('viz-marble').textContent = m ? m.name : '\u2014';\n    return;\n  }\n\n  \/\/ Sinon mode cube CSS\n  const max = Math.max(d.w, d.h, d.d);\n  const SCALE = 200 \/ max;\n  const W = d.w * SCALE, H = d.h * SCALE, D = d.d * SCALE;\n\n  const cube = document.getElementById('cube');\n  cube.style.width  = W + 'px';\n  cube.style.height = H + 'px';\n  cube.innerHTML = `\n    <div class=\"face\" style=\"width:${W}px; height:${H}px; left:0; top:0; transform: translateZ(${D\/2}px);\"><\/div>\n    <div class=\"face\" style=\"width:${W}px; height:${H}px; left:0; top:0; transform: translateZ(-${D\/2}px) rotateY(180deg);\"><\/div>\n    <div class=\"face\" style=\"width:${D}px; height:${H}px; left:${(W - D)\/2}px; top:0; transform: translateX(${W\/2 - D\/2 + D\/2}px) rotateY(90deg) translateX(-${W\/2 - D\/2}px);\"><\/div>\n    <div class=\"face\" style=\"width:${D}px; height:${H}px; left:${(W - D)\/2}px; top:0; transform: translateX(-${W\/2 - D\/2 + D\/2}px) rotateY(-90deg) translateX(${W\/2 - D\/2}px);\"><\/div>\n    <div class=\"face\" style=\"width:${W}px; height:${D}px; left:0; top:${(H - D)\/2}px; transform: translateY(-${H\/2 - D\/2 + D\/2}px) rotateX(90deg) translateY(${H\/2 - D\/2}px);\"><\/div>\n    <div class=\"face\" style=\"width:${W}px; height:${D}px; left:0; top:${(H - D)\/2}px; transform: translateY(${H\/2 - D\/2 + D\/2}px) rotateX(-90deg) translateY(-${H\/2 - D\/2}px);\"><\/div>\n  `;\n  if (m) {\n    cube.querySelectorAll('.face').forEach(f => {\n      f.style.setProperty('--c1', m.c1);\n      f.style.setProperty('--c2', m.c2);\n    });\n  }\n\n  document.getElementById('viz-overlay').innerHTML = `<strong>${m ? m.name : t('viz.bloc_default')}<\/strong>`;\n  document.getElementById('viz-dims').innerHTML    = `${d.w.toFixed(2)} \u00d7 ${d.h.toFixed(2)} \u00d7 ${d.d.toFixed(2)} <small>m<\/small>`;\n  document.getElementById('viz-volume').innerHTML  = `${fmt(r.real, 3).replace(\/0+$\/,'').replace(\/\\.$\/,'')} <small>m\u00b3<\/small>`;\n  document.getElementById('viz-weight').innerHTML  = fmtKg(r.weight);\n  document.getElementById('viz-marble').textContent = m ? m.name : '\u2014';\n}\n\n\/* \u2500\u2500\u2500 Prix \u2500\u2500\u2500 *\/\nfunction renderPrice() {\n  const r = compute();\n  const m = STATE.marble, fr = STATE.finishR, fh = STATE.finishH;\n  const d = STATE.dim;\n\n  document.getElementById('price-value').textContent = fmt(Math.round(r.total \/ 100) * 100);\n  document.getElementById('price-range').textContent = `${t('price.range')} : ${fmtCHF(r.totalLow)} \u2014 ${fmtCHF(r.totalHigh)}`;\n\n  const finishLabel = (fr && fh) ? `${fr.id} ${t(fr.id.toLowerCase()+'.name')} + ${fh.id} ${t(fh.id.toLowerCase()+'.name')}` : '\u2014';\n  document.getElementById('recap-marble').textContent = m ? `${m.name} (${t('tier.' + m.tierClass)})` : '\u2014';\n  document.getElementById('recap-finish').textContent = finishLabel;\n  document.getElementById('recap-dim').textContent    = `${d.w.toFixed(2)} \u00d7 ${d.h.toFixed(2)} \u00d7 ${d.d.toFixed(2)} m`;\n  document.getElementById('recap-vol').textContent    = fmtVol(r.real);\n  document.getElementById('recap-weight').textContent = fmtKg(r.weight);\n  document.getElementById('recap-delay').textContent  = fmtDays(r.days);\n\n  document.getElementById('bd-matiere').textContent  = fmtCHF(r.matiere);\n  document.getElementById('bd-finition').textContent =\n    `${fmtCHF(r.surcoutR)} (R) + ${r.heuresMain.toFixed(0)} h \u00d7 ${CONFIG.rateHand} = ${fmtCHF(r.prixH)}`;\n  document.getElementById('bd-setup').textContent    = fmtCHF(r.setup);\n  document.getElementById('bd-total').textContent    = fmtCHF(r.total);\n\n  document.getElementById('monumental-msg').classList.toggle('show', r.isMonumental);\n\n  const subject = encodeURIComponent(`${t('devis.mail_subject')} \u00b7 ${m ? m.name : ''} \u00b7 ${finishLabel}`);\n  const body = encodeURIComponent(\n    `${t('devis.mail_intro')}\\n\\n` +\n    `${t('devis.mail_marble')} : ${m ? m.name + ' (' + t('tier.' + m.tierClass) + ')' : '\u2014'}\\n` +\n    `${t('devis.mail_finish')} : ${finishLabel}\\n` +\n    `${t('devis.mail_dim')} : ${d.w.toFixed(2)} \u00d7 ${d.h.toFixed(2)} \u00d7 ${d.d.toFixed(2)} m\\n` +\n    `${t('devis.mail_vol')} : ${fmtVol(r.real)}\\n` +\n    `${t('devis.mail_est')} : ${fmtCHF(r.total)}\\n\\n` +\n    `${t('devis.mail_contact')} :\\n${t('devis.mail_name')} :\\n${t('devis.mail_company')} :\\n${t('devis.mail_phone')} :\\n\\n${t('devis.mail_thanks')}`\n  );\n  document.getElementById('cta-final-link').href = `mailto:contact@ikonera.art?subject=${subject}&body=${body}`;\n}\n\n\/* \u2500\u2500\u2500 Navigation \u2500\u2500\u2500 *\/\nfunction gotoStep(n) {\n  if (!canReachStep(n)) return;\n  STATE.step = n;\n  document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));\n  document.getElementById('panel-' + n).classList.add('active');\n  document.querySelectorAll('.step').forEach(s => {\n    const sn = parseInt(s.dataset.step, 10);\n    s.classList.toggle('active', sn === n);\n    s.classList.toggle('complete', sn < n && stepComplete(sn));\n    s.classList.toggle('disabled', !canReachStep(sn));\n  });\n  if (n === 3) renderDimensions();\n  if (n === 4) renderViz();\n  if (n === 5) renderPrice();\n  updateFooter();\n  document.querySelector('.calc').scrollIntoView({ behavior: 'smooth', block: 'start' });\n}\nfunction stepComplete(n) {\n  if (n === 1) return !!STATE.marble;\n  if (n === 2) return !!STATE.finishR && !!STATE.finishH;\n  if (n === 3) return STATE.dim.w > 0 && STATE.dim.h > 0 && STATE.dim.d > 0;\n  if (n === 4) return stepComplete(3);\n  return true;\n}\nfunction canReachStep(n) {\n  for (let i = 1; i < n; i++) if (!stepComplete(i)) return false;\n  return true;\n}\n\n\/* \u2500\u2500\u2500 Footer \u2500\u2500\u2500 *\/\nfunction updateFooter() {\n  const status = document.getElementById('footer-status');\n  const text = document.getElementById('footer-status-text');\n  const next = document.getElementById('btn-next');\n  const prev = document.getElementById('btn-prev');\n\n  prev.style.display = STATE.step > 1 ? 'inline-block' : 'none';\n  prev.textContent = t('btn.back');\n\n  const labelKeys = ['btn.next_finish', 'btn.next_dim', 'btn.next_viz', 'btn.next_price', 'btn.next_devis'];\n  next.textContent = t(labelKeys[STATE.step - 1] || 'btn.next_finish');\n\n  let ok = false;\n  if (STATE.step === 1) {\n    ok = !!STATE.marble;\n    text.textContent = STATE.marble ? t('footer.marble_chosen') + STATE.marble.name : t('footer.no_marble');\n  } else if (STATE.step === 2) {\n    ok = !!STATE.finishR && !!STATE.finishH;\n    if (ok) {\n      text.textContent = t('footer.finish_set') + STATE.finishR.id + ' + ' + STATE.finishH.id;\n    } else if (STATE.finishR && !STATE.finishH) {\n      text.textContent = t(STATE.finishR.id.toLowerCase()+'.name') + t('footer.partial_r');\n    } else if (!STATE.finishR && STATE.finishH) {\n      text.textContent = t(STATE.finishH.id.toLowerCase()+'.name') + t('footer.partial_h');\n    } else {\n      text.textContent = t('footer.choose_both');\n    }\n  } else if (STATE.step === 3) {\n    ok = stepComplete(3);\n    text.textContent = t('footer.vol_sculpt') + fmtVol(compute().real);\n  } else if (STATE.step === 4) {\n    ok = true; text.textContent = t('footer.preview_ready');\n  } else if (STATE.step === 5) {\n    ok = true; text.textContent = t('footer.total_est') + fmtCHF(compute().total);\n  }\n\n  status.classList.toggle('ok', ok);\n  next.disabled = !ok;\n\n  if (STATE.step === 5) {\n    next.onclick = (e) => { e.preventDefault(); document.getElementById('cta-final-link').click(); };\n  } else {\n    next.onclick = () => gotoStep(STATE.step + 1);\n  }\n}\n\n\/* \u2500\u2500\u2500 Init \u2500\u2500\u2500 *\/\ndocument.addEventListener('DOMContentLoaded', () => {\n  \/\/ \u2500\u2500\u2500 I18N : d\u00e9tecter et appliquer la langue \u2500\u2500\u2500\n  CURRENT_LANG = detectLang();\n  applyI18n();\n  document.querySelectorAll('.lang-btn').forEach(b => {\n    b.classList.toggle('active', b.dataset.lang === CURRENT_LANG);\n    b.addEventListener('click', () => setLang(b.dataset.lang));\n  });\n\n  renderMarbles();\n  renderFinishR();\n  renderFinishH();\n  renderShapeTypes();\n  updateFooter();\n\n  document.querySelectorAll('.step').forEach(s => {\n    s.addEventListener('click', () => gotoStep(parseInt(s.dataset.step, 10)));\n  });\n  document.getElementById('btn-prev').addEventListener('click', () => gotoStep(STATE.step - 1));\n\n  ['w', 'h', 'd'].forEach((key) => {\n    const inp = document.getElementById('dim-' + key);\n    const sl  = document.getElementById('dim-' + key + '-slider');\n\n    inp.addEventListener('input', (e) => {\n      const v = parseFloat(e.target.value);\n      if (isFinite(v) && v > 0) {\n        STATE.dim[key] = v;\n        \/\/ Sync slider visuel (sans d\u00e9passer le max range)\n        if (sl) {\n          sl.value = Math.min(parseFloat(sl.max), v);\n          const pct = ((v - parseFloat(sl.min)) \/ (parseFloat(sl.max) - parseFloat(sl.min))) * 100;\n          sl.style.setProperty('--pct', Math.min(100, Math.max(0, pct)) + '%');\n        }\n        updateDimSummary();\n        updateFooter();\n      }\n    });\n\n    sl.addEventListener('input', (e) => {\n      const v = parseFloat(e.target.value);\n      if (isFinite(v) && v > 0) {\n        STATE.dim[key] = v;\n        if (inp) inp.value = v.toFixed(2);\n        const pct = ((v - parseFloat(sl.min)) \/ (parseFloat(sl.max) - parseFloat(sl.min))) * 100;\n        sl.style.setProperty('--pct', Math.min(100, Math.max(0, pct)) + '%');\n        updateDimSummary();\n        updateFooter();\n      }\n    });\n  });\n\n  \/\/ Init des barres de progression slider visuelles\n  setDimInputs();\n\n  \/* \u2500\u2500\u2500 Upload 3D \u2500\u2500\u2500 *\/\n  const uploadZone = document.getElementById('upload-zone');\n  const uploadInput = document.getElementById('upload-input');\n  const fileRemoveBtn = document.getElementById('file-remove');\n\n  uploadZone.addEventListener('click', (e) => {\n    \/\/ \u00c9vite de re-trigger le click si on clique sur le bouton \"Retirer\"\n    if (e.target.closest('#file-remove')) return;\n    uploadInput.click();\n  });\n  uploadInput.addEventListener('change', (e) => {\n    if (e.target.files && e.target.files[0]) handleFile(e.target.files[0]);\n  });\n  fileRemoveBtn.addEventListener('click', (e) => {\n    e.stopPropagation();\n    removeFile();\n  });\n\n  \/\/ Drag & drop\n  ['dragenter', 'dragover'].forEach(ev => {\n    uploadZone.addEventListener(ev, (e) => {\n      e.preventDefault();\n      e.stopPropagation();\n      uploadZone.classList.add('dragover');\n    });\n  });\n  ['dragleave', 'drop'].forEach(ev => {\n    uploadZone.addEventListener(ev, (e) => {\n      e.preventDefault();\n      e.stopPropagation();\n      uploadZone.classList.remove('dragover');\n    });\n  });\n  uploadZone.addEventListener('drop', (e) => {\n    if (e.dataTransfer && e.dataTransfer.files && e.dataTransfer.files[0]) {\n      handleFile(e.dataTransfer.files[0]);\n    }\n  });\n});\n\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\n    <div class=\"xs_social_share_widget xs_share_url after_content \t\tmain_content  wslu-style-1 wslu-share-box-shaped wslu-fill-colored wslu-none wslu-share-horizontal wslu-theme-font-no wslu-main_content\">\n\n\t\t\n        <ul>\n\t\t\t        <\/ul>\n    <\/div> \n","protected":false},"excerpt":{"rendered":"<p>FR IT DE IT FR IT DE IT - 1 2 3 4 5 \u2699\ufe0f \u270b \ud83d\udca1 m m m m - - - - - - - - m\u00b3 - kg - -CHF Gamma: - - - - - - - - - - IKONERA - ikonera.art<\/p>","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"postBodyCss":"","postBodyMargin":[],"postBodyPadding":[],"postBodyBackground":{"backgroundType":"classic","gradient":""},"footnotes":""},"class_list":["post-7055","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v27.0 (Yoast SEO v27.5) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>\ud83d\udd12 - IkonEra<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/ikonera.art\/it\/\ud83d\udd12\/\" \/>\n<meta property=\"og:locale\" content=\"it_IT\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"\ud83d\udd12\" \/>\n<meta property=\"og:description\" content=\"FR EN DE IT FR EN DE IT \u00b7 1 2 3 4 5 \u2699\ufe0f \u270b \ud83d\udca1 m m m \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 m \u2014 m\u00b3 \u2014 kg \u2014 \u2014CHF Fourchette : \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 IKONERA \u00b7 ikonera.art\" \/>\n<meta property=\"og:url\" content=\"https:\/\/ikonera.art\/it\/\ud83d\udd12\/\" \/>\n<meta property=\"og:site_name\" content=\"IkonEra\" \/>\n<meta property=\"article:modified_time\" content=\"2026-05-11T15:09:41+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Tempo di lettura stimato\" \/>\n\t<meta name=\"twitter:data1\" content=\"52 minuti\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/ikonera.art\\\/%f0%9f%94%92\\\/\",\"url\":\"https:\\\/\\\/ikonera.art\\\/%f0%9f%94%92\\\/\",\"name\":\"\ud83d\udd12 - IkonEra\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/ikonera.art\\\/#website\"},\"datePublished\":\"2026-05-04T15:25:18+00:00\",\"dateModified\":\"2026-05-11T15:09:41+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/ikonera.art\\\/%f0%9f%94%92\\\/#breadcrumb\"},\"inLanguage\":\"it-IT\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/ikonera.art\\\/%f0%9f%94%92\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/ikonera.art\\\/%f0%9f%94%92\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Accueil\",\"item\":\"https:\\\/\\\/ikonera.art\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"\ud83d\udd12\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/ikonera.art\\\/#website\",\"url\":\"https:\\\/\\\/ikonera.art\\\/\",\"name\":\"IkonEra\",\"description\":\"De l&#039;ic\u00f4ne \u00e0 la pierre.\",\"publisher\":{\"@id\":\"https:\\\/\\\/ikonera.art\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/ikonera.art\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"it-IT\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/ikonera.art\\\/#organization\",\"name\":\"IkonEra\",\"url\":\"https:\\\/\\\/ikonera.art\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"it-IT\",\"@id\":\"https:\\\/\\\/ikonera.art\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/ikonera.art\\\/wp-content\\\/uploads\\\/2026\\\/03\\\/cropped-Transparant-sans-trait-A.png\",\"contentUrl\":\"https:\\\/\\\/ikonera.art\\\/wp-content\\\/uploads\\\/2026\\\/03\\\/cropped-Transparant-sans-trait-A.png\",\"width\":945,\"height\":183,\"caption\":\"IkonEra\"},\"image\":{\"@id\":\"https:\\\/\\\/ikonera.art\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"\ud83d\udd12 - IkonEra","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:\/\/ikonera.art\/it\/\ud83d\udd12\/","og_locale":"it_IT","og_type":"article","og_title":"\ud83d\udd12","og_description":"FR EN DE IT FR EN DE IT \u00b7 1 2 3 4 5 \u2699\ufe0f \u270b \ud83d\udca1 m m m \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 m \u2014 m\u00b3 \u2014 kg \u2014 \u2014CHF Fourchette : \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 \u2014 IKONERA \u00b7 ikonera.art","og_url":"https:\/\/ikonera.art\/it\/\ud83d\udd12\/","og_site_name":"IkonEra","article_modified_time":"2026-05-11T15:09:41+00:00","twitter_card":"summary_large_image","twitter_misc":{"Tempo di lettura stimato":"52 minuti"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/ikonera.art\/%f0%9f%94%92\/","url":"https:\/\/ikonera.art\/%f0%9f%94%92\/","name":"\ud83d\udd12 - IkonEra","isPartOf":{"@id":"https:\/\/ikonera.art\/#website"},"datePublished":"2026-05-04T15:25:18+00:00","dateModified":"2026-05-11T15:09:41+00:00","breadcrumb":{"@id":"https:\/\/ikonera.art\/%f0%9f%94%92\/#breadcrumb"},"inLanguage":"it-IT","potentialAction":[{"@type":"ReadAction","target":["https:\/\/ikonera.art\/%f0%9f%94%92\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/ikonera.art\/%f0%9f%94%92\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Accueil","item":"https:\/\/ikonera.art\/"},{"@type":"ListItem","position":2,"name":"\ud83d\udd12"}]},{"@type":"WebSite","@id":"https:\/\/ikonera.art\/#website","url":"https:\/\/ikonera.art\/","name":"IkonEra","description":"Dall'icona alla pietra.","publisher":{"@id":"https:\/\/ikonera.art\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/ikonera.art\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"it-IT"},{"@type":"Organization","@id":"https:\/\/ikonera.art\/#organization","name":"IkonEra","url":"https:\/\/ikonera.art\/","logo":{"@type":"ImageObject","inLanguage":"it-IT","@id":"https:\/\/ikonera.art\/#\/schema\/logo\/image\/","url":"https:\/\/ikonera.art\/wp-content\/uploads\/2026\/03\/cropped-Transparant-sans-trait-A.png","contentUrl":"https:\/\/ikonera.art\/wp-content\/uploads\/2026\/03\/cropped-Transparant-sans-trait-A.png","width":945,"height":183,"caption":"IkonEra"},"image":{"@id":"https:\/\/ikonera.art\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/pages\/7055","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/comments?post=7055"}],"version-history":[{"count":22,"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/pages\/7055\/revisions"}],"predecessor-version":[{"id":7341,"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/pages\/7055\/revisions\/7341"}],"wp:attachment":[{"href":"https:\/\/ikonera.art\/it\/wp-json\/wp\/v2\/media?parent=7055"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}