{"id":8463,"date":"2025-10-20T16:53:11","date_gmt":"2025-10-20T08:53:11","guid":{"rendered":"https:\/\/haircosys.com\/?page_id=8463"},"modified":"2025-11-12T17:39:44","modified_gmt":"2025-11-12T09:39:44","slug":"%e5%a4%b4%e5%8f%91%e5%81%a5%e5%ba%b7%e5%88%86%e6%9e%90","status":"publish","type":"page","link":"https:\/\/haircosys.com\/zh_sc\/%e5%a4%b4%e5%8f%91%e5%81%a5%e5%ba%b7%e5%88%86%e6%9e%90\/","title":{"rendered":"\u5934\u53d1\u5065\u5eb7\u5206\u6790"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"8463\" class=\"elementor elementor-8463\" data-elementor-post-type=\"page\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-502a192 elementor-section-boxed elementor-section-height-default elementor-section-height-default\" data-id=\"502a192\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-default\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-d12ad1d\" data-id=\"d12ad1d\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-abfe4e0 elementor-widget elementor-widget-html\" data-id=\"abfe4e0\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!-- CryptoJS CDN 3 -->\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/crypto-js@4.2.0\/crypto-js.min.js\"><\/script>\n<!-- Vue CDN -->\n<script src=\"https:\/\/unpkg.com\/vue@3\/dist\/vue.global.prod.js\"><\/script>\n\n\n\n<div id=\"camera-page\" style=\"background-color: #FFFDFA; \">\n  \n  <!-- <div style=\"height: calc(100vh - 110px); overflow-y: scroll;\"> -->\n  <div>\n  \n  <!-- Modal for error messages -->\n  <div class=\"error-modal\" v-if=\"showModal\" @click=\"resetModal\">\n    <div class=\"error-modal-wrapper\">\n      <div class=\"error-modal-container\">\n        <span>{{ errorText }}<\/span>\n        <button style=\"height: 30px; width: 40px;\" class=\"blackButtonDefault\" @click=\"resetModal\"> Ok <\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n\n  <!-- Terms and Conditions Modal -->\n  <div class=\"terms-modal\" v-if=\"showTermsModal\" @click=\"closeTermsModal\">\n    <div class=\"terms-modal-wrapper\">\n      <div class=\"terms-modal-container\" @click.stop>\n        <h2 class=\"terms-modal-title\">Terms and Conditions<\/h2>\n        <h3 class=\"terms-modal-subtitle\">Privacy Agreement<\/h3>\n        <div class=\"terms-modal-content\">\n          <p>\n            As the service provider of AI Hair & Scalp Analysis, HairCoSys Limited (\u201cwe \u201c, \u201cour \u201d or \u201cus \u201c) respects your legal rights of privacy when collecting, storing, using and transmitting Personal Information (as defined below) and this Privacy Policy explains our privacy practices. It is our policy and obligation to comply with the requirements of the Personal Data (Privacy) Ordinance (Cap. 486 of the laws of Hong Kong). In doing so, we will ensure compliance by our staff to the strictest standard of security and confidentiality.Please read the following carefully to understand our policy and practices regarding how your Personal Information will be treated. This Privacy Policy applies to all registered and unregistered Users of the Sites and Service App and may from time to time be revised, or otherwise changed where necessary.Words or terms used in this Privacy Policy which are not specifically defined in this Privacy Policy have the meaning given to them in the Agreement (as defined below).By being one of our Users or visitors to our Sites and\/or Service App you agree to be bound by all the terms and conditions set out in this Privacy Policy and THAT YOU GIVE US YOUR CONSENT to collect, use and disclose such Personal Information. If you do not accept the terms of this Privacy Policy or disagree with any subsequent amendments, changes, or updates we made, you MUST NOT access the Sites or the Service App and not use any Service provided by us. You hereby agree your only recourse in this case is to cease and desist from further use of the Service.We strongly recommend you read this Privacy Policy and if you have any questions, please don\u2019t hesitate to send us an email at <a href=\"mailto:mail@haircosys.com\">mail@haircosys.com<\/a>.1. DEFINITIONS AND INTREPRETATIONS1.1 The following shall have the meaning describe hereinbelow: \u2013(a) \u201cAgreement\u201d means the user agreement entered between us as the provider of the Service on the one part and you as the User on the other made effective upon your continued use of the Sites and\/or Service.(b) \u201cAccount\u201d means an established relationship between the User and the network of information service provided by us including the username and password created by the User at his own initiative or provided by us and accepted by us for the purpose to enable the User to gain access to the Service provided by us.(c) \u201cAssociates\u201d means each and every one of our respective shareholders, subsidiaries, employees, advisers, contractors, agents, directors, officers, partners, insurers, and attorneys.(d) \u201cInternet\u201d means a global computer network through which the almost-instant delivery of data or files occurs between connected computers.(e) \u201cMinor\u201d means a person under the age of 18 (or the age that a person is permitted to enter into a contractual relationship under applicable law at your jurisdiction).(f) \u201cNon-Personal Information\u201d means information that does not identify you as an individual as stipulated in Clause 3.7.(g) \u201cParent\u201d or \u201cLegal Guardian\u201d means the person who has the legal authority (and the corresponding duty) to care for the person and property interest of a User who is a Minor.(h) \u201cPerson\u201d includes an individual, association, partnership, corporation, other body corporate, trust, and any form of legal organization or entity.(i) \u201cPersonal Information\u201d means information and photos about an identifiable individual, business, organization or other entity, but does not include the name, title, business address, or telephone number of an employee and\/or associate, partners of a business, organization or other entity.(j) \u201cPrivacy Policy\u201d means this policy.(k) \u201cService\u201d means any of the services, functions, or features offered on the Sites or by the Service App provided by us for online collaboration and\/or management including but not limited to point-of-sales systems, office automation systems and human resources management systems.(l) \u201cService App\u201d means the software product(s) owned or licensed by us available at Apple App Store or Google Play to which we grant you access as part of the Service, including program documentation, and any program updates provided as part of the Service. The term \u201cService App\u201d does not include Separately Licensed Third-Party Technology.(m) \u201cSeparately Licensed Third-Party Technology\u201d means third party technology that is licensed under separate terms and not under the terms of the Agreement.(n) \u201cSites\u201d means the website(s) including but not limited to (haircosys.com) or any other website(s) and sub-links owned and operated by us.(o) \u201cUsers\u201d means those individuals, employees, contractors and end users, as applicable, authorized by you or on your behalf to use the Service in accordance with the Agreement.1.2 The headings and sub-headings in this Privacy Policy are for ease of reference only and are not to be taken into account in the construction or interpretation of any provision or provisions to which they refer.1.3 Unless otherwise specified in this Privacy Policy, words importing the singular include the plural and vice versa and words importing gender include all genders.2. OVERVIEW2.1 This Privacy Policy applies to all our Sites, Service App and Service provided by us (haircosys.com).2.2 You expressly give consent to our use and disclosure of your Personal Information in accordance with this Privacy Policy and applicable laws once you have accepted this Privacy Policy and the Agreement upon registration.2.3 This Privacy Policy is part of the Agreement, and the contents of this Privacy Policy are subject to the terms and conditions in the Agreement.2.4 This Privacy Policy sets out the Personal Information which we may collect and receive from you and what may happen to your Personal Information.3. COLLECTION OF PERSONAL INFORMATION3.1 You may be asked to provide your Personal Information (including any Personal Information of your client (if any)) at any time you are in contact with us or the Associates. We and the Associates may share your Personal Information with each other and use it in accordance with this Privacy Policy. We may also combine your Personal Information with other information to provide and to improve our products, Service, contents, and any other area related to our products, Service and contents. If you choose not to provide the Personal Information that we have requested, in many cases we may not be able to provide you with the Service or respond to any queries you may have.3.2 In order to use the Service, you must establish an Account by providing us with a valid email address and\/or a mobile number for registration and choose a username or alias at your own initiative or provided by us and approved by us that can represent you on the Service. We may also require additional information such as, including but not limited to, your address, your contact number and\/or your billing address when necessary.3.3 You may connect to or register the Account via external third-party applications and\/or webpages and we may receive your Personal Information from such applications and\/or webpages. You may choose whether to connect the Account to such applications and\/or webpages.3.4 The types of information collected by us include Personal Information and Non-Personal Information.3.5 We may also receive, collect and store any Personal Information including but not limited to personal and financial information you provide to us when you or your business:(a) enquire of or make an application for Service;(b) register to use and\/or use any Service;(c) upload and\/or store information with us using the Service; or(d) when you communicate with us through email, SMS, a website or portal, or the telephone or other electronic means.3.6 Such personal and financial information may include but not limited to your or your client\u2019s: \u2013(a) name (including first name and family name), date of birth, gender, email address, billing address, username, password, photograph, address, nationality and country of residence;(b) card primary account number, card expiry date, CVC details, bank and\/or issuer details;(c) IP addresses or similar identifiers; and(d) information relating to any items purchased, including the location of the purchase, the value, the time and any feedback that is given in relation to such purchase.3.7 We may also collect data in a form that does not, on its own, permit direct association with any specific individual. We may collect, use, transfer, and disclose Non-Personal Information for any purpose, including but not limited to the following: \u2013(a) information such as occupation, language, area code, unique device identifier, URL, location, and the time zone where our product is used so that we can better understand your behavior and improve our products, Service, and advertising;(b) information collected by cookies and other technologies;(c) information regarding your activities on our Sites, and from our other products and Service. This information is aggregated and used to help us to provide more useful information to you and other customers of ours and to understand which parts of the Sites, products, and Service are of most interest. Aggregated data is considered as NonPersonal Information for the purposes of this Privacy Policy;(d) details of how you use the Service, including search queries. This information may be used to improve the relevancy of results provided by the Service except in limited instances to ensure quality of the Service over the Internet, such information will not be associated with your IP address; and(e) with your consent, we may collect data about how you use your device and the Service App to help us and our app developers improve and\/or develop the Service App.3.8 Non-Personal Information will not be protected under this Privacy Policy. However, if we do combine Non-Personal Information with Personal Information (whether coincidentally or not), such combined information will be treated as Personal Information so long as it remains combined.3.9 From time to time, we may offer you the opportunity to participate in promotions such as sweepstakes, contests, offers, and\/or surveys (\u201cSpecial Promotions\u201d) through the Service. A Special Promotion may be governed by a privacy policy and\/or terms and conditions that are additional to, or separate from, this Privacy Policy and the Agreement. If the provisions of the Special Promotion\u2019s privacy policy or terms and conditions conflict with this Privacy Policy or the Agreement, those additional or separate provisions shall prevail. If you participate in a Special Promotion, we may ask you for certain information in addition to what is stated in this Privacy Policy, including Personal Information. That additional information may be combined with other Account information and may be used and shared as described in this Privacy Policy.3.10 HairCoSys and\/or a third party acting on our behalf or at our direction may collect, capture, store, use, receive or otherwise obtain a scan of your face and\/or head or any data or information based on the scan of your face and\/or head which may include your face geometry (\u201cBiometric Information\u201d) for the purpose of providing personalized Services. HairCoSys and\/or any such third parties do not and will not use facial recognition or identification technology in providing the Services. HairCoSys and\/or any such third parties do not and will not store, use, possess, retain or have access to your biometric information after your use of the Services has completed, at which time the initial purpose for collecting, capturing, storing, using, receiving or otherwise obtaining the biometric information has been satisfied.4. PERSONS UNDER THE LEGAL AGE4.1 Service will only be available to individuals who are:-(a) aged 18 or older; or(b) of legal age at their jurisdiction; or(c) Minors with the supervision by their Legal Guardian or Parent,provided that you or your Legal Guardian or Parent (if you are a Minor) have read and agreed to comply with the Agreement and this Privacy Policy.5. YOUR PERSONAL INFORMATION5.1 You are free to decide whether to provide, edit or remove your Personal Information disclosed in the Service. Except your username, you are free to amend your Personal Information which has been submitted to us and the profile information of the Account so long as that Personal Information is true and accurate. The action can be performed directly on your profile in the Sites or the Service App.5.2 You may opt out of receiving direct marking materials from us by following the instructions provided in those direct marketing materials. Should you decide to opt out from receiving such materials, we may still send you non-promotional communications, such as those about the Account, previous purchases, our ongoing business relations or change in operation, etc.5.3 In accordance with the statutory requirements, we will honor your request not to use your Personal Information for the purposes of direct marketing.6. USE OF PERSONAL INFORMATION6.1 We collect Personal Information that you and your employees, your affiliates or your contractors provide directly to us.6.2 We use the information we collect to improve and personalize the Services and to develop new ones. The ways of which we may use your Personal Information, once collected, may include but not limited to the followings: \u2013(a) to allow you to take part in all real-time interactive features and your health-related tracking on our Sites and\/or Service App. We also use your information to make inferences and show you more relevant content.(b) to process, assess and determine any applications or requests made by you in connection with our services or products and maintaining your account.(c) to help us create, develop, operate, deliver, and improve our products, Service, contents and advertisements, and for loss prevention and anti-fraud purposes.(d) to verify identity, assist with identification of Users, and to determine appropriate Service.(e) to keep you posted on our latest product announcements, software updates, and upcoming events.(f) to send important notices, such as communications about purchases and changes to our terms, conditions and policies.(g) for our internal purposes, such as auditing, data analysis, and research to improve our products, Service, and customer communications.(h) to administer any lottery, contest or similar promotional activities we and\/or the Associates may hold from time to time.6.3 We may allow others to provide analytics on our behalf via the Service which are aggregated or de-identified information with third parties, which cannot reasonably be used to identify you. These entities may use cookies, web beacons and other technologies to collect information about your use of the Service and other websites, including but not limited to your IP address, web browser, pages viewed, time spent on pages, links clicked and conversion information. This information may be used by us and others to, among other things, analyze and track data, determine the popularity of certain content, providing a better user experience and customer assistance in the Service, Site and Service App (i.e. knowing your first name to let us welcome you the next time you visit the Sites or automatically changing the language to your country\/ computer\u2019s default language), understand your online activity and facilitate and measure the effectiveness of advertisements and web searches.6.4 We may review the Personal Information from time to time to identify problems and solve disputes, and to identify Users who use multiple user identifications or aliases. We may compare and review your Personal Information for errors, omissions and accuracy.7. DISCLOSURE OF INFORMATION7.1 At times we may make certain Personal Information available to strategic partners that work with us to provide products and services, or that help us market to customers. The Account will be governed by us and your carrier\u2019s respective privacy policy(ies). Personal Information will only be shared by us to provide or improve our products, Service and advertising and will not be shared with third parties for their own marketing purposes.7.2 We share Personal Information with companies who provide services such as information processing, extending credit, fulfilling customer orders, delivering products to you, managing and enhancing customer data, providing customer service, assessing your interest in our products and Service, and conducting customer research or satisfaction surveys. These companies are obligated to protect your Personal Information and may be located wherever we operate. You further agree that: \u2013(a) you shall read the full contents of the Privacy Policy of any such third-party;(b) we have no control over the contents and operation of such third-party and their use to the Personal Information (although we shall use our reasonable endeavor to ensure they have appropriate personal data privacy mechanism in place); and(c) you shall indemnify us, the Associates, contractors, suppliers or distributors against liability from any of your interaction with such third-party.7.3 Unless prior written consent has been obtained from you or it is required by applicable laws or court orders, we will not sell, send, share or disclose your Personal Information to third parties.7.4 It may be necessary \u2212 by law, legal process, litigation, and\/or requests from public and governmental authorities within or outside your country of residence \u2212 for us to disclose your Personal Information. We may also disclose information about you if we determine that for purposes of national security, law enforcement, or other issues of public importance, such disclosure is necessary or appropriate.]7.5 We may also disclose your Personal Information if we determine that disclosure is reasonably necessary to enforce the Agreement or protect our operations or our customers. Additionally, in the event of a reorganization, merger, amalgamation or sale, we may transfer any or all Personal Information that we collected to the relevant third party. Your continued usage of the Sites (where ownership of the Sites may no longer be owned by us) shall constitute as your consent to such transfer (if any).7.6 We treat your Biometric Information with care and confidentiality. We do not share, sell, rent, or trade Biometric Information with third parties for any promotional purposes. Where our affiliates or service providers process your Biometric Information, they will do so solely on our instructions and comply with strict contractual requirements for the security of your Biometric Information.Although we make every effort to preserve user privacy, we may need to disclose your Biometric Information when required by law, such as when we have a good-faith belief that such action is necessary to comply with a current judicial proceeding, a court order, or litigation or other legal process or action (whether or not initiated by Perfect) to protect Perfect\u2019s, our users\u2019 or third parties\u2019 rights, property or safety. We will transmit data to public authorities such as law enforcement or tax authorities only in the case of a legal obligation to do so based on a request for information from the respective authority.7.7 To ensure the operational stability of our services or enable users to use related functions, we may insert third-party Software Development Kits (SDKs) to achieve the aforementioned purpose. You can visit the third-party Software Development Kits (SDKs) privacy policy website, which is disclosed by our company, for more details about the specific situation of collecting your personal data. If the third party does not provide specific privacy policy related to its SDKs, we may provide its related official website link for helping you to understand its specific information. Currently, the details of inserting third-party SDKs are as follows:Android systemSDK:\u2022\tPurpose and Official Website Related to SDK usage and Collection of Personal DataAlipay:\u2022\tInvolving Personal Data: device identification code, MAC address, IP address, WLAN access point, Wi-Fi list, device sensor information, network information, device information\u2022\tPurpose of Use: to help users use Alipay in the app\u2022\tUsage Scenario: To be used when users pay with Alipay (other scenarios will not trigger this SDK)\u2022\tCollection Method: SDK collection\u2022\tSDK Official Website Link:<a href=\"https:\/\/render.alipay.com\/p\/f\/fd-iwwyijeh\/index.html\u2022\" rel=\"nofollow\">https:\/\/render.alipay.com\/p\/f\/fd-iwwyijeh\/index.html\u2022<\/a>\tSDK Privacy Policy:<a href=\"https:\/\/docs.open.alipay.com\/54\u2022\" rel=\"nofollow\">https:\/\/docs.open.alipay.com\/54\u2022<\/a>\tSDK Related Link:<a href=\"https:\/\/mcgw.alipay.com8\" rel=\"nofollow\">https:\/\/mcgw.alipay.com8<\/a>. SECURITY AND CONFIDENTIALITY8.1 We limit access to your Personal Information by our staff, Suppliers, business service providers and business-related partners who, we believe reasonably, need to utilize that information to provide Services to you or do their jobs.8.2 Your Personal Information may be processed and stored in locations other than in Hong Kong for our business operations if needed; under such circumstances reasonable steps to protect your Personal Information will be taken.8.3 We will take reasonable steps to protect your Personal Information, and you are reminded that no online or offline data transmission or data storage system can be guaranteed to be 100% safe and risk free. We shall not be held liable for any loss and damages caused to you or any third party arising from any data loss or leakage or any matter in connection therewith.8.4 By your continued use of the Service, you hereby declare that you understand and accept the risk(s) as disclosed hereinabove. You further acknowledge and voluntarily consent to HairCoSys, its affiliates, and service providers collecting, processing, and storing your Biometric Information as outlined in this Privacy Policy. You further acknowledge and confirm that you are legally able to enter into this agreement allowing HairCoSys, our affiliates, and service providers to collect, process, and store your Biometric Information, or you are the legally authorized representative of the user(s) who will be using HairCoSys\u2019s Services. You further acknowledge and consent to HairCoSys disclosing your Biometric Information to our affiliates and service providers in providing the Services to you and as required by our ordinary business purposes.8.5 You further acknowledge and agree that consenting to the collecting, storing, and processing of your Biometric Information is a condition to you using our Services, including our mobile apps. If you do not consent to HairCoSys, our affiliates, and service providers collecting, storing, and\/or processing your Biometric Information, or cannot legally consent, you should not use our Services.9. PERSONAL INFORMATION RETENTION9.1 We will retain your Personal Information as long as the Account is still in existence, or we need to provide Service(s) to you. If you do not want us to use your Personal Information, you may terminate the Account.9.2 For most of our Services, we do not and will not store, use, possess, retain or have access to your Biometric Information after your use of the Services our products and services has completed, at which time the initial purpose for collecting, capturing, storing, using, receiving or otherwise obtaining the Biometric Information has been satisfied. You will be asked to upload your photo to our server for processing, and your photo will be permanently deleted from our server after your use of such service is completed.9.3 If you choose to delete your account, we delete things you have posted, such as your photos and status updates, unless subjected to a valid warrant, subpoena issued by a court of competent jurisdiction, or other legal or regulatory proceeding, we will comply with this retention schedule and destruction guidelines.9.4 We also may terminate the Account in accordance with the Agreement and your Personal Information will also be deleted and\/or archived.9.5 We will retain and use your Personal Information if necessary to comply with legal obligations, enforce the Agreement and \/ or resolve disputes.10. CHANGE OF THIS PRIVACY POLICY10.1 We may amend this Privacy Policy at any time by email (mail@haircosys.com), or via posting the amended Privacy Policy on the Sites (haircosys.com). All amended Privacy Policy shall automatically be effective [7] days after the posting day of such amended Privacy Policy. Your continued use of the Service after the effective date of such amended terms and \/ or conditions constitutes your acceptance of the amended Privacy Policy.\"\n          <\/p>\n        <\/div>\n        <button class=\"terms-close-button\" @click=\"closeTermsModal\">Close<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n  <h1 class=\"analysis-title\">Hair Health Analysis<\/h1>\n  <!-- Camera selection -->\n  <div class=\"initial-section\" v-show=\"init\">\n    <div class=\"camera-selection-header\">\n      <!-- <h1 class=\"analysis-title\">Frontal Analysis<\/h1> -->\n      <p class=\"analysis-description\">Welcome to your first step toward data-driven hair health! Please follow the steps below to get you hair analysis.<\/p>\n    <\/div>\n    \n    <div class=\"selection-container\">\n      <h3 class=\"step-title\">Step 1: Select Camera<\/h3>\n      <p class=\"step-description\">Choose a camera to start your hair analysis<\/p>\n      \n      <!-- <button style=\"width: 200px; height: 50px; border-radius: 25px; font-size: 18px;\" v-if=\"listCam.length === 0\" @click=\"list(false)\">\n        <span>Begin A.I. Detection<\/span>\n      <\/button> -->\n      <div v-if=\"listCam.length > 0\">\n        <!-- <label style=\"margin-right: 10px;\" for=\"camList\">Choose a camera:<\/label> -->\n        <div class=\"camera-select-row\">\n          <select name=\"camList\" id=\"camList\" v-model=\"camId\" class=\"camera-dropdown\">\n            <option value=\"\" disabled>Select a Camera<\/option>\n            <option v-for=\"cam in listCam\" :value=\"cam.id\">{{ cam.name }}<\/option>\n          <\/select>\n          <button class=\"refresh-icon-btn\" @click=\"list(true)\" aria-label=\"Refresh cameras\" title=\"Refresh cameras\">\u21bb<\/button>\n        <\/div>\n        <div style=\"height: 20px; text-align: center;\">\n          <span v-show=\"refreshed\" style=\"color: red; font-style: italic; font-size: small;\">Refreshed<\/span>\n        <\/div>\n      <\/div>\n      \n      <!-- Privacy Statement Checkbox -->\n      <div class=\"privacy-checkbox-container\">\n        <label class=\"privacy-checkbox-label\">\n          <input \n            type=\"checkbox\" \n            v-model=\"privacyAccepted\" \n            class=\"privacy-checkbox\"\n          >\n          <span class=\"privacy-text\">\n            By checking this box, you are confirming that you have read, understood and agree to HairCoSys's privacy statement and \n            <div @click=\"openTerms\" class=\"terms-link\">and terms.<\/div>\n          <\/span>\n        <\/label>\n      <\/div>\n      \n      <!-- Next Button -->\n      <div class=\"next-button-container\">\n        <button \n          class=\"next-button\" \n          style=\"justify-self: end; width: 180px; height: 35px; border-radius: 15px; font-size: 14px; margin-left: 10px;\"\n          @click=\"proceedToCamera\"\n        >\n          Next\n        <\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Photo taking and image upload -->\n  <div class=\"ai-body\" v-show=\"showCam\">\n    <div class=\"selection-container\">\n      <div class=\"step-header\">\n        <h3 class=\"step-title\">Step 2: Take Photos<\/h3>\n        <button class=\"back-button-step2\" @click=\"resetToCameraSelection\" aria-label=\"Back\">\n          <!-- \u2190 -->\n          &#8592;\n        <\/button>\n      <\/div>\n      <!-- <div class=\"step-header-mobile\">\n        <button class=\"back-button-step2-mobile\" @click=\"resetToCameraSelection\" aria-label=\"Back\">\n          \u2190 Back\n        <\/button>\n      <\/div> -->\n      <p class=\"step-description\">{{ getPositionInstruction() }}<\/p>\n      \n      <div class=\"camera-box\">\n        <div class=\"camera-preview-container\">\n          <img v-show=\"showCamera\" class=\"camera-guideline\" :style=\"calculateGuideLineMargin()\" :src=\"bucketLink + guidelineImageList[currentIndex]\" :width=\"calculateGuideLineSize()\" :height=\"calculateGuideLineSize()\">\n          <video v-show=\"showCamera\" class=\"camera\" ref=\"camera\" id=\"camera\" autoplay muted playsinline><\/video>\n          <img v-show=\"showCanvas\" :src=\"tempUnconfirmedImage\" class=\"unconfirmed-image\" alt=\"Unconfirmed photo\">\n          <!-- Hidden canvas for image capture -->\n          <canvas id=\"canvas\" class=\"hidden-canvas\"><\/canvas>\n        <\/div>\n        \n        <div class=\"pos-row\">\n          <div v-for=\"(pos, index) in frontalPositions\" class=\"position-thumbnail\" @click=\"changePos(index)\" :class=\"{ activePos: currentIndex === index }\">\n            <div class=\"thumbnail-container\">\n              <img v-if=\"listPhotos[index]\" :src=\"listPhotos[index]\" class=\"thumbnail-image\" alt=\"Position image\" @error=\"handleImageError(index)\" @load=\"handleImageLoad(index)\">\n              <div v-else class=\"placeholder-image\">\n                <span class=\"placeholder-icon\">+<\/span>\n              <\/div>\n            <\/div>\n            <span class=\"position-name\">{{ pos }}<\/span>\n          <\/div>\n        <\/div>\n        <div class=\"camera-controls\">\n          <div v-if=\"showCanvas\" class=\"button-row\" :style=\"getButtonReportJustfication()\">\n            <button @click=\"retryPicture\" class=\"cam-confirmation-buttons activePos\">Retry<\/button>\n            <button v-if=\"unconfirmedPicture\" @click=\"confirmPicture\" class=\"cam-confirmation-buttons activePos\">Confirm<\/button>\n            <button v-if=\"captureComplete\" class=\"cam-confirmation-buttons activePos\" @click=\"sendBigHeadRequest\">Finish<\/button>\n          <\/div>\n          <div v-else-if=\"showCamera\" style=\"justify-content: center;\" class=\"button-row\">\n            <button style=\"width: 100% !important; max-width: 100% !important; padding: 0px !important;\" class=\"cam-confirmation-buttons activePos\" @click=\"takepicture\">Take Photo<\/button>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n\n  <!-- Questionnaire Section -->\n  <div class=\"questionnaire-section\" v-if=\"showQuestionnaire\">\n    <div class=\"selection-container\">\n      <div class=\"step-header\">\n        <h3 class=\"step-title\">Step 3: Questionnaire<\/h3>\n        <button class=\"back-button-step2\" @click=\"goBackToCamera\" aria-label=\"Back\">\n          &#8592;\n        <\/button>\n      <\/div>\n      <p class=\"step-description\">Before getting the AI report, please tell us a little about your business and goals. This helps our patented AI system tailor your experience.<\/p>\n      \n      <!-- Loading state -->\n      <div v-if=\"isLoadingQuestions\" class=\"centre-load\">\n        <svg style=\"width: 100px; height: 100px;\" class=\"spinner spinner--circle\" viewBox=\"0 0 66 66\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n          <circle class=\"path\" fill=\"none\" stroke-width=\"6\" stroke-linecap=\"round\" cx=\"33\" cy=\"33\" r=\"30\"><\/circle>\n        <\/svg>\n        <span style=\"display: block; margin-top: 10px;\">Loading questions...<\/span>\n      <\/div>\n      \n      <!-- Dynamic Questionnaire Form -->\n      <form v-else @submit.prevent=\"submitQuestionnaire\" class=\"questionnaire-form\">\n        <div \n          v-for=\"question in questions\" \n          :key=\"question.question_id\" \n          class=\"form-group\"\n        >\n          <label class=\"form-label\">\n            {{ question.question }}\n            \n          <\/label>\n          \n          <!-- Special case: All answers have other=true AND required=true - show inline inputs directly -->\n          <div \n            v-if=\"question.possible_answers.every(a => a.other === true && a.required === true)\"\n            class=\"inline-input-group\"\n          >\n            <!-- Merged Phone Input: If question has phone-related fields, merge them -->\n            <div \n              v-if=\"isPhoneQuestion(question)\"\n              class=\"merged-phone-input-container\"\n            >\n              <div class=\"phone-input-wrapper\">\n                <input \n                  type=\"tel\"\n                  :value=\"getCountryCodeValue(question.question_id)\"\n                  @input=\"updateCountryCodeInput(question.question_id, $event)\"\n                  @keydown.enter.prevent=\"handlePhoneInputEnter\"\n                  placeholder=\"+852\"\n                  class=\"form-input merged-phone-country-code\"\n                  required\n                >\n                <input \n                  type=\"tel\"\n                  :value=\"getMergedPhoneValue(question.question_id)\"\n                  @input=\"updatePhoneNumberInput(question.question_id, $event)\"\n                  @keydown.enter.prevent=\"handlePhoneInputEnter\"\n                  placeholder=\"99999999\"\n                  class=\"form-input merged-phone-input\"\n                  required\n                >\n              <\/div>\n            <\/div>\n            \n            <!-- Regular inline inputs for non-phone questions -->\n            <div \n              v-else\n              v-for=\"answer in question.possible_answers\" \n              :key=\"answer.answer_id\" \n              :class=\"['inline-input-row', { 'inline-input-row-small': answer.small === true }]\"\n            >\n              <input \n                :type=\"getInputType(answer.answer)\"\n                :value=\"questionnaireResponses[question.question_id]?.userInputs[answer.answer_id] || ''\"\n                @input=\"updateUserInputDirect(question.question_id, answer.answer_id, $event.target.value)\"\n                @keydown.enter.prevent=\"handleInputEnter\"\n                :placeholder=\"`${answer.answer}`\"\n                :class=\"['form-input', 'inline-input-field', { 'inline-input-field-small': answer.small === true }]\"\n                required\n              >\n            <\/div>\n          <\/div>\n          \n          <!-- Check if all answers have other=true (allows multiple selection) -->\n          <div \n            v-else-if=\"question.possible_answers.every(a => a.other === true)\"\n            class=\"checkbox-group\"\n          >\n            <label \n              v-for=\"answer in question.possible_answers\" \n              :key=\"answer.answer_id\" \n              class=\"checkbox-label\"\n            >\n              <input \n                type=\"checkbox\" \n                :checked=\"isAnswerSelected(question.question_id, answer.answer_id)\"\n                @change=\"toggleAnswer(question.question_id, answer.answer_id)\"\n              >\n              <span>{{ answer.answer }}<\/span>\n\n            <\/label>\n            \n            <!-- Show input fields for selected answers with other=true -->\n            <div \n              v-for=\"answer in question.possible_answers\" \n              :key=\"`input-${answer.answer_id}`\"\n              v-show=\"isAnswerSelected(question.question_id, answer.answer_id) && answer.other === true\"\n              class=\"answer-input-container\"\n            >\n              <input \n                :type=\"getInputType(answer.answer)\"\n                :value=\"questionnaireResponses[question.question_id]?.userInputs[answer.answer_id] || ''\"\n                @input=\"updateUserInput(question.question_id, answer.answer_id, $event.target.value)\"\n                @keydown.enter.prevent=\"handleInputEnter\"\n                :placeholder=\"`Enter ${answer.answer.toLowerCase()}`\"\n                class=\"form-input answer-input\"\n                :required=\"answer.required === true\"\n              >\n            <\/div>\n          <\/div>\n          \n          <!-- Radio group (single selection) when not all answers have other=true -->\n          <div \n            v-else\n            class=\"radio-group\"\n          >\n            <label \n              v-for=\"answer in question.possible_answers\" \n              :key=\"answer.answer_id\" \n              class=\"radio-label\"\n            >\n              <input \n                type=\"radio\" \n                :checked=\"isAnswerSelected(question.question_id, answer.answer_id)\"\n                @change=\"toggleAnswer(question.question_id, answer.answer_id)\"\n                :name=\"`question-${question.question_id}`\"\n                :required=\"answer.required === true\"\n              >\n              <span>{{ answer.answer }}<\/span>\n            <\/label>\n            \n            <!-- Show input field for selected answer with other=true -->\n            <div \n              v-for=\"answer in question.possible_answers\" \n              :key=\"`input-${answer.answer_id}`\"\n              v-show=\"isAnswerSelected(question.question_id, answer.answer_id) && answer.other === true\"\n              class=\"answer-input-container\"\n            >\n              <input \n                :type=\"getInputType(answer.answer)\"\n                :value=\"questionnaireResponses[question.question_id]?.userInputs[answer.answer_id] || ''\"\n                @input=\"updateUserInput(question.question_id, answer.answer_id, $event.target.value)\"\n                @keydown.enter.prevent=\"handleInputEnter\"\n                :placeholder=\"`Enter ${answer.answer.toLowerCase()}`\"\n                class=\"form-input answer-input\"\n                :required=\"answer.required === true\"\n              >\n            <\/div>\n          <\/div>\n          \n          <!-- Validation error message -->\n          <div \n            v-if=\"validationErrors[question.question_id]\" \n            class=\"validation-error\"\n          >\n            {{ validationErrors[question.question_id] }}\n          <\/div>\n        <\/div>\n\n        <!-- Submit and Skip Buttons -->\n        <div class=\"questionnaire-submit-container\">\n          <!-- <button type=\"button\" class=\"next-button\" @click=\"skipQuestionnaire\" :disabled=\"isSubmittingQuestionnaire\">\n            Skip\n          <\/button> -->\n          <button type=\"submit\" class=\"next-button\" :disabled=\"isSubmittingQuestionnaire\">\n            <span v-if=\"!isSubmittingQuestionnaire\">Submit<\/span>\n            <span v-else>Submitting...<\/span>\n          <\/button>\n          \n        <\/div>\n      <\/form>\n    <\/div>\n  <\/div>\n\n  <!-- Loading circle -->\n  <div class=\"centre-load\" v-if=\"load\">\n    <svg style=\"width: 100px; height: 100px;\" class=\"spinner spinner--circle\" viewBox=\"0 0 66 66\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\">\n      <circle class=\"path\" fill=\"none\" stroke-width=\"6\" stroke-linecap=\"round\" cx=\"33\" cy=\"33\" r=\"30\"><\/circle>\n    <\/svg>\n    <span style=\"display: block; margin-top: 10px;\">Analysing<\/span>\n  <\/div>\n\n  <!-- Report -->\n  <div v-if=\"showReport\" class=\"report-container\" ref=\"reportContainer\">\n    <!-- Photo thumbnails at top -->\n    <div class=\"selection-container\">\n      <div class=\"photo-thumbnails\">\n        <div v-for=\"(image, index) in photoList\">\n          <div class=\"thumbnail-item\">\n            <img v-if=\"image != null\" class=\"thumbnail-img\" :src=\"bucketLink + image\" alt=\"Analysis image\"><\/img>\n          <\/div>\n          <div class=\"thumbnail-label\">{{ frontalPositions[index] }}<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- Hair Health Index Section -->\n    <div class=\"selection-container\">\n      <div class=\"section-header\">\n        <h2 class=\"section-title\">Hair Health Index<\/h2>\n      <\/div>\n      <div class=\"hair-health-content\">\n        <div class=\"gauge-label\" :style=\"getGaugeLabelStyle()\">{{ getHairHealthLabel() }}<\/div>\n        <!-- <div class=\"score-display\">\n          <div class=\"score-number\">{{ 100 - bigHead?.hair_loss_index * 4 }}<\/div>\n          <div class=\"score-total\">\/100 points<\/div>\n        <\/div> -->\n        <div class=\"circular-gauge\">\n          <div class=\"gauge-circle\" :style=\"getGaugeStyle()\">\n            <div class=\"gauge-number\">{{ 100 - bigHead?.hair_loss_index * 4 }}<\/div>\n          <\/div>\n          \n        <\/div>\n        <div class=\"health-description\">\n          <p>{{ bigHeadMessages['en_US'] }}<\/p>\n        <\/div>\n      <\/div>\n    <\/div>\n\n    <!-- Hair Loss Tendency Section -->\n    <div class=\"selection-container\">\n      <div class=\"section-header\">\n        <h2 class=\"section-title\">Hair Loss Tendency<\/h2>\n        <div class=\"legend\">\n          <div class=\"legend-item\">\n            <div class=\"legend-color healthy\"><\/div>\n            <span>Healthy<\/span>\n          <\/div>\n          <div class=\"legend-item\">\n            <div class=\"legend-color mild\"><\/div>\n            <span>Mild<\/span>\n          <\/div>\n          <div class=\"legend-item\">\n            <div class=\"legend-color extreme\"><\/div>\n            <span>Extreme<\/span>\n          <\/div>\n        <\/div>\n      <\/div>\n      <div class=\"tendency-results\">\n        <div v-for=\"result in bigHeadResults\" :key=\"result.title\">\n          <div class=\"tendency-label\">{{ result.title }}<\/div>\n          <div class=\"tendency-item\" >\n            <div class=\"tendency-bar\">\n              <div class=\"bar-fill\" :style=\"getTendencyBarStyle(result.value)\"><\/div>\n            <\/div>\n            <div class=\"tendency-result\" :class=\"getTendencyClass(result.value)\">{{ getBarResult(result.value) }}<\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n\n  <!-- Demo Invitation Section -->\n  <div class=\"selection-container demo-invite-box\">\n    <p class=\"demo-invite-header\"><strong>Unlock Your Full 20+ Symptom Analysis &amp; Revenue Forecast<\/strong><\/p>\n    <p class=\"demo-invite-message\">\n      The full Bloomtastic AI platform can instantly deliver <strong>20+ quantifiable metrics<\/strong> (hair count, thickness, terminal-to-vellus ratio) that turn guesswork into proven revenue. Get the complete analysis and see how <strong>Bloomtastic AI<\/strong> can integrate with your CRM <strong>right now.<\/strong>\n    <\/p>\n    <p class=\"demo-invite-message\">\n      Stop using opinions. Start using <strong>data<\/strong>. Get a personalized 15-minute demo to see how Bloomtastic AI is trusted by trichologists to <strong>increase treatment acceptance<\/strong> and <strong>improve client retention.<\/strong>\n    <\/p>\n    <button class=\"demo-invite-button\" @click=\"redirectToDemo\">\n      <strong>Unlock Full Report &amp; Book 15-Min Live Demo<\/strong>\n    <\/button>\n    <p class=\"demo-invite-note\">\n      <em>We will personally walk you through your specific report to show you exactly how Bloomtastic enhances your business. Limited slots available this week.<\/em>\n    <\/p>\n  <\/div>\n\n  <!-- Learn More Section -->\n  <div class=\"selection-container learn-more-box\">\n    <img data-recalc-dims=\"1\" decoding=\"async\"\n      class=\"learn-more-image\"\n      src=\"https:\/\/i0.wp.com\/haircosys-images.oss-ap-southeast-1.aliyuncs.com\/wordpress_cta_image.png?w=800&#038;ssl=1\"\n      alt=\"Bloomtastic AI overview\"\n    \/>\n    <div class=\"learn-more-content\">\n      <p class=\"demo-invite-message\" style=\"text-align: center;\">\n        Adopt the only AI platform that provides clinical precision while accelerating your hair care business growth and earning your patients\u2019 unwavering trust!\n      <\/p>\n      <button class=\"demo-invite-button learn-more-button\" @click=\"openBloomtasticPlatform\">Explore the Bloomtastic B2B Platform<\/button>\n    <\/div>\n  <\/div>\n\n    <!-- Call to Action and Back Button for Results -->\n    <div class=\"results-actions-container\">\n      <button class=\"cta-button\" @click=\"redirectToMoreInfo\">\n        <span>Subscribe to our newsletter!<\/span>\n      <\/button>\n      <button class=\"back-button-results\" @click=\"resetToCameraSelection\">\n        <span>Back to Hair Analysis<\/span>\n      <\/button>\n    <\/div>\n\n  <\/div>\n  \n\n<\/div>\n\n\n\n<!-- <footer class=\"analysis-footer\">\n    <div class=\"analysis-footer-content\">\n      <p class=\"analysis-footer-text\">\n        Ready to turn this data into a revenue driver? The world's leading trichologists trust Bloomtastic AI to standardize care and boost sales. Click below to explore the full B2B platform.\n      <\/p>\n      <button class=\"analysis-footer-button\" @click=\"openBloomtasticPlatform\">\n        Explore the Bloomtastic B2B Platform\n      <\/button>\n    <\/div>\n  <\/footer> -->\n<\/div>\n\n<style>\nbody {\n  background-color: #FFFDFA;\n  height: 100%;\n}\n\n.step-header {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  margin-bottom: 10px;\n  position: relative;\n}\n\n.step-header-mobile {\n  display: none;\n  margin-bottom: 10px;\n  text-align: center;\n}\n\n#camera-page .back-button-step2 {\n  background-color: transparent;\n  color: #FFB6A7;\n  \/* background-color: #FFB6A7;\n  color: white; *\/\n  border: none;\n  border-radius: 50%;\n  padding: 0;\n  width: 36px;\n  height: 36px;\n  font-size: 36px;\n  \/* font-weight: bold; *\/\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  display: inline-flex;\n  align-items: center;\n  justify-content: center;\n  position: absolute;\n  \/* right: 0; *\/\n  left: 0;\n}\n\n#camera-page .back-button-step2:hover {\n  background-color: #FF9A85;\n  transform: translateY(-1px);\n}\n\n#camera-page .back-button-step2:active {\n  transform: translateY(0);\n}\n\n.back-button-step2-mobile {\n  background-color: #FFB6A7;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 8px 16px !important;\n  font-size: 14px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n}\n\n.back-button-step2-mobile:hover {\n  background-color: #FF9A85;\n  transform: translateY(-1px);\n}\n\n.back-button-step2-mobile:active {\n  transform: translateY(0);\n}\n\n.results-actions-container {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 20px;\n  margin-top: 40px;\n  padding-bottom: 40px;\n}\n\n.cta-button {\n  background-color: #4CAF50;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 15px 30px !important;\n  font-size: 16px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  width: 250px;\n}\n\n.cta-button:hover {\n  background-color: #45a049;\n  transform: translateY(-1px);\n}\n\n.cta-button:active {\n  transform: translateY(0);\n}\n\n.back-button-results {\n  background-color: #FFB6A7;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 12px 24px !important;\n  font-size: 14px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  width: 250px;\n}\n\n.back-button-results:hover {\n  background-color: #FF9A85;\n  transform: translateY(-1px);\n}\n\n.back-button-results:active {\n  transform: translateY(0);\n}\n\n.privacy-checkbox-container {\n  margin: 20px 0;\n  padding: 15px;\n  background-color: #f9f9f9;\n  border-radius: 8px;\n  border: 1px solid #e0e0e0;\n}\n\n.privacy-checkbox-label {\n  display: flex;\n  align-items: flex-start;\n  gap: 10px;\n  cursor: pointer;\n  line-height: 1.5;\n}\n\n#camera-page .privacy-checkbox {\n  margin: 0;\n  accent-color: #FFB6A7;\n  margin-top: 10px;\n  width: 20px; \/* Set explicit width *\/\n  height: 20px; \/* Set explicit height *\/\n  -webkit-appearance: none; \/* Remove default iOS styling *\/\n  appearance: none; \/* Remove default styling for other browsers *\/\n  background-color: #fff; \/* Background color for the checkbox *\/\n  border: 2px solid #FFB6A7; \/* Border to match your design *\/\n  border-radius: 3px; \/* Optional: slight rounding *\/\n  cursor: pointer;\n  position: relative;\n  padding: 8px;\n}\n\n\/* Style for checked state *\/\n#camera-page .privacy-checkbox:checked {\n  background-color: #FFB6A7; \/* Fill color when checked *\/\n}\n\n\/* Add checkmark for checked state *\/\n#camera-page .privacy-checkbox:checked::after {\n  content: '\\2713'; \/* Unicode for checkmark *\/\n  color: #fff; \/* Checkmark color *\/\n  font-size: 14px;\n  position: absolute;\n  top: 50%;\n  left: 50%;\n  transform: translate(-50%, -50%);\n}\n\n\n.privacy-text {\n  font-size: 14px;\n  color: #333;\n  flex: 1;\n}\n\n.terms-link {\n  color: #FF9800;\n  text-decoration: underline;\n  cursor: pointer;\n  transition: color 0.3s ease;\n  display: inline;\n}\n\n.terms-link:hover {\n  color: #F57C00;\n  text-decoration: underline;\n}\n\n.next-button-container {\n  display: flex;\n  justify-content: center;\n  margin-top: 20px;\n}\n\n.next-button {\n  background-color: #FFB6A7;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 12px 30px;\n  font-size: 16px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  min-width: 120px;\n}\n\n.next-button:hover:not(.disabled) {\n  background-color: #FF9A85;\n  transform: translateY(-1px);\n}\n\n.next-button:active:not(.disabled) {\n  transform: translateY(0);\n}\n\n.next-button.disabled {\n  background-color: #D9D9D9;\n  color: #999;\n  cursor: not-allowed;\n  transform: none;\n}\n\n.terms-modal {\n  position: fixed;\n  z-index: 10000;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0, 0, 0, 0.5);\n}\n\n.terms-modal-wrapper {\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.terms-modal-container {\n  width: 80%;\n  max-width: none;\n  max-height: 80vh;\n  margin: 5% auto;\n  padding: 30px;\n  background-color: #fff;\n  border-radius: 12px;\n  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);\n  display: flex;\n  flex-direction: column;\n  overflow-y: auto;\n}\n\n.terms-modal-title {\n  font-size: 1.8rem;\n  font-weight: bold;\n  color: #333;\n  margin: 0 0 10px 0;\n  text-align: center;\n}\n\n.terms-modal-subtitle {\n  font-size: 1.2rem;\n  font-weight: 600;\n  color: #666;\n  margin: 0 0 20px 0;\n  text-align: left;\n}\n\n.terms-modal-content {\n  flex: 1;\n  margin-bottom: 20px;\n}\n\n.terms-modal-content p {\n  font-size: 11px;\n  line-height: 1.6;\n  color: #555;\n  margin: 0 0 15px 0;\n  text-align: justify;\n}\n\n.terms-close-button {\n  background-color: #FFB6A7;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 12px 30px;\n  font-size: 16px;\n  font-weight: bold;\n  cursor: pointer;\n  transition: background-color 0.3s ease;\n  align-self: center;\n  min-width: 100px;\n}\n\n.terms-close-button:hover {\n  background-color: #FF9A85;\n}\n\n.canvasScale {\n  -webkit-transform: scaleX(-1);\n  transform: scaleX(-1);\n}\n\n.unconfirmed-image {\n  width: 100%;\n  height: auto;\n  display: block;\n  max-width: 100%;\n  max-height: 80vh; \/* Reduce height by 20% (from 100vh to 80vh) *\/\n  object-fit: contain; \/* Maintain aspect ratio *\/\n}\n\n.hidden-canvas {\n  display: none !important;\n  visibility: hidden !important;\n  position: absolute !important;\n  left: -9999px !important;\n  top: -9999px !important;\n  width: 1px !important;\n  height: 1px !important;\n  opacity: 0 !important;\n}\nvideo {\n  -webkit-transform: scaleX(-1);\n  transform: scaleX(-1);\n}\n.error-modal {\n  position: fixed;\n  z-index: 10001;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 100%;\n  background-color: rgba(0, 0, 0, 0.5);\n}\n.error-modal-container {\n  width: 30%;\n  height: 20%;\n  margin: 150px auto;\n  padding: 20px 30px;\n  background-color: #fff;\n  border-radius: 2px;\n  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);\n  display: flex;\n  flex-direction: column;\n  align-items: flex-end;\n  justify-content: space-evenly;\n}\n.error-modal-container p, .error-modal-container span {\n  width: 100%;\n  text-align: start;\n  line-height: 1.5;\n}\n.error-modal-wrapper {\n  height: 100%;\n}\n.initial-section {\n  display: flex;\n  height: auto;\n  justify-content: center;\n  flex-direction: column;\n  align-items: center;\n  padding: 20px;\n}\n.camera-selection-header {\n  text-align: center;\n  margin-bottom: 30px;\n}\n.analysis-title {\n  font-size: 2.5rem;\n  font-weight: bold;\n  text-align: center;\n  margin-top: 15px;\n  margin-bottom: 10px;\n  color: #333;\n}\n.analysis-description {\n  font-size: 1.1rem;\n  color: #666;\n  margin: 0;\n}\n.selection-container {\n  background-color: #fcf6f3;\n  border-radius: 20px;\n  padding: 30px;\n  \/* text-align: center; *\/\n  \/* min-width: 400px; *\/\n  width: 800px;\n}\n.demo-invite-box {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  gap: 20px;\n  text-align: center;\n}\n.demo-invite-header {\n  font-size: 1.3rem;\n  color: #2f2f2f;\n  margin: 0;\n}\n.demo-invite-message {\n  font-size: 1.15rem;\n  line-height: 1.6;\n  color: #333;\n  margin: 0;\n}\n.demo-invite-button {\n  background-color: #ff8f75;\n  color: #fff;\n  border: none;\n  border-radius: 12px;\n  padding: 14px 36px;\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  font-size: 1rem;\n}\n.demo-invite-button:hover {\n  background-color: #ff7556;\n  transform: translateY(-2px);\n}\n.demo-invite-note {\n  font-size: 0.9rem;\n  color: #666;\n  margin: 0;\n}\n.learn-more-box {\n  display: flex;\n  align-items: center;\n  gap: 30px;\n}\n.learn-more-image {\n  width: 40%;\n  max-width: 320px;\n  height: auto;\n  border-radius: 16px;\n}\n.learn-more-content {\n  display: flex;\n  align-items: center;\n  flex-direction: column;\n}\n.learn-more-content .demo-invite-message {\n  margin-bottom: 16px;\n}\n.learn-more-button {\n  padding: 14px 36px;\n}\n.step-title {\n  font-size: 1.5rem;\n  font-weight: bold;\n  margin-block: 0px;\n  color: #333;\n  text-align: center;\n}\n.step-description {\n  font-size: 0.8rem;\n  color: #666;\n  margin-bottom: 20px;\n  text-align: left;\n}\n.camera-dropdown {\n  height: 45px;\n  border-radius: 12px;\n  border: 2px solid #ddd;\n  padding: 0 15px;\n  font-size: 16px;\n  background-color: white;\n  color: #333;\n  cursor: pointer;\n  outline: none;\n  transition: border-color 0.3s ease;\n}\n.camera-select-row {\n  display: grid;\n  grid-template-columns: 1fr auto;\n  gap: 10px;\n  align-items: start;\n}\n#camera-page .refresh-icon-btn {\n  height: 45px;\n  width: 45px;\n  border-radius: 50%;\n  padding: 0px;\n  \/* color: #FFB6A7; *\/\n  background-color: #FFB6A7;\n  color: #fff;\n  border: none;\n  font-size: 20px;\n  line-height: 1;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  cursor: pointer;\n}\n.refresh-icon-btn:hover {\n  background-color: #FF9A85;\n}\n.camera-dropdown:focus {\n  border-color: #FEB6A6;\n}\n.camera-dropdown option:disabled {\n  color: #999;\n  font-style: italic;\n}\n.camera-controls {\n  margin-bottom: 20px;\n  width: min-content;\n  margin-left: auto;\n  margin-right: auto;\n  min-width: 640px;\n}\n.camera-preview-container {\n  width: min-content;\n  margin: auto;\n  min-width: 640px;\n  position: relative;\n  margin-bottom: 20px; \/* space below preview before position buttons *\/\n  max-height: 80vh; \/* Reduce height by 20% (from 100vh to 80vh) *\/\n}\n.finish-button-row {\n  justify-content: center;\n  margin-top: 20px;\n}\n.position-thumbnail {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  cursor: pointer;\n  \/* margin-right: 20px; *\/\n  margin-bottom: 15px;\n  transition: transform 0.2s ease;\n}\n\/* .position-thumbnail:hover {\n  transform: scale(1.05);\n} *\/\n.thumbnail-container {\n  width: 144px;\n  height: 77px; \/* Reduce height by 20% (from 96px to 77px) *\/\n  border-radius: 12px;\n  overflow: hidden;\n  border: 2px solid #ddd;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: #f5f5f5;\n}\n.thumbnail-image {\n  width: 100%;\n  height: 100%;\n  object-fit: cover;\n  \/* Ensure images display properly on mobile *\/\n  display: block;\n  -webkit-backface-visibility: hidden;\n  backface-visibility: hidden;\n  transform: translateZ(0);\n}\n.placeholder-image {\n  width: 100%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: #f0f0f0;\n}\n.placeholder-icon {\n  font-size: 36px;\n  color: #FFB6A7;\n  font-weight: bold;\n}\n.position-name {\n  margin-top: 8px;\n  font-size: 12px;\n  color: #666;\n  text-align: center;\n}\n.position-thumbnail.activePos .thumbnail-container {\n  border-color: #ddd; \/* keep container border unchanged when active *\/\n  border-width: 2px;\n}\n.position-thumbnail.activePos .position-name {\n  color: #333;\n  font-weight: bold;\n  background-color: #FEB6A6;\n  width: 100%;\n  color: white;\n  border-radius: 6px;\n  padding: 2px 8px;\n}\n.initial-section button {\n  \/* background-color: white; *\/\n  \/* border: black 1px solid; *\/\n  position: relative;\n  z-index: 1;\n  \/* color: black; *\/\n  display: flex;\n  justify-content: center;\n  flex-direction: row;\n  align-items: center;\n}\n.frontal-hair-index {\n  margin-top: 20px;\n  margin-bottom: 20px;\n  position: relative;\n  text-align: center;\n}\n.progress-hair-index {\n  height: 25px;\n  border-radius: 7px;\n}\n.progress-loader-hair-index {\n  width: 60%;\n  background: rgba(89, 89, 96, 0.253);\n  height: 25px;\n  border-radius: 7px;\n  display: inline-block;\n}\n.hair-index {\n  margin-left: auto;\n  margin-right: auto;\n  width: clamp(200px, 80%, 550px);\n  display: flex;\n  justify-content: center;\n  margin-bottom: 20px;\n}\n.centre-load {\n  display: flex;\n  align-content: center;\n  justify-content: center;\n  flex-wrap: wrap;\n  flex-direction: column;\n  height: 50vh;\n  text-align: center;\n}\n.pos-row {\n  display: flex;\n  margin-bottom: 10px;\n  justify-content: space-between;\n  width: min-content;\n  margin-left: auto;\n  margin-right: auto;\n  min-width: 640px;\n}\n.ai-body {\n  padding-top: 10px;\n  padding-left: 20px;\n  padding-right: 20px;\n  width: 100%;\n  align-items: center;\n  display: flex;\n  justify-content: center;\n}\n.camera-guideline {\n  position: absolute;\n  left: 50%;\n  top: 50%;\n  transform: translate(-50%, -50%);\n  z-index: 9;\n}\n.button-row {\n  display: flex;\n  justify-content: space-between;\n  height: 40px;\n  margin-bottom: 30px;\n}\n\n\n#camera-page .cam-confirmation-buttons {\n  width: clamp(80px, 40%, 200px);\n  border-radius: 8px;\n  background-color: #FFB6A7;\n  color: white;\n  border: 1px solid #FFB6A7;\n  display: flex;\n  justify-content: center;\n  flex-direction: row;\n  align-items: center;\n}\n#camera-page .pos-button {\n  background-color: white;\n  color: #FFB6A7;\n  border: 1px solid #FFB6A7;\n  margin-right: 20px;\n  margin-bottom: 15px;\n  height: 25px;\n  \n  display: flex;\n  justify-content: center;\n  flex-direction: row;\n  align-items: center;\n}\n#camera-page .blackButtonDefault {\n  color: white;\n  background-color: #FFB6A7;\n  \n  display: flex;\n  justify-content: center;\n  flex-direction: row;\n  align-items: center;\n}\n\/* Removed empty CSS rule for .activePos *\/\n#camera-page .unfinishedButton {\n  background-color: #D9D9D9;\n  border: 1px solid #D9D9D9;\n  color: white;\n  cursor: not-allowed;\n}\n#camera-page .finishedButton {\n  background-color: #FFB6A7;\n  border: 1px solid #FFB6A7;\n  color: white;\n}\n#camera-page button {\n  background-color: #FFB6A7;\n  \/* border-radius: 8px; *\/\n  \/* cursor: pointer;\n  padding: .5rem 1rem;\n  background-color: white;\n  border: 2px solid #FFB6A7;\n  color: #FFB6A7;\n  font-weight: bold; *\/\n  \/* margin-right: 20px; *\/\n}\n.bar {\n  position: relative;\n  width: 100%;\n  height: 100%;\n  max-height: 48px;\n}\n.barIndicatorColoredStartValueIs1 {\n  height: 25px;\n  width: 100%;\n  border-radius: 7px;\n}\n.barIndicatorColoredStartValueGreaterThan1 {\n  height: 25px;\n  width: 100%;\n  border-top-left-radius: 7px;\n  border-bottom-left-radius: 7px;\n}\n.barIndicatorColoredMiddle {\n  height: 25px;\n  width: 100%;\n}\n.barIndicatorColoredEnd {\n  height: 25px;\n  width: 100%;\n  border-top-right-radius: 7px;\n  border-bottom-right-radius: 7px;\n}\n.barIndicatorNormal {\n  height: 25px;\n  color: white;\n  width: 100%;\n}\n.barDivider {\n  text-align: end;\n  display: grid;\n  grid-template-columns: 20% 20% 20% 20% 20%;\n  position: absolute;\n  width: 100%;\n  left: 0;\n  line-height: 25px;\n}\n.textLeft {\n  color: black;\n  display: inline-block;\n  text-align: start;\n  margin-top: auto;\n  margin-bottom: auto;\n}\n.scoreRight {\n  align-self: center;\n  width: 70px;\n  padding-left: 5px;\n}\n.progress-loader {\n  height: 25px;\n  transform: translate(0, 12.5px);\n  background: rgba(89, 89, 96, 0.253);\n  border-radius: 7px;\n}\n.frontal-progress-bar {\n  display: grid;\n  grid-template-columns: 20fr 80fr;\n  min-height: 48px;\n}\n.progress-bar-plus-text {\n  display: grid;\n  grid-template-columns: 5fr 60fr 5fr 10fr;\n  align-items: center;\n}\n.progress-bar-box {\n  margin-bottom: 25px;\n  margin-left: auto;\n  margin-right: auto;\n  width: clamp(100px, 80%, 800px);\n}\n.column {\n  float: left;\n  width: clamp(200px, 15%, 300px);\n  padding: 5px;\n}\n.row {\n  display: flex;\n  margin: auto;\n  justify-content: center;\n  align-items: center;\n}\n.frontal-img {\n  width: 100%;\n}\n\n\/* SCSS converted to CSS *\/\n.spinner {\n  animation: circle-rotator 1.4s linear infinite;\n}\n.spinner * {\n  line-height: 0;\n  box-sizing: border-box;\n}\n@keyframes circle-rotator {\n  0% {\n    transform: rotate(0deg);\n  }\n  100% {\n    transform: rotate(270deg);\n  }\n}\n.path {\n  stroke-dasharray: 187;\n  stroke-dashoffset: 0;\n  transform-origin: center;\n  animation: circle-dash 1.4s ease-in-out infinite, circle-colors 5.6s ease-in-out infinite;\n}\n@keyframes circle-colors {\n  0%, 25%, 50%, 75%, 100% {\n      \n    stroke: #FFB6A7;\n    \/*stroke: #FF9234;*\/\n  }\n}\n@keyframes circle-dash {\n  0% {\n    stroke-dashoffset: 187;\n  }\n  50% {\n    stroke-dashoffset: 46.75;\n    transform: rotate(135deg);\n  }\n  100% {\n    stroke-dashoffset: 187;\n    transform: rotate(450deg);\n  }\n}\n\n\/* Report Styles *\/\n.report-container {\n  padding: 40px 20px;\n  max-width: 1200px;\n  margin: 0 auto;\n  background-color: #FFFDFA;\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n  justify-content: center;\n  gap: 40px;\n}\n\n.photo-thumbnails {\n  display: flex;\n  justify-content: space-around;\n  gap: 10px;\n  \/* margin-bottom: 30px; *\/\n}\n\n.thumbnail-label {\n  text-align: center;\n}\n\n.thumbnail-item {\n  width: 240px;\n  \/* height: 160px;  Remove fixed height to avoid clipping *\/\n  border-radius: 12px;\n  overflow: visible; \/* Do not clip; allow image to fully fit within bounds *\/\n  border: 2px solid #ddd;\n  margin-bottom: 20px;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  background-color: #fff;\n}\n\n.thumbnail-img {\n  width: auto;\n  height: auto;\n  max-width: 100%;\n  max-height: 300px; \/* ensure it stays within parent vertically *\/\n  object-fit: contain; \/* maintain aspect ratio without clipping *\/\n  display: block;\n  border-radius: 10px !important;\n}\n\n\/* Removed - now using selection-container class *\/\n\n.section-header {\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  margin-bottom: 20px;\n}\n\n.section-title {\n  font-size: 1.5rem;\n  font-weight: bold;\n  color: #333;\n  margin: 0;\n}\n\n.hair-health-content {\n  display: grid;\n  grid-template-columns: 1fr 1fr 2fr;\n  gap: 30px;\n  align-items: center;\n}\n\n.score-display {\n  text-align: center;\n}\n\n.score-number {\n  font-size: 3rem;\n  font-weight: bold;\n  color: #333;\n  line-height: 1;\n}\n\n.score-total {\n  font-size: 1.2rem;\n  color: #666;\n  margin-top: 5px;\n}\n\n.circular-gauge {\n  display: flex;\n  flex-direction: column;\n  align-items: center;\n}\n\n.gauge-circle {\n  width: 120px;\n  height: 120px;\n  border-radius: 50%;\n  background: conic-gradient(var(--gauge-color, #4CAF50) 0deg, var(--gauge-color, #4CAF50) var(--gauge-angle, 0deg), #e0e0e0 var(--gauge-angle, 0deg));\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  position: relative;\n}\n\n.gauge-circle::before {\n  content: '';\n  position: absolute;\n  width: 104px;\n  height: 104px;\n  background-color: white;\n  border-radius: 50%;\n}\n\n.gauge-number {\n  font-size: 1.8rem;\n  font-weight: bold;\n  color: #333;\n  z-index: 1;\n}\n\n.gauge-label {\n  \/* margin-top: 10px; *\/\n  font-size: 2.5rem;\n  font-weight: bold;\n  text-align: center;\n  color: #666;\n  font-weight: 500;\n}\n\n.health-description {\n  color: #666;\n  line-height: 1.6;\n}\n\n.health-description p {\n  margin: 0 0 10px 0;\n}\n\n.legend {\n  display: flex;\n  gap: 15px;\n}\n\n.legend-item {\n  display: flex;\n  align-items: center;\n  gap: 5px;\n  font-size: 0.9rem;\n  color: #666;\n}\n\n.legend-color {\n  width: 12px;\n  height: 12px;\n  border-radius: 2px;\n}\n\n.legend-color.healthy {\n  background-color: #4CAF50;\n}\n\n.legend-color.mild {\n  background-color: #FF9800;\n}\n\n.legend-color.extreme {\n  background-color: #F44336;\n}\n\n.tendency-results {\n  display: flex;\n  flex-direction: column;\n  gap: 15px;\n}\n\n.tendency-item {\n  display: grid;\n  grid-template-columns: 1fr 100px;\n  gap: 15px;\n  align-items: center;\n}\n\n.tendency-bar {\n  height: 8px;\n  background-color: #e0e0e0;\n  border-radius: 4px;\n  overflow: hidden;\n  width: 100%;\n}\n\n.bar-fill {\n  height: 100%;\n  border-radius: 4px;\n  transition: width 0.3s ease;\n}\n\n.tendency-label {\n  font-size: 0.9rem;\n  color: #666;\n}\n\n.tendency-result {\n  font-size: 0.9rem;\n  font-weight: bold;\n  text-align: left;\n}\n\n.tendency-result.healthy {\n  color: #4CAF50;\n}\n\n.tendency-result.mild {\n  color: #FF9800;\n}\n\n.tendency-result.extreme {\n  color: #F44336;\n}\n\n\/* Navigation buttons removed *\/\n\n\/* Mobile Responsive Styles *\/\n@media screen and (max-width: 768px) {\n  \/* Touch-friendly interactions *\/\n  button, .position-thumbnail, .terms-link {\n    -webkit-tap-highlight-color: rgba(0, 0, 0, 0.1);\n    touch-action: manipulation;\n  }\n  \n  \/* Prevent zoom on input focus *\/\n  input, select, textarea {\n    font-size: 16px;\n  }\n  \/* Main container adjustments *\/\n  #camera-page {\n    padding: 10px;\n    padding-bottom: 180px;\n  }\n  \n  .analysis-title {\n    font-size: 2rem;\n    margin-top: 10px;\n    margin-bottom: 15px;\n  }\n  \n  .analysis-description {\n    font-size: 1rem;\n    margin-bottom: 20px;\n  }\n  \n  \/* Selection container mobile adjustments *\/\n  .selection-container {\n    width: 100%;\n    max-width: 100%;\n    padding: 20px;\n    margin: 0;\n    border-radius: 15px;\n  }\n  .demo-invite-box {\n    gap: 16px;\n  }\n  .demo-invite-message {\n    font-size: 1rem;\n  }\n  .learn-more-box {\n    flex-direction: column;\n  }\n  .learn-more-image {\n    width: 100%;\n    max-width: 280px;\n  }\n  .learn-more-content {\n    width: 100%;\n    text-align: center;\n  }\n  .learn-more-button {\n    width: 100%;\n  }\n  \n  .step-title {\n    font-size: 1.3rem;\n    \/* margin-bottom: 8px; *\/\n    text-align: center;\n  }\n  \n  .step-description {\n    \/* font-size: 0.65rem; *\/\n    margin-bottom: 15px;\n  }\n  \n  \/* Camera dropdown mobile *\/\n  .camera-dropdown {\n    width: 100%;\n    height: 50px;\n    font-size: 16px;\n    margin-bottom: 15px;\n  }\n  \n  \/* Privacy checkbox mobile *\/\n  .privacy-checkbox-container {\n    margin: 15px 0;\n    padding: 12px;\n  }\n  \n  .privacy-text {\n    font-size: 13px;\n  }\n  \n  \/* Next button mobile *\/\n  #camera-page.next-button {\n    width: 100%;\n    max-width: 200px;\n    height: 45px;\n    font-size: 16px;\n  }\n  \n  \/* Camera section mobile adjustments *\/\n  .ai-body {\n    padding: 10px;\n  }\n  \n  .camera-preview-container {\n    min-width: 100%;\n    width: 100%;\n    max-width: 100%;\n    position: relative;\n    margin-bottom: 16px; \/* ensure separation on mobile too *\/\n  }\n  \n  \/* Ensure media stays within container *\/\n  #camera,\n  .camera,\n  #canvas,\n  .unconfirmed-image {\n    max-width: 100%;\n    height: auto;\n    display: block;\n    max-height: 80vh; \/* Reduce height by 20% to match container *\/\n  }\n  \n  .camera-controls {\n    min-width: 100%;\n    width: 100%;\n  }\n  \n  \/* Position thumbnails mobile layout *\/\n  .pos-row {\n    min-width: 100%;\n    width: 100%;\n    flex-direction: row;\n    gap: 10px;\n    margin-bottom: 15px;\n    overflow-x: auto;\n    -webkit-overflow-scrolling: touch;\n    scrollbar-width: none; \/* Firefox *\/\n  }\n  .pos-row::-webkit-scrollbar { display: none; }\n  \n  .position-thumbnail {\n    flex: 0 0 auto;\n    width: 144px;\n    margin: 0 6px 10px 0;\n  }\n  \n  .thumbnail-container {\n    width: 144px;\n    height: 77px; \/* Reduce height by 20% (from 96px to 77px) *\/\n    margin: 0 auto;\n    \/* Ensure proper image display on mobile *\/\n    background-color: #f5f5f5;\n    overflow: hidden;\n  }\n  \n  .position-name {\n    font-size: 14px;\n    margin-top: 5px;\n  }\n  \n  \/* Camera buttons mobile *\/\n  .button-row {\n    \/* flex-direction: column;\n    gap: 10px; *\/\n    height: auto;\n    margin-bottom: 20px;\n  }\n  \n  #camera-page .cam-confirmation-buttons {\n    \/* width: 100%; *\/\n    max-width: 200px;\n    height: 45px;\n    margin: 0 auto;\n  }\n  \n  \/* Report section mobile adjustments *\/\n  .report-container {\n    padding: 20px 10px;\n    gap: 30px;\n  }\n  \n  .photo-thumbnails {\n    flex-direction: column;\n    gap: 15px;\n    align-items: center;\n  }\n  \n  .thumbnail-item {\n    width: 100%;\n    max-width: 300px;\n    \/* height: 200px; *\/\n  }\n  \n  .thumbnail-label {\n    font-size: 14px;\n    margin-top: 5px;\n  }\n  \n  \/* Hair health section mobile *\/\n  .hair-health-content {\n    grid-template-columns: 1fr;\n    gap: 20px;\n    text-align: center;\n  }\n  \n  .gauge-label {\n    font-size: 2rem;\n    order: 1;\n  }\n  \n  .circular-gauge {\n    order: 2;\n  }\n  \n  .health-description {\n    order: 3;\n    text-align: left;\n  }\n  \n  .gauge-circle {\n    width: 100px;\n    height: 100px;\n  }\n  \n  .gauge-circle::before {\n    width: 88px;\n    height: 88px;\n  }\n  \n  .gauge-number {\n    font-size: 1.5rem;\n  }\n  \n  \/* Tendency results mobile *\/\n  .tendency-results {\n    gap: 12px;\n  }\n  \n  .tendency-item {\n    grid-template-columns: 1fr;\n    gap: 8px;\n  }\n  \n  .tendency-label {\n    font-size: 0.85rem;\n    text-align: left;\n  }\n  \n  .tendency-result {\n    font-size: 0.85rem;\n    text-align: left;\n  }\n  \n  \/* Legend mobile *\/\n  .legend {\n    flex-wrap: wrap;\n    gap: 10px;\n    justify-content: start;\n  }\n  \n  .legend-item {\n    font-size: 0.8rem;\n  }\n  \n  .results-actions-container {\n    gap: 15px;\n    margin-top: 30px;\n    padding-bottom: 30px;\n  }\n\n  .cta-button {\n    width: 100%;\n    max-width: 250px;\n    height: auto;\n    min-height: 50px;\n    font-size: 14px;\n  }\n\n  .back-button-results {\n    width: 100%;\n    max-width: 250px;\n    height: auto;\n    min-height: 45px;\n    font-size: 14px;\n  }\n\n  \/* Back buttons mobile *\/\n  \/* .step-header {\n    display: block;\n    text-align: center;\n  } *\/\n\/*   \n  #camera-page .back-button-step2 {\n    display: none; \n  } *\/\n  \n  .step-header-mobile {\n    display: block; \/* Show mobile back button *\/\n  }\n  \/* Specific tweaks *\/\n  #camera-page.back-button-step2 {\n    max-width: 100px; \/* reduce width on mobile *\/\n  }\n  #camera-page .back-button-results {\n    \/* height: 36px; allow wrapping *\/\n    padding: 0px;\n    padding-block: 15px;\n    \/* min-height: 56px; minimum touch target *\/\n    white-space: normal; \/* enable text wrapping *\/\n    overflow-wrap: anywhere; \/* break long words if needed *\/\n    word-break: break-word;\n    line-height: 1.2;\n    text-align: center;\n  }\n  \n  \n  \/* Modal adjustments for mobile *\/\n  .terms-modal-container {\n    width: 95%;\n    max-width: none;\n    margin: 2% auto;\n    padding: 20px;\n    max-height: 90vh;\n  }\n  \n  .terms-modal-title {\n    font-size: 1.5rem;\n  }\n  \n  .terms-modal-subtitle {\n    font-size: 1.1rem;\n  }\n  \n  .terms-modal-content p {\n    font-size: 10px;\n    line-height: 1.5;\n  }\n  \n  .terms-close-button {\n    width: 100%;\n    max-width: 150px;\n    height: 45px;\n  }\n  \n  .error-modal-container {\n    width: 90%;\n    height: auto;\n    min-height: 25%;\n    margin: 20% auto;\n    padding: 20px;\n  }\n  \n  \/* Loading spinner mobile *\/\n  .centre-load {\n    height: 40vh;\n  }\n  \n  .centre-load svg {\n    width: 80px;\n    height: 80px;\n  }\n  \n  .centre-load span {\n    font-size: 16px;\n  }\n}\n\n\/* Extra small mobile devices *\/\n@media screen and (max-width: 480px) {\n  .analysis-title {\n    font-size: 1.8rem;\n  }\n  .selection-container {\n    padding: 15px;\n  }\n  \n  .step-title {\n    font-size: 1.2rem;\n    text-align: center;\n  }\n  \n  .thumbnail-container {\n    height: 80px; \/* Reduce height by 20% (from 100px to 80px) *\/\n  }\n  \n  .gauge-circle {\n    width: 90px;\n    height: 90px;\n  }\n  \n  .gauge-circle::before {\n    width: 78px;\n    height: 78px;\n  }\n  \n  .gauge-number {\n    font-size: 1.3rem;\n  }\n  \n  .gauge-label {\n    font-size: 1.8rem;\n  }\n}\n\n\/* Questionnaire Styles *\/\n.questionnaire-section {\n  display: flex;\n  justify-content: center;\n  flex-direction: column;\n  align-items: center;\n  padding: 20px;\n}\n\n.questionnaire-form {\n  margin-top: 20px;\n}\n\n.form-group {\n  margin-bottom: 25px;\n}\n\n.form-label {\n  display: block;\n  font-size: 1rem;\n  font-weight: 600;\n  color: #333;\n  margin-bottom: 10px;\n}\n\n.form-input {\n  width: 100%;\n  padding: 12px 15px;\n  border: 2px solid #ddd !important;\n  border-radius: 8px;\n  font-size: 16px;\n  background-color: white;\n  color: #333 !important;\n  transition: border-color 0.3s ease;\n  box-sizing: border-box;\n}\n\n.form-input:focus {\n  outline: none;\n  border-color: #FFB6A7;\n}\n\n.name-inputs {\n  display: grid;\n  grid-template-columns: 1fr 1fr;\n  gap: 15px;\n}\n\n.phone-inputs {\n  display: grid;\n  grid-template-columns: auto 1fr;\n  gap: 15px;\n}\n\n.phone-extension {\n  width: 120px;\n}\n\n.phone-number {\n  flex: 1;\n}\n\n.merged-phone-input-container {\n  width: 100%;\n}\n\n.phone-input-wrapper {\n  display: flex;\n  align-items: stretch;\n  border: 2px solid #ddd;\n  border-radius: 8px;\n  background-color: white;\n  transition: border-color 0.3s ease;\n  overflow: hidden;\n}\n\n.phone-input-wrapper:focus-within {\n  border-color: #FFB6A7;\n}\n\n#camera-page .merged-phone-country-code {\n  flex: 0 0 auto;\n  width: 120px;\n  border: none !important;\n  border-right: 2px solid #ddd;\n  outline: none;\n  padding: 12px 15px;\n  font-size: 16px;\n  color: #333;\n  background-color: transparent;\n  border-radius: 0;\n}\n\n#camera-page .merged-phone-country-code::placeholder {\n  color: #999;\n}\n\n#camera-page .merged-phone-input {\n  flex: 1;\n\n  outline: none;\n  padding: 12px 15px;\n  font-size: 16px;\n  color: #333;\n  background-color: transparent;\n  border-radius: 0;\n  border-right: none !important;\n  border-block: none !important;\n}\n\n#camera-page .merged-phone-input::placeholder {\n  color: #999;\n}\n\n.radio-group {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.radio-label {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  cursor: pointer;\n  padding: 10px;\n  border-radius: 8px;\n  transition: background-color 0.2s ease;\n}\n\n.radio-label:hover {\n  background-color: #f9f9f9;\n}\n\n.radio-label input[type=\"radio\"] {\n  width: 20px;\n  height: 20px;\n  cursor: pointer;\n  accent-color: #FFB6A7;\n  margin: 0;\n}\n\n.radio-label span {\n  font-size: 0.95rem;\n  color: #333;\n  flex: 1;\n}\n\n.checkbox-group {\n  display: flex;\n  flex-direction: column;\n  gap: 12px;\n}\n\n.checkbox-label {\n  display: flex;\n  align-items: center;\n  gap: 10px;\n  cursor: pointer;\n  padding: 10px;\n  border-radius: 8px;\n  transition: background-color 0.2s ease;\n}\n\n.checkbox-label:hover {\n  background-color: #f9f9f9;\n}\n\n.checkbox-label input[type=\"checkbox\"] {\n  width: 20px;\n  height: 20px;\n  cursor: pointer;\n  accent-color: #FFB6A7;\n  margin: 0;\n}\n\n.checkbox-label span {\n  font-size: 0.95rem;\n  color: #333;\n  flex: 1;\n}\n\n.inline-input-group {\n  display: flex;\n  flex-direction: row;\n  gap: 15px;\n  align-items: center;\n  flex-wrap: wrap;\n}\n\n.inline-input-row {\n  display: flex;\n  align-items: center;\n  gap: 8px;\n  flex: 1;\n  min-width: 200px;\n}\n\n.inline-input-row-small {\n  flex: 0.5;\n  min-width: 100px;\n}\n\n.inline-input-label {\n  font-size: 0.95rem;\n  color: #333;\n  font-weight: 500;\n  white-space: nowrap;\n  flex-shrink: 0;\n}\n\n.inline-input-field {\n  flex: 1;\n  min-width: 150px;\n}\n\n.inline-input-field-small {\n  flex: 1;\n  min-width: 100px;\n}\n\n.answer-input-container {\n  margin-top: 12px;\n  margin-left: 30px;\n}\n\n.answer-input {\n  width: 100%;\n  max-width: 500px;\n}\n\n.validation-error {\n  color: #ff4444;\n  font-size: 0.875rem;\n  margin-top: 8px;\n  padding: 8px;\n  background-color: #ffe6e6;\n  border-radius: 4px;\n  border-left: 3px solid #ff4444;\n}\n\n.questionnaire-submit-container {\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  gap: 15px;\n  margin-top: 30px;\n  padding-top: 20px;\n  flex-wrap: wrap;\n}\n\n.questionnaire-submit-container .next-button:disabled {\n  background-color: #D9D9D9;\n  color: #999;\n  cursor: not-allowed;\n  transform: none;\n}\n\n.skip-button {\n  background-color: transparent;\n  color: #666;\n  border: 2px solid #ddd;\n  border-radius: 8px;\n  padding: 12px 30px;\n  font-size: 16px;\n  font-weight: 500;\n  cursor: pointer;\n  transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease, transform 0.2s ease;\n  min-width: 120px;\n}\n\n.skip-button:hover:not(:disabled) {\n  background-color: #f5f5f5;\n  border-color: #999;\n  color: #333;\n  transform: translateY(-1px);\n}\n\n.skip-button:active:not(:disabled) {\n  transform: translateY(0);\n}\n\n.skip-button:disabled {\n  background-color: #f5f5f5;\n  color: #ccc;\n  border-color: #e0e0e0;\n  cursor: not-allowed;\n  transform: none;\n}\n\n\/* Mobile Responsive Styles for Questionnaire *\/\n@media screen and (max-width: 768px) {\n  .name-inputs {\n    grid-template-columns: 1fr;\n    gap: 10px;\n  }\n\n  .phone-inputs {\n    grid-template-columns: 1fr;\n    gap: 10px;\n  }\n\n  .phone-extension {\n    width: 100%;\n  }\n\n  .merged-phone-input-container {\n    width: 100%;\n  }\n\n  #camera-page .merged-phone-country-code {\n    width: 100px;\n    font-size: 16px; \/* Prevents zoom on iOS *\/\n    padding: 12px 10px;\n  }\n\n  #camera-page .merged-phone-input {\n    font-size: 16px; \/* Prevents zoom on iOS *\/\n    padding: 12px 10px;\n  }\n\n  .form-input {\n    font-size: 16px; \/* Prevents zoom on iOS *\/\n  }\n\n  .radio-label {\n    padding: 12px;\n  }\n\n  .radio-label span {\n    font-size: 0.9rem;\n  }\n\n  .checkbox-label {\n    padding: 12px;\n  }\n\n  .checkbox-label span {\n    font-size: 0.9rem;\n  }\n\n  .answer-input-container {\n    margin-left: 0;\n    margin-top: 10px;\n  }\n\n  .answer-input {\n    max-width: 100%;\n  }\n\n  .inline-input-group {\n    flex-direction: column;\n    gap: 12px;\n  }\n\n  .inline-input-row {\n    flex-direction: row;\n    width: 100%;\n    min-width: auto;\n  }\n\n  .inline-input-label {\n    min-width: auto;\n    flex-shrink: 0;\n  }\n\n  .inline-input-field {\n    flex: 1;\n    width: 100%;\n    min-width: auto;\n  }\n}\n\n#camera-page .analysis-footer {\n  background-color: #FFFDFA;\n  \/* padding: 60px 0px; *\/\n  display: flex;\n  justify-content: center;\n  \/* position: fixed;\n  left: 0;\n  right: 0;\n  bottom: 0; *\/\n  margin-top: 10px;\n  width: 100%;\n  height: 100px;\n  \/* z-index: 1000; *\/\n  \/* box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.05); *\/\n}\n\n#camera-page .analysis-footer-content {\n  width: 100%;\n  \/* max-width: 940px; *\/\n  background-color: #fcf6f3;\n  border-radius: 20px;\n  padding: 20px 60px;\n  text-align: center;\n  box-shadow: 0 12px 32px rgba(255, 150, 133, 0.15);\n  display: grid;\n  gap: 10px;\n}\n\n#camera-page .analysis-footer-text {\n  font-size: 11px;\n  \/* font-weight: 500; *\/\n  color: #333;\n  margin: 0;\n  line-height: 1.6;\n}\n\n#camera-page .analysis-footer-button {\n  justify-self: center;\n  background-color: #FFB6A7;\n  color: white;\n  border: none;\n  border-radius: 8px;\n  padding: 7px 15px;\n  font-size: 14px;\n  \/* font-weight: bold; *\/\n  cursor: pointer;\n  transition: background-color 0.3s ease, transform 0.2s ease;\n  min-width: 240px;\n}\n\n#camera-page .analysis-footer-button:hover {\n  background-color: #FF9A85;\n  transform: translateY(-1px);\n}\n\n#camera-page .analysis-footer-button:active {\n  transform: translateY(0);\n}\n\n#camera-page .analysis-footer-button:focus {\n  outline: none;\n}\n\n<\/style>\n\n<script>\nconst { createApp } = Vue;\n\ncreateApp({\n  data() {\n    return {\n      \/\/ Constants\n      guidelineImageList: ['front_shape.png', 'hairLineShape.png', 'topHeadShape.png'],\n      frontalPositions: ['Front', 'Front Head', 'Top'],\n      positionInstructions: [\n        'Align your face with the guide line image.',\n        'Lift the hair to reveal your hairline, make sure your hand does not cover the hairline.',\n        'Tilt your head down to match the crown of you head with the guide line.'\n      ],\n      root: 'https:\/\/sg.haircosys.ai:8086',\n      \/\/ root: 'http:\/\/localhost:8080',\n      bucketLink: 'https:\/\/haircosys-images.oss-ap-southeast-1.aliyuncs.com\/',\n      cameraWidth: 0,\n      listCam: [],\n      showModal: false,\n      showCam: false,\n      init: true,\n      showCamera: true,\n      showCanvas: false,\n      showReport: false,\n      load: false,\n      refreshed: false,\n      unconfirmedPicture: false,\n      captureComplete: false,\n      mobile: false,\n      currentIndex: 0,\n      camera: null,\n      canvas: null,\n      capturedImages: 0,\n      listPhotos: [null, null, null],\n      tempUnconfirmedImage: null, \/\/ Temporary storage for unconfirmed image data URL\n      errorText: \"\",\n      bigHeadResults: [],\n      bigHeadMessages: {},\n      photoList: [],\n      bigHead: null,\n      products: [],\n      camId: \"\",\n      sessionId: 0,\n      privacyAccepted: false,\n      showTermsModal: false,\n      isValidationError: false,\n      showQuestionnaire: false,\n      isSubmittingQuestionnaire: false,\n      isLoadingQuestions: false,\n      questions: [], \/\/ Dynamic questions from API\n      questionnaireResponses: {}, \/\/ Store responses: { questionId: { selectedAnswers: [answerId], userInputs: { answerId: 'text' } } }\n      validationErrors: {} \/\/ Store validation errors: { questionId: 'error message' }\n    };\n  },\n  computed: {\n    canProceed() {\n      return this.privacyAccepted && this.camId && this.camId !== \"\";\n    }\n  },\n  mounted() {\n    this.onResize();\n    window.addEventListener('resize', this.onResize);\n    window.addEventListener('orientationchange', this.onResize);\n    this.list(false);\n    \/\/ this.fetchExternalSiteQuestions()\n  },\n  beforeUnmount() {\n    window.removeEventListener('resize', this.onResize);\n    window.removeEventListener('orientationchange', this.onResize);\n    if (this.camera?.srcObject) {\n      this.camera.srcObject.getTracks().forEach(track => track.stop());\n    }\n  },\n  methods: {\n    onResize() {\n      \/\/ Add a small delay for orientation changes to ensure proper dimensions\n      setTimeout(() => {\n        const tempWidth = window.innerWidth;\n        const tempHeight = window.innerHeight;\n        \/\/ Consider mobile if width is less than 768px (tablet breakpoint)\n        this.mobile = tempWidth < 768;\n        \n        \/\/ Adjust camera width based on device type\n        if (this.mobile) {\n          \/\/ For mobile, use most of the screen width but leave some padding\n          this.cameraWidth = Math.min(tempWidth * 0.9, 400);\n        } else {\n          \/\/ For desktop, maintain the original logic\n          this.cameraWidth = Math.min(Math.max(tempWidth * 0.8, 150), 640);\n        }\n        \n        if (this.canvas) {\n          this.canvas.width = this.cameraWidth;\n          this.canvas.height = this.canvas.width * (this.camera?.videoHeight \/ this.camera?.videoWidth || 1);\n        }\n        if (this.camera) {\n          this.camera.width = this.cameraWidth;\n        }\n      }, 100);\n    },\n    resetModal() {\n      this.errorText = \"\";\n      this.showModal = false;\n      \n      \/\/ Only reset to camera selection for system errors, not validation errors\n      if (this.isValidationError) {\n        this.isValidationError = false;\n        \/\/ Don't reset progress for validation errors\n      } else {\n        \/\/ Reset to camera selection for system errors\n        this.resetToCameraSelection();\n      }\n    },\n    resetToCameraSelection() {\n      \/\/ Stop camera stream if active\n      if (this.camera?.srcObject) {\n        this.camera.srcObject.getTracks().forEach(track => track.stop());\n      }\n      \n      \/\/ Reset all state variables to initial values\n      this.init = true;\n      this.showCam = false;\n      this.showCamera = true;\n      this.showCanvas = false;\n      this.showReport = false;\n      this.load = false;\n      this.unconfirmedPicture = false;\n      this.captureComplete = false;\n      this.currentIndex = 0;\n      this.capturedImages = 0;\n      this.listPhotos = [null, null, null];\n      this.tempUnconfirmedImage = null; \/\/ Clear temporary image data\n      this.bigHeadResults = [];\n      this.bigHeadMessages = {};\n      this.photoList = [];\n      this.bigHead = null;\n      this.products = [];\n      this.camId = \"\";\n      this.sessionId = 0;\n      this.errorText = \"\";\n      this.showModal = false;\n      this.showQuestionnaire = false;\n      this.isSubmittingQuestionnaire = false;\n      this.isLoadingQuestions = false;\n      \n      \/\/ Reset questionnaire data\n      this.questions = [];\n      this.questionnaireResponses = {};\n      this.validationErrors = {};\n      \n      \/\/ Clear canvas if it exists\n      if (this.canvas) {\n        const ctx = this.canvas.getContext(\"2d\");\n        ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);\n      }\n      \n      \/\/ Reset privacy acceptance\n      this.privacyAccepted = false;\n    },\n    openTerms(event) {\n      console.log(\"openTerms called, current showTermsModal:\", this.showTermsModal);\n      event.preventDefault();\n      this.showTermsModal = true;\n      console.log(\"After setting showTermsModal to true:\", this.showTermsModal);\n    },\n    closeTermsModal() {\n      this.showTermsModal = false;\n    },\n    proceedToCamera() {\n      if (this.canProceed) {\n        this.camGetMedia(this.camId);\n      } else {\n        \/\/ Show error modal with specific message based on what's missing\n        this.isValidationError = true;\n        if (!this.privacyAccepted && !this.camId) {\n          this.errorText = \"Please select a camera and agree to the privacy statement before proceeding.\";\n        } else if (!this.privacyAccepted) {\n          this.errorText = \"Please agree to the privacy statement before proceeding.\";\n        } else if (!this.camId) {\n          this.errorText = \"Please select a camera before proceeding.\";\n        }\n        this.showModal = true;\n      }\n    },\n    resetValidationModal() {\n      this.errorText = \"\";\n      this.showModal = false;\n      \/\/ Don't reset to camera selection for validation errors\n    },\n    getHairIndexWidth() {\n      const indexScore = 100 - (this.bigHead?.hair_loss_index * 4 || 0);\n      return `width: ${indexScore}%;`;\n    },\n    getHairIndexColor(cssTitle) {\n    \/\/   const indexColor = (100 - (this.bigHead?.hair_loss_index * 4 || 0)) > 70 ? \"lightBlue\" : \"#7c1515\";\n      const indexColor = (100 - (this.bigHead?.hair_loss_index * 4 || 0)) > 70 ? \"#03A9F4\" : \"#F44336\";\n      return `${cssTitle}: ${indexColor};`;\n    },\n    setFrontalProgressBarPlusGridTemplateColumns() {\n      return this.mobile ? \"grid-template-columns: 60fr 5fr 10fr;\" : \"grid-template-columns: 5fr 60fr 5fr 10fr;\";\n    },\n    setFrontalProgressBarDisplay() {\n      return this.mobile ? \"display: block;\" : \"display: grid;\";\n    },\n    getFrontalColor(cssTitle, point, tag) {\n      if (point < tag) {\n        return \"\";\n      } else if (cssTitle === 'color' && point === tag) {\n        return \"color: white;\";\n      } else if (point >= 4) {\n        \/\/ return `${cssTitle}: red;`;\n        return `${cssTitle}: #FF0000;`;\n      } else if (point >= 2) {\n        \/\/ return `${cssTitle}: orange;`;\n        return `${cssTitle}: #F18500;`;\n      } else {\n        \/\/ return `${cssTitle}: green;`;\n        return `${cssTitle}: #4CAF50;`;\n      }\n    },\n    getSpanClass(tag, barScore) {\n      if (tag === 1) {\n        return barScore === 1 ? 'barIndicatorColoredStartValueIs1' : 'barIndicatorColoredStartValueGreaterThan1';\n      } else if (tag < barScore) {\n        return 'barIndicatorColoredMiddle';\n      } else if (tag === barScore) {\n        return 'barIndicatorColoredEnd';\n      } else {\n        return 'barIndicatorNormal';\n      }\n    },\n    getBarResult(barScore) {\n      if (barScore >= 4) return \"EXTREME\";\n      if (barScore >= 2) return \"MILD\";\n      return \"HEALTHY\";\n    },\n    getGaugeStyle() {\n      const score = 100 - (this.bigHead?.hair_loss_index * 4 || 0);\n      const angle = (score \/ 100) * 360;\n      const color = this.getGaugeColor(score);\n      return `--gauge-angle: ${angle}deg; --gauge-color: ${color};`;\n    },\n    getGaugeColor(score) {\n      if (score <= 50) return '#F44336'; \/\/ Red for severe concern\n      if (score <= 70) return '#FF9800'; \/\/ Orange for moderate concern\n      return '#4CAF50'; \/\/ Green for normal\n    },\n    getGaugeLabelStyle() {\n      const score = 100 - (this.bigHead?.hair_loss_index * 4 || 0);\n      const color = this.getGaugeColor(score);\n      return `color: ${color};`;\n    },\n    getHairHealthLabel() {\n      const score = 100 - (this.bigHead?.hair_loss_index * 4 || 0);\n      if (score <= 50) return \"Severe concern\";\n      if (score <= 70) return \"Moderate concern\";\n      return \"Normal\";\n    },\n    getPositionInstruction() {\n      return this.positionInstructions[this.currentIndex] || 'Take photo using the overlay as a guide';\n    },\n    getTendencyBarStyle(value) {\n      const percentage = (value \/ 5) * 100;\n      let color = '#4CAF50'; \/\/ Healthy\n      if (value >= 4) color = '#F44336'; \/\/ Extreme\n      else if (value >= 2) color = '#FF9800'; \/\/ Mild\n      \n      return `width: ${percentage}%; background-color: ${color};`;\n    },\n    getTendencyClass(value) {\n      if (value >= 4) return 'extreme';\n      if (value >= 2) return 'mild';\n      return 'healthy';\n    },\n    handleBigHeadPhotoList(photo_list) {\n      this.photoList = [\n        photo_list.front,\n        photo_list.front_head,\n        photo_list.top,\n        ...(photo_list.left ? [photo_list.left] : []),\n        ...(photo_list.right ? [photo_list.right] : [])\n      ].filter(Boolean);\n    },\n    handleBigHeadModel(bigHead) {\n      this.bigHead = bigHead;\n      this.handleBigHeadPhotoList(bigHead.photo_list);\n      this.bigHeadResults = [\n        { title: \"Hairline Receding Rate (Overall)\", value: bigHead.ab_ratio },\n        { title: \"Temple Recession Rate\", value: bigHead.m_shape_result },\n        { title: \"Crown Hair Thinning Rate\", value: bigHead.o_shape_result },\n        { title: \"Hair Part Widening Rate\", value: bigHead.hair_part_result },\n        { title: \"Thinness Rate\", value: bigHead.thickness_level }\n      ];\n      this.showReport = true;\n      \n      \/\/ Scroll to the top of the report after it's displayed\n      this.scrollToReportTop();\n    },\n    async list(refresh) {\n      if (!navigator.mediaDevices?.enumerateDevices) {\n        this.errorText = \"Camera enumeration not supported.\";\n        this.showModal = true;\n        return;\n      }\n      try {\n        await navigator.mediaDevices.getUserMedia({ audio: false, video: true });\n        this.listCam = [];\n        const devices = await navigator.mediaDevices.enumerateDevices();\n        devices.forEach(device => {\n          if (device.kind === 'videoinput') {\n            this.listCam.push({ name: device.label, id: device.deviceId });\n          }\n        });\n        this.refreshed = refresh;\n      } catch (err) {\n        this.errorText = `Error accessing cameras: ${err.message}`;\n        this.showModal = true;\n      }\n    },\n    async camGetMedia(id) {\n      const constraints = { \n        audio: false, \n        video: { \n          deviceId: { exact: id },\n          \/\/ width: { ideal: 1280, max: 1280, min: 1280 },\n          \/\/ height: { ideal: 960, max: 960, min: 960 }\n        } \n      };\n      try {\n        const stream = await navigator.mediaDevices.getUserMedia(constraints);\n        console.log(\"cameraWidth is: \", this.cameraWidth);\n        this.camera = this.$refs.camera;\n        this.canvas = document.getElementById(\"canvas\");\n        console.log(\"camera.videoWidth is: \", this.camera.videoWidth);\n        console.log(\"camera.videoHeight is: \", this.camera.videoHeight);\n        this.camera.setAttribute(\"width\", this.cameraWidth);\n        this.canvas.setAttribute(\"width\", this.cameraWidth);\n        this.camera.srcObject = stream;\n        setTimeout(() => {\n          this.showCam = true;\n          this.init = false;\n          this.canvas.setAttribute(\"height\", this.canvas.width * (this.camera.videoHeight \/ this.camera.videoWidth));\n          const context = this.canvas.getContext(\"2d\");\n          context.translate(this.canvas.width, 0);\n          context.scale(-1, 1);\n        }, 500);\n      } catch (err) {\n        this.errorText = `Camera access error: ${err.message}`;\n        this.showModal = true;\n      }\n    },\n    calculateGuideLineMargin() {\n      \/\/ CSS now centers the guideline; just ensure it cannot exceed container\n      return 'max-width: 100%; max-height: 100%;';\n    },\n    calculateGuideLineSize() {\n      const availableWidth = this.cameraWidth || 0;\n      const availableHeight = this.canvas ? this.canvas.height : availableWidth;\n      const maxSquare = Math.min(availableWidth, availableHeight);\n      const factor = this.mobile ? 0.7 : 0.45;\n      return Math.floor(maxSquare * factor);\n    },\n    dataUrlToFile(dataUrl, filename) {\n      const arr = dataUrl.split(\",\");\n      const mime = arr[0].match(\/:(.*?);\/)[1];\n      const bstr = atob(arr[1]);\n      let n = bstr.length;\n      const u8arr = new Uint8Array(n);\n      while (n--) u8arr[n] = bstr.charCodeAt(n);\n      return new File([u8arr], filename, { type: mime });\n    },\n    getButtonReportJustfication() {\n    return \"justify-content: space-between;\";\n      \/\/ return this.unconfirmedPicture ? \"justify-content: space-between;\" : \"justify-content: center;\";\n    },\n    takepicture() {\n      try {\n        const context = this.canvas.getContext(\"2d\");\n        \n        \/\/ Ensure canvas is properly sized for mobile\n        if (this.mobile) {\n          \/\/ Force canvas to be visible and properly sized\n          this.canvas.style.display = 'block';\n          this.canvas.style.visibility = 'visible';\n        }\n        \n        \/\/ Draw within canvas bounds maintaining aspect ratio\n        const videoAspect = this.camera.videoWidth \/ this.camera.videoHeight;\n        const canvasAspect = this.canvas.width \/ this.canvas.height;\n        let drawWidth = this.canvas.width;\n        let drawHeight = this.canvas.height;\n        let dx = 0;\n        let dy = 0;\n        \n        if (videoAspect > canvasAspect) {\n          \/\/ video is wider than canvas: fit width, adjust height\n          drawHeight = this.canvas.width \/ videoAspect;\n          dy = (this.canvas.height - drawHeight) \/ 2;\n        } else if (videoAspect < canvasAspect) {\n          \/\/ video is taller than canvas: fit height, adjust width\n          drawWidth = this.canvas.height * videoAspect;\n          dx = (this.canvas.width - drawWidth) \/ 2;\n        }\n        \n        \/\/ Reset canvas transformation to prevent flipping\n        context.setTransform(1, 0, 0, 1, 0, 0);\n        context.clearRect(0, 0, this.canvas.width, this.canvas.height);\n        \n        \/\/ Apply horizontal flip transformation for the image capture\n        context.translate(this.canvas.width, 0);\n        context.scale(-1, 1);\n        \n        context.drawImage(this.camera, dx, dy, drawWidth, drawHeight);\n        \n        \/\/ Generate image data URL and store in temporary variable\n        const quality = this.mobile ? 0.95 : 0.9;\n        this.tempUnconfirmedImage = this.canvas.toDataURL(\"image\/jpeg\", quality);\n        \n        \/\/ Hide camera first to prevent flash, then show image\n        this.showCamera = false;\n        \n        \/\/ Use nextTick to ensure the DOM update is processed before showing image\n        this.$nextTick(() => {\n          this.showCanvas = true;\n          this.unconfirmedPicture = true;\n        });\n      } catch (error) {\n        console.error('Error taking picture:', error);\n        this.errorText = \"Failed to capture image. Please try again.\";\n        this.showModal = true;\n      }\n    },\n    confirmPicture() {\n      try {\n        \/\/ Use the temporary image data that was already generated\n        const data = this.tempUnconfirmedImage;\n        \n        \/\/ Validate that we got a proper data URL\n        if (!data || data === 'data:,') {\n          throw new Error('No image data available to confirm');\n        }\n        \n        \/\/ Store the image data\n        this.listPhotos[this.currentIndex] = data;\n        \n        \/\/ Force Vue reactivity update\n        this.$forceUpdate();\n        \n        this.capturedImages += 1;\n        this.unconfirmedPicture = false;\n        \n        if (this.capturedImages < 3) {\n          \/\/ Hide canvas first, then show camera for next photo\n          this.showCanvas = false;\n          this.currentIndex += 1;\n          this.$nextTick(() => {\n            this.showCamera = true;\n          });\n        } else {\n          this.captureComplete = true;\n          this.changePos(2);\n        }\n      } catch (error) {\n        console.error('Error confirming picture:', error);\n        this.errorText = \"Failed to save image. Please try again.\";\n        this.showModal = true;\n      }\n    },\n    retryPicture() {\n      \/\/ Clear the temporary image data\n      this.tempUnconfirmedImage = null;\n      this.unconfirmedPicture = false;\n      \n      if (this.listPhotos[this.currentIndex]) {\n        this.listPhotos[this.currentIndex] = null;\n        this.capturedImages -= 1;\n      }\n      if (this.captureComplete) this.captureComplete = false;\n      \n      \/\/ Hide image first to prevent flash, then show camera\n      this.showCanvas = false;\n      \n      \/\/ Use nextTick to ensure the DOM update is processed before showing camera\n      this.$nextTick(() => {\n        this.showCamera = true;\n      });\n    },\n    changePos(index) {\n        \/\/ currentIndex === index, blackButtonDefault: listPhotos[index] && currentIndex !== index\n        \n        console.log(\"index is: \", index);\n        console.log(\"currentIndex is: \", this.currentIndex);\n        console.log(\"this.currentIndex !== index: \", this.currentIndex !== index);\n        console.log(\"this.listPhotos[index] && this.currentIndex !== index: \", this.listPhotos[index] && this.currentIndex !== index);\n        \n      if (this.unconfirmedPicture) {\n        this.errorText = \"Please confirm or reset the current picture before moving to another position\";\n        this.showModal = true;\n      } else if (this.listPhotos[index]) {\n        \/\/ Set the confirmed image data to display\n        this.tempUnconfirmedImage = this.listPhotos[index];\n        this.currentIndex = index;\n        this.showCamera = false;\n        this.showCanvas = true;\n      } else {\n        this.currentIndex = index;\n        this.showCamera = true;\n        this.showCanvas = false;\n      }\n    },\n    scrollToReportTop() {\n      \/\/ Scroll to the top of the report container\n      this.$nextTick(() => {\n        if (this.$refs.reportContainer) {\n          this.$refs.reportContainer.scrollIntoView({ \n            behavior: 'smooth', \n            block: 'start' \n          });\n        }\n      });\n    },\n    redirectToMoreInfo() {\n      \/\/ Redirect to a separate webpage - you can change this URL as needed\n      window.open('https:\/\/haircosys.com\/en\/contact-2\/', '_blank');\n    },\n    redirectToDemo() {\n      window.open('https:\/\/calendar.app.google\/bdft9EHyMuHPgvWr7', '_blank');\n    },\n    handleImageError(index) {\n      console.error(`Image failed to load for position ${index}:`, this.listPhotos[index]);\n      \/\/ If image fails to load, clear it and show error\n      this.listPhotos[index] = null;\n      this.errorText = `Image for ${this.frontalPositions[index]} failed to load. Please retake this photo.`;\n      this.showModal = true;\n    },\n    handleImageLoad(index) {\n      console.log(`Image loaded successfully for position ${index}`);\n    },\n    async sendBigHeadRequest() {\n      \/\/ Go directly to loading\/analysis\n      const imageList = this.listPhotos.map((element, index) =>\n        this.dataUrlToFile(element, `${this.frontalPositions[index]}.jpg`)\n      );\n      const url = `${this.root}\/api\/v1\/shopify\/createFrontalRecord`;\n      const body = {\n        ai_checked: true,\n        name_list: [\"front\", \"front_head\", \"top\"],\n        images: imageList,\n        company_secret: \"cnuszhzcfb\"\n      };\n      this.load = true;\n      this.showCam = false;\n      console.log(\"a\");\n      this.sendXMLReq(url, body, this.handleCreateBigHeadReturn);\n    },\n    goBackToCamera() {\n      this.showQuestionnaire = false;\n      this.showCam = true;\n    },\n    async submitQuestionnaire() {\n      \/\/ Validate before submission\n      if (!this.validateQuestionnaire()) {\n        \/\/ Scroll to first error\n        this.$nextTick(() => {\n          const firstErrorElement = document.querySelector('.validation-error');\n          if (firstErrorElement) {\n            firstErrorElement.scrollIntoView({ behavior: 'smooth', block: 'center' });\n          }\n        });\n        return;\n      }\n      \n      this.isSubmittingQuestionnaire = true;\n      \n      \/\/ Prepare questionnaire data in the required format\n      const questionIds = [];\n      const answerIds = [];\n      const userInputs = [];\n      \n      this.questions.forEach(question => {\n        const response = this.questionnaireResponses[question.question_id];\n        \n        \/\/ Check if all answers have both other=true AND required=true (inline inputs)\n        const allAnswersOtherAndRequired = question.possible_answers.every(a => a.other === true && a.required === true);\n        \n        if (allAnswersOtherAndRequired) {\n          \/\/ For inline inputs, include all answers that have input (auto-selected)\n          question.possible_answers.forEach(answer => {\n            const userInput = response?.userInputs[answer.answer_id];\n            if (userInput && userInput.trim()) {\n              questionIds.push(question.question_id);\n              answerIds.push(answer.answer_id);\n              userInputs.push(userInput);\n            }\n          });\n        } else if (response && response.selectedAnswers && response.selectedAnswers.length > 0) {\n          response.selectedAnswers.forEach(answerId => {\n            questionIds.push(question.question_id);\n            answerIds.push(answerId);\n            \/\/ Get user input if this answer has other=true, otherwise null\n            const answer = question.possible_answers.find(a => a.answer_id === answerId);\n            if (answer && answer.other === true) {\n              userInputs.push(response.userInputs[answerId] || '');\n            } else {\n              userInputs.push(null);\n            }\n          });\n        }\n      });\n\n      const questionnairePayload = {\n        question_id: questionIds.join(', '),\n        answer_id: answerIds.join(', '),\n        user_input: userInputs.map(input => input === null ? '' : input).join(', '),\n        prediction_session_id: this.sessionId,\n        company_secret: \"cnuszhzcfb\"\n      };\n\n      \/\/ Send questionnaire data to backend\n      try {\n        const url = `${this.root}\/api\/v1\/questionnaire\/submitExternalSiteAnswers`;\n        await this.sendXMLReqPromise(url, questionnairePayload);\n        console.log('Questionnaire submitted successfully');\n      } catch (error) {\n        console.error('Questionnaire submission failed:', error);\n        this.errorText = `Failed to submit questionnaire: ${error.message}. Please try again.`;\n        this.showModal = true;\n        this.isSubmittingQuestionnaire = false;\n        return;\n      }\n      \n      \/\/ Hide questionnaire and proceed to get session result\n      this.showQuestionnaire = false;\n      this.isSubmittingQuestionnaire = false;\n      this.load = true;\n      this.getSessionResult();\n    },\n    skipQuestionnaire() {\n      \/\/ Skip questionnaire without submitting answers\n      \/\/ Hide questionnaire and proceed directly to get session result\n      this.showQuestionnaire = false;\n      this.isSubmittingQuestionnaire = false;\n      this.load = true;\n      this.getSessionResult();\n    },\n    sendXMLReqPromise(url, body) {\n      return new Promise((resolve, reject) => {\n        const formData = new FormData();\n        const enData = this.encrypt(body);\n        formData.append(\"vars\", enData);\n        \n        const xhr = new XMLHttpRequest();\n        xhr.open(\"POST\", url);\n        xhr.onload = () => {\n          if (xhr.status === 200) {\n            let decodedString = JSON.parse(xhr.responseText);\n            decodedString = this.decrypt(decodedString);\n            if (decodedString.result === 0) {\n              resolve(decodedString);\n            } else {\n              reject(new Error(decodedString.error || 'Unknown error'));\n            }\n          } else {\n            reject(new Error(`Request failed with status: ${xhr.status}`));\n          }\n        };\n        xhr.onerror = () => reject(new Error('Network error'));\n        xhr.send(formData);\n      });\n    },\n    getSessionResult() {\n      const url = `${this.root}\/api\/v1\/shopify\/getShopifySession`;\n      const body = { session_id: this.sessionId, company_secret: \"cnuszhzcfb\" };\n      this.sendXMLReq(url, body, this.handleGetSessionResultReturn);\n    },\n    async handleCreateBigHeadReturn(returnText) {\n        console.log(\"b\");\n        console.log(returnText);\n      if (returnText.result === 0) {\n        this.sessionId = returnText.data.session.session_id;\n        \/\/ Load questions before showing questionnaire\n        this.load = false;\n        await this.fetchExternalSiteQuestions();\n        \/\/ this.showQuestionnaire = true;\n      } else {\n        this.errorText = `${returnText.error}. Please refresh and try again.`;\n        this.showModal = true;\n        this.load = false;\n      }\n    },\n    async fetchExternalSiteQuestions() {\n      this.isLoadingQuestions = true;\n      try {\n        const url = `${this.root}\/api\/v1\/questionnaire\/getExternalSiteQuestions`;\n        const body = {\n          company_secret: \"cnuszhzcfb\"\n        };\n        const response = await this.sendXMLReqPromise(url, body);\n        if (response.result === 0 && response.data && response.data.questions) {\n          this.questions = response.data.questions;\n          \/\/ Initialize responses object\n          this.questions.forEach(question => {\n            this.questionnaireResponses[question.question_id] = {\n              selectedAnswers: [],\n              userInputs: {}\n            };\n          });\n          this.showQuestionnaire = true;\n        } else {\n          throw new Error(response.error || 'Failed to load questions');\n        }\n      } catch (error) {\n        console.error('Error fetching questions:', error);\n        this.errorText = `Failed to load questionnaire: ${error.message}. Please refresh and try again.`;\n        this.showModal = true;\n      } finally {\n        this.isLoadingQuestions = false;\n      }\n    },\n    getInputType(answerText) {\n      const text = answerText.toLowerCase();\n      if (text.includes('email')) return 'email';\n      if (text.includes('phone') || text.includes('number')) return 'tel';\n      return 'text';\n    },\n    isPhoneQuestion(question) {\n      \/\/ Check if question has exactly 2 answers and both are phone-related\n      if (question.possible_answers.length !== 2) return false;\n      \n      const answerTexts = question.possible_answers.map(a => a.answer.toLowerCase());\n      const hasCountryCode = answerTexts.some(text => \n        text.includes('country') || text.includes('code') || text.includes('extension')\n      );\n      const hasPhoneNumber = answerTexts.some(text => \n        text.includes('phone') || text.includes('number')\n      );\n      \n      return hasCountryCode && hasPhoneNumber;\n    },\n    getCountryCodeValue(questionId) {\n      const response = this.questionnaireResponses[questionId];\n      if (!response || !response.userInputs) return '';\n      \n      const question = this.questions.find(q => q.question_id === questionId);\n      if (!question || question.possible_answers.length !== 2) return '';\n      \n      \/\/ Get the country code value\n      const countryCodeAnswer = question.possible_answers.find(a => {\n        const text = a.answer.toLowerCase();\n        return text.includes('country') || text.includes('code') || text.includes('extension');\n      });\n      \n      if (countryCodeAnswer) {\n        return response.userInputs[countryCodeAnswer.answer_id] || '';\n      }\n      \n      return '';\n    },\n    getMergedPhoneValue(questionId) {\n      const response = this.questionnaireResponses[questionId];\n      if (!response || !response.userInputs) return '';\n      \n      const question = this.questions.find(q => q.question_id === questionId);\n      if (!question || question.possible_answers.length !== 2) return '';\n      \n      \/\/ Get the phone number value (not country code)\n      const phoneAnswer = question.possible_answers.find(a => {\n        const text = a.answer.toLowerCase();\n        return text.includes('phone') || text.includes('number');\n      });\n      \n      if (phoneAnswer) {\n        return response.userInputs[phoneAnswer.answer_id] || '';\n      }\n      \n      return '';\n    },\n    updateCountryCodeInput(questionId, event) {\n      const question = this.questions.find(q => q.question_id === questionId);\n      if (!question || question.possible_answers.length !== 2) return;\n      \n      if (!this.questionnaireResponses[questionId]) {\n        this.questionnaireResponses[questionId] = {\n          selectedAnswers: [],\n          userInputs: {}\n        };\n      }\n      \n      \/\/ Find country code answer ID\n      const countryCodeAnswer = question.possible_answers.find(a => {\n        const text = a.answer.toLowerCase();\n        return text.includes('country') || text.includes('code') || text.includes('extension');\n      });\n      \n      const response = this.questionnaireResponses[questionId];\n      \n      let value = event.target.value;\n      \n      \/\/ Filter to only allow '+' and numbers\n      let sanitizedValue = value.replace(\/[^+\\d]\/g, '');\n      \n      \/\/ Ensure '+' only appears at the start (remove any '+' not at the start)\n      if (sanitizedValue.includes('+')) {\n        const hasPlusAtStart = sanitizedValue.startsWith('+');\n        const numbersOnly = sanitizedValue.replace(\/\\+\/g, '');\n        sanitizedValue = hasPlusAtStart ? '+' + numbersOnly : numbersOnly;\n      }\n      \n      \/\/ If value is not empty and first character is not '+', prepend it\n      if (sanitizedValue && sanitizedValue.length > 0 && !sanitizedValue.startsWith('+')) {\n        sanitizedValue = '+' + sanitizedValue;\n      }\n      \n      \/\/ Update the input field value directly\n      if (event.target.value !== sanitizedValue) {\n        event.target.value = sanitizedValue;\n      }\n      \n      \/\/ Set country code to the sanitized value\n      if (countryCodeAnswer) {\n        response.userInputs[countryCodeAnswer.answer_id] = sanitizedValue;\n        \/\/ Auto-select answer if there's input, deselect if input is cleared\n        if (sanitizedValue && sanitizedValue.trim()) {\n          if (!response.selectedAnswers.includes(countryCodeAnswer.answer_id)) {\n            response.selectedAnswers.push(countryCodeAnswer.answer_id);\n          }\n        } else {\n          const index = response.selectedAnswers.indexOf(countryCodeAnswer.answer_id);\n          if (index > -1) {\n            response.selectedAnswers.splice(index, 1);\n          }\n        }\n      }\n      \n      \/\/ Clear validation errors for this question\n      delete this.validationErrors[questionId];\n    },\n    updatePhoneNumberInput(questionId, event) {\n      const question = this.questions.find(q => q.question_id === questionId);\n      if (!question || question.possible_answers.length !== 2) return;\n      \n      if (!this.questionnaireResponses[questionId]) {\n        this.questionnaireResponses[questionId] = {\n          selectedAnswers: [],\n          userInputs: {}\n        };\n      }\n      \n      \/\/ Find phone number answer ID\n      const phoneAnswer = question.possible_answers.find(a => {\n        const text = a.answer.toLowerCase();\n        return text.includes('phone') || text.includes('number');\n      });\n      \n      const response = this.questionnaireResponses[questionId];\n      \n      let value = event.target.value;\n      \n      \/\/ Filter to only allow numbers\n      const sanitizedValue = value.replace(\/\\D\/g, '');\n      \n      \/\/ Update the input field value directly\n      if (event.target.value !== sanitizedValue) {\n        event.target.value = sanitizedValue;\n      }\n      \n      \/\/ Set phone number to the sanitized value\n      if (phoneAnswer) {\n        response.userInputs[phoneAnswer.answer_id] = sanitizedValue;\n        \/\/ Auto-select answer if there's input, deselect if input is cleared\n        if (sanitizedValue && sanitizedValue.trim()) {\n          if (!response.selectedAnswers.includes(phoneAnswer.answer_id)) {\n            response.selectedAnswers.push(phoneAnswer.answer_id);\n          }\n        } else {\n          const index = response.selectedAnswers.indexOf(phoneAnswer.answer_id);\n          if (index > -1) {\n            response.selectedAnswers.splice(index, 1);\n          }\n        }\n      }\n      \n      \/\/ Clear validation errors for this question\n      delete this.validationErrors[questionId];\n    },\n    handlePhoneInputEnter(event) {\n      \/\/ Prevent form submission when Enter is pressed on phone input\n      \/\/ Instead, blur the field to dismiss keyboard on mobile\n      event.target.blur();\n    },\n    handleInputEnter(event) {\n      \/\/ Prevent form submission when Enter is pressed on input fields\n      \/\/ Instead, blur the field to dismiss keyboard on mobile\n      event.target.blur();\n    },\n    isAnswerSelected(questionId, answerId) {\n      const response = this.questionnaireResponses[questionId];\n      return response && response.selectedAnswers.includes(answerId);\n    },\n    toggleAnswer(questionId, answerId) {\n      if (!this.questionnaireResponses[questionId]) {\n        this.questionnaireResponses[questionId] = {\n          selectedAnswers: [],\n          userInputs: {}\n        };\n      }\n      \n      const response = this.questionnaireResponses[questionId];\n      const question = this.questions.find(q => q.question_id === questionId);\n      const answer = question?.possible_answers.find(a => a.answer_id === answerId);\n      \n      \/\/ Check if all answers have other=true\n      const allAnswersHaveOther = question?.possible_answers.every(a => a.other === true);\n      \n      \/\/ If not all answers have other=true, only one answer can be selected (radio behavior)\n      if (!allAnswersHaveOther) {\n        response.selectedAnswers = [answerId];\n        response.userInputs = {};\n      } else {\n        \/\/ If all answers have other=true, multiple can be selected (checkbox behavior)\n        const index = response.selectedAnswers.indexOf(answerId);\n        if (index > -1) {\n          response.selectedAnswers.splice(index, 1);\n          delete response.userInputs[answerId];\n        } else {\n          response.selectedAnswers.push(answerId);\n          if (answer?.other) {\n            response.userInputs[answerId] = '';\n          }\n        }\n      }\n      \n      \/\/ Clear validation errors for this question\n      delete this.validationErrors[questionId];\n    },\n    updateUserInput(questionId, answerId, value) {\n      if (!this.questionnaireResponses[questionId]) {\n        this.questionnaireResponses[questionId] = {\n          selectedAnswers: [],\n          userInputs: {}\n        };\n      }\n      this.questionnaireResponses[questionId].userInputs[answerId] = value;\n      \n      \/\/ Clear validation errors for this question\n      delete this.validationErrors[questionId];\n    },\n    updateUserInputDirect(questionId, answerId, value) {\n      \/\/ For inline inputs (all other=true and required=true), auto-select the answer when user types\n      if (!this.questionnaireResponses[questionId]) {\n        this.questionnaireResponses[questionId] = {\n          selectedAnswers: [],\n          userInputs: {}\n        };\n      }\n      \n      const response = this.questionnaireResponses[questionId];\n      \n      \/\/ Auto-select answer if there's input, deselect if input is cleared\n      if (value && value.trim()) {\n        if (!response.selectedAnswers.includes(answerId)) {\n          response.selectedAnswers.push(answerId);\n        }\n      } else {\n        \/\/ Remove from selected if input is cleared\n        const index = response.selectedAnswers.indexOf(answerId);\n        if (index > -1) {\n          response.selectedAnswers.splice(index, 1);\n        }\n      }\n      \n      \/\/ Update input value\n      response.userInputs[answerId] = value;\n      \n      \/\/ Clear validation errors for this question\n      delete this.validationErrors[questionId];\n    },\n    validateQuestionnaire() {\n      this.validationErrors = {};\n      let isValid = true;\n      \n      this.questions.forEach(question => {\n        const response = this.questionnaireResponses[question.question_id];\n        \n        \/\/ Check if all answers have both other=true AND required=true\n        const allAnswersOtherAndRequired = question.possible_answers.every(a => a.other === true && a.required === true);\n        \n        if (allAnswersOtherAndRequired) {\n          \/\/ For inline inputs (including phone questions), check if all inputs are provided\n          const missingInputs = [];\n          question.possible_answers.forEach(answer => {\n            const userInput = response?.userInputs[answer.answer_id] || '';\n            if (!userInput || !userInput.trim()) {\n              missingInputs.push(answer.answer);\n            }\n          });\n          \n          if (missingInputs.length > 0) {\n            \/\/ Special handling for phone questions\n            if (this.isPhoneQuestion(question)) {\n              this.validationErrors[question.question_id] = 'Please provide both country code and phone number';\n            } else {\n              this.validationErrors[question.question_id] = `Please provide input for: ${missingInputs.join(', ')}`;\n            }\n            isValid = false;\n          }\n        } else if (!response || !response.selectedAnswers || response.selectedAnswers.length === 0) {\n          \/\/ All questions must have at least one answer selected\n          this.validationErrors[question.question_id] = 'Please select at least one answer';\n          isValid = false;\n        } else {\n          \/\/ Check if required answers are selected and have input if needed\n          question.possible_answers.forEach(answer => {\n            if (answer.required === true) {\n              const isSelected = response.selectedAnswers.includes(answer.answer_id);\n              \n              \/\/ Check if all answers in question have other=true\n              const allAnswersHaveOther = question.possible_answers.every(a => a.other === true);\n              \n              if (allAnswersHaveOther) {\n                \/\/ If all answers have other=true, only text input is required for required answers\n                \/\/ Check if input exists (selection is handled by showing\/hiding input in UI)\n                const userInput = response.userInputs[answer.answer_id] || '';\n                if (!userInput || !userInput.trim()) {\n                  this.validationErrors[question.question_id] = `Please provide input for \"${answer.answer}\"`;\n                  isValid = false;\n                }\n              } else {\n                \/\/ If not all answers have other=false, both selection and input are required\n                if (!isSelected) {\n                  this.validationErrors[question.question_id] = 'Please select a required answer';\n                  isValid = false;\n                } else if (isSelected && answer.other === true) {\n                  \/\/ If selected and has other=true, input is required\n                  const userInput = response.userInputs[answer.answer_id] || '';\n                  if (!userInput.trim()) {\n                    this.validationErrors[question.question_id] = `Please provide input for \"${answer.answer}\"`;\n                    isValid = false;\n                  }\n                }\n              }\n            }\n          });\n          \n          \/\/ Also check that all selected answers with other=true have input\n          response.selectedAnswers.forEach(answerId => {\n            const answer = question.possible_answers.find(a => a.answer_id === answerId);\n            if (answer && answer.other === true) {\n              const userInput = response.userInputs[answerId] || '';\n              if (!userInput.trim()) {\n                this.validationErrors[question.question_id] = `Please provide input for \"${answer.answer}\"`;\n                isValid = false;\n              }\n            }\n          });\n        }\n      });\n      \n      return isValid;\n    },\n    async handleGetSessionResultReturn(returnText) {\n      console.log(returnText);\n      if (returnText.result === 0) {\n        this.products = returnText.data.session.shopify_products || [];\n        this.bigHeadMessages = returnText.data.session.big_head_messages;\n        this.handleBigHeadModel(returnText.data.session.big_head_prediction_result);\n        this.load = false;\n        \n        \/\/ Send email after results are successfully loaded\n        await this.sendPredictionSessionEmail();\n      } else if (returnText.result === 401 || returnText.error === \"not ready\") {\n        setTimeout(() => this.getSessionResult(), 5000);\n      } else {\n        this.errorText = `${returnText.error}. Please refresh and try again.`;\n        this.showModal = true;\n        this.load = false;\n      }\n    },\n    extractEmailAndName() {\n      let email = '';\n      let name = '';\n      \n      \/\/ Extract email and name from questionnaire responses\n      this.questions.forEach(question => {\n        const response = this.questionnaireResponses[question.question_id];\n        if (!response) return;\n        \n        question.possible_answers.forEach(answer => {\n          const answerText = answer.answer.toLowerCase();\n          const userInput = response.userInputs[answer.answer_id];\n          \n          if (userInput && userInput.trim()) {\n            \/\/ Check for email\n            if (answerText.includes('email') && !email) {\n              email = userInput.trim();\n            }\n            \/\/ Check for name fields (first name, last name, or just name)\n            if ((answerText.includes('name') || answerText.includes('first') || answerText.includes('last')) && !name) {\n              name = userInput.trim();\n            }\n          }\n        });\n      });\n      \n      \/\/ If we have first name and last name separately, combine them\n      let firstName = '';\n      let lastName = '';\n      \n      this.questions.forEach(question => {\n        const response = this.questionnaireResponses[question.question_id];\n        if (!response) return;\n        \n        question.possible_answers.forEach(answer => {\n          const answerText = answer.answer.toLowerCase();\n          const userInput = response.userInputs[answer.answer_id];\n          \n          if (userInput && userInput.trim()) {\n            if (answerText.includes('first') && answerText.includes('name') && !firstName) {\n              firstName = userInput.trim();\n            }\n            if (answerText.includes('last') && answerText.includes('name') && !lastName) {\n              lastName = userInput.trim();\n            }\n          }\n        });\n      });\n      \n      \/\/ Combine first and last name if both exist\n      if (firstName || lastName) {\n        name = [firstName, lastName].filter(n => n).join(' ');\n      }\n      \n      return { email, name };\n    },\n    async sendPredictionSessionEmail() {\n      try {\n        const { email, name } = this.extractEmailAndName();\n        \n        \/\/ Only send if we have email\n        if (!email) {\n          console.warn('Email not found in questionnaire responses, skipping email send');\n          return;\n        }\n        \n        const url = `${this.root}\/api\/v1\/shopify\/sendPredictionSessionEmail`;\n        const body = {\n          session_id: this.sessionId,\n          email: email,\n          name: name || '',\n          company_secret: \"cnuszhzcfb\"\n        };\n        \n        const response = await this.sendXMLReqPromise(url, body);\n        console.log('Prediction session email sent successfully:', response);\n      } catch (error) {\n        \/\/ Log error but don't block the flow\n        console.error('Failed to send prediction session email:', error);\n      }\n    },\n    sendXMLReq(url, body, func) {\n      const formData = new FormData();\n      let tempImages = null;\n      if (body.images) {\n        tempImages = body.images;\n        delete body.images;\n      }\n      const enData = this.encrypt(body);\n      formData.append(\"vars\", enData);\n      if (tempImages) {\n        tempImages.forEach(file => formData.append(\"images\", file));\n      }\n      const xhr = new XMLHttpRequest();\n      xhr.open(\"POST\", url);\n      xhr.onload = () => {\n        if (xhr.status === 200) {\n          let decodedString = JSON.parse(xhr.responseText);\n          decodedString = this.decrypt(decodedString);\n          func(decodedString);\n        } else {\n          this.errorText = `Request failed with status: ${xhr.status}`;\n          this.showModal = true;\n          this.load = false;\n        }\n      };\n      xhr.send(formData);\n    },\n    encrypt(str) {\n      const key = CryptoJS.enc.Utf8.parse(\"cBlGdZHJyylWsgco\");\n      const iv = CryptoJS.enc.Utf8.parse(\"HDwWkJthgEbIXrEr\");\n      const data = CryptoJS.enc.Utf8.parse(JSON.stringify(str));\n      const encrypt = CryptoJS.AES.encrypt(data, key, { iv, mode: CryptoJS.mode.CBC });\n      return CryptoJS.enc.Base64.stringify(encrypt.ciphertext);\n    },\n    decrypt(str) {\n      const key = CryptoJS.enc.Utf8.parse(\"cBlGdZHJyylWsgco\");\n      const iv = CryptoJS.enc.Utf8.parse(\"HDwWkJthgEbIXrEr\");\n      const base64 = CryptoJS.enc.Base64.parse(str);\n      const src = CryptoJS.enc.Base64.stringify(base64);\n      const decrypt = CryptoJS.AES.decrypt(src, key, { iv, mode: CryptoJS.mode.CBC });\n      return JSON.parse(CryptoJS.enc.Utf8.stringify(decrypt));\n    },\n    openBloomtasticPlatform() {\n      \/\/ window.open('https:\/\/haircosys.com\/en\/bloomtastic\/', '_blank');\n      window.open('https:\/\/haircosys.com\/en\/bloomtastic-ai-hair-and-scalp-check\/', '_blank');\n    }\n  }\n}).mount('#camera-page');\n<\/script>\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>{{ errorText }} Ok Terms and Conditions Privacy Agreement As the service provider of AI Hair &#038; Scalp Analysis, HairCoSys Limited (\u201cwe \u201c, \u201cour \u201d or \u201cus \u201c) respects your legal rights of privacy when collecting, storing, using and transmitting Personal Information (as defined below) and this Privacy Policy explains our privacy practices. It is [&hellip;]<\/p>\n","protected":false},"author":220402893,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_crdt_document":"","om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"jetpack_post_was_ever_published":true,"cybocfi_hide_featured_image":"","footnotes":""},"class_list":["post-8463","page","type-page","status-publish","hentry"],"aioseo_notices":[],"jetpack_likes_enabled":true,"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/PdLLAn-2cv","jetpack-related-posts":[{"id":10252,"url":"https:\/\/haircosys.com\/zh_sc\/hcs-privacy-policy\/","url_meta":{"origin":8463,"position":0},"title":"HCS Privacy Policy","author":"nitishmelwani","date":"2026 \u5e74 2 \u6708 13 \u65e5","format":false,"excerpt":"Privacy Policy Last Updated:\u00a0February 13, 2026HCS (Hey! Check Scalp) is committed to protecting your privacy. This Privacy Policy explains how we collect, use, and safeguard your personal information when you use our mobile application and related services.\u00a0Information We CollectPersonal Information we may collect includes:Name, gender, and date of birthEmail address\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":4322,"url":"https:\/\/haircosys.com\/zh_sc\/%e6%9d%a1%e6%ac%be-%e9%9a%90%e7%a7%81\/","url_meta":{"origin":8463,"position":1},"title":"\u6761\u6b3e\u4e0e\u9690\u79c1","author":"web admin","date":"2023 \u5e74 9 \u6708 7 \u65e5","format":false,"excerpt":"Privacy Policy Last Updated: January 21, 2026 IntroductionHairCoSys Ltd. respects your privacy. This policy applies to all data collected through our websites, the Bloomtastic App, and other HairCoSys products. Data We CollectAccount Data: Name, email, phone number, and business details.Analysis Data: Photographs of hair\/scalp, self-reported hair concerns, and scan results.Usage\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":2637,"url":"https:\/\/haircosys.com\/zh_sc\/%e9%9a%90%e7%a7%81-5\/","url_meta":{"origin":8463,"position":2},"title":"\u65e7\u9690\u79c1","author":"web admin","date":"2023 \u5e74 8 \u6708 1 \u65e5","format":false,"excerpt":"Our Story Our Beliefs Team HairCoSys AI x Hair Care Experts Career A.I. Solutions A.I. Haircare Check Hair Care & Beauty HairCoBlog Hair Wisdom Hair Ecosystem News EdCoSys Q&A X Facebook Linkedin Instagram Privacy Policy Privacy Agreement As your Service provider, HairCoSys Limited (\"we \", \"our \" or \"us \")\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":7520,"url":"https:\/\/haircosys.com\/zh_sc\/%e5%b8%b8%e8%a7%81%e9%97%ae%e9%a2%98-2\/","url_meta":{"origin":8463,"position":3},"title":"\u5e38\u89c1\u95ee\u9898","author":"web admin","date":"2025 \u5e74 8 \u6708 15 \u65e5","format":false,"excerpt":"FAQ Want to know more about Haircosys A.I. ? A.I. Haircare Check Why do I need to check my hair\/scalp condition? \u201cPrevention is better than cure\u201d. Many people have very little awareness of hair and scalp health, and only seek help when hair loss occurs, missing out on a golden\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":7356,"url":"https:\/\/haircosys.com\/zh_sc\/ai-%e5%a4%b4%e5%8f%91%e6%a3%80%e6%9f%a5\/","url_meta":{"origin":8463,"position":4},"title":"\u4eba\u5de5\u667a\u80fd\u5934\u53d1\u68c0\u67e5","author":"web admin","date":"2025 \u5e74 9 \u6708 4 \u65e5","format":false,"excerpt":"Patented AI Hair & Scalp Analysis to Unlock Unprecedented Business Growth Transform your clinic's diagnostics, achieve 60% more qualified leads with the Gold Medal-winning Bloomtastic AI platform. Dare to Try it Online? Integration Made Easy! Say Goodbye To Manual Tasks And Attract New Clients!Bloomtastic AI: The New Standard in Hair\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2026\/01\/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2026-01-08-181357.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2026\/01\/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2026-01-08-181357.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2026\/01\/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2026-01-08-181357.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2026\/01\/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2026-01-08-181357.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2026\/01\/%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE-2026-01-08-181357.png?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":1935,"url":"https:\/\/haircosys.com\/zh_sc\/q-%e5%92%8c-a\/","url_meta":{"origin":8463,"position":5},"title":"\u95ee\u7b54","author":"web admin","date":"2023 \u5e74 7 \u6708 7 \u65e5","format":false,"excerpt":"Our Story Our Beliefs Team HairCoSys AI x Hair Care Experts Career A.I. Solutions A.I. Haircare Check Hair Care & Beauty HairCoBlog Hair Wisdom Hair Ecosystem News EdCoSys Q&A X Facebook Linkedin Instagram Q&A A.I. Haircare Check Frequently Asked Questions Hair Care & Beauty Frequently Asked Questions Q&AA.I. Haircare Check\u2026","rel":"","context":"\u7c7b\u4f3c\u6587\u7ae0","block_context":{"text":"\u7c7b\u4f3c\u6587\u7ae0","link":""},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/haircosys.com\/wp-content\/uploads\/2023\/06\/logo-with-slogan.jpg?resize=1400%2C800&ssl=1 4x"},"classes":[]}],"_links":{"self":[{"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/pages\/8463","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/users\/220402893"}],"replies":[{"embeddable":true,"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/comments?post=8463"}],"version-history":[{"count":100,"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/pages\/8463\/revisions"}],"predecessor-version":[{"id":9212,"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/pages\/8463\/revisions\/9212"}],"wp:attachment":[{"href":"https:\/\/haircosys.com\/zh_sc\/wp-json\/wp\/v2\/media?parent=8463"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}