{"id":2536,"date":"2026-05-24T10:01:36","date_gmt":"2026-05-24T10:01:36","guid":{"rendered":"https:\/\/millepiani.com\/stairlifts\/"},"modified":"2026-09-15T18:04:38","modified_gmt":"2026-09-15T18:04:38","slug":"stairlifts","status":"publish","type":"page","link":"https:\/\/millepiani.com\/en\/stairlifts\/","title":{"rendered":"STAIRLIFTS"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"2536\" class=\"elementor elementor-2536 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\">Tailor-made accessibility<\/p>\r\n\r\n      <h1>Stairlifts<\/h1>\r\n\r\n      <p>\r\n  Solutions designed to make moving\r\n between different floors of a building simpler, safer, and more comfortable. Stairlifts designed for people with reduced\r\n mobility, the elderly, and contexts where stairs represent a daily\r\n barrier. \r\n      <\/p>\r\n\r\n      <a href=\"#preventivo\" class=\"mp-outline-btn\">Request a quote<\/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>Why Choose Modernisation<\/p>\r\n        <h2>Millepiani<br\/>Stairlifts<\/h2>\r\n      <\/div>\r\n\r\n      <div class=\"mp-intro-text mp-reveal\">\r\n        <p>\r\n  The stairlift allows you to overcome flights of stairs comfortably and safely,\r\n improving autonomy, practicality, and quality of life. It can be installed\r\n in detached houses, multi-family buildings, offices, and public premises, even\r\n in the presence of narrow, winding, steep, curved stairs or those with changes in gradient. \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>Helps to overcome architectural barriers and level differences in a simple way.<\/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>Safe<\/h3>\r\n          <p>Designed for smooth, stable, and comfortable movement between multiple levels.<\/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>Versatile<\/h3>\r\n          <p>Suitable for straight, curved, narrow, steep, indoor or outdoor stairs.<\/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>Custom-made<\/h3>\r\n          <p>Each solution is modeled based on the staircase, the environment, and the user.<\/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>Minimally invasive<\/h3>\r\n          <p>In most cases, it does not require major structural work.<\/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>Discreet<\/h3>\r\n          <p>Materials, models, and colors integrate with public and private environments.<\/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\">Stairlift experience<\/p>\r\n\r\n      <h2 id=\"mpServoTitle\">\r\n  The staircase becomes<br\/> accessible\r\n      <\/h2>\r\n\r\n      <p id=\"mpServoText\">\r\n  Explore an immersive stairlift simulation: a custom-designed path\r\n to overcome level differences, straight or curved stairs, narrow spaces, or outdoor environments.\r\n      <\/p>\r\n\r\n      <div class=\"mp-servo-tags\" id=\"mpServoTags\">\r\n        <span>Custom-made<\/span>\r\n        <span>Minimum footprint<\/span>\r\n        <span>Comfort<\/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>3D experience not available<\/strong>\r\n        <span>The browser does not support WebGL or Three.js was not loaded.<\/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>Inspection<\/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>Comfort<\/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 Straight staircase\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"curved\">\r\n          <span>02<\/span>\r\n Curved staircase\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"narrow\">\r\n          <span>03<\/span>\r\n Narrow space\r\n        <\/button>\r\n\r\n        <button type=\"button\" data-route=\"outdoor\">\r\n          <span>04<\/span>\r\n Outdoor\r\n        <\/button>\r\n      <\/div>\r\n\r\n      <div class=\"mp-servo-info-card\" id=\"mpServoInfoCard\">\r\n        <p>Inspection<\/p>\r\n        <h3>Analysis of the environment and needs<\/h3>\r\n        <span>\r\n  The path is studied based on the staircase, the available space,\r\n the user, and the context in which the stairlift will be placed.\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\">Chair<\/button>\r\n        <button type=\"button\" data-mode=\"platform\">Platform<\/button>\r\n      <\/div>\r\n    <\/div>\r\n\r\n    <div class=\"mp-servo-dashboard\">\r\n      <div>\r\n        <span>Path<\/span>\r\n        <strong id=\"mpServoDashRoute\">Straight staircase<\/strong>\r\n      <\/div>\r\n\r\n      <div>\r\n        <span>Environment<\/span>\r\n        <strong id=\"mpServoDashContext\">Home \/ office<\/strong>\r\n      <\/div>\r\n\r\n      <div>\r\n        <span>Solution<\/span>\r\n        <strong id=\"mpServoDashMode\">Chair<\/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>Freedom of movement<br\/>in every context<\/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>Private homes<\/h3>\r\n          <p>To move between the floors of your home with more autonomy and safety.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>02<\/span>\r\n          <h3>Offices<\/h3>\r\n          <p>Functional solutions where stairs limit the accessibility of environments.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>03<\/span>\r\n          <h3>Public buildings<\/h3>\r\n          <p>To overcome small or large level differences and improve access to spaces.<\/p>\r\n        <\/article>\r\n\r\n        <article class=\"mp-servo-app-card mp-reveal\">\r\n          <span>04<\/span>\r\n          <h3>Complex contexts<\/h3>\r\n          <p>Curved, winding, steep, narrow stairs or those with changes in gradient.<\/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\">Contact us<\/p>\r\n\r\n      <h2 class=\"mp-quote-title\">\r\n        Request a<br>\r\n        free<br>\r\n        quote\r\n      <\/h2>\r\n\r\n      <p class=\"mp-quote-description\">\r\n        Are you interested in our products or looking for a solution\r\n        tailored specifically to your needs? Please get in touch:\r\n        we are here to assist you and help you choose\r\n        the solution that best meets your requirements.\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>Phone<\/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>Email<\/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>Office<\/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        <!-- Anti-spam honeypot -->\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\">First name *<\/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\">Last name *<\/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\">Email *<\/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\">Phone number<\/label>\r\n          <\/div>\r\n\r\n          <div class=\"mp-field\">\r\n            <select name=\"role\" id=\"mpRole\">\r\n              <option value=\"\" selected>Select<\/option>\r\n              <option value=\"progettista\">Designer<\/option>\r\n              <option value=\"impresa_di_costruzioni\">\r\n                Construction company\r\n              <\/option>\r\n              <option value=\"proprietario_edificio\">\r\n                Building owner\r\n              <\/option>\r\n              <option value=\"amministratore_edificio\">\r\n                Building manager\r\n              <\/option>\r\n              <option value=\"consulente_ascensori\">\r\n                Lift consultant\r\n              <\/option>\r\n              <option value=\"ente_pubblico\">Public authority<\/option>\r\n              <option value=\"cerco_lavoro\">Job seeker<\/option>\r\n              <option value=\"utente_passeggero\">\r\n                User\/Passenger\r\n              <\/option>\r\n              <option value=\"studente\">Student<\/option>\r\n              <option value=\"altro\">Other<\/option>\r\n            <\/select>\r\n\r\n            <label for=\"mpRole\">What is your role?<\/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\">Company name<\/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>Italy<\/option>\r\n              <option value=\"estero\">Outside Italy<\/option>\r\n            <\/select>\r\n\r\n            <label for=\"mpCountry\">Country\/Region *<\/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>Select<\/option>\r\n              <option value=\"AG\">Agrigento<\/option>\r\n              <option value=\"AL\">Alessandria<\/option>\r\n              <option value=\"AN\">Ancona<\/option>\r\n              <option value=\"AO\">Aosta<\/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\">Benevento<\/option>\r\n              <option value=\"BG\">Bergamo<\/option>\r\n              <option value=\"BI\">Biella<\/option>\r\n              <option value=\"BO\">Bologna<\/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\">Caserta<\/option>\r\n              <option value=\"CT\">Catania<\/option>\r\n              <option value=\"CZ\">Catanzaro<\/option>\r\n              <option value=\"CH\">Chieti<\/option>\r\n              <option value=\"CO\">Como<\/option>\r\n              <option value=\"CS\">Cosenza<\/option>\r\n              <option value=\"CR\">Cremona<\/option>\r\n              <option value=\"KR\">Crotone<\/option>\r\n              <option value=\"CN\">Cuneo<\/option>\r\n              <option value=\"EN\">Enna<\/option>\r\n              <option value=\"FM\">Fermo<\/option>\r\n              <option value=\"FE\">Ferrara<\/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\">Genoa<\/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\">Livorno<\/option>\r\n              <option value=\"LO\">Lodi<\/option>\r\n              <option value=\"LU\">Lucca<\/option>\r\n              <option value=\"MC\">Macerata<\/option>\r\n              <option value=\"MN\">Mantua<\/option>\r\n              <option value=\"MS\">Massa-Carrara<\/option>\r\n              <option value=\"MT\">Matera<\/option>\r\n              <option value=\"ME\">Messina<\/option>\r\n              <option value=\"MI\">Milan<\/option>\r\n              <option value=\"MO\">Modena<\/option>\r\n              <option value=\"MB\">Monza and Brianza<\/option>\r\n              <option value=\"NA\">Naples<\/option>\r\n              <option value=\"NO\">Novara<\/option>\r\n              <option value=\"NU\">Nuoro<\/option>\r\n              <option value=\"OR\">Oristano<\/option>\r\n              <option value=\"PD\">Padua<\/option>\r\n              <option value=\"PA\">Palermo<\/option>\r\n              <option value=\"PR\">Parma<\/option>\r\n              <option value=\"PV\">Pavia<\/option>\r\n              <option value=\"PG\">Perugia<\/option>\r\n              <option value=\"PU\">Pesaro and Urbino<\/option>\r\n              <option value=\"PE\">Pescara<\/option>\r\n              <option value=\"PC\">Piacenza<\/option>\r\n              <option value=\"PI\">Pisa<\/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\">Ragusa<\/option>\r\n              <option value=\"RA\">Ravenna<\/option>\r\n              <option value=\"RC\">Reggio Calabria<\/option>\r\n              <option value=\"RE\">Reggio Emilia<\/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\">Salerno<\/option>\r\n              <option value=\"SS\">Sassari<\/option>\r\n              <option value=\"SV\">Savona<\/option>\r\n              <option value=\"SI\">Siena<\/option>\r\n              <option value=\"SR\">Syracuse<\/option>\r\n              <option value=\"SO\">Sondrio<\/option>\r\n              <option value=\"SU\">South Sardinia<\/option>\r\n              <option value=\"TA\">Taranto<\/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\">Trento<\/option>\r\n              <option value=\"TV\">Treviso<\/option>\r\n              <option value=\"TS\">Trieste<\/option>\r\n              <option value=\"UD\">Udine<\/option>\r\n              <option value=\"VA\">Varese<\/option>\r\n              <option value=\"VE\">Venice<\/option>\r\n              <option value=\"VB\">Verbano-Cusio-Ossola<\/option>\r\n              <option value=\"VC\">Vercelli<\/option>\r\n              <option value=\"VR\">Verona<\/option>\r\n              <option value=\"VV\">Vibo Valentia<\/option>\r\n              <option value=\"VI\">Vicenza<\/option>\r\n              <option value=\"VT\">Viterbo<\/option>\r\n              <option value=\"ESTERO\">Province outside Italy<\/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\">City *<\/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>Select<\/option>\r\n            <option value=\"nuovo_impianto\">\r\n              Installing a new lift in a new\/existing building\r\n            <\/option>\r\n            <option value=\"sostituzione_ascensore\">\r\n              Replacing an existing lift\r\n            <\/option>\r\n            <option value=\"ammodernamento_impianto\">\r\n              Modernising a lift\r\n            <\/option>\r\n            <option value=\"assistenza_manutenzione\">\r\n              I am interested in support and maintenance services\r\n            <\/option>\r\n            <option value=\"ricambi\">\r\n              Spare parts for Millepiani lifts\r\n            <\/option>\r\n            <option value=\"altro\">Other<\/option>\r\n          <\/select>\r\n\r\n          <label for=\"mpContactReason\">\r\n            What would you like us to contact you about? *\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\">How can we help you?<\/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                I accept the\r\n                <a\r\n                  href=\"\/privacy-policy\/\"\r\n                  target=\"_blank\"\r\n                  rel=\"noopener noreferrer\"\r\n                >\r\n                  Privacy Policy\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                I already have a contract with 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>Send 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>Tailor-made accessibility Stairlifts Solutions designed to make moving between different floors of a building simpler, safer, and more comfortable. Stairlifts designed for people with reduced mobility, the elderly, and contexts where stairs represent a daily barrier. Request a quote Why Choose Modernisation MillepianiStairlifts The stairlift allows you to overcome flights of stairs comfortably and safely, [&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-2536","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=\"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<link rel=\"canonical\" href=\"https:\/\/millepiani.com\/en\/stairlifts\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\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=\"Custom stairlifts for home and office | Millepiani\" \/>\n\t\t<meta property=\"og:description\" content=\"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/millepiani.com\/en\/stairlifts\/\" \/>\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:04:38+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Custom stairlifts for home and office | Millepiani\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.\" \/>\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\\\/en\\\/stairlifts\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/millepiani.com\\\/en\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/stairlifts\\\/#listItem\",\"name\":\"STAIRLIFTS\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/stairlifts\\\/#listItem\",\"position\":2,\"name\":\"STAIRLIFTS\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#listItem\",\"name\":\"Home\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#organization\",\"name\":\"millepiani.com\",\"url\":\"https:\\\/\\\/millepiani.com\\\/en\\\/\"},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/stairlifts\\\/#webpage\",\"url\":\"https:\\\/\\\/millepiani.com\\\/en\\\/stairlifts\\\/\",\"name\":\"Custom stairlifts for home and office | Millepiani\",\"description\":\"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/stairlifts\\\/#breadcrumblist\"},\"datePublished\":\"2026-05-24T10:01:36+00:00\",\"dateModified\":\"2026-09-15T18:04:38+00:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#website\",\"url\":\"https:\\\/\\\/millepiani.com\\\/en\\\/\",\"name\":\"millepiani.com\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/millepiani.com\\\/en\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Custom stairlifts for home and office | Millepiani","description":"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.","canonical_url":"https:\/\/millepiani.com\/en\/stairlifts\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"BreadcrumbList","@id":"https:\/\/millepiani.com\/en\/stairlifts\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/millepiani.com\/en\/#listItem","position":1,"name":"Home","item":"https:\/\/millepiani.com\/en\/","nextItem":{"@type":"ListItem","@id":"https:\/\/millepiani.com\/en\/stairlifts\/#listItem","name":"STAIRLIFTS"}},{"@type":"ListItem","@id":"https:\/\/millepiani.com\/en\/stairlifts\/#listItem","position":2,"name":"STAIRLIFTS","previousItem":{"@type":"ListItem","@id":"https:\/\/millepiani.com\/en\/#listItem","name":"Home"}}]},{"@type":"Organization","@id":"https:\/\/millepiani.com\/en\/#organization","name":"millepiani.com","url":"https:\/\/millepiani.com\/en\/"},{"@type":"WebPage","@id":"https:\/\/millepiani.com\/en\/stairlifts\/#webpage","url":"https:\/\/millepiani.com\/en\/stairlifts\/","name":"Custom stairlifts for home and office | Millepiani","description":"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/millepiani.com\/en\/#website"},"breadcrumb":{"@id":"https:\/\/millepiani.com\/en\/stairlifts\/#breadcrumblist"},"datePublished":"2026-05-24T10:01:36+00:00","dateModified":"2026-09-15T18:04:38+00:00"},{"@type":"WebSite","@id":"https:\/\/millepiani.com\/en\/#website","url":"https:\/\/millepiani.com\/en\/","name":"millepiani.com","inLanguage":"en-US","publisher":{"@id":"https:\/\/millepiani.com\/en\/#organization"}}]},"og:locale":"en_US","og:site_name":"millepiani.com -","og:type":"article","og:title":"Custom stairlifts for home and office | Millepiani","og:description":"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote.","og:url":"https:\/\/millepiani.com\/en\/stairlifts\/","article:published_time":"2026-05-24T10:01:36+00:00","article:modified_time":"2026-09-15T18:04:38+00:00","twitter:card":"summary_large_image","twitter:title":"Custom stairlifts for home and office | Millepiani","twitter:description":"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote."},"aioseo_meta_data":{"post_id":"2536","title":"Custom stairlifts for home and office | Millepiani","description":"Discover Millepiani stairlifts: safe and custom-made solutions for straight, curved, narrow, indoor or outdoor stairs. Request a free quote. ","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:41","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\/en\/\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tSTAIRLIFTS\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/millepiani.com\/en\/"},{"label":"STAIRLIFTS","link":"https:\/\/millepiani.com\/en\/stairlifts\/"}],"_hostinger_reach_plugin_has_subscription_block":false,"_hostinger_reach_plugin_is_elementor":false,"_links":{"self":[{"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/pages\/2536","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/comments?post=2536"}],"version-history":[{"count":4,"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/pages\/2536\/revisions"}],"predecessor-version":[{"id":2566,"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/pages\/2536\/revisions\/2566"}],"wp:attachment":[{"href":"https:\/\/millepiani.com\/en\/wp-json\/wp\/v2\/media?parent=2536"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}