{"id":2534,"date":"2026-05-24T10:01:36","date_gmt":"2026-05-24T10:01:36","guid":{"rendered":"https:\/\/millepiani.com\/monte-escaliers\/"},"modified":"2026-09-15T18:18:16","modified_gmt":"2026-09-15T18:18:16","slug":"monte-escaliers","status":"publish","type":"page","link":"https:\/\/millepiani.com\/fr\/monte-escaliers\/","title":{"rendered":"MONTE-ESCALIERS"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2534\" class=\"elementor elementor-2534 elementor-393\" data-elementor-post-type=\"page\">\n\t\t\t\t<div class=\"elementor-element elementor-element-ec867e7 e-con-full e-flex e-con e-parent\" data-id=\"ec867e7\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-cd8106f elementor-widget elementor-widget-html\" data-id=\"cd8106f\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<div class=\"mp-servo-page\">\r\n\r\n  <!-- HERO -->\r\n  <section class=\"mp-servo-hero\" id=\"top\">\r\n    <div class=\"mp-servo-hero-bg\"><\/div>\r\n    <div class=\"mp-servo-hero-overlay\"><\/div>\r\n\r\n    <div class=\"mp-servo-hero-content mp-reveal\">\r\n      <p class=\"mp-kicker\">Accessibilit\u00e9 sur mesure<\/p>\r\n\r\n      <h1>Monte-escaliers<\/h1>\r\n\r\n      <p>\r\n Des solutions con\u00e7ues pour rendre plus simple, s\u00fbr et confortable le d\u00e9placement\r\n entre diff\u00e9rents niveaux d'un b\u00e2timent. Monte-escaliers con\u00e7us pour les personnes \u00e0\r\n mobilit\u00e9 r\u00e9duite, les personnes \u00e2g\u00e9es et les contextes o\u00f9 les escaliers repr\u00e9sentent un obstacle\r\n quotidien. \r\n      <\/p>\r\n\r\n      <a href=\"#preventivo\" class=\"mp-outline-btn\">Demander un devis<\/a>\r\n\r\n    <\/div>\r\n\r\n    <a href=\"#linea-servoscale\" class=\"mp-scroll-arrow\" aria-label=\"Scorri alla sezione successiva\">\r\n      <span><\/span>\r\n    <\/a>\r\n  <\/section>\r\n\r\n\r\n  <!-- INTRO -->\r\n  <section class=\"mp-servo-intro\" id=\"linea-servoscale\">\r\n    <div class=\"mp-container\">\r\n\r\n      <div class=\"mp-section-heading mp-reveal\">\r\n        <p>Pourquoi choisir<\/p>\r\n        <h2>Monte-escaliers<br\/>Millepiani<\/h2>\r\n      <\/div>\r\n\r\n      <div class=\"mp-intro-text mp-reveal\">\r\n        <p>\r\n Le monte-escalier permet de franchir des vol\u00e9es d'escaliers de mani\u00e8re confortable et s\u00fbre,\r\n am\u00e9liorant autonomie, praticit\u00e9 et qualit\u00e9 de vie. Il peut \u00eatre install\u00e9\r\n dans des maisons individuelles, des immeubles collectifs, des bureaux et des locaux publics, m\u00eame\r\n en pr\u00e9sence d'escaliers \u00e9troits, sinueux, raides, courbes ou avec des changements de pente. \r\n        <\/p>\r\n      <\/div>\r\n\r\n      <div class=\"mp-features-grid\">\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M3 20h18\"><\/path>\r\n              <path d=\"M5 17h4l2-3h4l2-3h2\"><\/path>\r\n              <path d=\"M8 17v3\"><\/path>\r\n              <path d=\"M13 14v6\"><\/path>\r\n              <path d=\"M18 11v9\"><\/path>\r\n            <\/path><\/path><\/path><\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>Accessible<\/h3>\r\n          <p>Aide \u00e0 surmonter les obstacles architecturaux et les d\u00e9nivel\u00e9s de mani\u00e8re simple.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M12 3l7 4v5c0 5-3.5 8-7 9-3.5-1-7-4-7-9V7l7-4z\"><\/path>\r\n              <path d=\"M9 12l2 2 4-5\"><\/path>\r\n            <\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>S\u00fbr<\/h3>\r\n          <p>Con\u00e7u pour des d\u00e9placements tranquilles, stables et confortables entre plusieurs niveaux.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M4 20h16\"><\/path>\r\n              <path d=\"M7 17c4-8 8-8 10-14\"><\/path>\r\n              <path d=\"M7 17h4\"><\/path>\r\n              <path d=\"M13 10h4\"><\/path>\r\n            <\/path><\/path><\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>Polyvalent<\/h3>\r\n          <p>Adapt\u00e9 aux escaliers droits, courbes, \u00e9troits, raides, int\u00e9rieurs ou ext\u00e9rieurs.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M4 4h16v16H4z\"><\/path>\r\n              <path d=\"M8 8h8v8H8z\"><\/path>\r\n              <path d=\"M12 4v4\"><\/path>\r\n              <path d=\"M12 16v4\"><\/path>\r\n            <\/path><\/path><\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>Sur mesure<\/h3>\r\n          <p>Chaque solution est adapt\u00e9e en fonction de l'escalier, de l'environnement et de l'utilisateur.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M5 20V7a2 2 0 0 1 2-2h4\"><\/path>\r\n              <path d=\"M11 5h6a2 2 0 0 1 2 2v13\"><\/path>\r\n              <path d=\"M8 12h8\"><\/path>\r\n              <path d=\"M8 16h8\"><\/path>\r\n            <\/path><\/path><\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>Peu invasif<\/h3>\r\n          <p>Dans la plupart des cas, ne n\u00e9cessite pas de travaux de ma\u00e7onnerie importants.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-feature-card mp-reveal\">\r\n          <span class=\"mp-feature-icon\">\r\n            <svg viewbox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M8 20h8\"><\/path>\r\n              <path d=\"M10 20V9a2 2 0 0 1 2-2h3\"><\/path>\r\n              <path d=\"M15 7v5h-4\"><\/path>\r\n              <path d=\"M7 12h4\"><\/path>\r\n              <path d=\"M7 16h8\"><\/path>\r\n            <\/path><\/path><\/path><\/path><\/path><\/svg>\r\n          <\/span>\r\n          <h3>Discret<\/h3>\r\n          <p>Mat\u00e9riaux, mod\u00e8les et coloris s'int\u00e8grent aux environnements publics et priv\u00e9s.<\/p>\r\n        <\/article>\r\n\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n\r\n  <!-- IMMERSIVE 3D EXPERIENCE -->\r\n  <section class=\"mp-servo-immersive\" id=\"servo-experience\">\r\n    <div class=\"mp-servo-immersive-bg\"><\/div>\r\n    <div class=\"mp-servo-immersive-grid\"><\/div>\r\n\r\n    <div class=\"mp-servo-copy\">\r\n      <p id=\"mpServoKicker\">Exp\u00e9rience monte-escalier<\/p>\r\n\r\n      <h2 id=\"mpServoTitle\">\r\n L'escalier devient<br\/> accessible\r\n      <\/h2>\r\n\r\n      <p id=\"mpServoText\">\r\n Explorez une simulation immersive du monte-escalier : un parcours con\u00e7u sur mesure\r\n pour franchir des d\u00e9nivel\u00e9s, escaliers droits, courbes, espaces \u00e9troits ou environnements ext\u00e9rieurs.\r\n      <\/p>\r\n\r\n      <div class=\"mp-servo-tags\" id=\"mpServoTags\">\r\n        <span>Sur mesure<\/span>\r\n        <span>Encombrement minimal<\/span>\r\n        <span>Confort<\/span>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"mp-servo-canvas-wrap\">\r\n      <canvas id=\"mpServoCanvas\" aria-label=\"Modello 3D interattivo servoscala\"><\/canvas>\r\n\r\n      <div class=\"mp-servo-fallback\" id=\"mpServoFallback\">\r\n        <strong>Exp\u00e9rience 3D non disponible<\/strong>\r\n        <span>Le navigateur ne prend pas en charge WebGL ou Three.js n'a pas \u00e9t\u00e9 charg\u00e9.<\/span>\r\n      <\/div>\r\n\r\n      <div class=\"mp-servo-hint\"><\/div>\r\n\r\n      <button class=\"mp-servo-hotspot is-active\" type=\"button\" data-point=\"survey\" style=\"--x: 22%; --y: 72%;\">\r\n        <i><\/i>\r\n        <span>Visite technique<\/span>\r\n      <\/button>\r\n\r\n      <button class=\"mp-servo-hotspot\" type=\"button\" data-point=\"install\" style=\"--x: 50%; --y: 48%;\">\r\n        <i><\/i>\r\n        <span>Installation<\/span>\r\n      <\/button>\r\n\r\n      <button class=\"mp-servo-hotspot\" type=\"button\" data-point=\"comfort\" style=\"--x: 74%; --y: 33%;\">\r\n        <i><\/i>\r\n        <span>Confort<\/span>\r\n      <\/button>\r\n    <\/div>\r\n\r\n    <div class=\"mp-servo-panel\">\r\n      <div class=\"mp-servo-switch\">\r\n        <button class=\"is-active\" type=\"button\" data-route=\"straight\">\r\n          <span>01<\/span>\r\n Escalier droit\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"curved\">\r\n          <span>02<\/span>\r\n Escalier courbe\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"narrow\">\r\n          <span>03<\/span>\r\n Espace \u00e9troit\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"outdoor\">\r\n          <span>04<\/span>\r\n Ext\u00e9rieur\r\n        <\/button>\r\n      <\/div>\r\n\r\n      <div class=\"mp-servo-info-card\" id=\"mpServoInfoCard\">\r\n        <p>Visite technique<\/p>\r\n        <h3>Analyse de l'environnement et des besoins<\/h3>\r\n        <span>\r\n Le parcours est \u00e9tudi\u00e9 en fonction de l'escalier, des espaces disponibles,\r\n de l'utilisateur et du contexte dans lequel le monte-escalier sera install\u00e9.\r\n        <\/span>\r\n      <\/div>\r\n\r\n      <div class=\"mp-servo-mode-row\">\r\n        <button class=\"is-active\" type=\"button\" data-mode=\"seat\">Si\u00e8ge<\/button>\r\n        <button type=\"button\" data-mode=\"platform\">Plateforme<\/button>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"mp-servo-dashboard\">\r\n      <div>\r\n        <span>Parcours<\/span>\r\n        <strong id=\"mpServoDashRoute\">Escalier droit<\/strong>\r\n      <\/div>\r\n\r\n      <div>\r\n        <span>Environnement<\/span>\r\n        <strong id=\"mpServoDashContext\">Maison \/ bureau<\/strong>\r\n      <\/div>\r\n\r\n      <div>\r\n        <span>Solution<\/span>\r\n        <strong id=\"mpServoDashMode\">Si\u00e8ge<\/strong>\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n\r\n  <!-- APPLICAZIONI -->\r\n  <section class=\"mp-servo-app-section\">\r\n    <div class=\"mp-container\">\r\n\r\n      <div class=\"mp-servo-app-heading mp-reveal\">\r\n        <p>Applications<\/p>\r\n        <h2>Libert\u00e9 de mouvement<br\/>dans tous les contextes<\/h2>\r\n      <\/div>\r\n\r\n      <div class=\"mp-servo-app-grid\">\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>01<\/span>\r\n          <h3>Maisons individuelles<\/h3>\r\n          <p>Pour se d\u00e9placer entre les \u00e9tages de son habitation avec plus d'autonomie et de s\u00e9curit\u00e9.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>02<\/span>\r\n          <h3>Bureaux<\/h3>\r\n          <p>Solutions fonctionnelles l\u00e0 o\u00f9 les escaliers limitent l'accessibilit\u00e9 des espaces.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>03<\/span>\r\n          <h3>B\u00e2timents publics<\/h3>\r\n          <p>Pour franchir des petits ou grands d\u00e9nivel\u00e9s et am\u00e9liorer l'acc\u00e8s aux espaces.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>04<\/span>\r\n          <h3>Contextes complexes<\/h3>\r\n          <p>Escaliers courbes, sinueux, raides, \u00e9troits ou avec des changements de pente.<\/p>\r\n        <\/article>\r\n\r\n      <\/div>\r\n    <\/div>\r\n  <\/section>\r\n\r\n<\/div>\r\n\r\n<script src=\"https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/three.js\/r128\/three.min.js\"><\/script>\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-6f9c2b1 elementor-widget elementor-widget-shortcode\" data-id=\"6f9c2b1\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><section class=\"section mp-quote-section\" id=\"preventivo\">\r\n  <div class=\"mp-quote-bg\"><\/div>\r\n  <div class=\"mp-quote-overlay\"><\/div>\r\n\r\n  <div class=\"mp-quote-inner\">\r\n    <div class=\"mp-quote-info\">\r\n      <p class=\"mp-quote-kicker\">Contactez-nous<\/p>\r\n\r\n      <h2 class=\"mp-quote-title\">\r\n        Demandez un<br>\r\n        devis<br>\r\n        gratuit\r\n      <\/h2>\r\n\r\n      <p class=\"mp-quote-description\">\r\n        Vous \u00eates int\u00e9ress\u00e9 par nos produits ou vous recherchez une solution\r\n        personnalis\u00e9e, sp\u00e9cialement con\u00e7ue pour vous ? N'h\u00e9sitez pas \u00e0\r\n        nous contacter : nous sommes \u00e0 votre disposition pour vous accompagner\r\n        et vous guider dans le choix de la solution la mieux adapt\u00e9e \u00e0 vos besoins.\r\n      <\/p>\r\n\r\n      <div class=\"mp-quote-contacts\">\r\n        <a href=\"tel:+390296690901\" class=\"mp-quote-contact\">\r\n          <span class=\"mp-quote-contact-icon\">\r\n            <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M22 16.92v3a2 2 0 0 1-2.18 2A19.86 19.86 0 0 1 11.19 19 19.5 19.5 0 0 1 5 12.81 19.86 19.86 0 0 1 2.08 4.18 2 2 0 0 1 4.06 2h3a2 2 0 0 1 2 1.72c.12.9.33 1.78.62 2.63a2 2 0 0 1-.45 2.11L8 9.69a16 16 0 0 0 6.31 6.31l1.23-1.23a2 2 0 0 1 2.11-.45c.85.29 1.73.5 2.63.62A2 2 0 0 1 22 16.92z\"><\/path>\r\n            <\/svg>\r\n          <\/span>\r\n\r\n          <span>\r\n            <strong>T\u00e9l\u00e9phone<\/strong>\r\n            +39 02 96690901\r\n          <\/span>\r\n        <\/a>\r\n\r\n        <a href=\"mailto:info@millepiani.com\" class=\"mp-quote-contact\">\r\n          <span class=\"mp-quote-contact-icon\">\r\n            <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M4 4h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2z\"><\/path>\r\n              <path d=\"m22 6-10 7L2 6\"><\/path>\r\n            <\/svg>\r\n          <\/span>\r\n\r\n          <span>\r\n            <strong>E-mail<\/strong>\r\n            info@millepiani.com\r\n          <\/span>\r\n        <\/a>\r\n\r\n        <div class=\"mp-quote-contact\">\r\n          <span class=\"mp-quote-contact-icon\">\r\n            <svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">\r\n              <path d=\"M21 10c0 7-9 12-9 12S3 17 3 10a9 9 0 1 1 18 0z\"><\/path>\r\n              <circle cx=\"12\" cy=\"10\" r=\"3\"><\/circle>\r\n            <\/svg>\r\n          <\/span>\r\n\r\n          <span>\r\n            <strong>Bureau<\/strong>\r\n            Via Della Longura, 21 Misinto MB\r\n          <\/span>\r\n        <\/div>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"mp-quote-form-card\">\r\n      <form\r\n        class=\"mp-quote-form\"\r\n        id=\"mpQuoteForm\"\r\n        enctype=\"multipart\/form-data\"\r\n        data-success-url=\"\/grazie\/\"\r\n        novalidate\r\n      >\r\n        <div\r\n          class=\"mp-form-status\"\r\n          id=\"mpFormStatus\"\r\n          aria-live=\"polite\"\r\n        ><\/div>\r\n\r\n        <!-- Pi\u00e8ge antispam -->\r\n        <input\r\n          type=\"text\"\r\n          name=\"website\"\r\n          tabindex=\"-1\"\r\n          autocomplete=\"off\"\r\n          aria-hidden=\"true\"\r\n          style=\"position:absolute;left:-9999px;opacity:0;height:0;width:0;\"\r\n        >\r\n\r\n        <div class=\"mp-form-grid\">\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"text\"\r\n              name=\"name\"\r\n              id=\"mpName\"\r\n              placeholder=\" \"\r\n              required\r\n              autocomplete=\"given-name\"\r\n            >\r\n            <label for=\"mpName\">Pr\u00e9nom *<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"text\"\r\n              name=\"surname\"\r\n              id=\"mpSurname\"\r\n              placeholder=\" \"\r\n              required\r\n              autocomplete=\"family-name\"\r\n            >\r\n            <label for=\"mpSurname\">Nom *<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"email\"\r\n              name=\"email\"\r\n              id=\"mpEmail\"\r\n              placeholder=\" \"\r\n              required\r\n              autocomplete=\"email\"\r\n            >\r\n            <label for=\"mpEmail\">E-mail *<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"tel\"\r\n              name=\"phone\"\r\n              id=\"mpPhone\"\r\n              placeholder=\" \"\r\n              required\r\n              autocomplete=\"tel\"\r\n            >\r\n            <label for=\"mpPhone\">Num\u00e9ro de t\u00e9l\u00e9phone<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <select name=\"role\" id=\"mpRole\">\r\n              <option value=\"\" selected>S\u00e9lectionnez<\/option>\r\n              <option value=\"progettista\">Concepteur<\/option>\r\n              <option value=\"impresa_di_costruzioni\">\r\n                Entreprise de construction\r\n              <\/option>\r\n              <option value=\"proprietario_edificio\">\r\n                Propri\u00e9taire de l'immeuble\r\n              <\/option>\r\n              <option value=\"amministratore_edificio\">\r\n                Gestionnaire de l'immeuble\r\n              <\/option>\r\n              <option value=\"consulente_ascensori\">\r\n                Consultant en ascenseurs\r\n              <\/option>\r\n              <option value=\"ente_pubblico\">Organisme public<\/option>\r\n              <option value=\"cerco_lavoro\">Je recherche un emploi<\/option>\r\n              <option value=\"utente_passeggero\">\r\n                Utilisateur\/Passager\r\n              <\/option>\r\n              <option value=\"studente\">\u00c9tudiant<\/option>\r\n              <option value=\"altro\">Autre<\/option>\r\n            <\/select>\r\n\r\n            <label for=\"mpRole\">Quel est votre r\u00f4le ?<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"text\"\r\n              name=\"company\"\r\n              id=\"mpCompany\"\r\n              placeholder=\" \"\r\n              autocomplete=\"organization\"\r\n            >\r\n            <label for=\"mpCompany\">Nom de l'entreprise<\/label>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"mp-form-grid mp-location-grid\">\r\n          <div class=\"mp-field\">\r\n            <select\r\n              name=\"country\"\r\n              id=\"mpCountry\"\r\n              required\r\n              autocomplete=\"country-name\"\r\n            >\r\n              <option value=\"italia\" selected>Italie<\/option>\r\n              <option value=\"estero\">\u00c9tranger<\/option>\r\n            <\/select>\r\n\r\n            <label for=\"mpCountry\">Pays\/R\u00e9gion *<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <select\r\n              name=\"province\"\r\n              id=\"mpProvince\"\r\n              required\r\n              autocomplete=\"address-level2\"\r\n            >\r\n              <option value=\"\" selected disabled>S\u00e9lectionnez<\/option>\r\n              <option value=\"AG\">Agrigente<\/option>\r\n              <option value=\"AL\">Alexandrie<\/option>\r\n              <option value=\"AN\">Anc\u00f4ne<\/option>\r\n              <option value=\"AO\">Aoste<\/option>\r\n              <option value=\"AR\">Arezzo<\/option>\r\n              <option value=\"AP\">Ascoli Piceno<\/option>\r\n              <option value=\"AT\">Asti<\/option>\r\n              <option value=\"AV\">Avellino<\/option>\r\n              <option value=\"BA\">Bari<\/option>\r\n              <option value=\"BT\">Barletta-Andria-Trani<\/option>\r\n              <option value=\"BL\">Belluno<\/option>\r\n              <option value=\"BN\">B\u00e9n\u00e9vent<\/option>\r\n              <option value=\"BG\">Bergame<\/option>\r\n              <option value=\"BI\">Biella<\/option>\r\n              <option value=\"BO\">Bologne<\/option>\r\n              <option value=\"BZ\">Bolzano<\/option>\r\n              <option value=\"BS\">Brescia<\/option>\r\n              <option value=\"BR\">Brindisi<\/option>\r\n              <option value=\"CA\">Cagliari<\/option>\r\n              <option value=\"CL\">Caltanissetta<\/option>\r\n              <option value=\"CB\">Campobasso<\/option>\r\n              <option value=\"CE\">Caserte<\/option>\r\n              <option value=\"CT\">Catane<\/option>\r\n              <option value=\"CZ\">Catanzaro<\/option>\r\n              <option value=\"CH\">Chieti<\/option>\r\n              <option value=\"CO\">C\u00f4me<\/option>\r\n              <option value=\"CS\">Cosenza<\/option>\r\n              <option value=\"CR\">Cr\u00e9mone<\/option>\r\n              <option value=\"KR\">Crotone<\/option>\r\n              <option value=\"CN\">Coni<\/option>\r\n              <option value=\"EN\">Enna<\/option>\r\n              <option value=\"FM\">Fermo<\/option>\r\n              <option value=\"FE\">Ferrare<\/option>\r\n              <option value=\"FI\">Florence<\/option>\r\n              <option value=\"FG\">Foggia<\/option>\r\n              <option value=\"FC\">Forl\u00ec-Cesena<\/option>\r\n              <option value=\"FR\">Frosinone<\/option>\r\n              <option value=\"GE\">G\u00eanes<\/option>\r\n              <option value=\"GO\">Gorizia<\/option>\r\n              <option value=\"GR\">Grosseto<\/option>\r\n              <option value=\"IM\">Imperia<\/option>\r\n              <option value=\"IS\">Isernia<\/option>\r\n              <option value=\"SP\">La Spezia<\/option>\r\n              <option value=\"AQ\">L'Aquila<\/option>\r\n              <option value=\"LT\">Latina<\/option>\r\n              <option value=\"LE\">Lecce<\/option>\r\n              <option value=\"LC\">Lecco<\/option>\r\n              <option value=\"LI\">Livourne<\/option>\r\n              <option value=\"LO\">Lodi<\/option>\r\n              <option value=\"LU\">Lucques<\/option>\r\n              <option value=\"MC\">Macerata<\/option>\r\n              <option value=\"MN\">Mantoue<\/option>\r\n              <option value=\"MS\">Massa-Carrara<\/option>\r\n              <option value=\"MT\">Matera<\/option>\r\n              <option value=\"ME\">Messine<\/option>\r\n              <option value=\"MI\">Milan<\/option>\r\n              <option value=\"MO\">Mod\u00e8ne<\/option>\r\n              <option value=\"MB\">Monza et Brianza<\/option>\r\n              <option value=\"NA\">Naples<\/option>\r\n              <option value=\"NO\">Novare<\/option>\r\n              <option value=\"NU\">Nuoro<\/option>\r\n              <option value=\"OR\">Oristano<\/option>\r\n              <option value=\"PD\">Padoue<\/option>\r\n              <option value=\"PA\">Palerme<\/option>\r\n              <option value=\"PR\">Parme<\/option>\r\n              <option value=\"PV\">Pavie<\/option>\r\n              <option value=\"PG\">P\u00e9rouse<\/option>\r\n              <option value=\"PU\">Pesaro et Urbino<\/option>\r\n              <option value=\"PE\">Pescara<\/option>\r\n              <option value=\"PC\">Plaisance<\/option>\r\n              <option value=\"PI\">Pise<\/option>\r\n              <option value=\"PT\">Pistoia<\/option>\r\n              <option value=\"PN\">Pordenone<\/option>\r\n              <option value=\"PZ\">Potenza<\/option>\r\n              <option value=\"PO\">Prato<\/option>\r\n              <option value=\"RG\">Raguse<\/option>\r\n              <option value=\"RA\">Ravenne<\/option>\r\n              <option value=\"RC\">Reggio de Calabre<\/option>\r\n              <option value=\"RE\">Reggio d'\u00c9milie<\/option>\r\n              <option value=\"RI\">Rieti<\/option>\r\n              <option value=\"RN\">Rimini<\/option>\r\n              <option value=\"RM\">Rome<\/option>\r\n              <option value=\"RO\">Rovigo<\/option>\r\n              <option value=\"SA\">Salerne<\/option>\r\n              <option value=\"SS\">Sassari<\/option>\r\n              <option value=\"SV\">Savone<\/option>\r\n              <option value=\"SI\">Sienne<\/option>\r\n              <option value=\"SR\">Syracuse<\/option>\r\n              <option value=\"SO\">Sondrio<\/option>\r\n              <option value=\"SU\">Sardaigne du Sud<\/option>\r\n              <option value=\"TA\">Tarente<\/option>\r\n              <option value=\"TE\">Teramo<\/option>\r\n              <option value=\"TR\">Terni<\/option>\r\n              <option value=\"TO\">Turin<\/option>\r\n              <option value=\"TP\">Trapani<\/option>\r\n              <option value=\"TN\">Trente<\/option>\r\n              <option value=\"TV\">Tr\u00e9vise<\/option>\r\n              <option value=\"TS\">Trieste<\/option>\r\n              <option value=\"UD\">Udine<\/option>\r\n              <option value=\"VA\">Var\u00e8se<\/option>\r\n              <option value=\"VE\">Venise<\/option>\r\n              <option value=\"VB\">Verbano-Cusio-Ossola<\/option>\r\n              <option value=\"VC\">Verceil<\/option>\r\n              <option value=\"VR\">V\u00e9rone<\/option>\r\n              <option value=\"VV\">Vibo Valentia<\/option>\r\n              <option value=\"VI\">Vicence<\/option>\r\n              <option value=\"VT\">Viterbe<\/option>\r\n              <option value=\"ESTERO\">Province \u00e0 l'\u00e9tranger<\/option>\r\n            <\/select>\r\n\r\n            <label for=\"mpProvince\">Province *<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <input\r\n              type=\"text\"\r\n              name=\"city\"\r\n              id=\"mpCity\"\r\n              placeholder=\" \"\r\n              required\r\n              autocomplete=\"address-level2\"\r\n            >\r\n            <label for=\"mpCity\">Ville *<\/label>\r\n          <\/div>\r\n        <\/div>\r\n\r\n        <div class=\"mp-field mp-field-full\">\r\n          <select\r\n            name=\"contact_reason\"\r\n            id=\"mpContactReason\"\r\n            required\r\n          >\r\n            <option value=\"\" selected disabled>S\u00e9lectionnez<\/option>\r\n            <option value=\"nuovo_impianto\">\r\n              Installation d'un nouvel ascenseur dans un b\u00e2timent neuf\/existant\r\n            <\/option>\r\n            <option value=\"sostituzione_ascensore\">\r\n              Remplacement d'un ascenseur existant\r\n            <\/option>\r\n            <option value=\"ammodernamento_impianto\">\r\n              Modernisation d'une installation\r\n            <\/option>\r\n            <option value=\"assistenza_manutenzione\">\r\n              Je suis int\u00e9ress\u00e9 par le service d'assistance et de maintenance\r\n            <\/option>\r\n            <option value=\"ricambi\">\r\n              Pi\u00e8ces d\u00e9tach\u00e9es pour ascenseurs Millepiani\r\n            <\/option>\r\n            <option value=\"altro\">Autre<\/option>\r\n          <\/select>\r\n\r\n          <label for=\"mpContactReason\">\r\n            Pour quel sujet souhaitez-vous \u00eatre contact\u00e9 ? *\r\n          <\/label>\r\n        <\/div>\r\n\r\n        <div class=\"mp-field mp-field-full\">\r\n          <textarea\r\n            name=\"message\"\r\n            id=\"mpMessage\"\r\n            rows=\"4\"\r\n            placeholder=\" \"\r\n          ><\/textarea>\r\n\r\n          <label for=\"mpMessage\">Comment pouvons-nous vous aider ?<\/label>\r\n        <\/div>\r\n\r\n        <div class=\"mp-form-foot\">\r\n          <div class=\"mp-privacy-box\">\r\n            <label\r\n              class=\"mp-privacy-check\"\r\n              for=\"mpPrivacyAcceptance\"\r\n            >\r\n              <input\r\n                type=\"checkbox\"\r\n                name=\"privacy_acceptance\"\r\n                id=\"mpPrivacyAcceptance\"\r\n                value=\"accepted\"\r\n                required\r\n              >\r\n\r\n              <span>\r\n                J'accepte la\r\n                <a\r\n                  href=\"\/privacy-policy\/\"\r\n                  target=\"_blank\"\r\n                  rel=\"noopener noreferrer\"\r\n                >\r\n                  Politique de confidentialit\u00e9\r\n                <\/a>. *\r\n              <\/span>\r\n            <\/label>\r\n\r\n            <label\r\n              class=\"mp-privacy-check\"\r\n              for=\"mpExistingContract\"\r\n            >\r\n              <input\r\n                type=\"checkbox\"\r\n                name=\"existing_contract\"\r\n                id=\"mpExistingContract\"\r\n                value=\"yes\"\r\n              >\r\n\r\n              <span>\r\n                J'ai d\u00e9j\u00e0 un contrat en cours avec Millepiani\r\n              <\/span>\r\n            <\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-captcha\">\r\n            <div\r\n              class=\"g-recaptcha\"\r\n              data-sitekey=\"6LfxY6ktAAAAAFBbvnofa2IpjG_zaaX5UdOtJuTS\"\r\n            ><\/div>\r\n          <\/div>\r\n\r\n          <button type=\"submit\" class=\"mp-submit-btn\">\r\n            <span>Envoyer le message<\/span>\r\n          <\/button>\r\n        <\/div>\r\n      <\/form>\r\n    <\/div>\r\n  <\/div>\r\n<\/section><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<div class=\"elementor-element elementor-element-ad21197 elementor-hidden-desktop elementor-hidden-tablet elementor-hidden-mobile elementor-widget elementor-widget-shortcode\" data-id=\"ad21197\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><script>document.documentElement.classList.add(\"mp-js-ready\");\r\n\r\ndocument.addEventListener(\"DOMContentLoaded\", function () {\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Reveal animation\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const revealItems = document.querySelectorAll(\".mp-reveal\");\r\n\r\n  if (\"IntersectionObserver\" in window) {\r\n    const revealObserver = new IntersectionObserver(function (entries, observer) {\r\n      entries.forEach(function (entry) {\r\n        if (!entry.isIntersecting) return;\r\n\r\n        entry.target.classList.add(\"is-visible\");\r\n        observer.unobserve(entry.target);\r\n      });\r\n    }, {\r\n      threshold: 0.16\r\n    });\r\n\r\n    revealItems.forEach(function (item) {\r\n      revealObserver.observe(item);\r\n    });\r\n  } else {\r\n    revealItems.forEach(function (item) {\r\n      item.classList.add(\"is-visible\");\r\n    });\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Smooth scroll\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  document.querySelectorAll('a[href^=\"#\"]').forEach(function (anchor) {\r\n    anchor.addEventListener(\"click\", function (event) {\r\n      const targetId = anchor.getAttribute(\"href\");\r\n\r\n      if (!targetId || targetId === \"#\") return;\r\n\r\n      const target = document.querySelector(targetId);\r\n\r\n      if (!target) return;\r\n\r\n      event.preventDefault();\r\n\r\n      target.scrollIntoView({\r\n        behavior: \"smooth\",\r\n        block: \"start\"\r\n      });\r\n    });\r\n  });\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Language\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const pageLang = (document.documentElement.lang || \"it\")\r\n    .toLowerCase()\r\n    .split(\"-\")[0];\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Route technical data\r\n  |--------------------------------------------------------------------------\r\n  |\r\n  | Questi valori NON vengono tradotti.\r\n  | Sono utilizzati esclusivamente dalla parte grafica \/ Three.js.\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const routeTechnicalData = {\r\n    straight: {\r\n      camera: {\r\n        x: 4.3,\r\n        y: 3.0,\r\n        z: 8.1\r\n      },\r\n      rotation: -0.58,\r\n      stepDepth: 2.05,\r\n      outdoor: false\r\n    },\r\n\r\n    curved: {\r\n      camera: {\r\n        x: 4.8,\r\n        y: 3.35,\r\n        z: 7.7\r\n      },\r\n      rotation: -0.92,\r\n      stepDepth: 1.95,\r\n      outdoor: false\r\n    },\r\n\r\n    narrow: {\r\n      camera: {\r\n        x: 3.55,\r\n        y: 3.05,\r\n        z: 8.7\r\n      },\r\n      rotation: -0.46,\r\n      stepDepth: 1.34,\r\n      outdoor: false\r\n    },\r\n\r\n    outdoor: {\r\n      camera: {\r\n        x: 4.55,\r\n        y: 3.1,\r\n        z: 8.35\r\n      },\r\n      rotation: -0.64,\r\n      stepDepth: 2.0,\r\n      outdoor: true\r\n    }\r\n  };\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Translations\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const servoTranslations = {\r\n\r\n    \/*\r\n    |--------------------------------------------------------------------------\r\n    | ITALIANO\r\n    |--------------------------------------------------------------------------\r\n    *\/\r\n\r\n    it: {\r\n      routes: {\r\n        straight: {\r\n          title: \"Percorso lineare,<br>installazione discreta\",\r\n          text: \"La soluzione ideale per scale dritte: pulita, elegante e poco invasiva, progettata per migliorare l\u2019autonomia quotidiana.\",\r\n          tags: [\r\n            \"Scala dritta\",\r\n            \"Casa \/ ufficio\",\r\n            \"Ingombro ridotto\"\r\n          ],\r\n          dashRoute: \"Scala dritta\",\r\n          dashContext: \"Casa \/ ufficio\"\r\n        },\r\n\r\n        curved: {\r\n          title: \"Segue la scala,<br>anche quando cambia\",\r\n          text: \"Per scale curve, tortuose o con cambi di pendenza: il binario viene modellato sul percorso reale dell\u2019ambiente.\",\r\n          tags: [\r\n            \"Scala curva\",\r\n            \"Su misura\",\r\n            \"Cambi pendenza\"\r\n          ],\r\n          dashRoute: \"Scala curva\",\r\n          dashContext: \"Percorso complesso\"\r\n        },\r\n\r\n        narrow: {\r\n          title: \"Compatto dove<br>lo spazio \u00e8 prezioso\",\r\n          text: \"Una configurazione pensata per ridurre l\u2019ingombro e preservare il passaggio anche in scale strette o ambienti vincolati.\",\r\n          tags: [\r\n            \"Spazi stretti\",\r\n            \"Compatto\",\r\n            \"Discreto\"\r\n          ],\r\n          dashRoute: \"Spazio stretto\",\r\n          dashContext: \"Ingombro ridotto\"\r\n        },\r\n\r\n        outdoor: {\r\n          title: \"Accessibilit\u00e0 anche<br>fuori dall\u2019edificio\",\r\n          text: \"Per ingressi, rampe esterne e dislivelli che limitano l\u2019accesso a case, uffici o spazi pubblici.\",\r\n          tags: [\r\n            \"Esterno\",\r\n            \"Accessi\",\r\n            \"Dislivelli\"\r\n          ],\r\n          dashRoute: \"Esterno\",\r\n          dashContext: \"Accesso pubblico \/ privato\"\r\n        }\r\n      },\r\n\r\n      points: {\r\n        survey: {\r\n          label: \"Sopralluogo\",\r\n          title: \"Analisi dell\u2019ambiente e delle esigenze\",\r\n          text: \"Il percorso viene studiato sulla base della scala, degli spazi disponibili, dell\u2019utente e del contesto in cui il servoscale sar\u00e0 inserito.\"\r\n        },\r\n\r\n        install: {\r\n          label: \"Installazione\",\r\n          title: \"Soluzione poco invasiva\",\r\n          text: \"Nella maggior parte delle situazioni il servoscale pu\u00f2 essere predisposto senza opere murarie importanti, preservando l\u2019ambiente.\"\r\n        },\r\n\r\n        comfort: {\r\n          label: \"Comfort\",\r\n          title: \"Seggiolino o piattaforma\",\r\n          text: \"La soluzione pu\u00f2 essere configurata con seggiolino richiudibile oppure piattaforma, in base all\u2019utilizzo e allo spazio disponibile.\"\r\n        }\r\n      },\r\n\r\n      modes: {\r\n        seat: {\r\n          label: \"Seggiolino\"\r\n        },\r\n\r\n        platform: {\r\n          label: \"Piattaforma\"\r\n        }\r\n      }\r\n    },\r\n\r\n\r\n    \/*\r\n    |--------------------------------------------------------------------------\r\n    | ENGLISH\r\n    |--------------------------------------------------------------------------\r\n    *\/\r\n\r\n    en: {\r\n      routes: {\r\n        straight: {\r\n          title: \"Straight route,<br>discreet installation\",\r\n          text: \"The ideal solution for straight staircases: clean, elegant and minimally invasive, designed to improve everyday independence.\",\r\n          tags: [\r\n            \"Straight staircase\",\r\n            \"Home \/ office\",\r\n            \"Compact design\"\r\n          ],\r\n          dashRoute: \"Straight staircase\",\r\n          dashContext: \"Home \/ office\"\r\n        },\r\n\r\n        curved: {\r\n          title: \"Follows the stairs,<br>even when they change\",\r\n          text: \"For curved or winding staircases and changes in gradient, the rail is custom-shaped to follow the actual route of the environment.\",\r\n          tags: [\r\n            \"Curved staircase\",\r\n            \"Custom-made\",\r\n            \"Gradient changes\"\r\n          ],\r\n          dashRoute: \"Curved staircase\",\r\n          dashContext: \"Complex route\"\r\n        },\r\n\r\n        narrow: {\r\n          title: \"Compact where<br>space is precious\",\r\n          text: \"A configuration designed to minimise obstruction and preserve passage space, even on narrow staircases or in restricted environments.\",\r\n          tags: [\r\n            \"Narrow spaces\",\r\n            \"Compact\",\r\n            \"Discreet\"\r\n          ],\r\n          dashRoute: \"Narrow space\",\r\n          dashContext: \"Compact footprint\"\r\n        },\r\n\r\n        outdoor: {\r\n          title: \"Accessibility<br>outside the building too\",\r\n          text: \"Designed for entrances, outdoor ramps and changes in level that can limit access to homes, offices or public spaces.\",\r\n          tags: [\r\n            \"Outdoor\",\r\n            \"Access\",\r\n            \"Changes in level\"\r\n          ],\r\n          dashRoute: \"Outdoor\",\r\n          dashContext: \"Public \/ private access\"\r\n        }\r\n      },\r\n\r\n      points: {\r\n        survey: {\r\n          label: \"Site survey\",\r\n          title: \"Analysis of the environment and requirements\",\r\n          text: \"The route is designed according to the staircase, available space, the user and the environment in which the stairlift will be installed.\"\r\n        },\r\n\r\n        install: {\r\n          label: \"Installation\",\r\n          title: \"Minimally invasive solution\",\r\n          text: \"In most situations, the stairlift can be installed without major building work, helping preserve the existing environment.\"\r\n        },\r\n\r\n        comfort: {\r\n          label: \"Comfort\",\r\n          title: \"Seat or platform\",\r\n          text: \"The solution can be configured with a folding seat or a platform according to its intended use and the available space.\"\r\n        }\r\n      },\r\n\r\n      modes: {\r\n        seat: {\r\n          label: \"Seat\"\r\n        },\r\n\r\n        platform: {\r\n          label: \"Platform\"\r\n        }\r\n      }\r\n    },\r\n\r\n\r\n    \/*\r\n    |--------------------------------------------------------------------------\r\n    | FRAN\u00c7AIS\r\n    |--------------------------------------------------------------------------\r\n    *\/\r\n\r\n    fr: {\r\n      routes: {\r\n        straight: {\r\n          title: \"Trajet lin\u00e9aire,<br>installation discr\u00e8te\",\r\n          text: \"La solution id\u00e9ale pour les escaliers droits : propre, \u00e9l\u00e9gante et peu invasive, con\u00e7ue pour am\u00e9liorer l\u2019autonomie au quotidien.\",\r\n          tags: [\r\n            \"Escalier droit\",\r\n            \"Maison \/ bureau\",\r\n            \"Encombrement r\u00e9duit\"\r\n          ],\r\n          dashRoute: \"Escalier droit\",\r\n          dashContext: \"Maison \/ bureau\"\r\n        },\r\n\r\n        curved: {\r\n          title: \"Suit l\u2019escalier,<br>m\u00eame lorsqu\u2019il change\",\r\n          text: \"Pour les escaliers courbes, sinueux ou pr\u00e9sentant des changements de pente, le rail est fa\u00e7onn\u00e9 sur mesure afin de suivre le parcours r\u00e9el.\",\r\n          tags: [\r\n            \"Escalier courbe\",\r\n            \"Sur mesure\",\r\n            \"Changements de pente\"\r\n          ],\r\n          dashRoute: \"Escalier courbe\",\r\n          dashContext: \"Parcours complexe\"\r\n        },\r\n\r\n        narrow: {\r\n          title: \"Compact lorsque<br>l\u2019espace est pr\u00e9cieux\",\r\n          text: \"Une configuration con\u00e7ue pour r\u00e9duire l\u2019encombrement et pr\u00e9server le passage, m\u00eame dans les escaliers \u00e9troits ou les espaces restreints.\",\r\n          tags: [\r\n            \"Espaces \u00e9troits\",\r\n            \"Compact\",\r\n            \"Discret\"\r\n          ],\r\n          dashRoute: \"Espace \u00e9troit\",\r\n          dashContext: \"Encombrement r\u00e9duit\"\r\n        },\r\n\r\n        outdoor: {\r\n          title: \"L\u2019accessibilit\u00e9 aussi<br>\u00e0 l\u2019ext\u00e9rieur\",\r\n          text: \"Pour les entr\u00e9es, les rampes ext\u00e9rieures et les diff\u00e9rences de niveau qui limitent l\u2019acc\u00e8s aux maisons, bureaux ou espaces publics.\",\r\n          tags: [\r\n            \"Ext\u00e9rieur\",\r\n            \"Acc\u00e8s\",\r\n            \"Diff\u00e9rences de niveau\"\r\n          ],\r\n          dashRoute: \"Ext\u00e9rieur\",\r\n          dashContext: \"Acc\u00e8s public \/ priv\u00e9\"\r\n        }\r\n      },\r\n\r\n      points: {\r\n        survey: {\r\n          label: \"Visite technique\",\r\n          title: \"Analyse de l\u2019environnement et des besoins\",\r\n          text: \"Le parcours est \u00e9tudi\u00e9 en fonction de l\u2019escalier, de l\u2019espace disponible, de l\u2019utilisateur et du contexte dans lequel le monte-escalier sera install\u00e9.\"\r\n        },\r\n\r\n        install: {\r\n          label: \"Installation\",\r\n          title: \"Une solution peu invasive\",\r\n          text: \"Dans la plupart des situations, le monte-escalier peut \u00eatre install\u00e9 sans travaux de ma\u00e7onnerie importants, tout en pr\u00e9servant l\u2019environnement existant.\"\r\n        },\r\n\r\n        comfort: {\r\n          label: \"Confort\",\r\n          title: \"Si\u00e8ge ou plateforme\",\r\n          text: \"La solution peut \u00eatre configur\u00e9e avec un si\u00e8ge rabattable ou une plateforme, selon l\u2019utilisation pr\u00e9vue et l\u2019espace disponible.\"\r\n        }\r\n      },\r\n\r\n      modes: {\r\n        seat: {\r\n          label: \"Si\u00e8ge\"\r\n        },\r\n\r\n        platform: {\r\n          label: \"Plateforme\"\r\n        }\r\n      }\r\n    },\r\n\r\n\r\n    \/*\r\n    |--------------------------------------------------------------------------\r\n    | \u0420\u0423\u0421\u0421\u041a\u0418\u0419\r\n    |--------------------------------------------------------------------------\r\n    *\/\r\n\r\n    ru: {\r\n      routes: {\r\n        straight: {\r\n          title: \"\u041f\u0440\u044f\u043c\u043e\u0439 \u043c\u0430\u0440\u0448\u0440\u0443\u0442,<br>\u043d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u0430\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\",\r\n          text: \"\u0418\u0434\u0435\u0430\u043b\u044c\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u043f\u0440\u044f\u043c\u044b\u0445 \u043b\u0435\u0441\u0442\u043d\u0438\u0446: \u0430\u043a\u043a\u0443\u0440\u0430\u0442\u043d\u043e\u0435, \u044d\u043b\u0435\u0433\u0430\u043d\u0442\u043d\u043e\u0435 \u0438 \u043a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u043e\u0435, \u0441\u043e\u0437\u0434\u0430\u043d\u043d\u043e\u0435 \u0434\u043b\u044f \u043f\u043e\u0432\u044b\u0448\u0435\u043d\u0438\u044f \u0441\u0430\u043c\u043e\u0441\u0442\u043e\u044f\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u0438 \u0432 \u043f\u043e\u0432\u0441\u0435\u0434\u043d\u0435\u0432\u043d\u043e\u0439 \u0436\u0438\u0437\u043d\u0438.\",\r\n          tags: [\r\n            \"\u041f\u0440\u044f\u043c\u0430\u044f \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0430\",\r\n            \"\u0414\u043e\u043c \/ \u043e\u0444\u0438\u0441\",\r\n            \"\u041a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u043e\u0441\u0442\u044c\"\r\n          ],\r\n          dashRoute: \"\u041f\u0440\u044f\u043c\u0430\u044f \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0430\",\r\n          dashContext: \"\u0414\u043e\u043c \/ \u043e\u0444\u0438\u0441\"\r\n        },\r\n\r\n        curved: {\r\n          title: \"\u0421\u043b\u0435\u0434\u0443\u0435\u0442 \u0437\u0430 \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0435\u0439,<br>\u0434\u0430\u0436\u0435 \u043f\u0440\u0438 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0438 \u0442\u0440\u0430\u0435\u043a\u0442\u043e\u0440\u0438\u0438\",\r\n          text: \"\u0414\u043b\u044f \u0438\u0437\u043e\u0433\u043d\u0443\u0442\u044b\u0445 \u0438 \u0441\u043b\u043e\u0436\u043d\u044b\u0445 \u043b\u0435\u0441\u0442\u043d\u0438\u0446, \u0430 \u0442\u0430\u043a\u0436\u0435 \u0443\u0447\u0430\u0441\u0442\u043a\u043e\u0432 \u0441 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435\u043c \u0443\u043a\u043b\u043e\u043d\u0430 \u043d\u0430\u043f\u0440\u0430\u0432\u043b\u044f\u044e\u0449\u0430\u044f \u0438\u0437\u0433\u043e\u0442\u0430\u0432\u043b\u0438\u0432\u0430\u0435\u0442\u0441\u044f \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u0440\u0435\u0430\u043b\u044c\u043d\u043e\u0439 \u0442\u0440\u0430\u0435\u043a\u0442\u043e\u0440\u0438\u0438 \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u044b.\",\r\n          tags: [\r\n            \"\u0418\u0437\u043e\u0433\u043d\u0443\u0442\u0430\u044f \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0430\",\r\n            \"\u041d\u0430 \u0437\u0430\u043a\u0430\u0437\",\r\n            \"\u0418\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0435 \u0443\u043a\u043b\u043e\u043d\u0430\"\r\n          ],\r\n          dashRoute: \"\u0418\u0437\u043e\u0433\u043d\u0443\u0442\u0430\u044f \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0430\",\r\n          dashContext: \"\u0421\u043b\u043e\u0436\u043d\u0430\u044f \u0442\u0440\u0430\u0435\u043a\u0442\u043e\u0440\u0438\u044f\"\r\n        },\r\n\r\n        narrow: {\r\n          title: \"\u041a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u043e\u0435 \u0440\u0435\u0448\u0435\u043d\u0438\u0435,<br>\u043a\u043e\u0433\u0434\u0430 \u0432\u0430\u0436\u0435\u043d \u043a\u0430\u0436\u0434\u044b\u0439 \u0441\u0430\u043d\u0442\u0438\u043c\u0435\u0442\u0440\",\r\n          text: \"\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0440\u0430\u0437\u0440\u0430\u0431\u043e\u0442\u0430\u043d\u0430 \u0434\u043b\u044f \u043c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0433\u043e \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u0441\u043e\u0445\u0440\u0430\u043d\u0435\u043d\u0438\u044f \u0441\u0432\u043e\u0431\u043e\u0434\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0445\u043e\u0434\u0430 \u0434\u0430\u0436\u0435 \u043d\u0430 \u0443\u0437\u043a\u0438\u0445 \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u0430\u0445 \u0438 \u0432 \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0435\u043d\u043d\u044b\u0445 \u043f\u043e\u043c\u0435\u0449\u0435\u043d\u0438\u044f\u0445.\",\r\n          tags: [\r\n            \"\u0423\u0437\u043a\u0438\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430\",\r\n            \"\u041a\u043e\u043c\u043f\u0430\u043a\u0442\u043d\u043e\u0441\u0442\u044c\",\r\n            \"\u041d\u0435\u0437\u0430\u043c\u0435\u0442\u043d\u043e\u0441\u0442\u044c\"\r\n          ],\r\n          dashRoute: \"\u0423\u0437\u043a\u043e\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e\",\r\n          dashContext: \"\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u044b\u0435 \u0433\u0430\u0431\u0430\u0440\u0438\u0442\u044b\"\r\n        },\r\n\r\n        outdoor: {\r\n          title: \"\u0414\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0441\u0442\u044c<br>\u0438 \u0437\u0430 \u043f\u0440\u0435\u0434\u0435\u043b\u0430\u043c\u0438 \u0437\u0434\u0430\u043d\u0438\u044f\",\r\n          text: \"\u0420\u0435\u0448\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0432\u0445\u043e\u0434\u043e\u0432, \u043d\u0430\u0440\u0443\u0436\u043d\u044b\u0445 \u043b\u0435\u0441\u0442\u043d\u0438\u0446, \u043f\u0430\u043d\u0434\u0443\u0441\u043e\u0432 \u0438 \u043f\u0435\u0440\u0435\u043f\u0430\u0434\u043e\u0432 \u0432\u044b\u0441\u043e\u0442\u044b, \u043e\u0433\u0440\u0430\u043d\u0438\u0447\u0438\u0432\u0430\u044e\u0449\u0438\u0445 \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u0434\u043e\u043c\u0430\u043c, \u043e\u0444\u0438\u0441\u0430\u043c \u0438\u043b\u0438 \u043e\u0431\u0449\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u043c \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430\u043c.\",\r\n          tags: [\r\n            \"\u041d\u0430 \u0443\u043b\u0438\u0446\u0435\",\r\n            \"\u0414\u043e\u0441\u0442\u0443\u043f\",\r\n            \"\u041f\u0435\u0440\u0435\u043f\u0430\u0434\u044b \u0432\u044b\u0441\u043e\u0442\u044b\"\r\n          ],\r\n          dashRoute: \"\u041d\u0430\u0440\u0443\u0436\u043d\u0430\u044f \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\",\r\n          dashContext: \"\u041e\u0431\u0449\u0435\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \/ \u0447\u0430\u0441\u0442\u043d\u044b\u0439 \u0434\u043e\u0441\u0442\u0443\u043f\"\r\n        }\r\n      },\r\n\r\n      points: {\r\n        survey: {\r\n          label: \"\u041e\u0441\u043c\u043e\u0442\u0440 \u043e\u0431\u044a\u0435\u043a\u0442\u0430\",\r\n          title: \"\u0410\u043d\u0430\u043b\u0438\u0437 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430 \u0438 \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u0435\u0439\",\r\n          text: \"\u041c\u0430\u0440\u0448\u0440\u0443\u0442 \u043f\u0440\u043e\u0435\u043a\u0442\u0438\u0440\u0443\u0435\u0442\u0441\u044f \u0441 \u0443\u0447\u0451\u0442\u043e\u043c \u043a\u043e\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u0438 \u043b\u0435\u0441\u0442\u043d\u0438\u0446\u044b, \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430, \u043f\u043e\u0442\u0440\u0435\u0431\u043d\u043e\u0441\u0442\u0435\u0439 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f \u0438 \u0443\u0441\u043b\u043e\u0432\u0438\u0439, \u0432 \u043a\u043e\u0442\u043e\u0440\u044b\u0445 \u0431\u0443\u0434\u0435\u0442 \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u043b\u0435\u043d \u043b\u0435\u0441\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u043f\u043e\u0434\u044a\u0451\u043c\u043d\u0438\u043a.\"\r\n        },\r\n\r\n        install: {\r\n          label: \"\u0423\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0430\",\r\n          title: \"\u041c\u0438\u043d\u0438\u043c\u0430\u043b\u044c\u043d\u043e\u0435 \u0432\u043c\u0435\u0448\u0430\u0442\u0435\u043b\u044c\u0441\u0442\u0432\u043e\",\r\n          text: \"\u0412 \u0431\u043e\u043b\u044c\u0448\u0438\u043d\u0441\u0442\u0432\u0435 \u0441\u043b\u0443\u0447\u0430\u0435\u0432 \u043b\u0435\u0441\u0442\u043d\u0438\u0447\u043d\u044b\u0439 \u043f\u043e\u0434\u044a\u0451\u043c\u043d\u0438\u043a \u043c\u043e\u0436\u043d\u043e \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0431\u0435\u0437 \u0441\u0435\u0440\u044c\u0451\u0437\u043d\u044b\u0445 \u0441\u0442\u0440\u043e\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0440\u0430\u0431\u043e\u0442, \u0441\u043e\u0445\u0440\u0430\u043d\u0438\u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044e\u0449\u0435\u0435 \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u043e.\"\r\n        },\r\n\r\n        comfort: {\r\n          label: \"\u041a\u043e\u043c\u0444\u043e\u0440\u0442\",\r\n          title: \"\u0421\u0438\u0434\u0435\u043d\u044c\u0435 \u0438\u043b\u0438 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430\",\r\n          text: \"\u0421\u0438\u0441\u0442\u0435\u043c\u0430 \u043c\u043e\u0436\u0435\u0442 \u0431\u044b\u0442\u044c \u043e\u0441\u043d\u0430\u0449\u0435\u043d\u0430 \u0441\u043a\u043b\u0430\u0434\u043d\u044b\u043c \u0441\u0438\u0434\u0435\u043d\u044c\u0435\u043c \u0438\u043b\u0438 \u043f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u043e\u0439 \u0432 \u0437\u0430\u0432\u0438\u0441\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0442 \u0441\u043f\u043e\u0441\u043e\u0431\u0430 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u043d\u0438\u044f \u0438 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0441\u0442\u0440\u0430\u043d\u0441\u0442\u0432\u0430.\"\r\n        }\r\n      },\r\n\r\n      modes: {\r\n        seat: {\r\n          label: \"\u0421\u0438\u0434\u0435\u043d\u044c\u0435\"\r\n        },\r\n\r\n        platform: {\r\n          label: \"\u041f\u043b\u0430\u0442\u0444\u043e\u0440\u043c\u0430\"\r\n        }\r\n      }\r\n    }\r\n  };\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Active translations\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const activeServoTranslations =\r\n    servoTranslations[pageLang] ||\r\n    servoTranslations.it;\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Build route data\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const routeData = {};\r\n\r\n  Object.keys(routeTechnicalData).forEach(function (routeKey) {\r\n    routeData[routeKey] = Object.assign(\r\n      {},\r\n      routeTechnicalData[routeKey],\r\n      activeServoTranslations.routes[routeKey]\r\n    );\r\n  });\r\n\r\n  const pointData = activeServoTranslations.points;\r\n  const modeData = activeServoTranslations.modes;\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | DOM\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  const canvas = document.querySelector(\"#mpServoCanvas\");\r\n  const fallback = document.querySelector(\"#mpServoFallback\");\r\n\r\n  const servoTitle = document.querySelector(\"#mpServoTitle\");\r\n  const servoText = document.querySelector(\"#mpServoText\");\r\n  const servoTags = document.querySelector(\"#mpServoTags\");\r\n\r\n  const dashRoute = document.querySelector(\"#mpServoDashRoute\");\r\n  const dashContext = document.querySelector(\"#mpServoDashContext\");\r\n  const dashMode = document.querySelector(\"#mpServoDashMode\");\r\n\r\n  const routeButtons = document.querySelectorAll(\".mp-servo-switch button\");\r\n  const modeButtons = document.querySelectorAll(\".mp-servo-mode-row button\");\r\n  const hotspotButtons = document.querySelectorAll(\".mp-servo-hotspot\");\r\n  const infoCard = document.querySelector(\"#mpServoInfoCard\");\r\n  const canvasWrap = document.querySelector(\".mp-servo-canvas-wrap\");\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | UI helpers\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  let currentRoute = \"straight\";\r\n  let currentMode = \"seat\";\r\n\r\n\r\n  function animateElement(element) {\r\n    if (!element || !element.animate) return;\r\n\r\n    element.animate([\r\n      {\r\n        opacity: 0.35,\r\n        transform: \"translateY(18px)\"\r\n      },\r\n      {\r\n        opacity: 1,\r\n        transform: \"translateY(0)\"\r\n      }\r\n    ], {\r\n      duration: 430,\r\n      easing: \"cubic-bezier(0.22, 1, 0.36, 1)\"\r\n    });\r\n  }\r\n\r\n\r\n  function setRouteUI(routeKey) {\r\n    const route = routeData[routeKey];\r\n\r\n    if (!route) return;\r\n\r\n    if (servoTitle) {\r\n      servoTitle.innerHTML = route.title;\r\n    }\r\n\r\n    if (servoText) {\r\n      servoText.textContent = route.text;\r\n    }\r\n\r\n    if (servoTags) {\r\n      servoTags.innerHTML = \"\";\r\n\r\n      route.tags.forEach(function (tag) {\r\n        const span = document.createElement(\"span\");\r\n\r\n        span.textContent = tag;\r\n\r\n        servoTags.appendChild(span);\r\n      });\r\n    }\r\n\r\n    if (dashRoute) {\r\n      dashRoute.textContent = route.dashRoute;\r\n    }\r\n\r\n    if (dashContext) {\r\n      dashContext.textContent = route.dashContext;\r\n    }\r\n\r\n    routeButtons.forEach(function (button) {\r\n      button.classList.toggle(\r\n        \"is-active\",\r\n        button.getAttribute(\"data-route\") === routeKey\r\n      );\r\n    });\r\n\r\n    animateElement(\r\n      document.querySelector(\".mp-servo-copy\")\r\n    );\r\n  }\r\n\r\n\r\n  function setPoint(pointKey) {\r\n    const point = pointData[pointKey];\r\n\r\n    if (!point || !infoCard) return;\r\n\r\n    infoCard.innerHTML =\r\n      \"<p>\" + point.label + \"<\/p>\" +\r\n      \"<h3>\" + point.title + \"<\/h3>\" +\r\n      \"<span>\" + point.text + \"<\/span>\";\r\n\r\n    hotspotButtons.forEach(function (button) {\r\n      button.classList.toggle(\r\n        \"is-active\",\r\n        button.getAttribute(\"data-point\") === pointKey\r\n      );\r\n    });\r\n\r\n    animateElement(infoCard);\r\n  }\r\n\r\n\r\n  function setModeUI(modeKey) {\r\n    const mode = modeData[modeKey];\r\n\r\n    if (!mode) return;\r\n\r\n    currentMode = modeKey;\r\n\r\n    if (dashMode) {\r\n      dashMode.textContent = mode.label;\r\n    }\r\n\r\n    modeButtons.forEach(function (button) {\r\n      button.classList.toggle(\r\n        \"is-active\",\r\n        button.getAttribute(\"data-mode\") === modeKey\r\n      );\r\n    });\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | THREE JS - lightweight procedural model\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  let renderer = null;\r\n  let scene = null;\r\n  let camera = null;\r\n\r\n  let mainGroup = null;\r\n  let routeGroup = null;\r\n  let liftGroup = null;\r\n  let particleGroup = null;\r\n\r\n  let animationFrameId = null;\r\n  let isDestroyed = false;\r\n\r\n  let mouse = {\r\n    x: 0,\r\n    y: 0\r\n  };\r\n\r\n  let scrollProgress = 0;\r\n  let liftProgress = 0.18;\r\n  let liftDirection = 1;\r\n\r\n  let cameraTarget = {\r\n    x: routeData.straight.camera.x,\r\n    y: routeData.straight.camera.y,\r\n    z: routeData.straight.camera.z\r\n  };\r\n\r\n  let modelState = {\r\n    rotation: routeData.straight.rotation,\r\n    targetRotation: routeData.straight.rotation\r\n  };\r\n\r\n  let materials = null;\r\n\r\n\r\n  function showFallback() {\r\n    if (fallback) {\r\n      fallback.style.display = \"grid\";\r\n    }\r\n  }\r\n\r\n\r\n  function createMaterial(options) {\r\n    return new THREE.MeshStandardMaterial(options);\r\n  }\r\n\r\n\r\n  function buildMaterials() {\r\n    materials = {\r\n      concrete: createMaterial({\r\n        color: 0x4b4b4b,\r\n        roughness: 0.78,\r\n        metalness: 0.08\r\n      }),\r\n\r\n      tread: createMaterial({\r\n        color: 0x777777,\r\n        roughness: 0.58,\r\n        metalness: 0.16\r\n      }),\r\n\r\n      edge: createMaterial({\r\n        color: 0xf3f3f3,\r\n        roughness: 0.28,\r\n        metalness: 0.1,\r\n        emissive: 0xffffff,\r\n        emissiveIntensity: 0.05\r\n      }),\r\n\r\n      wall: createMaterial({\r\n        color: 0x1a1a1a,\r\n        roughness: 0.72,\r\n        metalness: 0.08\r\n      }),\r\n\r\n      glass: createMaterial({\r\n        color: 0xbfefff,\r\n        roughness: 0.08,\r\n        metalness: 0.02,\r\n        transparent: true,\r\n        opacity: 0.2,\r\n        depthWrite: false\r\n      }),\r\n\r\n      rail: createMaterial({\r\n        color: 0xf4f4f4,\r\n        roughness: 0.18,\r\n        metalness: 0.46,\r\n        emissive: 0xffffff,\r\n        emissiveIntensity: 0.08\r\n      }),\r\n\r\n      support: createMaterial({\r\n        color: 0xa8a8a8,\r\n        roughness: 0.42,\r\n        metalness: 0.55\r\n      }),\r\n\r\n      dark: createMaterial({\r\n        color: 0x151515,\r\n        roughness: 0.52,\r\n        metalness: 0.48\r\n      }),\r\n\r\n      white: createMaterial({\r\n        color: 0xffffff,\r\n        roughness: 0.24,\r\n        metalness: 0.12,\r\n        emissive: 0xffffff,\r\n        emissiveIntensity: 0.04\r\n      }),\r\n\r\n      outdoor: createMaterial({\r\n        color: 0x5c5c5c,\r\n        roughness: 0.82,\r\n        metalness: 0.06\r\n      }),\r\n\r\n      green: createMaterial({\r\n        color: 0x26382a,\r\n        roughness: 0.95,\r\n        metalness: 0.02\r\n      }),\r\n\r\n      particle: createMaterial({\r\n        color: 0xffffff,\r\n        roughness: 0.5,\r\n        metalness: 0.02,\r\n        emissive: 0xffffff,\r\n        emissiveIntensity: 0.25\r\n      })\r\n    };\r\n  }\r\n\r\n\r\n  function disposeMaterials() {\r\n    if (!materials) return;\r\n\r\n    Object.keys(materials).forEach(function (key) {\r\n      if (\r\n        materials[key] &&\r\n        materials[key].dispose\r\n      ) {\r\n        materials[key].dispose();\r\n      }\r\n    });\r\n\r\n    materials = null;\r\n  }\r\n\r\n\r\n  function createBox(width, height, depth, material) {\r\n    const mesh = new THREE.Mesh(\r\n      new THREE.BoxGeometry(\r\n        width,\r\n        height,\r\n        depth\r\n      ),\r\n      material\r\n    );\r\n\r\n    mesh.castShadow = false;\r\n    mesh.receiveShadow = false;\r\n\r\n    return mesh;\r\n  }\r\n\r\n\r\n  function createCylinderBetween(\r\n    start,\r\n    end,\r\n    radius,\r\n    material,\r\n    segments\r\n  ) {\r\n    const direction =\r\n      new THREE.Vector3()\r\n        .subVectors(end, start);\r\n\r\n    const length =\r\n      direction.length();\r\n\r\n    if (!length) {\r\n      return new THREE.Group();\r\n    }\r\n\r\n    const midpoint =\r\n      new THREE.Vector3()\r\n        .addVectors(start, end)\r\n        .multiplyScalar(0.5);\r\n\r\n    const geometry =\r\n      new THREE.CylinderGeometry(\r\n        radius,\r\n        radius,\r\n        length,\r\n        segments || 10\r\n      );\r\n\r\n    const mesh =\r\n      new THREE.Mesh(\r\n        geometry,\r\n        material\r\n      );\r\n\r\n    mesh.position.copy(midpoint);\r\n\r\n    const quaternion =\r\n      new THREE.Quaternion();\r\n\r\n    quaternion.setFromUnitVectors(\r\n      new THREE.Vector3(\r\n        0,\r\n        1,\r\n        0\r\n      ),\r\n      direction\r\n        .clone()\r\n        .normalize()\r\n    );\r\n\r\n    mesh.quaternion.copy(\r\n      quaternion\r\n    );\r\n\r\n    return mesh;\r\n  }\r\n\r\n\r\n  function disposeGroup(group) {\r\n    if (!group) return;\r\n\r\n    group.traverse(function (object) {\r\n      if (object.geometry) {\r\n        object.geometry.dispose();\r\n      }\r\n    });\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Route geometry helpers\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function getCenterPoint(t, routeKey) {\r\n    const y =\r\n      -0.34 +\r\n      t *\r\n      (\r\n        routeKey === \"outdoor\"\r\n          ? 1.92\r\n          : 2.18\r\n      );\r\n\r\n    if (routeKey === \"curved\") {\r\n      const p0 = {\r\n        x: -3.55,\r\n        z: -0.48\r\n      };\r\n\r\n      const p1 = {\r\n        x: -1.35,\r\n        z: -0.52\r\n      };\r\n\r\n      const p2 = {\r\n        x: -0.30,\r\n        z: 1.30\r\n      };\r\n\r\n      const p3 = {\r\n        x: 3.60,\r\n        z: 1.35\r\n      };\r\n\r\n      const u = 1 - t;\r\n\r\n      return new THREE.Vector3(\r\n        u * u * u * p0.x +\r\n        3 * u * u * t * p1.x +\r\n        3 * u * t * t * p2.x +\r\n        t * t * t * p3.x,\r\n\r\n        y,\r\n\r\n        u * u * u * p0.z +\r\n        3 * u * u * t * p1.z +\r\n        3 * u * t * t * p2.z +\r\n        t * t * t * p3.z\r\n      );\r\n    }\r\n\r\n    return new THREE.Vector3(\r\n      -3.55 + t * 7.1,\r\n      y,\r\n      0\r\n    );\r\n  }\r\n\r\n\r\n  function getTangent(t, routeKey) {\r\n    const a = getCenterPoint(\r\n      Math.max(0, t - 0.01),\r\n      routeKey\r\n    );\r\n\r\n    const b = getCenterPoint(\r\n      Math.min(1, t + 0.01),\r\n      routeKey\r\n    );\r\n\r\n    return new THREE.Vector3()\r\n      .subVectors(b, a)\r\n      .normalize();\r\n  }\r\n\r\n\r\n  function getSideNormal(t, routeKey) {\r\n    const tangent =\r\n      getTangent(t, routeKey);\r\n\r\n    const flat =\r\n      new THREE.Vector3(\r\n        tangent.x,\r\n        0,\r\n        tangent.z\r\n      ).normalize();\r\n\r\n    if (!flat.length()) {\r\n      return new THREE.Vector3(\r\n        0,\r\n        0,\r\n        1\r\n      );\r\n    }\r\n\r\n    return new THREE.Vector3(\r\n      -flat.z,\r\n      0,\r\n      flat.x\r\n    ).normalize();\r\n  }\r\n\r\n\r\n  function getYawFromTangent(t, routeKey) {\r\n    const tangent =\r\n      getTangent(t, routeKey);\r\n\r\n    return -Math.atan2(\r\n      tangent.z,\r\n      tangent.x\r\n    );\r\n  }\r\n\r\n\r\n  function getRailPoint(t, routeKey) {\r\n    const route =\r\n      routeData[routeKey] ||\r\n      routeData.straight;\r\n\r\n    const center =\r\n      getCenterPoint(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    const normal =\r\n      getSideNormal(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    const wallOffset =\r\n      route.stepDepth * 0.5 +\r\n      (\r\n        routeKey === \"narrow\"\r\n          ? 0.16\r\n          : 0.24\r\n      );\r\n\r\n    return center\r\n      .clone()\r\n      .add(\r\n        normal\r\n          .clone()\r\n          .multiplyScalar(\r\n            -wallOffset\r\n          )\r\n      )\r\n      .add(\r\n        new THREE.Vector3(\r\n          0,\r\n          0.42,\r\n          0\r\n        )\r\n      );\r\n  }\r\n\r\n\r\n  function getLiftPoint(t, routeKey) {\r\n    const normal =\r\n      getSideNormal(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    const offset =\r\n      currentMode === \"platform\"\r\n        ? 0.62\r\n        : 0.48;\r\n\r\n    return getRailPoint(\r\n      t,\r\n      routeKey\r\n    )\r\n      .add(\r\n        normal.multiplyScalar(\r\n          offset\r\n        )\r\n      )\r\n      .add(\r\n        new THREE.Vector3(\r\n          0,\r\n          0.1,\r\n          0\r\n        )\r\n      );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Stair steps\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function addStep(\r\n    group,\r\n    t,\r\n    routeKey,\r\n    index\r\n  ) {\r\n    const route =\r\n      routeData[routeKey] ||\r\n      routeData.straight;\r\n\r\n    const center =\r\n      getCenterPoint(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    const yaw =\r\n      getYawFromTangent(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    const stepWidth =\r\n      routeKey === \"curved\"\r\n        ? 0.56\r\n        : 0.62;\r\n\r\n    const stepHeight =\r\n      routeKey === \"outdoor\"\r\n        ? 0.17\r\n        : 0.16;\r\n\r\n    const riser =\r\n      createBox(\r\n        stepWidth,\r\n        stepHeight,\r\n        route.stepDepth,\r\n        routeKey === \"outdoor\"\r\n          ? materials.outdoor\r\n          : materials.concrete\r\n      );\r\n\r\n    riser.position.copy(center);\r\n    riser.rotation.y = yaw;\r\n\r\n    group.add(riser);\r\n\r\n    const tread =\r\n      createBox(\r\n        stepWidth,\r\n        0.035,\r\n        route.stepDepth + 0.03,\r\n        materials.tread\r\n      );\r\n\r\n    tread.position.copy(\r\n      center\r\n        .clone()\r\n        .add(\r\n          new THREE.Vector3(\r\n            0,\r\n            stepHeight * 0.58,\r\n            0\r\n          )\r\n        )\r\n    );\r\n\r\n    tread.rotation.y = yaw;\r\n\r\n    group.add(tread);\r\n\r\n    const nose =\r\n      createBox(\r\n        0.045,\r\n        0.042,\r\n        route.stepDepth + 0.06,\r\n        materials.edge\r\n      );\r\n\r\n    const tangent =\r\n      getTangent(\r\n        t,\r\n        routeKey\r\n      );\r\n\r\n    nose.position.copy(\r\n      center\r\n        .clone()\r\n        .add(\r\n          tangent.multiplyScalar(\r\n            -stepWidth * 0.5\r\n          )\r\n        )\r\n        .add(\r\n          new THREE.Vector3(\r\n            0,\r\n            stepHeight * 0.76,\r\n            0\r\n          )\r\n        )\r\n    );\r\n\r\n    nose.rotation.y = yaw;\r\n\r\n    group.add(nose);\r\n\r\n    if (\r\n      routeKey === \"narrow\" &&\r\n      index % 3 === 0\r\n    ) {\r\n      const clearance =\r\n        createBox(\r\n          0.045,\r\n          0.52,\r\n          0.045,\r\n          materials.support\r\n        );\r\n\r\n      const normal =\r\n        getSideNormal(\r\n          t,\r\n          routeKey\r\n        );\r\n\r\n      clearance.position.copy(\r\n        center\r\n          .clone()\r\n          .add(\r\n            normal.multiplyScalar(\r\n              route.stepDepth * 0.5 +\r\n              0.08\r\n            )\r\n          )\r\n          .add(\r\n            new THREE.Vector3(\r\n              0,\r\n              0.38,\r\n              0\r\n            )\r\n          )\r\n      );\r\n\r\n      group.add(clearance);\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Interior panels\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function addInteriorPanels(\r\n    group,\r\n    routeKey\r\n  ) {\r\n    const route =\r\n      routeData[routeKey] ||\r\n      routeData.straight;\r\n\r\n    const panelCount =\r\n      routeKey === \"curved\"\r\n        ? 9\r\n        : 6;\r\n\r\n    for (\r\n      let i = 0;\r\n      i < panelCount;\r\n      i += 1\r\n    ) {\r\n      const t =\r\n        (i + 0.5) \/\r\n        panelCount;\r\n\r\n      const center =\r\n        getCenterPoint(\r\n          t,\r\n          routeKey\r\n        );\r\n\r\n      const normal =\r\n        getSideNormal(\r\n          t,\r\n          routeKey\r\n        );\r\n\r\n      const yaw =\r\n        getYawFromTangent(\r\n          t,\r\n          routeKey\r\n        );\r\n\r\n      const length =\r\n        routeKey === \"curved\"\r\n          ? 0.82\r\n          : 1.1;\r\n\r\n      const wall =\r\n        createBox(\r\n          length,\r\n          2.42,\r\n          0.12,\r\n          materials.wall\r\n        );\r\n\r\n      wall.position.copy(\r\n        center\r\n          .clone()\r\n          .add(\r\n            normal\r\n              .clone()\r\n              .multiplyScalar(\r\n                -(\r\n                  route.stepDepth *\r\n                  0.5 +\r\n                  0.55\r\n                )\r\n              )\r\n          )\r\n          .add(\r\n            new THREE.Vector3(\r\n              0,\r\n              0.82,\r\n              0\r\n            )\r\n          )\r\n      );\r\n\r\n      wall.rotation.y = yaw;\r\n\r\n      group.add(wall);\r\n\r\n      const glass =\r\n        createBox(\r\n          length * 0.92,\r\n          0.92,\r\n          0.045,\r\n          materials.glass\r\n        );\r\n\r\n      glass.position.copy(\r\n        center\r\n          .clone()\r\n          .add(\r\n            normal\r\n              .clone()\r\n              .multiplyScalar(\r\n                route.stepDepth *\r\n                0.5 +\r\n                0.28\r\n              )\r\n          )\r\n          .add(\r\n            new THREE.Vector3(\r\n              0,\r\n              0.62,\r\n              0\r\n            )\r\n          )\r\n      );\r\n\r\n      glass.rotation.y = yaw;\r\n\r\n      group.add(glass);\r\n\r\n      const glassTop =\r\n        createCylinderBetween(\r\n          glass.position\r\n            .clone()\r\n            .add(\r\n              new THREE.Vector3(\r\n                -Math.cos(yaw) *\r\n                length *\r\n                0.42,\r\n                0.49,\r\n                Math.sin(yaw) *\r\n                length *\r\n                0.42\r\n              )\r\n            ),\r\n\r\n          glass.position\r\n            .clone()\r\n            .add(\r\n              new THREE.Vector3(\r\n                Math.cos(yaw) *\r\n                length *\r\n                0.42,\r\n                0.49,\r\n                -Math.sin(yaw) *\r\n                length *\r\n                0.42\r\n              )\r\n            ),\r\n\r\n          0.018,\r\n          materials.support,\r\n          8\r\n        );\r\n\r\n      group.add(glassTop);\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Outdoor environment\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function addOutdoorEnvironment(group) {\r\n    const ground =\r\n      createBox(\r\n        9.5,\r\n        0.16,\r\n        5.0,\r\n        materials.outdoor\r\n      );\r\n\r\n    ground.position.set(\r\n      0,\r\n      -0.72,\r\n      0.12\r\n    );\r\n\r\n    group.add(ground);\r\n\r\n    const entryLanding =\r\n      createBox(\r\n        1.4,\r\n        0.18,\r\n        2.5,\r\n        materials.outdoor\r\n      );\r\n\r\n    entryLanding.position.set(\r\n      4.16,\r\n      1.53,\r\n      0\r\n    );\r\n\r\n    group.add(entryLanding);\r\n\r\n    for (\r\n      let i = 0;\r\n      i < 6;\r\n      i += 1\r\n    ) {\r\n      const t = i \/ 5;\r\n\r\n      const center =\r\n        getCenterPoint(\r\n          t,\r\n          \"outdoor\"\r\n        );\r\n\r\n      const normal =\r\n        getSideNormal(\r\n          t,\r\n          \"outdoor\"\r\n        );\r\n\r\n      const post =\r\n        createCylinderBetween(\r\n          center\r\n            .clone()\r\n            .add(\r\n              normal\r\n                .clone()\r\n                .multiplyScalar(\r\n                  1.25\r\n                )\r\n            )\r\n            .add(\r\n              new THREE.Vector3(\r\n                0,\r\n                -0.16,\r\n                0\r\n              )\r\n            ),\r\n\r\n          center\r\n            .clone()\r\n            .add(\r\n              normal\r\n                .clone()\r\n                .multiplyScalar(\r\n                  1.25\r\n                )\r\n            )\r\n            .add(\r\n              new THREE.Vector3(\r\n                0,\r\n                0.9,\r\n                0\r\n              )\r\n            ),\r\n\r\n          0.035,\r\n          materials.support,\r\n          8\r\n        );\r\n\r\n      group.add(post);\r\n\r\n      if (i < 5) {\r\n        const next =\r\n          getCenterPoint(\r\n            (i + 1) \/ 5,\r\n            \"outdoor\"\r\n          );\r\n\r\n        const nextNormal =\r\n          getSideNormal(\r\n            (i + 1) \/ 5,\r\n            \"outdoor\"\r\n          );\r\n\r\n        group.add(\r\n          createCylinderBetween(\r\n            center\r\n              .clone()\r\n              .add(\r\n                normal\r\n                  .clone()\r\n                  .multiplyScalar(\r\n                    1.25\r\n                  )\r\n              )\r\n              .add(\r\n                new THREE.Vector3(\r\n                  0,\r\n                  0.76,\r\n                  0\r\n                )\r\n              ),\r\n\r\n            next\r\n              .clone()\r\n              .add(\r\n                nextNormal\r\n                  .clone()\r\n                  .multiplyScalar(\r\n                    1.25\r\n                  )\r\n              )\r\n              .add(\r\n                new THREE.Vector3(\r\n                  0,\r\n                  0.76,\r\n                  0\r\n                )\r\n              ),\r\n\r\n            0.022,\r\n            materials.support,\r\n            8\r\n          )\r\n        );\r\n      }\r\n    }\r\n\r\n    const canopy =\r\n      createBox(\r\n        2.1,\r\n        0.08,\r\n        1.25,\r\n        materials.glass\r\n      );\r\n\r\n    canopy.position.set(\r\n      3.08,\r\n      2.18,\r\n      -1.25\r\n    );\r\n\r\n    canopy.rotation.z = -0.12;\r\n\r\n    group.add(canopy);\r\n\r\n    for (\r\n      let i = 0;\r\n      i < 12;\r\n      i += 1\r\n    ) {\r\n      const shrub =\r\n        new THREE.Mesh(\r\n          new THREE.SphereGeometry(\r\n            0.12 +\r\n            Math.random() *\r\n            0.1,\r\n            8,\r\n            6\r\n          ),\r\n          materials.green\r\n        );\r\n\r\n      shrub.position.set(\r\n        -4.35 +\r\n        Math.random() *\r\n        8.7,\r\n\r\n        -0.55,\r\n\r\n        1.95 +\r\n        Math.random() *\r\n        0.72\r\n      );\r\n\r\n      shrub.scale.y = 0.45;\r\n\r\n      group.add(shrub);\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Rail\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function rebuildRail(\r\n    group,\r\n    routeKey\r\n  ) {\r\n    const route =\r\n      routeData[routeKey] ||\r\n      routeData.straight;\r\n\r\n    const points = [];\r\n\r\n    const count =\r\n      routeKey === \"curved\"\r\n        ? 38\r\n        : 28;\r\n\r\n    for (\r\n      let i = 0;\r\n      i <= count;\r\n      i += 1\r\n    ) {\r\n      points.push(\r\n        getRailPoint(\r\n          i \/ count,\r\n          routeKey\r\n        )\r\n      );\r\n    }\r\n\r\n    for (\r\n      let i = 0;\r\n      i < points.length - 1;\r\n      i += 1\r\n    ) {\r\n      group.add(\r\n        createCylinderBetween(\r\n          points[i],\r\n          points[i + 1],\r\n          0.052,\r\n          materials.rail,\r\n          10\r\n        )\r\n      );\r\n\r\n      const lowerA =\r\n        points[i]\r\n          .clone()\r\n          .add(\r\n            new THREE.Vector3(\r\n              0,\r\n              -0.16,\r\n              0\r\n            )\r\n          );\r\n\r\n      const lowerB =\r\n        points[i + 1]\r\n          .clone()\r\n          .add(\r\n            new THREE.Vector3(\r\n              0,\r\n              -0.16,\r\n              0\r\n            )\r\n          );\r\n\r\n      const lowerRail =\r\n        createCylinderBetween(\r\n          lowerA,\r\n          lowerB,\r\n          0.032,\r\n          materials.support,\r\n          8\r\n        );\r\n\r\n      lowerRail.name =\r\n        \"platformRail\";\r\n\r\n      lowerRail.visible =\r\n        currentMode === \"platform\";\r\n\r\n      group.add(lowerRail);\r\n\r\n      if (\r\n        i %\r\n        (\r\n          routeKey === \"curved\"\r\n            ? 4\r\n            : 5\r\n        ) === 0\r\n      ) {\r\n        const supportStart =\r\n          points[i]\r\n            .clone()\r\n            .add(\r\n              new THREE.Vector3(\r\n                0,\r\n                -0.56,\r\n                0\r\n              )\r\n            );\r\n\r\n        const supportEnd =\r\n          points[i]\r\n            .clone()\r\n            .add(\r\n              new THREE.Vector3(\r\n                0,\r\n                -0.05,\r\n                0\r\n              )\r\n            );\r\n\r\n        group.add(\r\n          createCylinderBetween(\r\n            supportStart,\r\n            supportEnd,\r\n            0.026,\r\n            materials.support,\r\n            8\r\n          )\r\n        );\r\n\r\n        const normal =\r\n          getSideNormal(\r\n            i \/ count,\r\n            routeKey\r\n          );\r\n\r\n        group.add(\r\n          createCylinderBetween(\r\n            points[i]\r\n              .clone()\r\n              .add(\r\n                new THREE.Vector3(\r\n                  0,\r\n                  -0.06,\r\n                  0\r\n                )\r\n              ),\r\n\r\n            points[i]\r\n              .clone()\r\n              .add(\r\n                normal\r\n                  .clone()\r\n                  .multiplyScalar(\r\n                    -0.22\r\n                  )\r\n              )\r\n              .add(\r\n                new THREE.Vector3(\r\n                  0,\r\n                  -0.1,\r\n                  0\r\n                )\r\n              ),\r\n\r\n            0.02,\r\n            materials.support,\r\n            8\r\n          )\r\n        );\r\n      }\r\n    }\r\n\r\n    if (routeKey === \"curved\") {\r\n      const landing =\r\n        createBox(\r\n          1.45,\r\n          0.13,\r\n          route.stepDepth + 0.1,\r\n          materials.concrete\r\n        );\r\n\r\n      landing.position.copy(\r\n        getCenterPoint(\r\n          0.48,\r\n          routeKey\r\n        ).add(\r\n          new THREE.Vector3(\r\n            0,\r\n            0.02,\r\n            0\r\n          )\r\n        )\r\n      );\r\n\r\n      landing.rotation.y =\r\n        getYawFromTangent(\r\n          0.48,\r\n          routeKey\r\n        );\r\n\r\n      group.add(landing);\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Route objects\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function rebuildRouteObjects(routeKey) {\r\n    if (!mainGroup) return;\r\n\r\n    if (routeGroup) {\r\n      mainGroup.remove(routeGroup);\r\n      disposeGroup(routeGroup);\r\n    }\r\n\r\n    routeGroup =\r\n      new THREE.Group();\r\n\r\n    const route =\r\n      routeData[routeKey] ||\r\n      routeData.straight;\r\n\r\n    const steps =\r\n      routeKey === \"curved\"\r\n        ? 15\r\n        : 13;\r\n\r\n    for (\r\n      let i = 0;\r\n      i < steps;\r\n      i += 1\r\n    ) {\r\n      addStep(\r\n        routeGroup,\r\n        i \/ (steps - 1),\r\n        routeKey,\r\n        i\r\n      );\r\n    }\r\n\r\n    if (route.outdoor) {\r\n      addOutdoorEnvironment(\r\n        routeGroup\r\n      );\r\n    } else {\r\n      addInteriorPanels(\r\n        routeGroup,\r\n        routeKey\r\n      );\r\n    }\r\n\r\n    rebuildRail(\r\n      routeGroup,\r\n      routeKey\r\n    );\r\n\r\n    mainGroup.add(routeGroup);\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Lift\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function buildLift() {\r\n    if (!mainGroup) return;\r\n\r\n    if (liftGroup) {\r\n      mainGroup.remove(liftGroup);\r\n      disposeGroup(liftGroup);\r\n    }\r\n\r\n    liftGroup =\r\n      new THREE.Group();\r\n\r\n    const carriage =\r\n      createBox(\r\n        0.34,\r\n        0.24,\r\n        0.28,\r\n        materials.dark\r\n      );\r\n\r\n    carriage.position.set(\r\n      0,\r\n      0.12,\r\n      0\r\n    );\r\n\r\n    liftGroup.add(carriage);\r\n\r\n    const connector =\r\n      createBox(\r\n        0.52,\r\n        0.08,\r\n        0.16,\r\n        materials.support\r\n      );\r\n\r\n    connector.position.set(\r\n      -0.02,\r\n      0.25,\r\n      -0.05\r\n    );\r\n\r\n    liftGroup.add(connector);\r\n\r\n    const mast =\r\n      createBox(\r\n        0.12,\r\n        0.7,\r\n        0.12,\r\n        materials.support\r\n      );\r\n\r\n    mast.position.set(\r\n      0.22,\r\n      0.55,\r\n      0.02\r\n    );\r\n\r\n    mast.name = \"seat\";\r\n\r\n    liftGroup.add(mast);\r\n\r\n    const seatBase =\r\n      createBox(\r\n        0.62,\r\n        0.12,\r\n        0.58,\r\n        materials.white\r\n      );\r\n\r\n    seatBase.position.set(\r\n      -0.18,\r\n      0.44,\r\n      0.20\r\n    );\r\n\r\n    seatBase.name = \"seat\";\r\n\r\n    liftGroup.add(seatBase);\r\n\r\n    const back =\r\n      createBox(\r\n        0.14,\r\n        0.68,\r\n        0.58,\r\n        materials.white\r\n      );\r\n\r\n    back.position.set(\r\n      0.12,\r\n      0.82,\r\n      0.20\r\n    );\r\n\r\n    back.name = \"seat\";\r\n\r\n    liftGroup.add(back);\r\n\r\n    const armLeft =\r\n      createBox(\r\n        0.58,\r\n        0.055,\r\n        0.06,\r\n        materials.support\r\n      );\r\n\r\n    armLeft.position.set(\r\n      -0.16,\r\n      0.72,\r\n      -0.12\r\n    );\r\n\r\n    armLeft.name = \"seat\";\r\n\r\n    liftGroup.add(armLeft);\r\n\r\n    const armRight =\r\n      createBox(\r\n        0.58,\r\n        0.055,\r\n        0.06,\r\n        materials.support\r\n      );\r\n\r\n    armRight.position.set(\r\n      -0.16,\r\n      0.72,\r\n      0.52\r\n    );\r\n\r\n    armRight.name = \"seat\";\r\n\r\n    liftGroup.add(armRight);\r\n\r\n    const foot =\r\n      createBox(\r\n        0.66,\r\n        0.065,\r\n        0.36,\r\n        materials.support\r\n      );\r\n\r\n    foot.position.set(\r\n      -0.55,\r\n      0.18,\r\n      0.2\r\n    );\r\n\r\n    foot.name = \"seat\";\r\n\r\n    liftGroup.add(foot);\r\n\r\n    const platform =\r\n      createBox(\r\n        1.06,\r\n        0.09,\r\n        0.92,\r\n        materials.white\r\n      );\r\n\r\n    platform.position.set(\r\n      -0.34,\r\n      0.24,\r\n      0.23\r\n    );\r\n\r\n    platform.name =\r\n      \"platform\";\r\n\r\n    liftGroup.add(platform);\r\n\r\n    const guardA =\r\n      createBox(\r\n        0.06,\r\n        0.52,\r\n        0.92,\r\n        materials.glass\r\n      );\r\n\r\n    guardA.position.set(\r\n      0.18,\r\n      0.55,\r\n      0.23\r\n    );\r\n\r\n    guardA.name =\r\n      \"platform\";\r\n\r\n    liftGroup.add(guardA);\r\n\r\n    const guardB =\r\n      createBox(\r\n        0.95,\r\n        0.48,\r\n        0.055,\r\n        materials.support\r\n      );\r\n\r\n    guardB.position.set(\r\n      -0.34,\r\n      0.52,\r\n      -0.23\r\n    );\r\n\r\n    guardB.name =\r\n      \"platform\";\r\n\r\n    liftGroup.add(guardB);\r\n\r\n    const safetyBar =\r\n      createCylinderBetween(\r\n        new THREE.Vector3(\r\n          -0.75,\r\n          0.7,\r\n          0.68\r\n        ),\r\n\r\n        new THREE.Vector3(\r\n          0.05,\r\n          0.7,\r\n          0.68\r\n        ),\r\n\r\n        0.026,\r\n        materials.support,\r\n        8\r\n      );\r\n\r\n    safetyBar.name =\r\n      \"platform\";\r\n\r\n    liftGroup.add(\r\n      safetyBar\r\n    );\r\n\r\n    const ramp =\r\n      createBox(\r\n        0.18,\r\n        0.045,\r\n        0.92,\r\n        materials.support\r\n      );\r\n\r\n    ramp.position.set(\r\n      -0.95,\r\n      0.18,\r\n      0.23\r\n    );\r\n\r\n    ramp.rotation.z = 0.18;\r\n\r\n    ramp.name =\r\n      \"platform\";\r\n\r\n    liftGroup.add(ramp);\r\n\r\n    liftGroup.scale.set(\r\n      0.86,\r\n      0.86,\r\n      0.86\r\n    );\r\n\r\n    mainGroup.add(\r\n      liftGroup\r\n    );\r\n\r\n    updateMode3D(\r\n      currentMode\r\n    );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Particles\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function buildParticles() {\r\n    if (!mainGroup) return;\r\n\r\n    particleGroup =\r\n      new THREE.Group();\r\n\r\n    for (\r\n      let i = 0;\r\n      i < 28;\r\n      i += 1\r\n    ) {\r\n      const particle =\r\n        new THREE.Mesh(\r\n          new THREE.SphereGeometry(\r\n            0.012 +\r\n            Math.random() *\r\n            0.015,\r\n            6,\r\n            6\r\n          ),\r\n          materials.particle\r\n        );\r\n\r\n      particle.position.set(\r\n        -4 +\r\n        Math.random() *\r\n        8,\r\n\r\n        -0.3 +\r\n        Math.random() *\r\n        3,\r\n\r\n        -2.2 +\r\n        Math.random() *\r\n        4.4\r\n      );\r\n\r\n      particle.userData.speed =\r\n        0.0015 +\r\n        Math.random() *\r\n        0.003;\r\n\r\n      particle.userData.phase =\r\n        Math.random() *\r\n        Math.PI *\r\n        2;\r\n\r\n      particleGroup.add(\r\n        particle\r\n      );\r\n    }\r\n\r\n    mainGroup.add(\r\n      particleGroup\r\n    );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Build scene\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function buildSceneObjects() {\r\n    mainGroup =\r\n      new THREE.Group();\r\n\r\n    mainGroup.rotation.y =\r\n      modelState.rotation;\r\n\r\n    mainGroup.rotation.x =\r\n      0.07;\r\n\r\n    mainGroup.position.set(\r\n      0.75,\r\n      -0.25,\r\n      0\r\n    );\r\n\r\n    scene.add(mainGroup);\r\n\r\n    rebuildRouteObjects(\r\n      currentRoute\r\n    );\r\n\r\n    buildLift();\r\n    buildParticles();\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Update route\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function updateRoute3D(routeKey) {\r\n    const data =\r\n      routeData[routeKey];\r\n\r\n    if (!data) return;\r\n\r\n    currentRoute =\r\n      routeKey;\r\n\r\n    modelState.targetRotation =\r\n      data.rotation;\r\n\r\n    cameraTarget.x =\r\n      data.camera.x;\r\n\r\n    cameraTarget.y =\r\n      data.camera.y;\r\n\r\n    cameraTarget.z =\r\n      data.camera.z;\r\n\r\n    rebuildRouteObjects(\r\n      routeKey\r\n    );\r\n\r\n    setRouteUI(\r\n      routeKey\r\n    );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Update mode\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function updateMode3D(modeKey) {\r\n    setModeUI(modeKey);\r\n\r\n    if (liftGroup) {\r\n      liftGroup.children.forEach(function (child) {\r\n        if (child.name === \"seat\") {\r\n          child.visible =\r\n            modeKey === \"seat\";\r\n        }\r\n\r\n        if (child.name === \"platform\") {\r\n          child.visible =\r\n            modeKey === \"platform\";\r\n        }\r\n      });\r\n    }\r\n\r\n    if (routeGroup) {\r\n      routeGroup.children.forEach(function (child) {\r\n        if (\r\n          child.name ===\r\n          \"platformRail\"\r\n        ) {\r\n          child.visible =\r\n            modeKey === \"platform\";\r\n        }\r\n      });\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Lift animation\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function updateLift(delta) {\r\n    if (!liftGroup) return;\r\n\r\n    liftProgress +=\r\n      liftDirection *\r\n      delta *\r\n      0.000052;\r\n\r\n    if (liftProgress > 1) {\r\n      liftProgress = 1;\r\n      liftDirection = -1;\r\n    }\r\n\r\n    if (liftProgress < 0) {\r\n      liftProgress = 0;\r\n      liftDirection = 1;\r\n    }\r\n\r\n    const t =\r\n      Math.max(\r\n        0.015,\r\n        Math.min(\r\n          0.985,\r\n          liftProgress\r\n        )\r\n      );\r\n\r\n    const point =\r\n      getLiftPoint(\r\n        t,\r\n        currentRoute\r\n      );\r\n\r\n    const tangent =\r\n      getTangent(\r\n        t,\r\n        currentRoute\r\n      );\r\n\r\n    const yaw =\r\n      -Math.atan2(\r\n        tangent.z,\r\n        tangent.x\r\n      );\r\n\r\n    const slope =\r\n      Math.atan2(\r\n        tangent.y,\r\n        Math.sqrt(\r\n          tangent.x *\r\n          tangent.x +\r\n          tangent.z *\r\n          tangent.z\r\n        )\r\n      );\r\n\r\n    liftGroup.position.copy(\r\n      point\r\n    );\r\n\r\n    liftGroup.rotation.y +=\r\n      (\r\n        yaw -\r\n        liftGroup.rotation.y\r\n      ) *\r\n      0.12;\r\n\r\n    liftGroup.rotation.z +=\r\n      (\r\n        -slope *\r\n        0.42 -\r\n        liftGroup.rotation.z\r\n      ) *\r\n      0.12;\r\n\r\n    liftGroup.position.y +=\r\n      Math.sin(\r\n        performance.now() *\r\n        0.0016\r\n      ) *\r\n      0.012;\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Particle animation\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function updateParticles() {\r\n    if (!particleGroup) return;\r\n\r\n    const now =\r\n      performance.now();\r\n\r\n    particleGroup.children.forEach(function (particle) {\r\n      particle.position.y +=\r\n        particle.userData.speed;\r\n\r\n      particle.position.x +=\r\n        Math.sin(\r\n          now *\r\n          0.001 +\r\n          particle.userData.phase\r\n        ) *\r\n        0.001;\r\n\r\n      if (\r\n        particle.position.y >\r\n        3.1\r\n      ) {\r\n        particle.position.y =\r\n          -0.35;\r\n      }\r\n    });\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Scroll progress\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function updateScrollProgress() {\r\n    const section =\r\n      document.querySelector(\r\n        \".mp-servo-immersive\"\r\n      );\r\n\r\n    if (!section) return;\r\n\r\n    const rect =\r\n      section.getBoundingClientRect();\r\n\r\n    const viewport =\r\n      window.innerHeight ||\r\n      document.documentElement.clientHeight;\r\n\r\n    const total =\r\n      rect.height +\r\n      viewport;\r\n\r\n    const visible =\r\n      viewport -\r\n      rect.top;\r\n\r\n    scrollProgress =\r\n      Math.max(\r\n        0,\r\n        Math.min(\r\n          1,\r\n          visible \/ total\r\n        )\r\n      );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Init Servo 3D\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function initServo3D() {\r\n    if (\r\n      !canvas ||\r\n      !window.THREE ||\r\n      !window.WebGLRenderingContext\r\n    ) {\r\n      showFallback();\r\n      return;\r\n    }\r\n\r\n    try {\r\n      isDestroyed = false;\r\n\r\n      scene =\r\n        new THREE.Scene();\r\n\r\n      scene.fog =\r\n        new THREE.FogExp2(\r\n          0x050505,\r\n          0.052\r\n        );\r\n\r\n      camera =\r\n        new THREE.PerspectiveCamera(\r\n          36,\r\n          1,\r\n          0.1,\r\n          100\r\n        );\r\n\r\n      camera.position.set(\r\n        cameraTarget.x,\r\n        cameraTarget.y,\r\n        cameraTarget.z\r\n      );\r\n\r\n      camera.lookAt(\r\n        0,\r\n        0.6,\r\n        0\r\n      );\r\n\r\n      renderer =\r\n        new THREE.WebGLRenderer({\r\n          canvas: canvas,\r\n          antialias: true,\r\n          alpha: true,\r\n          powerPreference: \"high-performance\"\r\n        });\r\n\r\n      renderer.setPixelRatio(\r\n        Math.min(\r\n          window.devicePixelRatio || 1,\r\n          1.6\r\n        )\r\n      );\r\n\r\n      renderer.setClearColor(\r\n        0x000000,\r\n        0\r\n      );\r\n\r\n      renderer.shadowMap.enabled =\r\n        false;\r\n\r\n      buildMaterials();\r\n\r\n\r\n      \/*\r\n      |--------------------------------------------------------------------------\r\n      | Lights\r\n      |--------------------------------------------------------------------------\r\n      *\/\r\n\r\n      const ambient =\r\n        new THREE.AmbientLight(\r\n          0xffffff,\r\n          0.62\r\n        );\r\n\r\n      scene.add(ambient);\r\n\r\n      const keyLight =\r\n        new THREE.DirectionalLight(\r\n          0xffffff,\r\n          1.35\r\n        );\r\n\r\n      keyLight.position.set(\r\n        4.8,\r\n        6.2,\r\n        5.2\r\n      );\r\n\r\n      scene.add(keyLight);\r\n\r\n      const fillLight =\r\n        new THREE.PointLight(\r\n          0xffffff,\r\n          0.72,\r\n          12\r\n        );\r\n\r\n      fillLight.position.set(\r\n        -4.2,\r\n        2.1,\r\n        -3.6\r\n      );\r\n\r\n      scene.add(fillLight);\r\n\r\n      const rimLight =\r\n        new THREE.PointLight(\r\n          0xffffff,\r\n          1.0,\r\n          10\r\n        );\r\n\r\n      rimLight.position.set(\r\n        3.5,\r\n        2.6,\r\n        -4.1\r\n      );\r\n\r\n      scene.add(rimLight);\r\n\r\n\r\n      \/*\r\n      |--------------------------------------------------------------------------\r\n      | Scene objects\r\n      |--------------------------------------------------------------------------\r\n      *\/\r\n\r\n      buildSceneObjects();\r\n      resizeServo3D();\r\n\r\n      let lastTime =\r\n        performance.now();\r\n\r\n\r\n      \/*\r\n      |--------------------------------------------------------------------------\r\n      | Animation loop\r\n      |--------------------------------------------------------------------------\r\n      *\/\r\n\r\n      function animate(now) {\r\n        if (\r\n          isDestroyed ||\r\n          !renderer ||\r\n          !scene ||\r\n          !camera\r\n        ) {\r\n          return;\r\n        }\r\n\r\n        const delta =\r\n          Math.min(\r\n            33,\r\n            now - lastTime\r\n          );\r\n\r\n        lastTime = now;\r\n\r\n        updateScrollProgress();\r\n        updateLift(delta);\r\n        updateParticles();\r\n\r\n        modelState.rotation +=\r\n          (\r\n            modelState.targetRotation -\r\n            modelState.rotation\r\n          ) *\r\n          0.045;\r\n\r\n        const scrollCameraLift =\r\n          scrollProgress *\r\n          0.48;\r\n\r\n        const scrollCameraSide =\r\n          (\r\n            scrollProgress -\r\n            0.5\r\n          ) *\r\n          0.38;\r\n\r\n        camera.position.x +=\r\n          (\r\n            (\r\n              cameraTarget.x +\r\n              mouse.x *\r\n              0.2 +\r\n              scrollCameraSide\r\n            ) -\r\n            camera.position.x\r\n          ) *\r\n          0.036;\r\n\r\n        camera.position.y +=\r\n          (\r\n            (\r\n              cameraTarget.y +\r\n              mouse.y *\r\n              0.1 +\r\n              scrollCameraLift\r\n            ) -\r\n            camera.position.y\r\n          ) *\r\n          0.036;\r\n\r\n        camera.position.z +=\r\n          (\r\n            (\r\n              cameraTarget.z -\r\n              scrollProgress *\r\n              0.58\r\n            ) -\r\n            camera.position.z\r\n          ) *\r\n          0.036;\r\n\r\n        camera.lookAt(\r\n          0,\r\n          0.62 +\r\n          scrollProgress *\r\n          0.22,\r\n          0\r\n        );\r\n\r\n        if (mainGroup) {\r\n          mainGroup.rotation.y +=\r\n            (\r\n              (\r\n                modelState.rotation +\r\n                mouse.x *\r\n                0.055 +\r\n                scrollProgress *\r\n                0.06\r\n              ) -\r\n              mainGroup.rotation.y\r\n            ) *\r\n            0.04;\r\n\r\n          mainGroup.rotation.x +=\r\n            (\r\n              (\r\n                0.07 +\r\n                mouse.y *\r\n                0.022\r\n              ) -\r\n              mainGroup.rotation.x\r\n            ) *\r\n            0.04;\r\n        }\r\n\r\n        renderer.render(\r\n          scene,\r\n          camera\r\n        );\r\n\r\n        animationFrameId =\r\n          requestAnimationFrame(\r\n            animate\r\n          );\r\n      }\r\n\r\n      animationFrameId =\r\n        requestAnimationFrame(\r\n          animate\r\n        );\r\n\r\n    } catch (error) {\r\n      console.warn(\r\n        \"Millepiani servoscala 3D non inizializzato:\",\r\n        error\r\n      );\r\n\r\n      showFallback();\r\n      destroyServo3D();\r\n    }\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Resize Servo 3D\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function resizeServo3D() {\r\n    if (\r\n      !canvas ||\r\n      !renderer ||\r\n      !camera\r\n    ) {\r\n      return;\r\n    }\r\n\r\n    const rect =\r\n      canvas.getBoundingClientRect();\r\n\r\n    if (\r\n      !rect.width ||\r\n      !rect.height\r\n    ) {\r\n      return;\r\n    }\r\n\r\n    renderer.setSize(\r\n      rect.width,\r\n      rect.height,\r\n      false\r\n    );\r\n\r\n    camera.aspect =\r\n      rect.width \/\r\n      rect.height;\r\n\r\n    camera.updateProjectionMatrix();\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Destroy Servo 3D\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  function destroyServo3D() {\r\n    isDestroyed = true;\r\n\r\n    if (animationFrameId) {\r\n      cancelAnimationFrame(\r\n        animationFrameId\r\n      );\r\n\r\n      animationFrameId =\r\n        null;\r\n    }\r\n\r\n    if (scene) {\r\n      disposeGroup(scene);\r\n    }\r\n\r\n    disposeMaterials();\r\n\r\n    if (renderer) {\r\n      renderer.dispose();\r\n\r\n      if (\r\n        renderer.forceContextLoss\r\n      ) {\r\n        renderer.forceContextLoss();\r\n      }\r\n    }\r\n\r\n    renderer = null;\r\n    scene = null;\r\n    camera = null;\r\n    mainGroup = null;\r\n    routeGroup = null;\r\n    liftGroup = null;\r\n    particleGroup = null;\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Events\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  window.addEventListener(\r\n    \"resize\",\r\n    resizeServo3D\r\n  );\r\n\r\n  window.addEventListener(\r\n    \"scroll\",\r\n    updateScrollProgress,\r\n    {\r\n      passive: true\r\n    }\r\n  );\r\n\r\n  window.addEventListener(\r\n    \"pagehide\",\r\n    destroyServo3D\r\n  );\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Mouse movement\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  if (canvasWrap) {\r\n    canvasWrap.addEventListener(\r\n      \"mousemove\",\r\n      function (event) {\r\n        const rect =\r\n          canvasWrap.getBoundingClientRect();\r\n\r\n        mouse.x =\r\n          (\r\n            (\r\n              event.clientX -\r\n              rect.left\r\n            ) \/\r\n            rect.width -\r\n            0.5\r\n          ) *\r\n          2;\r\n\r\n        mouse.y =\r\n          (\r\n            (\r\n              event.clientY -\r\n              rect.top\r\n            ) \/\r\n            rect.height -\r\n            0.5\r\n          ) *\r\n          -2;\r\n      }\r\n    );\r\n\r\n    canvasWrap.addEventListener(\r\n      \"mouseleave\",\r\n      function () {\r\n        mouse.x = 0;\r\n        mouse.y = 0;\r\n      }\r\n    );\r\n  }\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Route buttons\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  routeButtons.forEach(function (button) {\r\n    button.addEventListener(\r\n      \"click\",\r\n      function () {\r\n        updateRoute3D(\r\n          button.getAttribute(\r\n            \"data-route\"\r\n          )\r\n        );\r\n      }\r\n    );\r\n  });\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Mode buttons\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  modeButtons.forEach(function (button) {\r\n    button.addEventListener(\r\n      \"click\",\r\n      function () {\r\n        updateMode3D(\r\n          button.getAttribute(\r\n            \"data-mode\"\r\n          )\r\n        );\r\n      }\r\n    );\r\n  });\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Hotspot buttons\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  hotspotButtons.forEach(function (button) {\r\n    button.addEventListener(\r\n      \"click\",\r\n      function () {\r\n        setPoint(\r\n          button.getAttribute(\r\n            \"data-point\"\r\n          )\r\n        );\r\n      }\r\n    );\r\n  });\r\n\r\n\r\n  \/*\r\n  |--------------------------------------------------------------------------\r\n  | Initial state\r\n  |--------------------------------------------------------------------------\r\n  *\/\r\n\r\n  setRouteUI(\"straight\");\r\n  setPoint(\"survey\");\r\n  setModeUI(\"seat\");\r\n  initServo3D();\r\n\r\n});<\/script><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>Accessibilit\u00e9 sur mesure Monte-escaliers Des solutions con\u00e7ues pour rendre plus simple, s\u00fbr et confortable le d\u00e9placement entre diff\u00e9rents niveaux d&rsquo;un b\u00e2timent. Monte-escaliers con\u00e7us pour les personnes \u00e0 mobilit\u00e9 r\u00e9duite, les personnes \u00e2g\u00e9es et les contextes o\u00f9 les escaliers repr\u00e9sentent un obstacle quotidien. Demander un devis Pourquoi choisir Monte-escaliersMillepiani Le monte-escalier permet de franchir des vol\u00e9es [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_header_footer","meta":{"footnotes":""},"class_list":["post-2534","page","type-page","status-publish","hentry"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<link rel=\"canonical\" href=\"https:\/\/millepiani.com\/fr\/monte-escaliers\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"fr_FR\" \/>\n\t\t<meta property=\"og:site_name\" content=\"millepiani.com -\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Monte-escaliers sur mesure pour maison et bureau | Millepiani\" \/>\n\t\t<meta property=\"og:description\" content=\"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/millepiani.com\/fr\/monte-escaliers\/\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-05-24T10:01:36+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-09-15T18:18:16+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Monte-escaliers sur mesure pour maison et bureau | Millepiani\" \/>\n\t\t<meta name=\"twitter:description\" content=\"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/#listItem\",\"name\":\"MONTE-ESCALIERS\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/#listItem\",\"position\":2,\"name\":\"MONTE-ESCALIERS\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#listItem\",\"name\":\"Home\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#organization\",\"name\":\"millepiani.com\",\"url\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/#webpage\",\"url\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/\",\"name\":\"Monte-escaliers sur mesure pour maison et bureau | Millepiani\",\"description\":\"D\\u00e9couvrez les monte-escaliers Millepiani : solutions s\\u00fbres et sur mesure pour escaliers droits, courbes, \\u00e9troits, int\\u00e9rieurs ou ext\\u00e9rieurs. Demandez un devis gratuit.\",\"inLanguage\":\"fr-FR\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/monte-escaliers\\\/#breadcrumblist\"},\"datePublished\":\"2026-05-24T10:01:36+00:00\",\"dateModified\":\"2026-09-15T18:18:16+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#website\",\"url\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/\",\"name\":\"millepiani.com\",\"inLanguage\":\"fr-FR\",\"publisher\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/fr\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Monte-escaliers sur mesure pour maison et bureau | Millepiani","description":"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.","canonical_url":"https:\/\/millepiani.com\/fr\/monte-escaliers\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BreadcrumbList","@id":"https:\/\/millepiani.com\/fr\/monte-escaliers\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/millepiani.com\/fr\/#listItem","position":1,"name":"Home","item":"https:\/\/millepiani.com\/fr\/","nextItem":{"@type":"ListItem","@id":"https:\/\/millepiani.com\/fr\/monte-escaliers\/#listItem","name":"MONTE-ESCALIERS"}},{"@type":"ListItem","@id":"https:\/\/millepiani.com\/fr\/monte-escaliers\/#listItem","position":2,"name":"MONTE-ESCALIERS","previousItem":{"@type":"ListItem","@id":"https:\/\/millepiani.com\/fr\/#listItem","name":"Home"}}]},{"@type":"Organization","@id":"https:\/\/millepiani.com\/fr\/#organization","name":"millepiani.com","url":"https:\/\/millepiani.com\/fr\/"},{"@type":"WebPage","@id":"https:\/\/millepiani.com\/fr\/monte-escaliers\/#webpage","url":"https:\/\/millepiani.com\/fr\/monte-escaliers\/","name":"Monte-escaliers sur mesure pour maison et bureau | Millepiani","description":"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.","inLanguage":"fr-FR","isPartOf":{"@id":"https:\/\/millepiani.com\/fr\/#website"},"breadcrumb":{"@id":"https:\/\/millepiani.com\/fr\/monte-escaliers\/#breadcrumblist"},"datePublished":"2026-05-24T10:01:36+00:00","dateModified":"2026-09-15T18:18:16+00:00"},{"@type":"WebSite","@id":"https:\/\/millepiani.com\/fr\/#website","url":"https:\/\/millepiani.com\/fr\/","name":"millepiani.com","inLanguage":"fr-FR","publisher":{"@id":"https:\/\/millepiani.com\/fr\/#organization"}}]},"og:locale":"fr_FR","og:site_name":"millepiani.com -","og:type":"article","og:title":"Monte-escaliers sur mesure pour maison et bureau | Millepiani","og:description":"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit.","og:url":"https:\/\/millepiani.com\/fr\/monte-escaliers\/","article:published_time":"2026-05-24T10:01:36+00:00","article:modified_time":"2026-09-15T18:18:16+00:00","twitter:card":"summary_large_image","twitter:title":"Monte-escaliers sur mesure pour maison et bureau | Millepiani","twitter:description":"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit."},"aioseo_meta_data":{"post_id":"2534","title":"Monte-escaliers sur mesure pour maison et bureau | Millepiani","description":"D\u00e9couvrez les monte-escaliers Millepiani : solutions s\u00fbres et sur mesure pour escaliers droits, courbes, \u00e9troits, int\u00e9rieurs ou ext\u00e9rieurs. Demandez un devis gratuit. ","keywords":null,"keyphrases":{"focus":{"keyphrase":"servoscale su misura","score":0,"analysis":[]},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_custom_url":null,"og_image_custom_fields":null,"og_image_url":null,"og_image_width":null,"og_image_height":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_image_url":null,"twitter_title":null,"twitter_description":null,"schema_type":"default","schema_type_options":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"WebPage","isEnabled":true},"graphs":[]},"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"breadcrumb_settings":null,"seo_analyzer_scan_date":null,"created":"2026-09-14 20:23:12","updated":"2026-09-15 18:48:12","focus_keyword":"servoscale su misura","additional_keywords":null,"truseo_locale":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/millepiani.com\/fr\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tMONTE-ESCALIERS\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/millepiani.com\/fr\/"},{"label":"MONTE-ESCALIERS","link":"https:\/\/millepiani.com\/fr\/monte-escaliers\/"}],"_hostinger_reach_plugin_has_subscription_block":false,"_hostinger_reach_plugin_is_elementor":false,"_links":{"self":[{"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/pages\/2534","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/comments?post=2534"}],"version-history":[{"count":4,"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/pages\/2534\/revisions"}],"predecessor-version":[{"id":2575,"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/pages\/2534\/revisions\/2575"}],"wp:attachment":[{"href":"https:\/\/millepiani.com\/fr\/wp-json\/wp\/v2\/media?parent=2534"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}