{"id":153229,"date":"2026-10-19T10:41:22","date_gmt":"2026-10-19T08:41:22","guid":{"rendered":"https:\/\/builders24.net\/?page_id=153229"},"modified":"2026-10-20T07:51:20","modified_gmt":"2026-10-20T07:51:20","slug":"ai-renovation-studio","status":"publish","type":"page","link":"https:\/\/builders24.net\/zw\/ai-renovation-studio\/","title":{"rendered":"AI Renovation Studio"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"153229\" class=\"elementor elementor-153229\">\n\t\t\t\t<div class=\"elementor-element elementor-element-5168976 e-con e-atomic-element e-flexbox-base e-default-div e-87f22aa \" data-id=\"5168976\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"5168976\">\n    \t\t<div class=\"elementor-element elementor-element-f77e422 elementor-widget elementor-widget-html\" data-id=\"f77e422\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t\t<!DOCTYPE html>\r\n<html lang=\"en\">\r\n<head>\r\n<meta charset=\"UTF-8\">\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, viewport-fit=cover\">\r\n<title>AI Design Studio | Room, Renovation and New Home Design | Builders24<\/title>\r\n<meta name=\"description\" content=\"Upload a photo of your room or house, or enter your stand size. AI Design Studio gives you a redesigned image, a layout check against your stand and a materials list with quantities and prices, ready to send as a quote request.\">\r\n<meta name=\"robots\" content=\"index, follow, max-image-preview:large\">\r\n<meta property=\"og:type\" content=\"website\">\r\n<meta property=\"og:site_name\" content=\"Builders24\">\r\n<meta property=\"og:title\" content=\"AI Design Studio | Builders24\">\r\n<meta property=\"og:description\" content=\"Upload a photo of your room or house, or enter your stand size. AI Design Studio gives you a redesigned image, a layout check against your stand and a materials list with quantities and prices, ready to send as a quote request.\">\r\n<meta property=\"og:image\" content=\"https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg\">\r\n<meta property=\"og:image:alt\" content=\"AI Design Studio by Builders24: redesign a room, renovate a house or design a home for your stand\">\r\n<meta name=\"twitter:card\" content=\"summary_large_image\">\r\n<meta name=\"twitter:title\" content=\"AI Design Studio | Builders24\">\r\n<meta name=\"twitter:description\" content=\"Upload a photo of your room or house, or enter your stand size. AI Design Studio gives you a redesigned image, a layout check against your stand and a materials list with quantities and prices, ready to send as a quote request.\">\r\n<meta name=\"twitter:image\" content=\"https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg\">\r\n<script type=\"application\/ld+json\">\r\n{\"@context\":\"https:\/\/schema.org\",\"@type\":\"WebApplication\",\"name\":\"AI Design Studio\",\"applicationCategory\":\"DesignApplication\",\"operatingSystem\":\"Any\",\"browserRequirements\":\"Requires JavaScript\",\"description\":\"Upload a photo of your room or house, or enter your stand size. AI Design Studio gives you a redesigned image, a layout check against your stand and a materials list with quantities and prices, ready to send as a quote request.\",\"featureList\":[\"AI redesign of a room from a photo\",\"AI renovation concepts for an existing house\",\"Home design for an empty stand with a layout check against the stand size\",\"Materials list with quantities and prices\",\"Turn a design into a quote request\"],\"provider\":{\"@type\":\"Organization\",\"name\":\"Builders24\",\"url\":\"https:\/\/builders24.net\"}}\r\n<\/script>\r\n<meta name=\"theme-color\" content=\"#0D1B3E\">\r\n<link rel=\"icon\" href=\"https:\/\/app.builders24.net\/favicon.ico\" sizes=\"any\">\r\n<link rel=\"apple-touch-icon\" href=\"https:\/\/app.builders24.net\/icons\/apple-touch-icon.png\">\r\n<link rel=\"manifest\" href=\"https:\/\/app.builders24.net\/manifest.json\">\r\n<link rel=\"preconnect\" href=\"https:\/\/fonts.googleapis.com\">\r\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Archivo+Narrow:wght@400;500;600;700&display=swap\" rel=\"stylesheet\"><!-- Archivo Narrow is the Builders24 font on all pages -->\r\n<style>\r\n\/* Everything is scoped to #b24ds so nothing here can restyle the WordPress site around it. *\/\r\n#b24ds{\r\n    --navy:#0D1B3E; --navy-2:#16295A; --navy-3:#091430; --orange:#F0A500;\r\n    --border:rgba(255,255,255,0.18); --pad:20px;\r\n    --green:#4ADE80; --red:#FF8080;\r\n  }\r\n#b24ds, #b24ds *{box-sizing:border-box; margin:0; padding:0;}\r\n#b24ds{ background-color:#0a1530; }\r\n#b24ds{\r\n    font-family:'Archivo Narrow', sans-serif; color:#fff; -webkit-font-smoothing:antialiased;\r\n    background-image:linear-gradient(rgba(8,17,40,0.45), rgba(8,17,40,0.6)), url('https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg');\r\n    background-size:cover; background-position:center; background-attachment:fixed; background-repeat:no-repeat;\r\n    background-color:#0a1530;\r\n  }\r\n#b24ds #app#app h1,#b24ds #app#app h2,#b24ds #app#app h3,#b24ds #app#app h4,#b24ds #app#app h5,#b24ds #app#app h6{ color:#fff !important; -webkit-text-fill-color:#fff !important; }\r\n#b24ds #app#app .orange{ color:var(--orange) !important; -webkit-text-fill-color:var(--orange) !important; }\r\n#b24ds a{ text-decoration:none; color:inherit; }\r\n#b24ds button{ font-family:inherit; border:none; cursor:pointer; background:none; color:inherit; }\r\n#b24ds input,#b24ds select,#b24ds textarea{ font-family:inherit; }\r\n#b24ds ::-webkit-scrollbar{ display:none; }\r\n#b24ds, #b24ds *{ scrollbar-width:none; }\r\n#b24ds [hidden]{ display:none !important; }\r\n#b24ds .page{ padding:36px var(--pad) 140px; background:var(--navy); position:relative; z-index:1; margin-top:6px; border-top:2px solid rgba(240,165,0,0.35); box-shadow:0 -20px 40px rgba(0,0,0,0.35); }\r\n@media(min-width:900px){#b24ds .page{ max-width:1200px; margin:6px auto 0; padding:56px 40px 60px; }\r\n }\r\n#b24ds .glass{ background:rgba(20,32,66,0.62); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px); border:1px solid var(--border); border-radius:16px; }\r\n#b24ds section{ margin-top:40px; }\r\n#b24ds section:first-child{ margin-top:0; }\r\n#b24ds .sec-head{ margin-bottom:18px; }\r\n#b24ds .sec-head h2{ font-size:22px; font-weight:700; margin-bottom:4px; }\r\n#b24ds .sec-head p{ font-size:14px; }\r\n#b24ds \/* ===== HERO ===== *\/\r\n  .hero-eyebrow{ color:var(--orange); font-size:13px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-bottom:10px; }\r\n#b24ds .thero{ position:relative; min-height:0; display:flex; align-items:center; overflow:hidden; background:var(--navy-3); }\r\n@media(min-width:900px){#b24ds .thero{ border-radius:0 0 24px 24px; }\r\n }\r\n#b24ds .thero-bg{ position:absolute; inset:0; background-image:url('https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg'); background-size:cover; background-position:center; transform:scale(1.05); }\r\n@keyframes hero-pan{ 0%{ transform:scale(1.05) translateX(0); } 100%{ transform:scale(1.1) translateX(-1.5%); } }\r\n#b24ds .thero-overlay{ position:absolute; inset:0; background:linear-gradient(120deg, rgba(9,15,32,0.96) 10%, rgba(9,15,32,0.75) 55%, rgba(9,15,32,0.5) 100%); }\r\n#b24ds .thero-grid{ position:absolute; inset:0; background-image:linear-gradient(rgba(240,165,0,0.05) 1px,transparent 1px),linear-gradient(90deg,rgba(240,165,0,0.05) 1px,transparent 1px); background-size:52px 52px; pointer-events:none; }\r\n#b24ds .thero-orb{ position:absolute; top:-110px; right:-60px; width:380px; height:380px; border-radius:50%; background:radial-gradient(circle, rgba(240,165,0,0.14) 0%, transparent 70%); pointer-events:none; }\r\n@keyframes orb-float{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-20px); } }\r\n#b24ds .thero-inner{ position:relative; z-index:2; padding:26px var(--pad) 24px; width:100%; text-align:center; }\r\n@media(min-width:900px){#b24ds .thero-inner{ padding:34px 40px 30px; }\r\n }\r\n#b24ds .thero h1{ font-size:clamp(26px,4.4vw,38px); font-weight:700; line-height:1.15; margin:0 auto 10px; max-width:680px; }\r\n#b24ds .thero h1 .orange{ color:var(--orange); }\r\n#b24ds .thero p{ font-size:16px; max-width:560px; margin:0 auto 14px; line-height:1.5; }\r\n#b24ds .hero-trust{ display:flex; flex-wrap:wrap; justify-content:center; gap:8px 10px; max-width:760px; margin:0 auto; }\r\n#b24ds .hero-trust span{ display:inline-flex; align-items:center; gap:6px; padding:7px 12px; border-radius:999px; border:1px solid var(--border); background:rgba(20,32,66,0.62); font-size:13px; font-weight:700; }\r\n#b24ds .hero-trust span:before{ content:''; width:7px; height:7px; border-radius:50%; background:var(--orange); }\r\n#b24ds \/* ===== STEP CARDS ===== *\/\r\n  .tcard-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; perspective:1200px; }\r\n@media(min-width:768px){#b24ds .tcard-grid{ grid-template-columns:repeat(4,1fr); }\r\n }\r\n#b24ds .tcard{ position:relative; border-radius:16px; padding:16px 14px; transition:border-color .3s; }\r\n#b24ds .tcard:hover{ border-color:rgba(240,165,0,0.4) !important; }\r\n#b24ds .tcard .n{ width:28px; height:28px; border-radius:50%; background:var(--orange); color:var(--navy-3); font-weight:700; font-size:13px; display:grid; place-items:center; margin-bottom:10px; }\r\n#b24ds .tcard h3,#b24ds .tcard h4{ font-size:15px; font-weight:700; margin-bottom:4px; }\r\n#b24ds .tcard p{ font-size:13px; line-height:1.45; }\r\n#b24ds \/* ===== TOOL ===== *\/\r\n  .vz-tool{ padding:0; overflow:hidden; }\r\n#b24ds .vz-q{ font-size:clamp(20px,3vw,26px); font-weight:700; margin-bottom:6px; }\r\n#b24ds .vz-qs{ font-size:15px; margin-bottom:18px; }\r\n#b24ds .vz-choice{ display:grid; grid-template-columns:1fr; gap:12px; }\r\n@media(min-width:700px){#b24ds .vz-choice{ grid-template-columns:repeat(3,1fr); }\r\n }\r\n#b24ds .vz-pick{ text-align:left; padding:20px 18px; border-radius:16px; border:1.5px solid var(--border); background:rgba(255,255,255,0.07); min-height:120px; display:flex; flex-direction:column; gap:6px; transition:border-color .2s, transform .2s, background .2s; }\r\n#b24ds .vz-pick:hover,#b24ds .vz-pick:focus-visible{ border-color:var(--orange); background:var(--navy-2); outline:none; }\r\n#b24ds .vz-pick svg{ color:var(--orange); margin-bottom:4px; }\r\n#b24ds .vz-pick b{ font-size:19px; font-weight:700; }\r\n#b24ds .vz-pick span{ font-size:14px; line-height:1.4; }\r\n#b24ds .vz-back{ display:inline-flex; align-items:center; gap:6px; min-height:44px; font-size:15px; font-weight:700; color:var(--orange); }\r\n#b24ds .vz-formhead{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:18px; }\r\n#b24ds .vz-formhead h3{ font-size:clamp(19px,3vw,24px); font-weight:700; }\r\n#b24ds .vz-reveal{ animation:vzreveal .35s ease both; }\r\n@keyframes vzreveal{ from{ opacity:0; transform:translateY(14px); } to{ opacity:1; transform:none; } }\r\n#b24ds .vz-credits{ padding:14px 16px 16px; border-radius:14px; background:rgba(255,255,255,0.07); border:1px solid var(--border); margin-bottom:24px; }\r\n#b24ds .vz-cr-sel{ display:flex; align-items:center; gap:8px; font-size:14px; font-weight:600; }\r\n#b24ds .vz-cr-sel select{ width:auto; min-height:42px; padding:8px 12px; font-size:15px; max-width:230px; }\r\n#b24ds .vz-cr-wrap{ display:flex; flex-wrap:wrap; align-items:center; gap:16px 22px; }\r\n#b24ds .vz-donut{ position:relative; width:116px; height:116px; flex:none; }\r\n#b24ds .vz-donut svg{ width:100%; height:100%; transform:rotate(-90deg); display:block; }\r\n#b24ds .vz-donut .trk{ fill:none; stroke:rgba(255,255,255,0.2); stroke-width:11; }\r\n#b24ds .vz-donut .arc{ fill:none; stroke:var(--orange); stroke-width:11; stroke-linecap:round; stroke-dasharray:0 263.9; transition:stroke-dasharray .8s ease; }\r\n#b24ds .vz-credits.out .vz-donut .arc{ stroke:var(--red); }\r\n#b24ds .vz-donut .mid{ position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:1.05; }\r\n#b24ds .vz-donut .mid b{ font-size:30px; font-weight:700; color:var(--orange); }\r\n#b24ds .vz-donut .mid span{ font-size:13px; font-weight:600; margin-top:2px; }\r\n#b24ds .vz-cr-info{ flex:1 1 240px; min-width:0; display:flex; flex-direction:column; gap:12px; }\r\n#b24ds .vz-cr-sub{ font-size:14px; line-height:1.45; }\r\n#b24ds \/* toasts *\/\r\n  .vz-toasts{ position:fixed; left:50%; transform:translateX(-50%); bottom:96px; z-index:1100; width:min(420px, calc(100% - 32px)); display:flex; flex-direction:column-reverse; gap:10px; pointer-events:none; }\r\n#b24ds .vz-toast{ display:flex; align-items:center; gap:12px; padding:13px 16px; border-radius:14px; background:rgba(9,20,48,0.96); border:1.5px solid var(--border); box-shadow:0 12px 32px rgba(0,0,0,0.45); font-size:15px; font-weight:600; animation:vztin .35s cubic-bezier(.2,.9,.3,1.2) both; }\r\n#b24ds .vz-toast[data-state=ok]{ border-color:var(--green); }\r\n#b24ds .vz-toast[data-state=err]{ border-color:var(--red); }\r\n#b24ds .vz-toast[data-state=busy]{ border-color:var(--orange); }\r\n#b24ds .vz-toast.out{ animation:vztout .3s ease both; }\r\n@keyframes vztin{ from{ opacity:0; transform:translateY(24px) scale(.95); } to{ opacity:1; transform:none; } }\r\n@keyframes vztout{ to{ opacity:0; transform:translateY(12px) scale(.96); } }\r\n#b24ds .vz-ti{ flex:none; width:26px; height:26px; display:grid; place-items:center; }\r\n#b24ds .vz-tspin{ width:22px; height:22px; border-radius:50%; border:3px solid rgba(255,255,255,0.25); border-top-color:var(--orange); animation:vzspin .8s linear infinite; }\r\n#b24ds .vz-ti svg{ width:26px; height:26px; }\r\n#b24ds .vz-ti .ring{ fill:none; stroke-width:2.5; stroke-dasharray:70; stroke-dashoffset:70; animation:vzdraw .45s ease forwards; }\r\n#b24ds .vz-ti .tick{ fill:none; stroke-width:3; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:30; stroke-dashoffset:30; animation:vzdraw .35s .25s ease forwards; }\r\n#b24ds [data-state=ok] .vz-ti .ring,#b24ds [data-state=ok] .vz-ti .tick{ stroke:var(--green); }\r\n#b24ds [data-state=err] .vz-ti .ring,#b24ds [data-state=err] .vz-ti .tick{ stroke:var(--red); }\r\n@keyframes vzdraw{ to{ stroke-dashoffset:0; } }\r\n#b24ds .vz-body{ padding:22px 18px 26px; }\r\n@media(min-width:700px){#b24ds .vz-body{ padding:30px 32px 34px; }\r\n }\r\n#b24ds .vz-field{ margin-bottom:20px; }\r\n#b24ds .vz-lbl{ display:block; font-size:15px; font-weight:700; margin-bottom:8px; }\r\n#b24ds .vz-lbl i{ font-style:normal; color:var(--red); }\r\n#b24ds .vz-hint{ font-size:13px; margin-bottom:10px; line-height:1.45; }\r\n#b24ds .vz-in{ width:100%; min-height:48px; background:rgba(255,255,255,0.08); border:1px solid var(--border); border-radius:10px; padding:12px 14px; font-size:16px; color:#fff; outline:none; }\r\n#b24ds .vz-in:focus{ border-color:var(--orange); }\r\n#b24ds textarea.vz-in{ min-height:110px; resize:vertical; line-height:1.5; }\r\n#b24ds select.vz-in option{ background:#10182f; color:#fff; }\r\n#b24ds .vz-in::placeholder{ color:#E6ECFA; opacity:1; }\r\n#b24ds .vz-grid2{ display:grid; grid-template-columns:1fr; gap:14px; }\r\n#b24ds .vz-grid3{ display:grid; grid-template-columns:1fr; gap:14px; }\r\n@media(min-width:600px){#b24ds .vz-grid2{ grid-template-columns:1fr 1fr; }\r\n#b24ds .vz-grid3{ grid-template-columns:repeat(3,1fr); }\r\n }\r\n#b24ds .vz-size{ display:grid; grid-template-columns:1fr; gap:12px; align-items:end; }\r\n@media(min-width:700px){#b24ds .vz-size{ grid-template-columns:1fr 1fr auto 1fr; }\r\n }\r\n#b24ds .vz-or{ text-align:center; font-weight:700; font-size:14px; padding-bottom:14px; }\r\n@media(max-width:699px){#b24ds .vz-or{ padding:0; }\r\n }\r\n#b24ds .vz-mini{ display:block; font-size:13px; font-weight:700; margin-bottom:6px; }\r\n#b24ds .vz-seg{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }\r\n#b24ds .vz-seg button{ min-height:52px; border-radius:12px; border:1.5px solid var(--border); background:rgba(255,255,255,0.06); font-size:15px; font-weight:700; padding:10px; }\r\n#b24ds .vz-seg button.on{ border-color:var(--orange); background:rgba(240,165,0,0.2); color:#fff; box-shadow:inset 0 0 0 1px var(--orange); }\r\n#b24ds .vz-extras{ border:1px solid var(--border); border-radius:12px; padding:0; margin-bottom:20px; background:rgba(255,255,255,0.04); }\r\n#b24ds .vz-extras summary{ list-style:none; cursor:pointer; padding:14px 16px; font-weight:700; font-size:15px; display:flex; justify-content:space-between; align-items:center; min-height:52px; }\r\n#b24ds .vz-extras summary::-webkit-details-marker{ display:none; }\r\n#b24ds .vz-extras summary:after{ content:'+'; font-size:20px; color:var(--orange); }\r\n#b24ds .vz-extras[open] summary:after{ content:'\u2013'; }\r\n#b24ds .vz-extras-body{ padding:4px 16px 4px; }\r\n#b24ds .vz-link{ color:var(--orange); font-weight:700; font-size:14px; text-decoration:underline; text-underline-offset:3px; min-height:44px; display:inline-flex; align-items:center; }\r\n#b24ds .vz-submit{ width:100%; min-height:54px; background:var(--orange); color:var(--navy-3); font-weight:700; font-size:17px; border-radius:12px; padding:14px 22px; }\r\n@media(min-width:700px){#b24ds .vz-submit{ width:auto; min-width:300px; }\r\n }\r\n#b24ds .vz-submit:disabled{ background:rgba(255,255,255,0.18); color:#fff; cursor:not-allowed; }\r\n#b24ds .vz-note{ font-size:13px; margin-top:10px; }\r\n#b24ds .vz-err{ color:var(--red); font-size:15px; font-weight:700; margin:0 0 14px; line-height:1.45; }\r\n#b24ds .vz-err:empty{ display:none; }\r\n#b24ds \/* photos *\/\r\n  .vz-photos{ display:grid; grid-template-columns:repeat(auto-fill,minmax(92px,1fr)); gap:10px; }\r\n#b24ds .vz-photo{ position:relative; aspect-ratio:1; border-radius:12px; overflow:hidden; border:1.5px solid var(--border); background:rgba(255,255,255,0.04); }\r\n#b24ds .vz-photo img{ width:100%; height:100%; object-fit:cover; display:block; }\r\n#b24ds .vz-photo button{ position:absolute; top:5px; right:5px; width:28px; height:28px; border-radius:50%; background:rgba(0,0,0,0.75); font-size:13px; display:flex; align-items:center; justify-content:center; }\r\n#b24ds .vz-add{ aspect-ratio:1; border-radius:12px; border:1.5px dashed var(--border); background:rgba(255,255,255,0.04); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; color:var(--orange); font-size:13px; font-weight:700; min-height:92px; }\r\n#b24ds .vz-add:hover{ border-color:var(--orange); }\r\n#b24ds .vz-photo-btns{ display:flex; flex-wrap:wrap; gap:10px; margin-top:12px; }\r\n#b24ds .vz-photo-btns button{ min-height:46px; padding:10px 18px; border-radius:10px; font-weight:700; font-size:14px; background:rgba(255,255,255,0.1); border:1.5px solid var(--border); }\r\n#b24ds .vz-photo-btns button.p{ background:var(--orange); color:var(--navy-3); border-color:var(--orange); }\r\n#b24ds .vz-photo-btns svg{ margin-right:6px; vertical-align:-3px; }\r\n#b24ds \/* gate *\/\r\n  .vz-gate{ text-align:center; padding:34px 14px; }\r\n#b24ds .vz-gate h3{ font-size:21px; font-weight:700; margin-bottom:8px; }\r\n#b24ds .vz-gate p{ font-size:15px; max-width:480px; margin:0 auto 20px; line-height:1.55; }\r\n#b24ds .vz-gate a{ display:inline-flex; align-items:center; justify-content:center; min-height:52px; padding:14px 28px; background:var(--orange); color:var(--navy-3); font-weight:700; border-radius:12px; font-size:16px; }\r\n#b24ds \/* progress *\/\r\n  .vz-prog{ text-align:center; padding:26px 8px; }\r\n#b24ds .vz-spin{ width:54px; height:54px; border-radius:50%; border:5px solid rgba(255,255,255,0.2); border-top-color:var(--orange); margin:0 auto 18px; animation:vzspin 1s linear infinite; }\r\n@keyframes vzspin{ to{ transform:rotate(360deg); } }\r\n#b24ds .vz-prog h3{ font-size:20px; font-weight:700; margin-bottom:6px; }\r\n#b24ds .vz-steps{ display:inline-flex; flex-direction:column; gap:10px; text-align:left; margin:18px 0 8px; }\r\n#b24ds .vz-step{ display:flex; align-items:center; gap:10px; font-size:15px; font-weight:700; }\r\n#b24ds .vz-step b{ width:26px; height:26px; border-radius:50%; border:2px solid var(--border); display:grid; place-items:center; font-size:12px; flex-shrink:0; }\r\n#b24ds .vz-step.on b{ border-color:var(--orange); background:var(--orange); color:var(--navy-3); }\r\n#b24ds .vz-step.done b{ border-color:var(--green); background:var(--green); color:var(--navy-3); }\r\n#b24ds \/* result *\/\r\n  .vz-res-head{ display:flex; flex-wrap:wrap; justify-content:space-between; align-items:flex-start; gap:12px 16px; margin-bottom:18px; }\r\n#b24ds .vz-res-head h3{ font-size:clamp(20px,3vw,26px); font-weight:700; }\r\n#b24ds .vz-res-head p{ font-size:14px; margin-top:4px; }\r\n#b24ds .vz-actions{ display:flex; flex-wrap:wrap; gap:10px; }\r\n#b24ds .vz-btn{ min-height:48px; padding:12px 20px; border-radius:11px; font-weight:700; font-size:15px; background:rgba(255,255,255,0.1); border:1.5px solid var(--border); display:inline-flex; align-items:center; justify-content:center; }\r\n#b24ds .vz-btn.p{ background:var(--orange); color:var(--navy-3); border-color:var(--orange); }\r\n#b24ds .vz-btn:disabled{ background:rgba(255,255,255,0.18); cursor:not-allowed; }\r\n#b24ds .vz-ba{ display:grid; grid-template-columns:1fr; gap:14px; margin-bottom:22px; }\r\n@media(min-width:760px){#b24ds .vz-ba{ grid-template-columns:1fr 1.4fr; }\r\n }\r\n#b24ds .vz-ba figure{ border-radius:14px; overflow:hidden; border:1px solid var(--border); background:#091430; }\r\n#b24ds .vz-ba img{ width:100%; display:block; max-height:560px; object-fit:contain; background:#091430; }\r\n#b24ds .vz-ba figcaption{ padding:10px 14px; font-size:13px; font-weight:700; text-transform:uppercase; letter-spacing:.06em; }\r\n#b24ds .vz-box{ padding:18px; margin-bottom:16px; }\r\n#b24ds .vz-box h4{ font-size:17px; font-weight:700; margin-bottom:10px; }\r\n#b24ds .vz-box p{ font-size:15px; line-height:1.6; }\r\n#b24ds .vz-pal{ display:flex; gap:8px; margin:12px 0 0; flex-wrap:wrap; }\r\n#b24ds .vz-sw{ width:34px; height:34px; border-radius:9px; border:1.5px solid #fff; }\r\n#b24ds .vz-list{ list-style:none; display:flex; flex-direction:column; gap:9px; }\r\n#b24ds .vz-list li{ display:flex; gap:10px; font-size:15px; line-height:1.45; }\r\n#b24ds .vz-list li:before{ content:'\u2022'; color:var(--orange); font-weight:700; flex:none; }\r\n#b24ds .vz-checks li:before{ content:none; }\r\n#b24ds .vz-checks .ic{ flex:none; width:22px; height:22px; border-radius:50%; display:grid; place-items:center; font-size:13px; font-weight:700; color:var(--navy-3); }\r\n#b24ds .vz-checks .ok{ background:var(--green); }\r\n#b24ds .vz-checks .bad{ background:var(--red); }\r\n#b24ds .vz-two{ display:grid; grid-template-columns:1fr; gap:16px; }\r\n@media(min-width:760px){#b24ds .vz-two{ grid-template-columns:1fr 1fr; }\r\n }\r\n#b24ds .vz-bar{ height:12px; border-radius:6px; background:rgba(255,255,255,0.2); margin:10px 0 6px; overflow:hidden; }\r\n#b24ds .vz-bar i{ display:block; height:100%; background:var(--green); border-radius:6px; }\r\n#b24ds .vz-bar.over i{ background:var(--red); }\r\n#b24ds .vz-kv{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-top:12px; }\r\n@media(min-width:760px){#b24ds .vz-kv{ grid-template-columns:repeat(4,1fr); }\r\n }\r\n#b24ds .vz-kv div{ background:rgba(255,255,255,0.08); border-radius:10px; padding:10px 12px; }\r\n#b24ds .vz-kv b{ display:block; font-size:19px; font-weight:700; }\r\n#b24ds .vz-kv span{ font-size:12.5px; font-weight:600; }\r\n#b24ds .vz-table{ width:100%; border-collapse:collapse; font-size:14px; }\r\n#b24ds .vz-table th{ text-align:left; font-size:12.5px; text-transform:uppercase; letter-spacing:.05em; padding:8px 8px; border-bottom:1px solid var(--border); }\r\n#b24ds .vz-table td{ padding:11px 8px; border-bottom:1px solid rgba(255,255,255,0.12); vertical-align:top; }\r\n#b24ds .vz-table td.r,#b24ds .vz-table th.r{ text-align:right; white-space:nowrap; }\r\n#b24ds .vz-scroll{ overflow-x:auto; }\r\n#b24ds .vz-mat{ display:none; }\r\n@media(max-width:699px){#b24ds .vz-scroll .vz-table{ display:none; }\r\n#b24ds .vz-mat{ display:block; }\r\n\r\n  }\r\n#b24ds .vz-mat-item{ padding:13px 0; border-bottom:1px solid rgba(255,255,255,0.15); }\r\n#b24ds .vz-mat-item:last-child{ border-bottom:none; }\r\n#b24ds .vz-mat-item b{ display:block; font-size:15px; }\r\n#b24ds .vz-mat-item .m{ display:flex; justify-content:space-between; gap:10px; font-size:14px; margin-top:6px; }\r\n#b24ds .vz-total{ display:flex; justify-content:space-between; gap:12px; padding:14px 0 0; font-size:17px; font-weight:700; }\r\n#b24ds .vz-shop{ color:var(--orange); font-weight:700; }\r\n#b24ds .vz-fine{ font-size:13px; line-height:1.5; margin-top:12px; }\r\n#b24ds \/* my designs *\/\r\n  .vz-mine{ display:grid; grid-template-columns:repeat(2,1fr); gap:12px; }\r\n@media(min-width:760px){#b24ds .vz-mine{ grid-template-columns:repeat(4,1fr); }\r\n }\r\n#b24ds .vz-card{ text-align:left; border-radius:14px; overflow:hidden; border:1px solid var(--border); background:rgba(20,32,66,0.62); display:flex; flex-direction:column; }\r\n#b24ds .vz-card:hover{ border-color:var(--orange); }\r\n#b24ds .vz-thumb{ aspect-ratio:4\/3; background:#091430 center\/cover no-repeat; display:grid; place-items:center; font-size:13px; font-weight:700; }\r\n#b24ds .vz-card-b{ padding:10px 12px 12px; }\r\n#b24ds .vz-card-b b{ display:block; font-size:14px; line-height:1.3; }\r\n#b24ds .vz-card-b span{ font-size:12.5px; font-weight:600; }\r\n#b24ds \/* styles *\/\r\n  .style-grid{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-bottom:6px; }\r\n@media(min-width:520px){#b24ds .style-grid{ grid-template-columns:repeat(3,1fr); }\r\n }\r\n@media(min-width:900px){#b24ds .style-grid{ grid-template-columns:repeat(6,1fr); }\r\n }\r\n#b24ds .style-chip{ padding:14px 10px; text-align:center; cursor:pointer; transition:border-color .15s; animation:none; }\r\n#b24ds .style-chip:hover,#b24ds .style-chip.active{ border-color:var(--orange); }\r\n#b24ds .style-chip .sw{ width:100%; height:56px; border-radius:8px; margin-bottom:8px; background-size:cover; background-position:center; }\r\n#b24ds .style-chip span{ font-size:13px; font-weight:700; }\r\n#b24ds \/* design references (Builders24 house plans) *\/\r\n  .vz-refgrid{ display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-bottom:6px; }\r\n@media(min-width:520px){#b24ds .vz-refgrid{ grid-template-columns:repeat(3,1fr); }\r\n }\r\n#b24ds .vz-refchip{ padding:10px; text-align:left; cursor:pointer; transition:border-color .15s; animation:none; border:2px solid transparent; }\r\n#b24ds .vz-refchip:hover,#b24ds .vz-refchip.active{ border-color:var(--orange); }\r\n#b24ds .vz-refchip .sw{ width:100%; height:110px; border-radius:8px; margin-bottom:8px; background-size:contain; background-repeat:no-repeat; background-position:center; background-color:#091430; }\r\n#b24ds .vz-refchip b{ display:block; font-size:14px; font-weight:700; }\r\n#b24ds .vz-refchip small{ display:block; font-size:12px; font-weight:500; margin-top:2px; }\r\n#b24ds \/* quiz modal *\/\r\n  .pref-overlay{ display:none; position:fixed; inset:0; z-index:999; background:rgba(9,15,32,0.82); backdrop-filter:blur(4px); align-items:center; justify-content:center; padding:16px; }\r\n#b24ds .pref-overlay.open{ display:flex; }\r\n#b24ds .pref-modal{ background:var(--navy); border:1px solid var(--border); border-radius:18px; width:100%; max-width:480px; max-height:90vh; overflow-y:auto; padding:26px 22px; }\r\n#b24ds .pref-progress{ display:flex; gap:5px; margin-bottom:18px; }\r\n#b24ds .pref-progress-seg{ flex:1; height:4px; border-radius:2px; background:var(--border); }\r\n#b24ds .pref-progress-seg.done{ background:var(--orange); }\r\n#b24ds .pref-step-label{ font-size:12px; font-weight:700; letter-spacing:.1em; text-transform:uppercase; color:var(--orange); margin-bottom:8px; }\r\n#b24ds .pref-question{ font-size:18px; font-weight:700; margin-bottom:16px; line-height:1.3; }\r\n#b24ds .pref-options{ display:flex; flex-direction:column; gap:9px; margin-bottom:20px; }\r\n#b24ds .pref-opt{ text-align:left; padding:13px 14px; border-radius:10px; border:1.5px solid var(--border); background:rgba(255,255,255,0.05); font-size:14.5px; font-weight:600; min-height:48px; }\r\n#b24ds .pref-opt:hover{ border-color:var(--orange); }\r\n#b24ds .pref-opt.selected{ border-color:var(--orange); background:rgba(240,165,0,0.16); }\r\n#b24ds .pref-nav{ display:flex; justify-content:space-between; align-items:center; }\r\n#b24ds .pref-skip{ font-size:14px; font-weight:700; text-decoration:underline; padding:10px 0; }\r\n#b24ds .pref-next{ background:var(--orange); color:var(--navy-3); font-weight:700; font-size:15px; padding:12px 24px; border-radius:10px; min-height:46px; }\r\n#b24ds .pref-next:disabled{ background:rgba(255,255,255,0.18); color:#fff; cursor:not-allowed; }\r\n#b24ds \/* why \/ stats \/ trust *\/\r\n  .stats-row{ display:grid; grid-template-columns:repeat(3,1fr); gap:12px; }\r\n#b24ds .stat-card{ padding:18px 8px; text-align:center; }\r\n@media(max-width:420px){#b24ds .stats-row{ gap:8px; }\r\n#b24ds .stat-card .val{ font-size:19px; }\r\n#b24ds .stat-card .lbl{ font-size:12px; }\r\n }\r\n#b24ds .stat-card .val{ font-size:22px; font-weight:700; color:var(--orange); }\r\n#b24ds .stat-card .lbl{ font-size:13px; margin-top:4px; }\r\n#b24ds .trust-strip{ display:grid; grid-template-columns:repeat(2,1fr); gap:14px; padding:22px; }\r\n@media(min-width:768px){#b24ds .trust-strip{ grid-template-columns:repeat(5,1fr); }\r\n }\r\n#b24ds .trust-item{ text-align:center; }\r\n#b24ds .trust-item b{ display:block; font-size:14px; font-weight:700; margin-bottom:2px; }\r\n#b24ds .trust-item span{ font-size:12.5px; }\r\n#b24ds \/* motion: off by default. Only the hero pan and orb run,#b24ds and only on large screens with a mouse and no reduced-motion setting *\/\r\n  @media(min-width:900px) and (hover:hover) and (prefers-reduced-motion:no-preference){\r\n    .thero-bg{ animation:hero-pan 18s ease-in-out infinite alternate; }\r\n    .thero-orb{ animation:orb-float 8s ease-in-out infinite; }\r\n  }\r\n@keyframes hero-pan{ 0%{ transform:scale(1.05) translateX(0); } 100%{ transform:scale(1.1) translateX(-1.5%); } }\r\n@keyframes orb-float{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-20px); } }\r\n@media(prefers-reduced-motion:reduce){#b24ds .wz-card,#b24ds .wz-bar i{ animation:none !important; transition:none !important; }\r\n#b24ds .vz-spin,#b24ds .vz-tspin,#b24ds .vz-reveal,#b24ds .vz-toast,#b24ds .vz-ti .ring,#b24ds .vz-ti .tick,#b24ds .vz-skel{ animation:none !important; }\r\n#b24ds .vz-ti .ring,#b24ds .vz-ti .tick{ stroke-dashoffset:0; }\r\n#b24ds .vz-donut .arc,#b24ds .vz-pbar i{ transition:none; }\r\n }\r\n#b24ds \/* ===== audit additions ===== *\/\r\n  .vz-skel{ background:linear-gradient(90deg, rgba(255,255,255,0.08), rgba(255,255,255,0.18), rgba(255,255,255,0.08)); background-size:200% 100%; animation:vzsk 1.4s linear infinite; border-radius:12px; }\r\n@keyframes vzsk{ to{ background-position:-200% 0; } }\r\n#b24ds .vz-load{ display:grid; gap:12px; padding:6px 0; }\r\n#b24ds .vz-load .vz-skel:nth-child(1){ height:84px; }\r\n#b24ds .vz-load .vz-skel:nth-child(2){ height:150px; }\r\n#b24ds .vz-gal{ display:flex; gap:12px; overflow-x:auto; padding-bottom:6px; scroll-snap-type:x proximity; }\r\n#b24ds .vz-gcard{ flex:0 0 min(260px,78%); scroll-snap-align:start; text-align:left; border-radius:14px; overflow:hidden; border:1.5px solid var(--border); background:rgba(20,32,66,0.62); }\r\n#b24ds .vz-gcard:hover,#b24ds .vz-gcard:focus-visible{ border-color:var(--orange); outline:none; }\r\n#b24ds .vz-gcard .gi{ height:160px; background:#091430 center\/cover no-repeat; }\r\n#b24ds .vz-gcard .gb{ padding:10px 12px 12px; }\r\n#b24ds .vz-gcard b{ display:block; font-size:15px; font-weight:700; }\r\n#b24ds .vz-gcard small{ display:block; font-size:12.5px; font-weight:600; margin-top:2px; }\r\n#b24ds .vz-pick .pv{ height:120px; border-radius:10px; margin-bottom:8px; background:rgba(255,255,255,0.08) center\/cover no-repeat; display:grid; place-items:center; }\r\n#b24ds .vz-pick .pv.has-img svg{ display:none; }\r\n#b24ds .vz-pick{ min-height:0; }\r\n#b24ds .vz-pick .pv{ height:auto; background:transparent; margin-bottom:4px; place-items:start; }\r\n#b24ds .vz-alt{ margin-top:16px; }\r\n#b24ds .vz-alt button{ min-height:44px; padding:10px 4px; font-size:15px; font-weight:700; text-decoration:underline; text-underline-offset:3px; }\r\n#b24ds .vz-failed{ display:grid; gap:8px; margin-top:14px; }\r\n#b24ds .vz-frow{ display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 14px; border-radius:12px; border:1px solid var(--border); background:rgba(20,32,66,0.62); }\r\n#b24ds .vz-frow div{ min-width:0; }\r\n#b24ds .vz-frow b{ display:block; font-size:14px; line-height:1.3; }\r\n#b24ds .vz-frow span{ font-size:12.5px; font-weight:600; }\r\n#b24ds .vz-frow button{ flex:none; min-height:44px; padding:8px 16px; border-radius:10px; border:1.5px solid var(--border); font-size:14px; font-weight:700; }\r\n#b24ds .vz-frow button:hover{ border-color:var(--orange); }\r\n#b24ds .vz-or2{ font-size:14px; font-weight:700; margin:14px 0 8px; }\r\n#b24ds .vz-in.big{ font-size:20px; min-height:56px; font-weight:700; }\r\n#b24ds .vz-calc{ font-size:14px; font-weight:700; margin-top:8px; color:var(--orange); }\r\n#b24ds .vz-extras summary small{ display:block; font-size:12.5px; font-weight:600; margin-top:2px; }\r\n#b24ds .vz-chips{ display:flex; flex-wrap:wrap; gap:8px; margin:0 0 10px; }\r\n#b24ds .vz-chip{ min-height:40px; padding:8px 14px; border-radius:999px; border:1.5px solid var(--border); background:rgba(255,255,255,0.08); font-size:14px; font-weight:700; }\r\n#b24ds .vz-chip:hover{ border-color:var(--orange); }\r\n#b24ds \/* conversational flow for an existing property *\/\r\n  .wz-top{ display:flex; align-items:center; gap:12px; margin-bottom:20px; }\r\n#b24ds .wz-bar{ flex:1; height:6px; border-radius:99px; background:rgba(255,255,255,0.22); overflow:hidden; }\r\n#b24ds .wz-bar i{ display:block; height:100%; width:0; background:var(--orange); transition:width .3s; }\r\n#b24ds .wz-count{ font-size:13px; font-weight:700; white-space:nowrap; }\r\n#b24ds .wz-card{ animation:vzreveal .3s ease both; }\r\n#b24ds .wz-q{ font-size:clamp(20px,3.4vw,27px); font-weight:700; line-height:1.25; margin-bottom:6px; }\r\n#b24ds .wz-sub{ font-size:14.5px; line-height:1.5; margin-bottom:18px; }\r\n#b24ds .wz-opts{ display:grid; gap:10px; grid-template-columns:1fr; margin-bottom:18px; }\r\n@media(min-width:600px){#b24ds .wz-opts.two{ grid-template-columns:1fr 1fr; }\r\n }\r\n#b24ds .wz-opt{ text-align:left; min-height:56px; padding:14px 16px; border-radius:14px; border:1.5px solid var(--border); background:rgba(255,255,255,0.07); font-size:16px; font-weight:700; line-height:1.35; display:flex; align-items:center; gap:12px; transition:border-color .2s, background .2s; }\r\n#b24ds .wz-opt:hover,#b24ds .wz-opt:focus-visible{ border-color:var(--orange); outline:none; }\r\n#b24ds .wz-opt.on{ border-color:var(--orange); background:rgba(240,165,0,0.2); box-shadow:inset 0 0 0 1px var(--orange); }\r\n#b24ds .wz-opt .ck{ flex:none; width:22px; height:22px; border-radius:6px; border:1.5px solid #fff; display:grid; place-items:center; font-size:14px; color:var(--navy-3); }\r\n#b24ds .wz-opt.on .ck{ background:var(--orange); border-color:var(--orange); }\r\n#b24ds .wz-opt.single .ck{ border-radius:50%; }\r\n#b24ds .wz-nav{ display:flex; gap:12px; justify-content:space-between; align-items:center; margin-top:8px; flex-wrap:wrap; }\r\n#b24ds .wz-nav .vz-btn.p{ min-width:160px; }\r\n#b24ds .wz-restart{ font-size:14px; font-weight:700; min-height:44px; text-decoration:underline; }\r\n#b24ds .wz-sum{ display:grid; gap:12px; margin-bottom:18px; }\r\n#b24ds .wz-sumbox{ padding:14px 16px; border-radius:12px; border:1px solid var(--border); background:rgba(255,255,255,0.05); }\r\n#b24ds .wz-sumbox h4{ font-size:16px; margin:0 0 6px; }\r\n#b24ds .wz-sumbox p,#b24ds .wz-sumbox li{ font-size:14.5px; line-height:1.5; margin:0; }\r\n#b24ds .wz-sumbox ul{ margin:4px 0 0; padding-left:18px; }\r\n#b24ds .wz-sumact{ display:flex; gap:14px; margin-top:8px; flex-wrap:wrap; }\r\n#b24ds .wz-sumact button{ font-size:14px; font-weight:700; color:var(--orange); text-decoration:underline; min-height:36px; }\r\n#b24ds .wz-thumbs{ display:flex; gap:6px; margin:6px 0; flex-wrap:wrap; }\r\n#b24ds .wz-thumbs img{ width:56px; height:56px; object-fit:cover; border-radius:8px; border:1px solid var(--border); }\r\n#b24ds .vz-area{ margin-bottom:16px; }\r\n#b24ds .vz-area h4{ margin-bottom:4px; }\r\n#b24ds .vz-areaitems{ font-size:14.5px; line-height:1.5; margin:0 0 10px; }\r\n#b24ds .vz-notice{ border-left:4px solid var(--orange); padding:14px 16px; font-size:15px; font-weight:700; line-height:1.5; }\r\n#b24ds .vz-count{ font-size:12.5px; font-weight:600; text-align:right; margin-top:4px; }\r\n#b24ds .vz-get{ padding:14px 16px; border-radius:12px; border:1px solid var(--border); background:rgba(255,255,255,0.05); margin:0 0 16px; }\r\n#b24ds .vz-get b{ display:block; font-size:15px; margin-bottom:6px; }\r\n#b24ds .vz-get ul{ margin:0; padding-left:18px; font-size:14px; line-height:1.6; }\r\n#b24ds .vz-caveat{ font-size:13px; font-weight:700; margin:0 0 10px; }\r\n#b24ds .vz-photo .main{ position:absolute; left:5px; bottom:5px; padding:3px 8px; border-radius:999px; background:var(--orange); color:var(--navy-3); font-size:11.5px; font-weight:700; }\r\n#b24ds .vz-photo button.star{ left:5px; right:auto; top:5px; }\r\n#b24ds .vz-photo img{ cursor:zoom-in; }\r\n#b24ds .vz-ico{ display:inline-block; vertical-align:-3px; margin-right:6px; }\r\n#b24ds .vz-quizbtn{ min-height:46px; }\r\n#b24ds .vz-pbar{ height:10px; border-radius:6px; background:rgba(255,255,255,0.2); max-width:380px; margin:14px auto 6px; overflow:hidden; }\r\n#b24ds .vz-pbar i{ display:block; height:100%; width:0; background:var(--orange); border-radius:6px; transition:width .6s ease; }\r\n#b24ds .vz-timer{ font-size:14px; font-weight:700; }\r\n#b24ds .vz-acts{ display:grid; gap:14px; }\r\n#b24ds .vz-acts-main{ display:flex; flex-wrap:wrap; align-items:center; gap:10px 16px; }\r\n#b24ds .vz-next{ font-size:14px; line-height:1.45; flex:1 1 260px; }\r\n#b24ds .vz-acts-sec{ display:flex; flex-wrap:wrap; gap:10px; padding-top:12px; border-top:1px solid var(--border); }\r\n#b24ds .vz-btn.ghost{ background:transparent; }\r\n#b24ds .vz-ba img{ aspect-ratio:4\/3; object-fit:cover; max-height:none; cursor:zoom-in; }\r\n@media(min-width:760px){#b24ds .vz-ba{ grid-template-columns:1fr 1fr; }\r\n }\r\n#b24ds .vz-noimg{ min-height:180px; padding:18px; display:grid; place-items:center; text-align:center; font-size:15px; font-weight:700; line-height:1.5; }\r\n#b24ds .vz-fp{ width:100%; display:block; border-radius:12px; background:#fff; cursor:zoom-in; }\r\n#b24ds .vz-tools{ display:flex; flex-wrap:wrap; gap:10px; margin:0 0 12px; }\r\n#b24ds .vz-card{ position:relative; }\r\n#b24ds .vz-card .open{ text-align:left; display:flex; flex-direction:column; width:100%; }\r\n#b24ds .vz-badge{ position:absolute; top:8px; left:8px; padding:3px 9px; border-radius:999px; background:var(--navy-3); border:1px solid var(--border); font-size:11.5px; font-weight:700; }\r\n#b24ds .vz-badge.ok{ border-color:var(--green); }\r\n#b24ds .vz-badge.bad{ border-color:var(--red); }\r\n#b24ds .vz-badge.busy{ border-color:var(--orange); }\r\n#b24ds .vz-kind{ position:absolute; top:8px; right:8px; width:28px; height:28px; border-radius:8px; background:var(--navy-3); border:1px solid var(--border); display:grid; place-items:center; }\r\n#b24ds .vz-dots{ position:absolute; right:6px; bottom:6px; width:36px; height:36px; border-radius:50%; font-size:20px; font-weight:700; line-height:1; }\r\n#b24ds .vz-menu{ position:absolute; right:8px; bottom:46px; z-index:5; min-width:170px; padding:6px; border-radius:12px; background:var(--navy-3); border:1px solid var(--border); box-shadow:0 12px 30px rgba(0,0,0,0.5); }\r\n#b24ds .vz-menu button{ display:block; width:100%; text-align:left; min-height:44px; padding:10px 12px; font-size:15px; font-weight:700; border-radius:8px; }\r\n#b24ds .vz-menu button:hover{ background:rgba(255,255,255,0.1); }\r\n#b24ds .vz-more{ margin:16px auto 0; display:flex; }\r\n#b24ds .vz-empty{ padding:22px; text-align:center; font-size:15px; font-weight:600; }\r\n#b24ds .vz-modal-box{ background:var(--navy); border:1px solid var(--border); border-radius:18px; width:100%; max-width:520px; max-height:90vh; overflow-y:auto; padding:24px 22px; }\r\n#b24ds .vz-modal-box h3{ font-size:20px; font-weight:700; margin-bottom:10px; }\r\n#b24ds .vz-modal-box p{ font-size:15px; line-height:1.55; margin-bottom:14px; }\r\n#b24ds .vz-modal-box textarea{ min-height:220px; font-size:14px; }\r\n#b24ds .vz-modal-act{ display:flex; flex-wrap:wrap; justify-content:flex-end; gap:10px; margin-top:14px; }\r\n#b24ds .vz-lightbox img{ max-width:100%; max-height:84vh; display:block; margin:0 auto; border-radius:10px; }\r\n#b24ds .style-chip .sw.noimg{ background-image:none !important; background-color:var(--navy-2); }\r\n#b24ds .pref-top{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:6px; }\r\n#b24ds .pref-top .pref-step-label{ margin-bottom:0; }\r\n#b24ds .pref-skip{ min-height:44px; }\r\n#b24ds .pref-nav2{ display:flex; justify-content:space-between; align-items:center; gap:10px; margin-bottom:10px; }\r\n#b24ds .pref-nav2 button{ font-size:14px; font-weight:700; text-decoration:underline; min-height:44px; padding:6px 0; }\r\n#b24ds .pref-nav2 button:disabled{ visibility:hidden; }\r\n#b24ds .pref-intro{ font-size:15px; line-height:1.55; margin-bottom:18px; }\r\n\r\n\r\n<\/style>\r\n<\/head>\r\n<body>\r\n<div id=\"b24ds\">\r\n<div class=\"app\" id=\"app\">\r\n\r\n  <div class=\"thero\">\r\n    <div class=\"thero-bg\"><\/div>\r\n    <div class=\"thero-overlay\"><\/div>\r\n    <div class=\"thero-grid\"><\/div>\r\n    <div class=\"thero-orb\"><\/div>\r\n    <div class=\"thero-inner\">\r\n      <div class=\"hero-eyebrow\">AI Design Studio<\/div>\r\n      <h1>Design It. <span class=\"orange\">Cost It. Get It Quoted.<\/span><\/h1>\r\n      <p>Upload photos of your room or house, or enter the size of your stand. The studio returns a redesigned image, checks the layout against your stand and lists the materials you need, with quantities and prices.<\/p>\r\n      <div class=\"hero-trust\"><span>Room redesign<\/span><span>House renovation<\/span><span>New home on your stand<\/span><span id=\"heroCredits\">Free credits every month<\/span><\/div>\r\n    <\/div>\r\n  <\/div>\r\n\r\n  <div class=\"page\">\r\n\r\n    <section id=\"what-is\" aria-labelledby=\"whatIsTitle\">\r\n      <div class=\"sec-head\">\r\n        <h2 id=\"whatIsTitle\">What is AI Design Studio?<\/h2>\r\n        <p>AI Design Studio is the Builders24 tool that turns your photos or your stand size into a design you can see, price and send to builders. You describe what you want. It shows you the result, works out what it needs, and lets you request quotes from verified professionals on Builders24.<\/p>\r\n      <\/div>\r\n      <div class=\"tcard-grid\">\r\n        <div class=\"glass tcard\"><div class=\"n\">1<\/div><h3>Redesign a room<\/h3><p>Upload photos of a lounge, kitchen, bathroom or bedroom, choose a style and see the room redesigned before you spend anything.<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">2<\/div><h3>Renovate a house<\/h3><p>Upload your existing house and describe the work. You get a concept image and a list of the areas and items the job covers.<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">3<\/div><h3>Build on an empty stand<\/h3><p>Enter your stand size and what you need. The studio checks whether the design fits the stand and gives you the layout.<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">4<\/div><h3>Get the materials and quotes<\/h3><p>Every design comes with a materials list with quantities and prices. Turn it into a quote request in one step.<\/p><\/div>\r\n      <\/div>\r\n    <\/section>\r\n\r\n    <section id=\"tool\">\r\n      <div class=\"glass vz-tool\">\r\n        <div class=\"vz-body\">\r\n\r\n          <div id=\"toolLoad\" class=\"vz-load\" aria-busy=\"true\" aria-label=\"Loading\"><div class=\"vz-skel\"><\/div><div class=\"vz-skel\"><\/div><\/div>\r\n\r\n          <div id=\"gate\" class=\"vz-gate\" hidden>\r\n            <h3>Log in to create your design<\/h3>\r\n            <p>Creating a design needs a free account. You get free credits every month and every design is saved to your account.<\/p>\r\n            <a id=\"gateLink\" href=\"https:\/\/app.builders24.net\/login.html?redirect=%2Fretail%2Fvisualize.html\">Log in or create an account<\/a>\r\n          <\/div>\r\n\r\n          <div id=\"toolMain\" hidden>\r\n            <div class=\"vz-credits\" id=\"credits\">\r\n              <div class=\"vz-cr-wrap\">\r\n                <div class=\"vz-donut\" role=\"img\" id=\"crDonut\" aria-label=\"Credits left\">\r\n                  <svg viewBox=\"0 0 100 100\" aria-hidden=\"true\"><circle class=\"trk\" cx=\"50\" cy=\"50\" r=\"42\"\/><circle class=\"arc\" id=\"crArc\" cx=\"50\" cy=\"50\" r=\"42\"\/><\/svg>\r\n                  <div class=\"mid\"><b id=\"crLeft\">\u2013<\/b><span>credits left<\/span><\/div>\r\n                <\/div>\r\n                <div class=\"vz-cr-info\">\r\n                  <div class=\"vz-cr-sub\" id=\"crSub\">Checking your credits\u2026<\/div>\r\n                  <div class=\"vz-cr-sel\"><label for=\"curSel\">Prices in<\/label><select class=\"vz-in\" id=\"curSel\"><\/select><\/div>\r\n                <\/div>\r\n              <\/div>\r\n            <\/div>\r\n\r\n            <div id=\"choice\">\r\n              <div class=\"vz-q\">What can we help you with?<\/div>\r\n              <div class=\"vz-qs\">Choose one and we will ask only what you need.<\/div>\r\n              <div class=\"vz-choice\" id=\"helpChoice\"><\/div>\r\n              <div class=\"vz-alt\"><button type=\"button\" id=\"helpWords\">Or describe it in your own words<\/button><\/div>\r\n            <\/div>\r\n\r\n            <div id=\"formWrap\" hidden>\r\n              <div class=\"vz-formhead\"><h3 id=\"modeTitle\"><\/h3><button type=\"button\" class=\"vz-back\" id=\"modeBack\">&larr; Change<\/button><\/div>\r\n\r\n            <!-- ============ ROOM ============ -->\r\n            <form id=\"formRoom\" novalidate>\r\n              <div class=\"vz-field\">\r\n                <label class=\"vz-lbl\" for=\"rType\">Which room? <i>*<\/i><\/label>\r\n                <select class=\"vz-in\" id=\"rType\"><\/select>\r\n              <\/div>\r\n              <div class=\"vz-field\">\r\n                <span class=\"vz-lbl\">Photo of the room <i>*<\/i><\/span>\r\n                <div class=\"vz-hint\">One clear photo that shows the whole room. You can add a few angles. The first photo is your main photo. Photos are optimised for processing, and tap a photo to enlarge it.<\/div>\r\n                <div id=\"rPhotos\"><\/div>\r\n              <\/div>\r\n              <div class=\"vz-field\">\r\n                <label class=\"vz-lbl\" for=\"rWants\">What do you want done? <i>*<\/i><\/label>\r\n                <textarea class=\"vz-in\" id=\"rWants\" maxlength=\"800\" placeholder=\"For example: modernise the kitchen, make this bedroom feel bigger, add an ensuite.\"><\/textarea>\r\n              <\/div>\r\n              <div class=\"vz-field\">\r\n                <span class=\"vz-lbl\">Pick a style (optional)<\/span>\r\n                <div class=\"vz-hint\">Tap a style to steer the design, tap it again to clear it, or skip and let the AI decide.<\/div>\r\n                <div class=\"style-grid\" id=\"style-grid\">\r\n        <div class=\"glass style-chip\" data-style=\"Modern Minimalist\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-modern-minimalist.webp')\"><\/div><span>Modern Minimalist<\/span><\/div>\r\n        <div class=\"glass style-chip\" data-style=\"Contemporary\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-contemporary.jpg')\"><\/div><span>Contemporary<\/span><\/div>\r\n        <div class=\"glass style-chip\" data-style=\"Scandinavian\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-scandinavian.webp')\"><\/div><span>Scandinavian<\/span><\/div>\r\n        <div class=\"glass style-chip\" data-style=\"Industrial\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-industrial.jpeg')\"><\/div><span>Industrial<\/span><\/div>\r\n        <div class=\"glass style-chip\" data-style=\"Classic Luxury\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-classic-luxury.jpg')\"><\/div><span>Classic Luxury<\/span><\/div>\r\n        <div class=\"glass style-chip\" data-style=\"Outdoor Living\"><div class=\"sw\" style=\"background-image:url('https:\/\/app.builders24.net\/assets\/style-outdoor-living.jpg')\"><\/div><span>Outdoor Living<\/span><\/div>\r\n      <\/div>\r\n                <button class=\"vz-btn quizBtn vz-quizbtn\" type=\"button\"><svg class=\"vz-ico\" width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 3l1.8 5.2L19 10l-5.2 1.8L12 17l-1.8-5.2L5 10l5.2-1.8z\"\/><\/svg>Take a 1-minute style quiz<\/button>\r\n              <\/div>\r\n              <details class=\"vz-extras\">\r\n                <summary><span>Helpful extras (optional)<small>3 optional fields: room size, things to keep, budget<\/small><\/span><\/summary>\r\n                <div class=\"vz-extras-body\">\r\n                  <div class=\"vz-field\">\r\n                    <span class=\"vz-lbl\">Room size, if you know it<\/span>\r\n                    <div class=\"vz-size\">\r\n                      <div><label class=\"vz-mini\" for=\"rW\">Width (m)<\/label><input class=\"vz-in\" id=\"rW\" type=\"number\" inputmode=\"decimal\" min=\"0.5\" step=\"0.1\"><\/div>\r\n                      <div><label class=\"vz-mini\" for=\"rD\">Depth (m)<\/label><input class=\"vz-in\" id=\"rD\" type=\"number\" inputmode=\"decimal\" min=\"0.5\" step=\"0.1\"><\/div>\r\n                      <div class=\"vz-or\">or<\/div>\r\n                      <div><label class=\"vz-mini\" for=\"rA\">Area (m\u00b2)<\/label><input class=\"vz-in\" id=\"rA\" type=\"number\" inputmode=\"decimal\" min=\"1\" step=\"0.1\"><\/div>\r\n                    <\/div>\r\n                    <div class=\"vz-calc\" id=\"rCalc\" aria-live=\"polite\"><\/div>\r\n                    <div class=\"vz-hint\" style=\"margin-top:8px;\">Enter width and depth, or the area. The other option switches off so only one is used. If you leave this out we use a typical size for the room and say so in the results.<\/div>\r\n                  <\/div>\r\n                  <div class=\"vz-field\">\r\n                    <label class=\"vz-lbl\" for=\"rKeep\">Anything you want to keep?<\/label>\r\n                    <textarea class=\"vz-in\" id=\"rKeep\" maxlength=\"500\" style=\"min-height:80px;\" placeholder=\"For example: keep the wooden floor and the window.\"><\/textarea>\r\n                  <\/div>\r\n                  <div class=\"vz-field\">\r\n                    <span class=\"vz-lbl\">Budget, rough range (<span class=\"curLbl\">USD<\/span>)<\/span>\r\n                    <div class=\"vz-grid2\">\r\n                      <input class=\"vz-in\" id=\"rBMin\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"From (USD)\" data-bph=\"From\">\r\n                      <input class=\"vz-in\" id=\"rBMax\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"To (USD)\" data-bph=\"To\">\r\n                    <\/div>\r\n                  <\/div>\r\n                <\/div>\r\n              <\/details>\r\n              <div class=\"vz-get\"><b>What you will get<\/b><ul><li>An AI concept image of your design<\/li><li>A schedule of accommodation<\/li><li>A materials list with quantities and estimated prices<\/li><li>A branded PDF and the option to request a quote<\/li><\/ul><\/div>\r\n              <div class=\"vz-caveat\">AI concept for planning. Not construction drawings.<\/div>\r\n              <div class=\"vz-err\" id=\"rErr\" role=\"alert\"><\/div>\r\n              <button class=\"vz-submit\" id=\"rSubmit\" type=\"submit\">Create My Design<\/button>\r\n              <div class=\"vz-note\">Uses 25 credits. Your photos and design are saved privately to your account.<\/div>\r\n            <\/form>\r\n\r\n            <!-- ============ HOUSE \/ STAND ============ -->\r\n            <form id=\"formBuild\" novalidate hidden>\r\n              <div class=\"vz-field\">\r\n                <span class=\"vz-lbl\" id=\"bPhotoLbl\">Photos <i>*<\/i><\/span>\r\n                <div class=\"vz-hint\" id=\"bPhotoHint\"><\/div>\r\n                <div id=\"bPhotos\"><\/div>\r\n              <\/div>\r\n              <div class=\"vz-field\">\r\n                <span class=\"vz-lbl\">Stand size <i>*<\/i><\/span>\r\n                <div style=\"max-width:340px;\"><label class=\"vz-mini\" for=\"bA\">Total area (m\u00b2). Use the figure on your title deed or survey.<\/label><input class=\"vz-in big\" id=\"bA\" type=\"number\" inputmode=\"decimal\" min=\"10\" step=\"1\"><\/div>\r\n                <div class=\"vz-or2\">Or enter the width and depth<\/div>\r\n                <div class=\"vz-grid2\">\r\n                  <div><label class=\"vz-mini\" for=\"bW\">Width (m)<\/label><input class=\"vz-in\" id=\"bW\" type=\"number\" inputmode=\"decimal\" min=\"1\" step=\"0.1\"><\/div>\r\n                  <div><label class=\"vz-mini\" for=\"bD\">Depth (m)<\/label><input class=\"vz-in\" id=\"bD\" type=\"number\" inputmode=\"decimal\" min=\"1\" step=\"0.1\"><\/div>\r\n                <\/div>\r\n                <div class=\"vz-calc\" id=\"bCalc\" aria-live=\"polite\"><\/div>\r\n                <div class=\"vz-hint\" style=\"margin-top:8px;\">The design is checked against this. Only one of the two options is used.<\/div>\r\n              <\/div>\r\n              <div class=\"vz-field\" id=\"bWorkWrap\">\r\n                <label class=\"vz-lbl\" for=\"bWork\">What do you want to do? <i>*<\/i><\/label>\r\n                <select class=\"vz-in\" id=\"bWork\"><\/select>\r\n              <\/div>\r\n              <div class=\"vz-field\" id=\"bBaseWrap\" hidden>\r\n                <label class=\"vz-lbl\" for=\"bBase\">Base design<\/label>\r\n                <div class=\"vz-hint\">The kind of home you have in mind. It sets the general shape and the number of storeys. Choose \"Open to suggestions\" if you are not sure.<\/div>\r\n                <select class=\"vz-in\" id=\"bBase\"><\/select>\r\n              <\/div>\r\n              <div class=\"vz-field\" id=\"bRefWrap\" hidden>\r\n                <span class=\"vz-lbl\">Design reference (optional)<\/span>\r\n                <div class=\"vz-hint\">Optional. Pick one of our home designs as inspiration for style and layout. It works with the base design, and your stand size and brief still decide the final design. Tap it again to clear it.<\/div>\r\n                <div class=\"vz-refgrid\" id=\"bRefGrid\"><\/div>\r\n              <\/div>\r\n              <div class=\"vz-field\">\r\n                <span class=\"vz-lbl\">The brief <i>*<\/i><\/span>\r\n                <div class=\"vz-grid3\">\r\n                  <div><label class=\"vz-mini\" for=\"bBeds\">Bedrooms<\/label><input class=\"vz-in\" id=\"bBeds\" type=\"number\" inputmode=\"numeric\" min=\"0\" max=\"12\" step=\"1\"><\/div>\r\n                  <div><label class=\"vz-mini\" for=\"bBaths\">Bathrooms (including ensuite)<\/label><input class=\"vz-in\" id=\"bBaths\" type=\"number\" inputmode=\"numeric\" min=\"0\" max=\"10\" step=\"1\"><\/div>\r\n                  <div><span class=\"vz-mini\">Ensuite?<\/span>\r\n                    <div class=\"vz-seg\" id=\"ensSeg\"><button type=\"button\" data-v=\"no\" class=\"on\">No<\/button><button type=\"button\" data-v=\"yes\">Yes<\/button><\/div>\r\n                  <\/div>\r\n                <\/div>\r\n              <\/div>\r\n              <details class=\"vz-extras\" id=\"bSizesWrap\" hidden>\r\n                <summary><span>Room sizes (optional)<small>Your design and estimate use exactly these sizes<\/small><\/span><\/summary>\r\n                <div class=\"vz-extras-body\">\r\n                  <div class=\"vz-hint\">Choose a size for each room. If you skip this we use Standard. Your room list and your materials estimate are built from these sizes.<\/div>\r\n                  <div class=\"vz-grid2\" id=\"bSizes\"><\/div>\r\n                <\/div>\r\n              <\/details>\r\n              <div class=\"vz-field\">\r\n                <label class=\"vz-lbl\" for=\"bMust\">Must-haves<\/label>\r\n                <div class=\"vz-chips\" id=\"bMustChips\"><\/div>\r\n                <textarea class=\"vz-in\" id=\"bMust\" maxlength=\"800\" placeholder=\"For example: double garage, open-plan kitchen, covered patio, room for a home office.\"><\/textarea>\r\n                <div class=\"vz-count\" id=\"bMustCount\">0 \/ 800<\/div>\r\n              <\/div>\r\n              <details class=\"vz-extras\">\r\n                <summary><span>Helpful extras (optional)<small>3 optional fields: stand position, location, budget<\/small><\/span><\/summary>\r\n                <div class=\"vz-extras-body\">\r\n                  <div class=\"vz-field\">\r\n                    <label class=\"vz-lbl\" for=\"bOrient\">Direction or position of the stand<\/label>\r\n                    <select class=\"vz-in\" id=\"bOrient\"><\/select>\r\n                  <\/div>\r\n                  <div class=\"vz-field\">\r\n                    <label class=\"vz-lbl\" for=\"bLoc\">Where is it? City or region<\/label>\r\n                    <input class=\"vz-in\" id=\"bLoc\" type=\"text\" maxlength=\"120\" placeholder=\"For example: Pretoria, Gauteng\">\r\n                  <\/div>\r\n                  <div class=\"vz-field\">\r\n                    <span class=\"vz-lbl\">Budget, rough range (<span class=\"curLbl\">USD<\/span>)<\/span>\r\n                    <div class=\"vz-grid2\">\r\n                      <input class=\"vz-in\" id=\"bBMin\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"From (USD)\" data-bph=\"From\">\r\n                      <input class=\"vz-in\" id=\"bBMax\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"To (USD)\" data-bph=\"To\">\r\n                    <\/div>\r\n                  <\/div>\r\n                <\/div>\r\n              <\/details>\r\n              <div class=\"vz-get\"><b>What you will get<\/b><ul><li>An AI concept image of your design<\/li><li>A schedule of accommodation<\/li><li>A materials list with quantities and estimated prices<\/li><li>A branded PDF and the option to request a quote<\/li><\/ul><\/div>\r\n              <div class=\"vz-caveat\">AI concept for planning. Not construction drawings.<\/div>\r\n              <div class=\"vz-err\" id=\"bErr\" role=\"alert\"><\/div>\r\n              <button class=\"vz-submit\" id=\"bSubmit\" type=\"submit\">Create My Design<\/button>\r\n              <div class=\"vz-note\">Uses 25 credits. Your photos and design are saved privately to your account.<\/div>\r\n            <\/form>\r\n\r\n            <!-- ============ EXISTING PROPERTY: conversational steps ============ -->\r\n            <div id=\"wiz\" hidden>\r\n              <div class=\"wz-top\"><div class=\"wz-bar\" role=\"progressbar\" aria-label=\"Your progress\" aria-valuemin=\"0\" aria-valuemax=\"100\"><i id=\"wzBar\"><\/i><\/div><div class=\"wz-count\" id=\"wzCount\"><\/div><\/div>\r\n              <div class=\"wz-card\" id=\"wzCard\" aria-live=\"polite\"><\/div>\r\n              <div class=\"vz-err\" id=\"wzErr\" role=\"alert\"><\/div>\r\n              <div class=\"wz-nav\"><button type=\"button\" class=\"vz-btn\" id=\"wzBack\">Back<\/button><button type=\"button\" class=\"wz-restart\" id=\"wzRestart\">Start again<\/button><button type=\"button\" class=\"vz-btn p\" id=\"wzNext\">Next<\/button><\/div>\r\n              <div class=\"vz-note\" id=\"wzNote\" hidden><\/div>\r\n              <div id=\"wzStash\" hidden><\/div>\r\n            <\/div>\r\n\r\n            <\/div>\r\n\r\n            <!-- progress -->\r\n            <div id=\"progress\" class=\"vz-prog\" hidden>\r\n              <div class=\"vz-spin\"><\/div>\r\n              <h3 id=\"progTitle\">Creating your design<\/h3>\r\n              <p id=\"progSub\">This usually takes a minute or two. You can leave this page, your design will be waiting in My Designs.<\/p>\r\n              <div class=\"vz-steps\" id=\"progSteps\"><\/div>\r\n              <div class=\"vz-pbar\" role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\"><i id=\"pbarFill\"><\/i><\/div>\r\n              <div class=\"vz-timer\" id=\"progTimer\">0:00<\/div>\r\n              <button class=\"vz-btn\" id=\"progStop\" type=\"button\" hidden style=\"margin-top:14px;\">Stop waiting and go to My Designs<\/button>\r\n            <\/div>\r\n\r\n            <!-- result -->\r\n            <div id=\"result\" hidden><\/div>\r\n          <\/div>\r\n        <\/div>\r\n      <\/div>\r\n    <\/section>\r\n\r\n    <section id=\"mineSec\" hidden>\r\n      <div class=\"sec-head\"><h2>My Designs<\/h2><p>Every design you create is saved here. Open one to see it again or request a quote.<\/p><\/div>\r\n      <div class=\"vz-mine\" id=\"mine\"><\/div>\r\n      <div class=\"vz-failed\" id=\"mineFailed\"><\/div>\r\n      <button class=\"vz-btn vz-more\" id=\"mineMore\" type=\"button\" hidden>Load more designs<\/button>\r\n    <\/section>\r\n\r\n    <section>\r\n      <div class=\"tcard-grid\">\r\n        <div class=\"glass tcard\"><div class=\"n\">1<\/div><h4>Add Your Photos<\/h4><p>A room, your house, your yard or an empty stand<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">2<\/div><h4>Tell Us What You Want<\/h4><p>Stand size, rooms and must-haves<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">3<\/div><h4>Get Your Design<\/h4><p>See it before you build it: an AI image, a layout check against your stand, and a materials list with quantities and prices<\/p><\/div>\r\n        <div class=\"glass tcard\"><div class=\"n\">4<\/div><h4>Quote &amp; Build<\/h4><p>Turn your design into a real quote request and get quotes from verified pros<\/p><\/div>\r\n      <\/div>\r\n    <\/section>\r\n\r\n    <section>\r\n      <div class=\"stats-row\">\r\n        <div class=\"glass stat-card\"><div class=\"val\">10K+<\/div><div class=\"lbl\">Projects Visualized<\/div><\/div>\r\n        <div class=\"glass stat-card\"><div class=\"val\">4.8\u2605<\/div><div class=\"lbl\">Average Rating<\/div><\/div>\r\n        <div class=\"glass stat-card\"><div class=\"val\">98%<\/div><div class=\"lbl\">Satisfaction Rate<\/div><\/div>\r\n      <\/div>\r\n    <\/section>\r\n\r\n  <\/div>\r\n<\/div>\r\n\r\n<!-- Style questionnaire (optional helper, no longer blocks anything) -->\r\n<div class=\"pref-overlay\" id=\"pref-overlay\" role=\"dialog\" aria-modal=\"true\" aria-labelledby=\"pref-question-text\">\r\n  <div class=\"pref-modal\">\r\n    <div class=\"pref-top\"><div class=\"pref-step-label\" id=\"pref-step-label\">Style quiz<\/div><button class=\"pref-skip\" id=\"pref-skip\" type=\"button\">Cancel quiz<\/button><\/div>\r\n    <div class=\"pref-progress\" id=\"pref-progress\"><\/div>\r\n    <div class=\"pref-question\" id=\"pref-question-text\"><\/div>\r\n    <div class=\"pref-intro\" id=\"pref-intro\" hidden><\/div>\r\n    <div class=\"pref-options\" id=\"pref-options\"><\/div>\r\n    <div class=\"pref-nav2\"><button id=\"pref-back\" type=\"button\">&larr; Back<\/button><button id=\"pref-skipq\" type=\"button\">Skip this question<\/button><\/div>\r\n    <div class=\"pref-nav\">\r\n      <span><\/span>\r\n      <button class=\"pref-next\" id=\"pref-next\" type=\"button\" disabled>Next<\/button>\r\n    <\/div>\r\n  <\/div>\r\n<\/div>\r\n\r\n<div class=\"vz-toasts\" id=\"toasts\" aria-live=\"polite\" aria-atomic=\"false\"><\/div>\r\n\r\n<\/div><!-- \/#b24ds -->\r\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@supabase\/supabase-js@2\"><\/script>\r\n<script src=\"https:\/\/app.builders24.net\/guest-cart.js\"><\/script>\r\n<script src=\"https:\/\/app.builders24.net\/b24app.js\"><\/script>\r\n<script>\r\n(function(){\r\n  var $ = function(id){ return document.getElementById(id); };\r\n  function E(s){ return (s===null||s===undefined?'':String(s)).replace(\/[&<>\"']\/g, function(c){ return {'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]; }); }\r\n  function money(n, cur, dec){\r\n    if (n===null || n===undefined || isNaN(n)) return '\u2014';\r\n    try { return new Intl.NumberFormat(undefined, { style:'currency', currency:cur||'USD', maximumFractionDigits: dec||0, minimumFractionDigits: dec||0 }).format(n); }\r\n    catch(e){ return (cur||'') + ' ' + Number(n).toLocaleString(undefined,{maximumFractionDigits:dec||0}); }\r\n  }\r\n  function num(v){ var n = parseFloat(String(v).replace(\/,\/g,'')); return isFinite(n) ? n : null; }\r\n  \/\/ The concept image is output_images[0] unless it was skipped (then only the floor plan was saved).\r\n  function conceptPath(p){ return ((p.plan && p.plan.skipped) || []).indexOf('image') >= 0 ? '' : (p.output_images || [])[0]; }\r\n  function floorPath(p){ return p.plan && p.plan.floor_plan && p.plan.floor_plan.path ? p.plan.floor_plan.path : ''; }\r\n  function dateShort(iso){ return iso ? new Date(iso).toLocaleDateString(undefined,{day:'numeric',month:'short',year:'numeric'}) : ''; }\r\n\r\n  \/\/ Shown whenever something fails on our side. Never show raw system messages to customers.\r\n  var BUSY = 'We are currently experiencing a high volume of creation requests. Please try again later.';\r\n  function safeErr(m){ var x = new Error(m); x.safe = true; return x; }\r\n\r\n  \/* ---- focus trap, used by every dialog ---- *\/\r\n  function trapFocus(overlay, close){\r\n    var prev = document.activeElement;\r\n    function items(){ return Array.prototype.slice.call(overlay.querySelectorAll('button:not([disabled]):not([hidden]), [href], input, select, textarea, [tabindex]:not([tabindex=\"-1\"])')).filter(function(n){ return n.offsetParent !== null; }); }\r\n    function onKey(e){\r\n      if (e.key === 'Escape') { e.preventDefault(); close(); return; }\r\n      if (e.key !== 'Tab') return;\r\n      var f = items(); if (!f.length) return;\r\n      var first = f[0], last = f[f.length - 1];\r\n      if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus(); }\r\n      else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus(); }\r\n    }\r\n    document.addEventListener('keydown', onKey);\r\n    setTimeout(function(){ var f = items(); if (f.length) f[0].focus(); }, 30);\r\n    return function release(){ document.removeEventListener('keydown', onKey); if (prev && prev.focus) { try { prev.focus(); } catch(e){} } };\r\n  }\r\n  \/* actions: [{label, primary, value}] ; resolves with the clicked value (or null when dismissed) *\/\r\n  function showModal(opt){\r\n    return new Promise(function(resolve){\r\n      var ov = document.createElement('div'); ov.className = 'pref-overlay open'; ov.setAttribute('role', 'dialog'); ov.setAttribute('aria-modal', 'true'); ov.setAttribute('aria-label', opt.title);\r\n      var box = document.createElement('div'); box.className = 'vz-modal-box';\r\n      box.innerHTML = '<h3>' + E(opt.title) + '<\/h3>' + (opt.text ? '<p>' + E(opt.text) + '<\/p>' : '') + (opt.html || '') +\r\n        '<div class=\"vz-modal-act\">' + (opt.actions || []).map(function(a, i){ return '<button type=\"button\" class=\"vz-btn' + (a.primary ? ' p' : '') + '\" data-i=\"' + i + '\">' + E(a.label) + '<\/button>'; }).join('') + '<\/div>';\r\n      ov.appendChild(box); (document.getElementById('b24ds')||document.body).appendChild(ov);\r\n      var done = false, release;\r\n      function finish(v){ if (done) return; done = true; release(); ov.remove(); resolve(v); }\r\n      release = trapFocus(ov, function(){ finish(null); });\r\n      ov.addEventListener('click', function(e){ if (e.target === ov) finish(null); });\r\n      box.querySelectorAll('[data-i]').forEach(function(b){ b.addEventListener('click', function(){ var a = opt.actions[+b.getAttribute('data-i')]; finish(opt.read ? { value:a.value, box:box } : a.value); }); });\r\n    });\r\n  }\r\n  function showLightbox(url, alt){\r\n    var ov = document.createElement('div'); ov.className = 'pref-overlay open vz-lightbox'; ov.setAttribute('role', 'dialog'); ov.setAttribute('aria-modal', 'true'); ov.setAttribute('aria-label', alt || 'Photo');\r\n    ov.innerHTML = '<div style=\"width:100%;max-width:1100px;\"><div style=\"text-align:right;margin-bottom:8px;\"><button type=\"button\" class=\"vz-btn\">Close<\/button><\/div><img decoding=\"async\" alt=\"' + E(alt || '') + '\" src=\"' + E(url) + '\"><\/div>';\r\n    (document.getElementById('b24ds')||document.body).appendChild(ov);\r\n    var release = trapFocus(ov, close);\r\n    function close(){ release(); ov.remove(); }\r\n    ov.addEventListener('click', function(e){ if (e.target === ov || e.target.tagName === 'BUTTON') close(); });\r\n  }\r\n\r\n  var STAGE_ORDER = ['reading','planning','rendering'];\r\n  var STAGE_TXT = {\r\n    reading:   ['Reading your photos and drafting the design', 'Photos read and design drafted'],\r\n    planning:  ['Pricing your materials', 'Materials priced'],\r\n    rendering: ['Creating your image', 'Image created']\r\n  };\r\n\r\n  var S = {\r\n    user:null, currency:'USD', currencies:[], mode:null, ensuite:false, style:'', refs:[], ref:'',\r\n    maxPhotos:5, quota:null, opts:{}, busy:false, poll:null, projects:[], current:null, pickers:{}, mineLimit:12, mineMore:false, timer:null\r\n  };\r\n\r\n  \/* ================= toasts ================= *\/\r\n  var toasts = {};\r\n  var ICON_OK  = '<svg viewBox=\"0 0 26 26\"><circle class=\"ring\" cx=\"13\" cy=\"13\" r=\"11\"\/><path class=\"tick\" d=\"M7.5 13.5l4 4 7-8\"\/><\/svg>';\r\n  var ICON_ERR = '<svg viewBox=\"0 0 26 26\"><circle class=\"ring\" cx=\"13\" cy=\"13\" r=\"11\"\/><path class=\"tick\" d=\"M9 9l8 8M17 9l-8 8\"\/><\/svg>';\r\n  function dismissToast(id){\r\n    var el = toasts[id]; if (!el) return;\r\n    delete toasts[id]; clearTimeout(el._t);\r\n    el.classList.add('out'); setTimeout(function(){ if (el.parentNode) el.parentNode.removeChild(el); }, 300);\r\n  }\r\n  function toast(id, text, state){\r\n    var host = $('toasts'), el = toasts[id];\r\n    if (el && el.getAttribute('data-state') === state && el.querySelector('.vz-tt').textContent === text) return;\r\n    if (!el) {\r\n      el = document.createElement('div'); el.className = 'vz-toast';\r\n      el.innerHTML = '<span class=\"vz-ti\"><\/span><span class=\"vz-tt\"><\/span>';\r\n      host.appendChild(el); toasts[id] = el;\r\n      var ids = Object.keys(toasts); if (ids.length > 5) dismissToast(ids[0]);\r\n    }\r\n    el.setAttribute('data-state', state);\r\n    el.querySelector('.vz-tt').textContent = text;\r\n    el.querySelector('.vz-ti').innerHTML = state === 'busy' ? '<i class=\"vz-tspin\"><\/i>' : state === 'ok' ? ICON_OK : ICON_ERR;\r\n    clearTimeout(el._t);\r\n    if (state !== 'busy') el._t = setTimeout(function(){ dismissToast(id); }, state === 'err' ? 7000 : 3200);\r\n  }\r\n  function settleBusyToasts(){\r\n    Object.keys(toasts).forEach(function(id){\r\n      var el = toasts[id];\r\n      if (el.getAttribute('data-state') === 'busy') toast(id, el.querySelector('.vz-tt').textContent, 'err');\r\n    });\r\n  }\r\n  function stageToasts(stage){\r\n    var idx = STAGE_ORDER.indexOf(stage);\r\n    if (stage === 'done') idx = STAGE_ORDER.length;\r\n    if (idx < 0) idx = 0;\r\n    STAGE_ORDER.forEach(function(k, i){\r\n      if (i < idx) toast('st-' + k, STAGE_TXT[k][1], 'ok');\r\n      else if (i === idx) toast('st-' + k, STAGE_TXT[k][0], 'busy');\r\n    });\r\n  }\r\n\r\n  \/* ================= photo picker ================= *\/\r\n  function resizeToJpeg(file){\r\n    return new Promise(function(resolve, reject){\r\n      var url = URL.createObjectURL(file), img = new Image();\r\n      img.onload = function(){\r\n        var max = 1600, w = img.naturalWidth || img.width, h = img.naturalHeight || img.height;\r\n        var k = Math.min(1, max \/ Math.max(w, h));\r\n        var cw = Math.max(1, Math.round(w*k)), ch = Math.max(1, Math.round(h*k));\r\n        var c = document.createElement('canvas'); c.width = cw; c.height = ch;\r\n        c.getContext('2d').drawImage(img, 0, 0, cw, ch);\r\n        c.toBlob(function(b){ URL.revokeObjectURL(url); b ? resolve(b) : reject(new Error('This photo could not be read.')); }, 'image\/jpeg', 0.85);\r\n      };\r\n      img.onerror = function(){ URL.revokeObjectURL(url); reject(new Error('This photo could not be read. Please try a JPG or PNG.')); };\r\n      img.src = url;\r\n    });\r\n  }\r\n\r\n  var ICON_UP = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M12 16V4M7 9l5-5 5 5M4 20h16\"\/><\/svg>';\r\n  var ICON_CAM = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M4 8h3l2-3h6l2 3h3v11H4z\"\/><circle cx=\"12\" cy=\"13\" r=\"3.5\"\/><\/svg>';\r\n  function makePicker(rootId, errId, limitFn){\r\n    var root = $(rootId), items = [];\r\n    var lim = function(){ return limitFn ? limitFn() : S.maxPhotos; };\r\n    var touch = window.matchMedia && window.matchMedia('(pointer:coarse)').matches;\r\n    root.innerHTML =\r\n      '<div class=\"vz-photos\" data-grid><\/div>' +\r\n      '<div class=\"vz-photo-btns\"><button type=\"button\"' + (touch ? '' : ' class=\"p\"') + ' data-act=\"file\">' + ICON_UP + 'Upload Photos<\/button><button type=\"button\"' + (touch ? ' class=\"p\"' : '') + ' data-act=\"cam\">' + ICON_CAM + 'Use Camera<\/button><\/div>' +\r\n      '<input type=\"file\" accept=\"image\/jpeg,image\/png,image\/webp\" multiple hidden data-in=\"file\">' +\r\n      '<input type=\"file\" accept=\"image\/*\" capture=\"environment\" hidden data-in=\"cam\">';\r\n    var grid = root.querySelector('[data-grid]');\r\n    var fileIn = root.querySelector('[data-in=file]'), camIn = root.querySelector('[data-in=cam]');\r\n\r\n    function paint(){\r\n      grid.innerHTML = '';\r\n      items.forEach(function(it, i){\r\n        var d = document.createElement('div'); d.className = 'vz-photo';\r\n        var im = document.createElement('img'); im.src = it.url; im.alt = 'Photo ' + (i+1) + '. Tap to enlarge.';\r\n        im.onclick = function(){ showLightbox(it.url, 'Photo ' + (i+1)); };\r\n        var x = document.createElement('button'); x.type = 'button'; x.textContent = '\u2715'; x.setAttribute('aria-label','Remove photo ' + (i+1));\r\n        x.onclick = function(){ URL.revokeObjectURL(it.url); items.splice(i,1); paint(); };\r\n        d.appendChild(im); d.appendChild(x);\r\n        if (i === 0) { var m = document.createElement('span'); m.className = 'main'; m.textContent = 'Main photo'; d.appendChild(m); }\r\n        else {\r\n          var st = document.createElement('button'); st.type = 'button'; st.className = 'star'; st.textContent = '\u2605'; st.setAttribute('aria-label', 'Make photo ' + (i+1) + ' the main photo'); st.title = 'Make this the main photo';\r\n          st.onclick = function(){ items.unshift(items.splice(i, 1)[0]); paint(); };\r\n          d.appendChild(st);\r\n        }\r\n        grid.appendChild(d);\r\n      });\r\n      if (items.length && items.length < lim()) {\r\n        var a = document.createElement('button'); a.type = 'button'; a.className = 'vz-add'; a.innerHTML = '<span style=\"font-size:22px;line-height:1;\">+<\/span>Add';\r\n        a.onclick = function(){ fileIn.click(); }; grid.appendChild(a);\r\n      }\r\n      root.querySelector('.vz-photo-btns').hidden = items.length >= lim();\r\n    }\r\n    async function add(files){\r\n      var err = $(errId); err.textContent = '';\r\n      for (var i = 0; i < files.length; i++) {\r\n        if (items.length >= lim()) { err.textContent = 'You can add up to ' + lim() + ' photos.'; break; }\r\n        var f = files[i];\r\n        if (['image\/jpeg','image\/png','image\/webp'].indexOf(f.type) < 0) { err.textContent = 'Please use JPG, PNG or WEBP photos.'; continue; }\r\n        if (f.size > 25*1024*1024) { err.textContent = 'One photo is too large. Please use photos under 25MB.'; continue; }\r\n        try { var b = await resizeToJpeg(f); items.push({ blob:b, url:URL.createObjectURL(b) }); }\r\n        catch(e){ err.textContent = e.message; }\r\n      }\r\n      paint();\r\n    }\r\n    root.addEventListener('click', function(e){\r\n      var b = e.target.closest('[data-act]'); if (!b) return;\r\n      (b.getAttribute('data-act') === 'cam' ? camIn : fileIn).click();\r\n    });\r\n    fileIn.addEventListener('change', function(){ add(Array.prototype.slice.call(fileIn.files)); fileIn.value = ''; });\r\n    camIn.addEventListener('change', function(){ add(Array.prototype.slice.call(camIn.files)); camIn.value = ''; });\r\n    paint();\r\n    return { items:function(){ return items; }, clear:function(){ items.forEach(function(i){ URL.revokeObjectURL(i.url); }); items = []; paint(); }, repaint:paint };\r\n  }\r\n\r\n  \/* ================= live lists ================= *\/\r\n  function fillSelect(sel, rows, placeholder){\r\n    sel.innerHTML = (placeholder ? '<option value=\"\">' + E(placeholder) + '<\/option>' : '') +\r\n      rows.map(function(r){ return '<option value=\"' + E(r.value) + '\">' + E(r.label) + '<\/option>'; }).join('');\r\n  }\r\n  async function loadLive(){\r\n    try {\r\n      var o = await sb.from('visualizer_options').select('kind,value,label,sort_order,num_value').eq('is_active', true).order('sort_order');\r\n      (o.data || []).forEach(function(r){ (S.opts[r.kind] = S.opts[r.kind] || []).push(r); });\r\n    } catch(e){}\r\n    try {\r\n      var s = await sb.from('visualizer_settings').select('key,int_value');\r\n      (s.data || []).forEach(function(r){ if (r.key === 'max_photos') S.maxPhotos = r.int_value; });\r\n    } catch(e){}\r\n    try {\r\n      var c = await sb.from('currencies').select('currency_code,currency_name').eq('is_active', true).order('currency_code');\r\n      var seen = {}; S.currencies = [];\r\n      (c.data || []).forEach(function(r){ if (r.currency_code && !seen[r.currency_code]) { seen[r.currency_code] = 1; S.currencies.push({ value:r.currency_code, label:r.currency_code + (r.currency_name ? ' \u2013 ' + r.currency_name : '') }); } });\r\n    } catch(e){ S.currencies = []; }\r\n    fillSelect($('rType'), S.opts.room_type || [], 'Choose a room\u2026');\r\n    fillSelect($('bWork'), S.opts.work_type || []);\r\n    fillSelect($('bOrient'), S.opts.orientation || [], 'Not sure \/ skip');\r\n    fillSelect($('bBase'), S.opts.base_design || []);\r\n    if (!(S.opts.base_design || []).some(function(r){ return r.value === 'any'; })) {\r\n      var anyOpt = document.createElement('option'); anyOpt.value = 'any'; anyOpt.textContent = 'Open to suggestions'; $('bBase').appendChild(anyOpt);\r\n    }\r\n    $('bBase').value = 'any';\r\n    fillSizes();\r\n    renderMustChips();\r\n    await loadRefs();\r\n  }\r\n\r\n  \/* ================= design references (Builders24 house plans, live from the database) ================= *\/\r\n  function refThumb(p){\r\n    var imgs = (p.house_plan_images || []).slice().sort(function(a,b){ return (a.sort_order||0) - (b.sort_order||0); });\r\n    var pick = imgs[0];\r\n    if (!pick) return '';\r\n    try { return sb.storage.from('house-plans').getPublicUrl(pick.path).data.publicUrl || ''; } catch(e){ return ''; }\r\n  }\r\n  function refSpecs(p){\r\n    var a = [];\r\n    if (p.bedrooms !== null && p.bedrooms !== undefined) a.push(p.bedrooms + ' bed');\r\n    if (p.bathrooms !== null && p.bathrooms !== undefined) a.push(Number(p.bathrooms) + ' bath');\r\n    if (p.storeys) a.push(p.storeys + (p.storeys === 1 ? ' storey' : ' storeys'));\r\n    return a.join(' \u00b7 ');\r\n  }\r\n  function selectRef(id){\r\n    S.ref = id || '';\r\n    document.querySelectorAll('.vz-refchip').forEach(function(c){ c.classList.toggle('active', c.getAttribute('data-id') === S.ref); });\r\n  }\r\n  async function loadRefs(){\r\n    S.refs = [];\r\n    try {\r\n      var r = await sb.from('house_plans').select('id,name,bedrooms,bathrooms,storeys,house_plan_images(kind,path,sort_order)').eq('is_active', true).order('name');\r\n      S.refs = (r.data || []).filter(function(p){ return p.name; });\r\n    } catch(e){ S.refs = []; }\r\n    var grid = $('bRefGrid'); grid.innerHTML = '';\r\n    S.refs.forEach(function(p){\r\n      var c = document.createElement('div');\r\n      c.className = 'glass vz-refchip'; c.setAttribute('data-id', p.id); c.setAttribute('role', 'button'); c.tabIndex = 0;\r\n      var url = refThumb(p);\r\n      c.innerHTML = '<div class=\"sw\"><\/div><b>' + E(p.name) + '<\/b><small>' + E(refSpecs(p)) + '<\/small>';\r\n      if (url) c.querySelector('.sw').style.backgroundImage = 'url(\"' + url.replace(\/\"\/g, '%22') + '\")';\r\n      var tap = function(){ selectRef(S.ref === p.id ? '' : p.id); };\r\n      c.addEventListener('click', tap);\r\n      c.addEventListener('keydown', function(e){ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tap(); } });\r\n      grid.appendChild(c);\r\n    });\r\n    $('bRefWrap').hidden = !S.refs.length;\r\n    renderHelp();\r\n  }\r\n  \/* ---- the \"What can we help you with?\" choices (live: visualizer_options kind help_choice) ---- *\/\r\n  var HELP_ICON = {\r\n    renovate:'<path d=\"M3 11 12 3l9 8\"\/><path d=\"M5 10v11h14V10\"\/><path d=\"M10 21v-6h4v6\"\/>',\r\n    room:'<rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><path d=\"M3 15h18M8 15v6M16 15v6\"\/>',\r\n    stand:'<path d=\"M3 21h18\"\/><path d=\"M5 21V9l7-5 7 5v12\" stroke-dasharray=\"3 3\"\/>',\r\n    extend:'<path d=\"M3 21V10l6-5 6 5v11\"\/><path d=\"M15 21h6v-8l-3-2.5\"\/><path d=\"M9 21v-5h0\"\/>',\r\n    yard:'<path d=\"M12 21v-8\"\/><path d=\"M12 13c-4 0-6-2.5-6-6 4 0 6 2 6 6z\"\/><path d=\"M12 15c3 0 5-2 5-5-3 0-5 1.5-5 5z\"\/><path d=\"M4 21h16\"\/>',\r\n    boundary:'<path d=\"M3 20V8M9 20V8M15 20V8M21 20V8\"\/><path d=\"M3 11h18M3 16h18\"\/>'\r\n  };\r\n  var HELP_GENERIC = '<path d=\"M3 11 12 3l9 8\"\/><path d=\"M5 10v11h14V10\"\/>';\r\n  function jobReady(v){ return v === 'room' || v === 'stand' || (wzOpts('work_type').some(function(o){ return o.value === v; }) && wzOpts('area_' + v).length > 0 && wzOpts('area_' + v).some(function(a){ return wzOffers(v, a.value).length > 0; })); }\r\n  \/\/ the checklist for a part of the property: offer_<job>__<area> when it exists, otherwise the job-wide offer_<job>\r\n  function wzOffers(job, area){ var own = wzOpts('offer_' + job + '__' + area); return own.length ? own : wzOpts('offer_' + job); }\r\n  \/\/ keep only the ticks that exist in this part of the property's checklist (the checklist differs by area)\r\n  function wzCleanItems(a){ if (!W || !a.area) return; var ok = wzOffers(W.job, a.area).map(function(o){ return o.value; }); a.items = a.items.filter(function(v){ return ok.indexOf(v) >= 0; }); }\r\n  function wzOfferLabel(job, area, v){ var o = wzOffers(job, area).filter(function(x){ return x.value === v; })[0]; return o ? o.label : wzLabel('offer_' + job, v); }\r\n  function renderHelp(){\r\n    var rows = (S.opts.help_choice || []).filter(function(o){ return jobReady(o.value); });\r\n    $('helpChoice').innerHTML = rows.map(function(o){\r\n      return '<button type=\"button\" class=\"vz-pick\" data-help=\"' + E(o.value) + '\"><div class=\"pv\"><svg width=\"30\" height=\"30\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.7\" stroke-linecap=\"round\" stroke-linejoin=\"round\">' + (HELP_ICON[o.value] || HELP_GENERIC) + '<\/svg><\/div><b>' + E(o.label) + '<\/b><\/button>';\r\n    }).join('');\r\n    $('helpWords').hidden = !(S.opts.work_type || []).some(function(o){ return jobReady(o.value); });\r\n  }\r\n  \/* ---- preset room sizes (live: visualizer_options kind size_<room>, num_value in m2) ---- *\/\r\n  var SIZE_ROOMS = [['main_bedroom','Main bedroom'],['bedroom','Other bedrooms'],['bathroom','Bathrooms'],['ensuite','Ensuite'],['kitchen','Kitchen'],['lounge','Lounge'],['dining','Dining room'],['double_garage','Double garage']];\r\n  function fillSizes(){\r\n    var box = $('bSizes'); box.innerHTML = ''; var any = false;\r\n    SIZE_ROOMS.forEach(function(r){\r\n      var rows = S.opts['size_' + r[0]] || []; if (!rows.length) return; any = true;\r\n      var f = document.createElement('div');\r\n      f.innerHTML = '<label class=\"vz-mini\" for=\"sz_' + r[0] + '\">' + E(r[1]) + '<\/label><select class=\"vz-in\" id=\"sz_' + r[0] + '\">' +\r\n        rows.map(function(o){ return '<option value=\"' + E(o.value) + '\">' + E(o.label) + (o.num_value ? ' (' + Number(o.num_value) + ' m\u00b2)' : '') + '<\/option>'; }).join('') + '<\/select>';\r\n      box.appendChild(f);\r\n      var sel = f.querySelector('select'); if (rows.some(function(o){ return o.value === 'standard'; })) sel.value = 'standard';\r\n    });\r\n    $('bSizesWrap').hidden = !any;\r\n  }\r\n  function collectSizes(){\r\n    var out = {}; SIZE_ROOMS.forEach(function(r){ var el = $('sz_' + r[0]); if (el && el.value) out[r[0]] = el.value; }); return out;\r\n  }\r\n  function renderMustChips(){\r\n    var box = $('bMustChips'); box.innerHTML = '';\r\n    (S.opts.must_have || []).forEach(function(o){\r\n      var b = document.createElement('button'); b.type = 'button'; b.className = 'vz-chip'; b.textContent = o.label;\r\n      b.onclick = function(){\r\n        var t = $('bMust'), cur = t.value.trim();\r\n        if (cur.toLowerCase().indexOf(o.label.toLowerCase()) >= 0) return;\r\n        var next = cur ? cur.replace(\/[,\\s]+$\/, '') + ', ' + o.label : o.label;\r\n        if (next.length > 800) return;\r\n        t.value = next; updateMustCount();\r\n      };\r\n      box.appendChild(b);\r\n    });\r\n    box.hidden = !box.children.length;\r\n  }\r\n  function updateMustCount(){ $('bMustCount').textContent = $('bMust').value.length + ' \/ 800'; }\r\n  $('bMust').addEventListener('input', updateMustCount);\r\n\r\n  \/* ================= credits ================= *\/\r\n  async function loadQuota(){\r\n    var box = $('credits');\r\n    try {\r\n      var r = await sb.rpc('visualizer_quota');\r\n      if (r.error) throw r.error;\r\n      var q = S.quota = r.data;\r\n      var pct = q.credits_total > 0 ? Math.round(q.credits_left \/ q.credits_total * 100) : 0;\r\n      $('crLeft').textContent = q.credits_left;\r\n      var C = 263.9, dash = Math.max(0, Math.min(100, pct)) \/ 100 * C;\r\n      $('crArc').style.strokeDasharray = dash + ' ' + (C - dash);\r\n      $('crDonut').setAttribute('aria-label', q.credits_left + ' of ' + q.credits_total + ' credits left');\r\n      var enough = q.credits_left >= q.cost;\r\n      box.classList.toggle('out', !enough);\r\n      $('crSub').textContent = enough\r\n        ? q.credits_total + ' free credits every month. Each design uses ' + q.cost + '. Your credits reset on ' + dateShort(q.next_reset_at) + '.'\r\n        : 'You do not have enough credits for another design. Your credits reset on ' + dateShort(q.next_reset_at) + '.';\r\n      document.querySelectorAll('.vz-note').forEach(function(n){ n.textContent = 'Uses ' + q.cost + ' credits. Your photos and design are saved privately to your account. Credits are monthly and reset on ' + dateShort(q.next_reset_at) + '.'; });\r\n      var nd = q.cost > 0 ? Math.floor(q.credits_total \/ q.cost) : 0;\r\n      $('heroCredits').textContent = q.credits_total + ' free credits every month, ' + nd + (nd === 1 ? ' design' : ' designs');\r\n      ['rSubmit','bSubmit'].forEach(function(id){ $(id).disabled = !enough || S.busy; });\r\n      if (W && !$('wiz').hidden && wzSteps()[W.pos] && wzSteps()[W.pos].k === 'summary') wzRender();\r\n    } catch(e){\r\n      $('crSub').textContent = 'Could not check your credits. Please refresh the page.';\r\n    }\r\n  }\r\n\r\n  \/* ================= screens ================= *\/\r\n  function render(){\r\n    if ($('toolLoad')) $('toolLoad').hidden = true;\r\n    var picking = !S.busy && !S.current;\r\n    $('progress').hidden = !S.busy;\r\n    $('result').hidden = !(!S.busy && S.current);\r\n    $('credits').hidden = !!S.busy;\r\n    $('choice').hidden = !(picking && !S.mode);\r\n    $('formWrap').hidden = !(picking && !!S.mode);\r\n    $('formRoom').hidden = S.mode !== 'room';\r\n    $('formBuild').hidden = S.mode !== 'stand';\r\n    $('wiz').hidden = S.mode !== 'house';\r\n  }\r\n  function setMode(mode){\r\n    S.mode = mode;\r\n    if (mode) {\r\n      var stand = mode === 'stand';\r\n      $('modeTitle').textContent = mode === 'room' ? wzLabel('help_choice', 'room') : stand ? wzLabel('help_choice', 'stand') : 'Your existing property';\r\n      $('bWorkWrap').hidden = stand;\r\n      $('bPhotoLbl').innerHTML = (stand ? 'Photos of the stand' : 'Photos of the house') + ' <i>*<\/i>';\r\n      $('bPhotoHint').textContent = stand\r\n        ? 'At least one clear photo of the empty stand, ideally showing the street side and the boundaries. The first photo is your main photo. Photos are optimised for processing, and tap a photo to enlarge it.'\r\n        : 'At least one clear photo of the outside. Add interior shots of the rooms you care about. The first photo is your main photo. Photos are optimised for processing, and tap a photo to enlarge it.';\r\n      syncBase();\r\n      if (mode === 'house') wzEnter(!!S.wzKeep);\r\n    }\r\n    render();\r\n    var w = $('formWrap'); w.classList.remove('vz-reveal'); void w.offsetWidth; if (mode) w.classList.add('vz-reveal');\r\n    if (mode) $('formWrap').scrollIntoView({ behavior:'smooth', block:'start' });\r\n  }\r\n  function syncBase(){\r\n    var show = S.mode === 'stand' || (S.mode === 'house' && ['rebuild','extend'].indexOf($('bWork').value) >= 0);\r\n    $('bBaseWrap').hidden = !show;\r\n  }\r\n  $('helpChoice').addEventListener('click', function(e){\r\n    var b = e.target.closest('[data-help]'); if (!b || S.busy) return;\r\n    var v = b.getAttribute('data-help');\r\n    if (v === 'room' || v === 'stand') { setMode(v); return; }\r\n    startJob(v);\r\n  });\r\n  $('helpWords').addEventListener('click', function(){ if (S.busy) return; S.wzKeep = false; setMode('house'); if (W && !W.job) W.src = 'words'; $('modeTitle').textContent = W && W.job ? wzLabel('work_type', W.job) : 'Tell us what you would like'; });\r\n  function startJob(job){\r\n    S.wzKeep = false; setMode('house');\r\n    \/\/ a saved, unfinished request for the same job is resumed; anything else starts fresh on this job\r\n    if (!(W && W.job === job && W.areas.length)) { wzReset(); W.job = job; W.areas = [wzNewArea()]; W.skipPrompt = true; W.skipJob = true; W.src = 'card'; W.pos = 0; wzRender(); }\r\n    $('modeTitle').textContent = wzLabel('work_type', job);\r\n  }\r\n  $('modeBack').addEventListener('click', function(){ if (!S.busy) setMode(null); });\r\n  $('bWork').addEventListener('change', syncBase);\r\n  $('ensSeg').addEventListener('click', function(e){\r\n    var b = e.target.closest('button'); if (!b) return;\r\n    S.ensuite = b.getAttribute('data-v') === 'yes';\r\n    this.querySelectorAll('button').forEach(function(x){ x.classList.toggle('on', x === b); });\r\n  });\r\n  function paintCurrency(){\r\n    document.querySelectorAll('.curLbl').forEach(function(n){ n.textContent = S.currency; });\r\n    document.querySelectorAll('[data-bph]').forEach(function(n){ n.placeholder = n.getAttribute('data-bph') + ' (' + S.currency + ')'; });\r\n  }\r\n  $('curSel').addEventListener('change', function(){\r\n    var before = S.currency, hasBudget = ['rBMin','rBMax','bBMin','bBMax'].some(function(id){ return $(id).value; });\r\n    S.currency = this.value; paintCurrency();\r\n    if (hasBudget && before !== S.currency) toast('cur', 'Budget amounts are not converted. Please check them in ' + S.currency + '.', 'err');\r\n    try { if (typeof setCustomerCurrency === 'function') setCustomerCurrency(S.currency); } catch(e){}\r\n  });\r\n\r\n  \/* ---- room \/ stand size: width x depth fills the area, typing an area switches width and depth off ---- *\/\r\n  function linkSize(wId, dId, aId, calcId){\r\n    var w = $(wId), d = $(dId), a = $(aId), c = $(calcId);\r\n    function sync(from){\r\n      if (from === 'a') {\r\n        var has = !!a.value && !a.readOnly;\r\n        w.disabled = d.disabled = has;\r\n        if (has) { w.value = ''; d.value = ''; c.textContent = ''; }\r\n        return;\r\n      }\r\n      var wv = num(w.value), dv = num(d.value);\r\n      if (wv && dv) { a.value = (Math.round(wv * dv * 10) \/ 10); a.readOnly = true; c.textContent = 'Width \u00d7 depth = ' + a.value + ' m\u00b2'; }\r\n      else if (a.readOnly) { a.value = ''; a.readOnly = false; c.textContent = ''; }\r\n    }\r\n    w.addEventListener('input', function(){ sync('wd'); });\r\n    d.addEventListener('input', function(){ sync('wd'); });\r\n    a.addEventListener('input', function(){ sync('a'); });\r\n    return function reset(){ a.readOnly = false; w.disabled = d.disabled = false; c.textContent = ''; };\r\n  }\r\n  var resetRoomSize = linkSize('rW', 'rD', 'rA', 'rCalc');\r\n  var resetStandSize = linkSize('bW', 'bD', 'bA', 'bCalc');\r\n\r\n  \/* ================= submit ================= *\/\r\n  function showErr(id, msg){ $(id).textContent = msg || ''; if (msg) $(id).scrollIntoView({ behavior:'smooth', block:'center' }); }\r\n  function fnMessage(error){\r\n    return new Promise(function(resolve){\r\n      try {\r\n        if (error && error.context && typeof error.context.json === 'function') {\r\n          error.context.json().then(function(j){ resolve((j && j.error) || BUSY); }, function(){ resolve(BUSY); });\r\n        } else resolve(BUSY);\r\n      } catch(e){ resolve(BUSY); }\r\n    });\r\n  }\r\n\r\n  function collectRoom(){\r\n    var type = $('rType').value, label = $('rType').selectedOptions[0] ? $('rType').selectedOptions[0].textContent : type;\r\n    var wants = $('rWants').value.trim();\r\n    if (!type) return { error:'Please choose which room this is.' };\r\n    if (!S.pickers.room.items().length) return { error:'Please add a photo of the room.' };\r\n    if (!wants) return { error:'Please tell us what you want done to the room.' };\r\n    return { input:{\r\n      title: label + ' redesign', room_type:type, room_label:label, wants:wants,\r\n      room_width_m:num($('rW').value), room_depth_m:num($('rD').value), room_area_m2:num($('rA').value),\r\n      keep:$('rKeep').value.trim(), budget_min:num($('rBMin').value), budget_max:num($('rBMax').value),\r\n      style_hint:S.style\r\n    }, photos:S.pickers.room.items() };\r\n  }\r\n  function collectBuild(){\r\n    var stand = S.mode === 'stand';\r\n    var w = num($('bW').value), d = num($('bD').value), a = num($('bA').value);\r\n    var beds = num($('bBeds').value), baths = num($('bBaths').value);\r\n    if (!S.pickers.build.items().length) return { error: stand ? 'Please add a photo of the stand.' : 'Please add at least one photo of the house.' };\r\n    if (!a && !(w && d)) return { error:'Please enter the stand size: width and depth, or the total area.' };\r\n    if (beds === null || baths === null) return { error:'Please enter how many bedrooms and bathrooms you want.' };\r\n    if (S.ensuite && baths < 1) return { error:'You asked for an ensuite, so bathrooms must be at least 1.' };\r\n    var baseOn = !$('bBaseWrap').hidden && $('bBase').value;\r\n    return { input:{\r\n      title: (stand ? 'New home on a stand' : 'House design') + ': ' + beds + ' bed, ' + baths + ' bath',\r\n      site_type: stand ? 'stand' : 'house', work_type: stand ? 'new_build' : $('bWork').value,\r\n      stand_width_m:w, stand_depth_m:d, stand_area_m2:a, bedrooms:beds, bathrooms:baths, ensuite:S.ensuite,\r\n      base_design: baseOn ? $('bBase').value : '', base_design_label: baseOn ? $('bBase').selectedOptions[0].textContent : '',\r\n      house_plan_id: S.ref || '', sizes: collectSizes(),\r\n      must_haves:$('bMust').value.trim(), orientation:$('bOrient').value ? $('bOrient').selectedOptions[0].textContent : '',\r\n      location:$('bLoc').value.trim(), budget_min:num($('bBMin').value), budget_max:num($('bBMax').value),\r\n      style_hint:''\r\n    }, photos:S.pickers.build.items() };\r\n  }\r\n\r\n  \/* ================= existing property: one question at a time =================\r\n     Jobs, areas, offers and answer chips all come from visualizer_options (live). Only the question wording is here. *\/\r\n  var WZ_Q = {\r\n    renovate:  [{ kind:'reno_style', q:'What overall style are you after?', multi:false }, { kind:'reno_palette', q:'Which colour scheme do you prefer?', multi:false }],\r\n    extend:    [{ kind:'ext_storeys', q:'Single or double storey?', multi:false }],\r\n    additions: [{ kind:'add_use', q:'What will it mainly be used for?', multi:false }, { kind:'add_service', q:'Fully self-contained, or just a room?', multi:false }, { kind:'add_build', q:'How should it be built?', multi:false }],\r\n    boundary:  [{ kind:'boundary_goal', q:'What matters most?', multi:true }, { kind:'boundary_style', q:'Which finish do you prefer?', multi:true }],\r\n    yard:      [{ kind:'yard_goal', q:'What is your main goal?', multi:true }, { kind:'yard_ground', q:'What is the ground like?', multi:false }, { kind:'yard_feel', q:'Low maintenance or lush?', multi:false }, { kind:'yard_material', q:'Which materials do you prefer?', multi:true }]\r\n  };\r\n  var WZ_M2 = {\r\n    boundary:'Roughly how many metres of wall or fence is this?',\r\n    yard:'Roughly how many m\u00b2 are we paving or landscaping here?',\r\n    additions:'Roughly how big should the new building be, in m\u00b2?',\r\n    extend:'Roughly how big should the extension be, in m\u00b2?',\r\n    renovate:'Roughly how big is the area being renovated, in m\u00b2?'\r\n  };\r\n  var WZ_KEY = 'vz-wiz-v1', W = null;\r\n  function wzFresh(){ return { job:'', areas:[], answers:{}, wants:'', stand:'', loc:'', bmin:'', bmax:'', pos:0, prompt:'', promptDone:'', skipPrompt:false, skipJob:false, ref:'', src:'', listPick:false, ret:false }; }\r\n  function wzNewArea(){ return { area:'', items:[], m2:'', note:'', picker:null, host:null, skipArea:false, more:'' }; }\r\n  function wzOpts(kind){ return S.opts[kind] || []; }\r\n  function wzLabel(kind, value){ var o = wzOpts(kind).filter(function(x){ return x.value === value; })[0]; return o ? o.label : String(value || '').replace(\/_\/g, ' '); }\r\n  function wzCount(a){ return a.picker ? a.picker.items().length : 0; }\r\n  function wzUsed(){ return W.areas.reduce(function(n, a){ return n + wzCount(a); }, 0); }\r\n  function wzCap(a){\r\n    var other = 0, empty = 0;\r\n    W.areas.forEach(function(x){ if (x === a) return; var n = wzCount(x); other += n; if (!n) empty++; });\r\n    return Math.max(1, S.maxPhotos - other - empty);\r\n  }\r\n  function wzPicker(a){\r\n    if (!a.picker) {\r\n      var id = 'wzPh' + Date.now() + Math.floor(Math.random() * 1000);\r\n      var host = document.createElement('div'); host.id = id;\r\n      host.innerHTML = '<div id=\"' + id + 'p\"><\/div><div class=\"vz-err\" id=\"' + id + 'e\"><\/div>';\r\n      $('wzStash').appendChild(host);\r\n      a.host = host;\r\n      a.picker = makePicker(id + 'p', id + 'e', function(){ return wzCap(a); });\r\n    }\r\n    return a.picker;\r\n  }\r\n  function wzSave(){\r\n    try {\r\n      sessionStorage.setItem(WZ_KEY, JSON.stringify({ job:W.job, answers:W.answers, wants:W.wants, stand:W.stand, loc:W.loc, bmin:W.bmin, bmax:W.bmax, pos:W.pos, prompt:W.prompt, promptDone:W.promptDone, skipPrompt:W.skipPrompt, skipJob:W.skipJob, ref:W.ref, src:W.src, listPick:W.listPick,\r\n        areas:W.areas.map(function(a){ return { area:a.area, items:a.items, m2:a.m2, note:a.note, skipArea:a.skipArea, more:a.more }; }) }));\r\n    } catch(e){}\r\n  }\r\n  function wzDrop(){ try { sessionStorage.removeItem(WZ_KEY); } catch(e){} }\r\n  function wzReset(){\r\n    if (W) W.areas.forEach(function(a){ if (a.picker) a.picker.clear(); if (a.host) a.host.remove(); });\r\n    W = wzFresh(); wzDrop();\r\n  }\r\n  function wzRestore(){\r\n    var raw = null; try { raw = sessionStorage.getItem(WZ_KEY); } catch(e){}\r\n    if (!raw) return false;\r\n    try {\r\n      var o = JSON.parse(raw);\r\n      if (!o || !o.job || !Array.isArray(o.areas) || !o.areas.length) return false;\r\n      wzReset();\r\n      W.job = String(o.job); W.answers = o.answers && typeof o.answers === 'object' ? o.answers : {};\r\n      W.prompt = String(o.prompt || ''); W.promptDone = String(o.promptDone || ''); W.skipPrompt = !!o.skipPrompt; W.skipJob = !!o.skipJob; W.ref = String(o.ref || ''); W.src = String(o.src || ''); W.listPick = !!o.listPick;\r\n      W.wants = String(o.wants || ''); W.stand = String(o.stand || ''); W.loc = String(o.loc || ''); W.bmin = String(o.bmin || ''); W.bmax = String(o.bmax || '');\r\n      W.areas = o.areas.slice(0, 3).map(function(a){ var n = wzNewArea(); n.area = String(a.area || ''); n.items = Array.isArray(a.items) ? a.items.map(String) : []; n.m2 = String(a.m2 || ''); n.note = String(a.note || ''); n.skipArea = !!a.skipArea; n.more = a.more === 'yes' || a.more === 'no' ? a.more : ''; return n; });\r\n      W.areas.forEach(wzCleanItems);\r\n      W.pos = Math.max(0, parseInt(o.pos, 10) || 0);\r\n      return true;\r\n    } catch(e){ return false; }\r\n  }\r\n  function wzFrom(inp){\r\n    wzReset();\r\n    W.job = inp.work_type || ''; W.skipPrompt = true; W.skipJob = !!W.job; W.ref = inp.house_plan_id || '';\r\n    W.areas = (inp.areas || []).slice(0, 3).map(function(a){ var n = wzNewArea(); n.area = a.area || ''; n.items = (a.items || []).slice(); n.m2 = a.area_m2 === null || a.area_m2 === undefined ? '' : String(a.area_m2); n.note = a.note || ''; return n; });\r\n    W.answers = inp.answers && typeof inp.answers === 'object' ? JSON.parse(JSON.stringify(inp.answers)) : {};\r\n    W.wants = inp.wants || ''; W.stand = inp.stand_area_m2 ? String(inp.stand_area_m2) : ''; W.loc = inp.location || '';\r\n    W.bmin = inp.budget_min === null || inp.budget_min === undefined ? '' : String(inp.budget_min); W.bmax = inp.budget_max === null || inp.budget_max === undefined ? '' : String(inp.budget_max);\r\n    if (!wzOpts('work_type').some(function(o){ return o.value === W.job; })) { W.job = ''; W.areas = []; W.answers = {}; }\r\n    if (!W.areas.length && W.job) W.areas = [wzNewArea()];\r\n    W.areas.forEach(wzCleanItems);\r\n    \/\/ photos are not kept, so a variation starts at the first photo step (or the area step for older designs)\r\n    W.areas.forEach(function(a, i){ a.skipArea = !!a.area; a.more = i < W.areas.length - 1 ? 'yes' : 'no'; });\r\n    W.pos = 0;\r\n  }\r\n  function wzSteps(){\r\n    var s = [];\r\n    if (!W.skipPrompt) s.push({ k:'prompt' });\r\n    if (!W.skipJob) s.push({ k:'job' });\r\n    if (!W.job) return s;\r\n    for (var i = 0; i < W.areas.length; i++) {\r\n      var a = W.areas[i];\r\n      if (!a.skipArea) s.push({ k:'area', i:i });\r\n      s.push({ k:'photo', i:i }, { k:'offers', i:i });\r\n      \/\/ after each area the customer decides whether to show another one; the answer can be changed by going back\r\n      if (i < 2 && (a.more === 'yes' || wzUsed() < S.maxPhotos)) s.push({ k:'more', i:i });\r\n      if (a.more !== 'yes') break;\r\n    }\r\n    (WZ_Q[W.job] || []).forEach(function(q){ if (wzOpts(q.kind).length) s.push({ k:'q', kind:q.kind }); });\r\n    if (S.refs.length) s.push({ k:'style' });\r\n    s.push({ k:'wants' }, { k:'extras' }, { k:'summary' });\r\n    return s;\r\n  }\r\n  function wzKeyOf(st){ return st.k + ':' + (st.i === undefined ? '' : st.i) + ':' + (st.kind || ''); }\r\n  function wzGo(k, i){\r\n    if (k === 'area' && W.areas[i]) W.areas[i].skipArea = false;\r\n    if (k === 'job') W.skipJob = false;\r\n    if (k === 'prompt') W.skipPrompt = false;\r\n    var steps = wzSteps();\r\n    for (var n = 0; n < steps.length; n++) if (steps[n].k === k && (i === undefined || steps[n].i === i)) { W.pos = n; wzRender(); return; }\r\n  }\r\n  function wzOpt(attr, val, label, on, multi){\r\n    return '<button type=\"button\" class=\"wz-opt ' + (multi ? 'multi' : 'single') + (on ? ' on' : '') + '\" ' + attr + '=\"' + E(val) + '\" aria-pressed=\"' + (on ? 'true' : 'false') + '\"><span class=\"ck\">' + (on ? '\u2713' : '') + '<\/span><span>' + E(label) + '<\/span><\/button>';\r\n  }\r\n  function wzRender(){\r\n    if (!W) return;\r\n    var steps = wzSteps();\r\n    if (W.pos > steps.length - 1) W.pos = steps.length - 1;\r\n    if (W.pos < 0) W.pos = 0;\r\n    var st = steps[W.pos], card = $('wzCard'), h = '', a = st.i !== undefined ? W.areas[st.i] : null;\r\n    var jobLabel = W.job ? wzLabel('work_type', W.job) : '';\r\n    $('wzErr').textContent = '';\r\n    \/\/ keep every photo picker in the stash while another step is showing\r\n    W.areas.forEach(function(x){ if (x.host && x.host.parentNode !== $('wzStash')) $('wzStash').appendChild(x.host); });\r\n\r\n    if (st.k === 'prompt') {\r\n      h += '<div class=\"wz-q\">What would you like to do to your property?<\/div><div class=\"wz-sub\">Tell us in your own words. We use it to set up your design, then ask only what we still need.<\/div>' +\r\n        '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"wzPrompt\">Your request<\/label><textarea class=\"vz-in\" id=\"wzPrompt\" maxlength=\"300\" placeholder=\"For example: renovate and modernise my house\">' + E(W.prompt) + '<\/textarea><\/div>' +\r\n        (wzOpts('prompt_example').length ? '<div class=\"vz-hint\">Tap an example to start from it.<\/div><div class=\"vz-chips\">' + wzOpts('prompt_example').map(function(o){ return '<button type=\"button\" class=\"vz-chip\" data-eg=\"' + E(o.label) + '\">' + E(o.label) + '<\/button>'; }).join('') + '<\/div>' : '') +\r\n        '<div><button type=\"button\" class=\"wz-restart\" data-skipprompt=\"1\">Or choose from a list<\/button><\/div>';\r\n    } else if (st.k === 'job') {\r\n      h += '<div class=\"wz-q\">What would you like to do to your property?<\/div><div class=\"wz-sub\">Choose one. You can show us more than one part of your property afterwards.<\/div><div class=\"wz-opts\">' +\r\n        wzOpts('work_type').filter(function(o){ return jobReady(o.value); }).map(function(o){ return wzOpt('data-job', o.value, o.label, W.job === o.value, false); }).join('') + '<\/div>';\r\n    } else if (st.k === 'area' && !wzOpts('area_' + W.job).length) {\r\n      h += '<div class=\"wz-q\">This option is not available at the moment.<\/div><div class=\"wz-sub\">Please go back and choose another, or try again later.<\/div>';\r\n    } else if (st.k === 'area') {\r\n      h += '<div class=\"wz-q\">' + (st.i === 0 ? 'Which part of your property will your first photo show?' : 'Which other part of your property will this photo show?') + '<\/div><div class=\"wz-sub\">' + E(jobLabel) + '<\/div><div class=\"wz-opts two\">' +\r\n        wzOpts('area_' + W.job).map(function(o){ return wzOpt('data-area', o.value, o.label, a.area === o.value, false); }).join('') + '<\/div>';\r\n    } else if (st.k === 'photo') {\r\n      var al = wzLabel('area_' + W.job, a.area).toLowerCase();\r\n      h += '<div class=\"wz-q\">Add a photo of your ' + E(al) + '<\/div><div class=\"wz-sub\">One clear photo that shows the whole area, taken from where you would like to see the result. You can add a second angle. Your design is drawn on your own photo. You can add up to ' + S.maxPhotos + ' photos in total.<\/div><div id=\"wzPhotoSlot\"><\/div>';\r\n    } else if (st.k === 'offers') {\r\n      var al2 = wzLabel('area_' + W.job, a.area).toLowerCase();\r\n      h += '<div class=\"wz-q\">What would you like for your ' + E(al2) + '?<\/div><div class=\"wz-sub\">Tick every change you want. Each one is drawn on your photo.<\/div><div class=\"wz-opts\">' +\r\n        wzOffers(W.job, a.area).map(function(o){ return wzOpt('data-offer', o.value, o.label, a.items.indexOf(o.value) >= 0, true); }).join('') + '<\/div>' +\r\n        '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"wzM2\">' + E(WZ_M2[W.job] || WZ_M2.renovate) + ' (optional)<\/label><input class=\"vz-in\" id=\"wzM2\" type=\"number\" inputmode=\"decimal\" min=\"1\" step=\"1\" value=\"' + E(a.m2) + '\"><div class=\"vz-hint\" style=\"margin-top:6px;\">If you leave this out we estimate it from your photo and say so in your results.<\/div><\/div>' +\r\n        '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"wzNote2\">Anything specific for this area? (optional)<\/label><textarea class=\"vz-in\" id=\"wzNote2\" maxlength=\"300\" style=\"min-height:80px;\" placeholder=\"For example: keep the jacaranda tree, paving to match the house.\">' + E(a.note) + '<\/textarea><\/div>';\r\n    } else if (st.k === 'more') {\r\n      h += '<div class=\"wz-q\">Would you like to show another part of your property?<\/div><div class=\"wz-sub\">For example the front yard as well as the back yard. You have used ' + wzUsed() + ' of ' + S.maxPhotos + ' photos.<\/div><div class=\"wz-opts two\">' +\r\n        wzOpt('data-more', 'yes', 'Yes, add another area', a.more === 'yes', false) + wzOpt('data-more', 'no', 'No, continue', a.more === 'no', false) + '<\/div>';\r\n    } else if (st.k === 'q') {\r\n      var q = (WZ_Q[W.job] || []).filter(function(x){ return x.kind === st.kind; })[0], cur = W.answers[st.kind] || [];\r\n      h += '<div class=\"wz-q\">' + E(q.q) + '<\/div><div class=\"wz-sub\">' + (q.multi ? 'Tick all that apply, or skip.' : 'Choose one, or skip.') + '<\/div><div class=\"wz-opts two\">' +\r\n        wzOpts(st.kind).map(function(o){ return wzOpt('data-ans', o.value, o.label, cur.indexOf(o.value) >= 0, q.multi); }).join('') + '<\/div>';\r\n    } else if (st.k === 'style') {\r\n      h += '<div class=\"wz-q\">Do you like the style of one of our designs?<\/div><div class=\"wz-sub\">Optional. Your result will lean towards its look: roof, finishes, windows and colours. It is a style guide only and does not set your rooms or layout. Tap it again to clear it, or skip.<\/div><div class=\"vz-refgrid\">' +\r\n        S.refs.map(function(p){ var u = refThumb(p); return '<div class=\"glass vz-refchip' + (W.ref === p.id ? ' active' : '') + '\" data-wzref=\"' + E(p.id) + '\" role=\"button\" tabindex=\"0\" aria-pressed=\"' + (W.ref === p.id ? 'true' : 'false') + '\"><div class=\"sw\"' + (u ? ' style=\"background-image:url(&quot;' + E(u) + '&quot;)\"' : '') + '><\/div><b>' + E(p.name) + '<\/b><\/div>'; }).join('') + '<\/div>';\r\n    } else if (st.k === 'wants') {\r\n      h += '<div class=\"wz-q\">Anything else we should know?<\/div><div class=\"wz-sub\">Optional. Tell us about must-haves, things to keep, or problems you know about.<\/div>' +\r\n        '<div class=\"vz-field\"><textarea class=\"vz-in\" id=\"wzWants\" maxlength=\"800\" placeholder=\"For example: we have two dogs, so the fence must be secure.\">' + E(W.wants) + '<\/textarea><\/div>';\r\n    } else if (st.k === 'extras') {\r\n      h += '<div class=\"wz-q\">A few last details<\/div><div class=\"wz-sub\">All optional. They make the quantities and the budget check more accurate.<\/div>' +\r\n        '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"wzStand\">Total stand size (m\u00b2)<\/label><input class=\"vz-in\" id=\"wzStand\" type=\"number\" inputmode=\"decimal\" min=\"10\" step=\"1\" value=\"' + E(W.stand) + '\"><\/div>' +\r\n        '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"wzLoc\">Where is it? City or region<\/label><input class=\"vz-in\" id=\"wzLoc\" type=\"text\" maxlength=\"120\" value=\"' + E(W.loc) + '\" placeholder=\"For example: Pretoria, Gauteng\"><\/div>' +\r\n        '<div class=\"vz-field\"><span class=\"vz-lbl\">Budget, rough range (' + E(S.currency) + ')<\/span><div class=\"vz-grid2\"><input class=\"vz-in\" id=\"wzBMin\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"From (' + E(S.currency) + ')\" value=\"' + E(W.bmin) + '\"><input class=\"vz-in\" id=\"wzBMax\" type=\"number\" inputmode=\"numeric\" min=\"0\" placeholder=\"To (' + E(S.currency) + ')\" value=\"' + E(W.bmax) + '\"><\/div><\/div>';\r\n    } else if (st.k === 'summary') {\r\n      h += '<div class=\"wz-q\">Check your design request<\/div><div class=\"wz-sub\">Change anything you like, then create your design.<\/div><div class=\"wz-sum\">' +\r\n        '<div class=\"wz-sumbox\"><h4>What you want to do<\/h4><p>' + E(jobLabel) + '<\/p>' + (W.prompt.trim() ? '<p>\u201c' + E(W.prompt.trim()) + '\u201d<\/p>' : '') + '<div class=\"wz-sumact\"><button type=\"button\" data-goto=\"job\">Change<\/button><\/div><\/div>';\r\n      W.areas.forEach(function(x, i){\r\n        var thumbs = x.picker ? x.picker.items().map(function(it){ return '<img decoding=\"async\" src=\"' + E(it.url) + '\" alt=\"\">'; }).join('') : '';\r\n        h += '<div class=\"wz-sumbox\"><h4>' + E(wzLabel('area_' + W.job, x.area) || 'Area ' + (i + 1)) + '<\/h4>' + (thumbs ? '<div class=\"wz-thumbs\">' + thumbs + '<\/div>' : '<p><b>No photo added yet.<\/b><\/p>') +\r\n          '<ul>' + x.items.map(function(v){ return '<li>' + E(wzOfferLabel(W.job, x.area, v)) + '<\/li>'; }).join('') + '<\/ul>' +\r\n          (x.m2 ? '<p>About ' + E(x.m2) + (W.job === 'boundary' ? ' m' : ' m\u00b2') + '<\/p>' : '') + (x.note ? '<p>' + E(x.note) + '<\/p>' : '') +\r\n          '<div class=\"wz-sumact\"><button type=\"button\" data-goto=\"area\" data-i=\"' + i + '\">Change area<\/button><button type=\"button\" data-goto=\"photo\" data-i=\"' + i + '\">Change photo<\/button><button type=\"button\" data-goto=\"offers\" data-i=\"' + i + '\">Change choices<\/button>' + (W.areas.length > 1 ? '<button type=\"button\" data-rm=\"' + i + '\">Remove this area<\/button>' : '') + '<\/div><\/div>';\r\n      });\r\n      var ans = Object.keys(W.answers).filter(function(k){ return (W.answers[k] || []).length; });\r\n      if (ans.length) h += '<div class=\"wz-sumbox\"><h4>Your answers<\/h4><ul>' + ans.map(function(k){ return '<li>' + E(W.answers[k].map(function(v){ return wzLabel(k, v); }).join(', ')) + '<\/li>'; }).join('') + '<\/ul><\/div>';\r\n      var refPick = S.refs.filter(function(p){ return p.id === W.ref; })[0];\r\n      if (refPick) h += '<div class=\"wz-sumbox\"><h4>Style inspiration<\/h4><p>' + E(refPick.name) + '<\/p><div class=\"wz-sumact\"><button type=\"button\" data-goto=\"style\">Change<\/button><\/div><\/div>';\r\n      if (W.wants.trim()) h += '<div class=\"wz-sumbox\"><h4>Notes<\/h4><p>' + E(W.wants) + '<\/p><div class=\"wz-sumact\"><button type=\"button\" data-goto=\"wants\">Change<\/button><\/div><\/div>';\r\n      h += '<\/div><div class=\"vz-get\"><b>What you will get<\/b><ul><li>Your own photo with your choices shown on it<\/li><li>A materials list with quantities and estimated prices<\/li><li>A branded PDF and the option to request a quote<\/li><\/ul><\/div><div class=\"vz-caveat\">AI concept for planning. Not construction drawings.<\/div>';\r\n    }\r\n    card.innerHTML = h;\r\n    if (st.k === 'photo') { var pk = wzPicker(a); $('wzPhotoSlot').appendChild(a.host); pk.repaint(); }\r\n\r\n    var last = st.k === 'summary', hideNext = false;\r\n    $('wzNext').hidden = hideNext;\r\n    if (last) W.ret = false;\r\n    $('wzNext').textContent = last ? 'Create My Design' : (st.k === 'prompt' ? 'Continue' : (W.ret ? 'Back to summary' : 'Next'));\r\n    var enough = !S.quota || S.quota.credits_left >= S.quota.cost;\r\n    $('wzNext').disabled = last && (!enough || S.busy);\r\n    $('wzNote').hidden = !last;\r\n    if (last && S.quota) $('wzNote').textContent = 'Uses ' + S.quota.cost + ' credits. Your photos and design are saved privately to your account. Credits are monthly and reset on ' + dateShort(S.quota.next_reset_at) + '.';\r\n    var pct = steps.length > 1 ? Math.round(W.pos \/ (steps.length - 1) * 100) : 0;\r\n    $('wzBar').style.width = pct + '%'; document.querySelector('#wiz .wz-bar').setAttribute('aria-valuenow', pct);\r\n    $('wzCount').textContent = 'Step ' + (W.pos + 1) + ' of ' + steps.length;\r\n    wzSave();\r\n  }\r\n  function wzReadFields(){\r\n    var st = wzSteps()[W.pos], a = st.i !== undefined ? W.areas[st.i] : null;\r\n    if (st.k === 'prompt' && $('wzPrompt')) W.prompt = $('wzPrompt').value;\r\n    else if (st.k === 'offers' && a) { var m = $('wzM2'), n = $('wzNote2'); if (m) a.m2 = m.value.trim(); if (n) a.note = n.value.trim(); }\r\n    else if (st.k === 'wants' && $('wzWants')) W.wants = $('wzWants').value;\r\n    else if (st.k === 'extras' && $('wzStand')) { W.stand = $('wzStand').value.trim(); W.loc = $('wzLoc').value.trim(); W.bmin = $('wzBMin').value.trim(); W.bmax = $('wzBMax').value.trim(); }\r\n  }\r\n  function wzCheck(st){\r\n    var a = st.i !== undefined ? W.areas[st.i] : null;\r\n    if (st.k === 'job' && !W.job) return 'Please choose what you would like to do.';\r\n    if (st.k === 'area' && !a.area) return 'Please choose which part of your property the photo shows.';\r\n    if (st.k === 'photo' && !wzCount(a)) return 'Please add a photo of this area.';\r\n    if (st.k === 'more' && !a.more) return 'Please choose Yes or No.';\r\n    if (st.k === 'offers') {\r\n      if (!a.items.length) return 'Please tick at least one thing for this area.';\r\n      if (a.m2 !== '' && !(num(a.m2) > 0)) return 'The area must be a number greater than zero, or leave it empty.';\r\n    }\r\n    if (st.k === 'extras') {\r\n      if (W.stand !== '' && !(num(W.stand) >= 10)) return 'The stand size must be 10 m\u00b2 or more, or leave it empty.';\r\n      if ((W.bmin !== '' && num(W.bmin) === null) || (W.bmax !== '' && num(W.bmax) === null)) return 'Please enter the budget as numbers only.';\r\n    }\r\n    return '';\r\n  }\r\n  async function wzIntent(text){\r\n    var r = null;\r\n    try {\r\n      r = await Promise.race([ sb.functions.invoke('visualize-plan', { body:{ action:'intent', text:text } }), new Promise(function(res){ setTimeout(function(){ res(null); }, 25000); }) ]);\r\n    } catch(e){ r = null; }\r\n    var d = r && r.data;\r\n    return d && d.ok && d.intent && d.intent.work_type ? d.intent : null;\r\n  }\r\n  async function wzNext(){\r\n    if (S.busy) return;\r\n    wzReadFields();\r\n    var steps = wzSteps(), st = steps[W.pos], msg = wzCheck(st);\r\n    if (msg) { $('wzErr').textContent = msg; return; }\r\n    if (st.k === 'prompt') {\r\n      var text = W.prompt.trim();\r\n      if (!text) { $('wzErr').textContent = 'Please tell us what you would like to do, or choose from a list.'; return; }\r\n      if (text !== W.promptDone) {\r\n        $('wzNext').disabled = true; $('wzNext').textContent = 'Reading your request\u2026';\r\n        var intent = await wzIntent(text);\r\n        $('wzNext').disabled = false;\r\n        W.promptDone = text;\r\n        \/\/ a new reading replaces what the earlier reading set up\r\n        W.areas.forEach(function(x){ if (x.picker) x.picker.clear(); if (x.host) x.host.remove(); });\r\n        W.areas = []; W.answers = {};\r\n        if (intent && jobReady(intent.work_type)) {\r\n          W.job = intent.work_type; W.skipJob = true;\r\n          W.areas = (intent.areas || []).slice(0, 3).map(function(x){ var n = wzNewArea(); n.area = x.area || ''; n.items = (x.items || []).slice(); n.skipArea = !!n.area; return n; });\r\n          if (!W.areas.length) W.areas = [wzNewArea()];\r\n        } else { W.job = ''; W.skipJob = false; }\r\n      }\r\n      W.pos++; wzRender(); return;\r\n    }\r\n    if (st.k === 'summary') {\r\n      \/\/ every area still needs its photo (answers can be restored after a refresh, photos cannot)\r\n      for (var i = 0; i < W.areas.length; i++) if (!wzCount(W.areas[i])) { wzGo('photo', i); $('wzErr').textContent = 'Please add a photo of this area again before you create your design.'; return; }\r\n      submit('build'); return;\r\n    }\r\n    if (W.ret) { W.ret = false; W.pos = wzSteps().length - 1; wzRender(); return; }\r\n    W.pos++; wzRender();\r\n  }\r\n  function wzBack(){\r\n    if (S.busy) return;\r\n    wzReadFields();\r\n    if (W.pos === 0) {\r\n      \/\/ the first visible step: step back through how this request was started before leaving the form\r\n      if (W.skipJob && W.listPick) { W.skipJob = false; W.listPick = false; wzRender(); return; }\r\n      if (W.skipPrompt && W.src === 'words') { W.skipPrompt = false; wzRender(); return; }\r\n      setMode(null); return;\r\n    }\r\n    W.pos--; wzRender();\r\n  }\r\n  $('wzNext').addEventListener('click', function(){ wzNext(); });\r\n  $('wzBack').addEventListener('click', wzBack);\r\n  $('wzRestart').addEventListener('click', function(){ if (S.busy) return; wzReset(); wzRender(); });\r\n  function wzPickRef(el){\r\n    var id = el.getAttribute('data-wzref'); W.ref = W.ref === id ? '' : id;\r\n    $('wzCard').querySelectorAll('[data-wzref]').forEach(function(c){ var on = c.getAttribute('data-wzref') === W.ref; c.classList.toggle('active', on); c.setAttribute('aria-pressed', on ? 'true' : 'false'); });\r\n    wzSave();\r\n  }\r\n  $('wzCard').addEventListener('keydown', function(e){ var c = e.target.closest('[data-wzref]'); if (c && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); wzPickRef(c); } });\r\n  $('wzCard').addEventListener('click', function(e){\r\n    var rf = e.target.closest('[data-wzref]'); if (rf && !S.busy) { wzPickRef(rf); return; }\r\n    var b = e.target.closest('button'); if (!b || S.busy) return;\r\n    if (b.hasAttribute('data-eg')) { $('wzPrompt').value = b.getAttribute('data-eg'); return; }\r\n    if (b.hasAttribute('data-skipprompt')) { wzReadFields(); W.skipPrompt = true; W.pos = 0; wzRender(); return; }\r\n    var st = wzSteps()[W.pos], a = st.i !== undefined ? W.areas[st.i] : null;\r\n    if (b.hasAttribute('data-job')) {\r\n      var jv = b.getAttribute('data-job');\r\n      if (W.job !== jv) { W.areas.forEach(function(x){ if (x.picker) x.picker.clear(); if (x.host) x.host.remove(); }); W.job = jv; W.areas = [wzNewArea()]; W.answers = {}; }\r\n      W.skipJob = true; W.listPick = true; wzRender();\r\n    } else if (b.hasAttribute('data-area')) {\r\n      var av = b.getAttribute('data-area');\r\n      if (a.area !== av) { a.area = av; wzCleanItems(a); }\r\n      W.pos++; wzRender();\r\n    } else if (b.hasAttribute('data-offer')) {\r\n      var ov = b.getAttribute('data-offer'), ix = a.items.indexOf(ov);\r\n      wzReadFields();\r\n      if (ix >= 0) a.items.splice(ix, 1); else a.items.push(ov);\r\n      var keepTop = window.scrollY; wzRender(); window.scrollTo(0, keepTop);\r\n    } else if (b.hasAttribute('data-more')) {\r\n      var cur = W.areas[st.i];\r\n      if (b.getAttribute('data-more') === 'yes') {\r\n        cur.more = 'yes';\r\n        if (!W.areas[st.i + 1]) W.areas.push(wzNewArea());\r\n      } else {\r\n        \/\/ \"No\" also removes any further area that was added by an earlier \"Yes\", with its photos\r\n        cur.more = 'no';\r\n        W.areas.slice(st.i + 1).forEach(function(x){ if (x.picker) x.picker.clear(); if (x.host) x.host.remove(); });\r\n        W.areas.length = st.i + 1;\r\n      }\r\n      W.pos++; wzRender();\r\n    } else if (b.hasAttribute('data-ans')) {\r\n      var q = (WZ_Q[W.job] || []).filter(function(x){ return x.kind === st.kind; })[0], v = b.getAttribute('data-ans'), cur = W.answers[st.kind] || [], j = cur.indexOf(v);\r\n      if (q.multi) { if (j >= 0) cur.splice(j, 1); else cur.push(v); W.answers[st.kind] = cur; var keep = window.scrollY; wzRender(); window.scrollTo(0, keep); }\r\n      else if (j >= 0) { W.answers[st.kind] = []; wzRender(); }\r\n      else { W.answers[st.kind] = [v]; W.pos++; wzRender(); }\r\n    } else if (b.hasAttribute('data-goto')) {\r\n      var gi = b.getAttribute('data-i'); W.ret = true; wzGo(b.getAttribute('data-goto'), gi === null ? undefined : +gi);\r\n    } else if (b.hasAttribute('data-rm')) {\r\n      var ri = +b.getAttribute('data-rm'), rm = W.areas[ri];\r\n      if (rm && W.areas.length > 1) { if (rm.picker) rm.picker.clear(); if (rm.host) rm.host.remove(); W.areas.splice(ri, 1); if (ri > 0 && ri === W.areas.length) W.areas[ri - 1].more = 'no'; wzRender(); }\r\n    }\r\n  });\r\n  function wzEnter(keep){\r\n    if (!keep && !(W && W.job && wzUsed())) { if (!wzRestore()) wzReset(); }\r\n    if (!W) W = wzFresh();\r\n    wzRender();\r\n  }\r\n  function wzCollect(){\r\n    wzReadFields();\r\n    if (!W || !W.job) return { error:'Please choose what you would like to do.' };\r\n    var areas = [], photos = [];\r\n    for (var i = 0; i < W.areas.length; i++) {\r\n      var a = W.areas[i], n = wzCount(a);\r\n      if (!a.area) return { error:'Please choose which part of your property each photo shows.' };\r\n      if (!n) return { error:'Please add a photo for each area.' };\r\n      if (!a.items.length) return { error:'Please tick at least one thing for each area.' };\r\n      areas.push({ area:a.area, items:a.items.slice(), area_m2:num(a.m2), note:a.note, photo_count:n });\r\n      a.picker.items().forEach(function(it){ photos.push(it); });\r\n    }\r\n    var answers = {};\r\n    Object.keys(W.answers).forEach(function(k){ if ((W.answers[k] || []).length) answers[k] = W.answers[k].slice(); });\r\n    return { input:{\r\n      title: wzLabel('work_type', W.job) + ': ' + wzLabel('area_' + W.job, W.areas[0].area) + (W.areas.length > 1 ? ' and more' : ''),\r\n      site_type:'house', work_type:W.job, stand_area_m2:num(W.stand), stand_width_m:null, stand_depth_m:null,\r\n      house_plan_id:W.ref || '', areas:areas, answers:answers, wants:W.wants.trim(), location:W.loc.trim(), budget_min:num(W.bmin), budget_max:num(W.bmax), style_hint:''\r\n    }, photos:photos };\r\n  }\r\n\r\n  function setBusy(on){\r\n    S.busy = on;\r\n    if (on) $('progress').scrollIntoView({ behavior:'smooth', block:'start' });\r\n    render();\r\n  }\r\n  function paintProgress(stage){\r\n    var idx = STAGE_ORDER.indexOf(stage);\r\n    if (idx < 0) idx = stage === 'done' ? STAGE_ORDER.length : 0;\r\n    var pct = Math.round(Math.min(idx + 0.5, STAGE_ORDER.length) \/ STAGE_ORDER.length * 100);\r\n    if (stage === 'done') pct = 100;\r\n    $('pbarFill').style.width = pct + '%'; document.querySelector('.vz-pbar').setAttribute('aria-valuenow', pct);\r\n    $('progSteps').innerHTML = STAGE_ORDER.map(function(k, i){\r\n      var cls = i < idx ? 'done' : i === idx ? 'on' : '';\r\n      return '<div class=\"vz-step ' + cls + '\"><b>' + (i < idx ? '\u2713' : (i+1)) + '<\/b>' + E(STAGE_TXT[k][0]) + '<\/div>';\r\n    }).join('');\r\n  }\r\n\r\n  function errFor(scenario){ return scenario === 'room' ? 'rErr' : (S.mode === 'house' ? 'wzErr' : 'bErr'); }\r\n  async function submit(scenario){\r\n    if (S.busy) return;\r\n    var errId = errFor(scenario);\r\n    showErr(errId, '');\r\n    var c = scenario === 'room' ? collectRoom() : (S.mode === 'house' ? wzCollect() : collectBuild());\r\n    if (c.error) { showErr(errId, c.error); toast('form', c.error, 'err'); return; }\r\n    if (S.quota && S.quota.credits_left < S.quota.cost) { showErr(errId, 'You do not have enough credits for another design.'); return; }\r\n    if (S.quota && S.quota.credits_left - S.quota.cost < S.quota.cost) {\r\n      var left = S.quota.credits_left - S.quota.cost;\r\n      var ok = await showModal({ title: 'Use your remaining credits?', text: 'This design uses ' + S.quota.cost + ' credits and leaves you with ' + left + ', which is not enough for another design until your credits reset on ' + dateShort(S.quota.next_reset_at) + '. Continue?', actions:[{ label:'Not now', value:false }, { label:'Create my design', primary:true, value:true }] });\r\n      if (!ok) return;\r\n    }\r\n\r\n    setBusy(true); paintProgress('reading'); $('progTitle').textContent = 'Starting your design';\r\n    var pid = null;\r\n    try {\r\n      toast('credits', 'Checking your credits', 'busy');\r\n      var cr = await sb.rpc('visualizer_create_project', { p_scenario:scenario, p_input:c.input, p_currency:S.currency });\r\n      if (cr.error) throw cr.error;\r\n      pid = cr.data.id;\r\n      toast('credits', cr.data.credits_used + ' credits reserved, ' + cr.data.credits_left + ' left', 'ok');\r\n\r\n      $('progTitle').textContent = 'Uploading your photos';\r\n      var paths = [];\r\n      for (var i = 0; i < c.photos.length; i++) {\r\n        toast('upload', 'Uploading photo ' + (i+1) + ' of ' + c.photos.length, 'busy');\r\n        var path = S.user.id + '\/' + pid + '\/in-' + (i+1) + '.jpg';\r\n        var up = await sb.storage.from('visualizer').upload(path, c.photos[i].blob, { contentType:'image\/jpeg', upsert:true });\r\n        if (up.error) throw safeErr('A photo could not be uploaded. Please check your connection and try again.');\r\n        paths.push(path);\r\n      }\r\n      toast('upload', c.photos.length === 1 ? 'Photo uploaded' : c.photos.length + ' photos uploaded', 'ok');\r\n\r\n      $('progTitle').textContent = 'Creating your design';\r\n      toast('start', 'Starting your design', 'busy');\r\n      var fn = await sb.functions.invoke('visualize-plan', { body:{ project_id:pid, photo_paths:paths } });\r\n      if (fn.error) throw safeErr(await fnMessage(fn.error));\r\n      if (fn.data && fn.data.error) throw safeErr(fn.data.error);\r\n      toast('start', 'Design started', 'ok');\r\n      pollProject(pid, scenario);\r\n    } catch(e){\r\n      settleBusyToasts();\r\n      if (pid) { try { await sb.rpc('visualizer_cancel_draft', { p_id:pid }); } catch(_){} }\r\n      setBusy(false);\r\n      var msg = (e && (e.safe || e.code === 'P0001') && e.message) || BUSY;\r\n      showErr(errId, msg); toast('fail', msg, 'err');\r\n      loadQuota();\r\n    }\r\n  }\r\n  $('formRoom').addEventListener('submit', function(e){ e.preventDefault(); submit('room'); });\r\n  $('formBuild').addEventListener('submit', function(e){ e.preventDefault(); submit('build'); });\r\n\r\n  \/* ================= polling ================= *\/\r\n  function stopPoll(){ if (S.poll) { clearTimeout(S.poll.t); S.poll = null; } clearInterval(S.timer); S.timer = null; $('progStop').hidden = true; }\r\n  var PROG_SUB = 'This usually takes a minute or two. You can leave this page, your design will be waiting in My Designs.';\r\n  var PROG_SLOW = 'This is taking longer than usual. We are still working on it. You can wait here or check My Designs later.';\r\n  function startTimer(){\r\n    clearInterval(S.timer); var t0 = Date.now();\r\n    $('progSub').textContent = PROG_SUB; $('progStop').hidden = true;\r\n    var tick = function(){\r\n      var sec = Math.floor((Date.now() - t0) \/ 1000);\r\n      $('progTimer').textContent = Math.floor(sec \/ 60) + ':' + ('0' + (sec % 60)).slice(-2);\r\n      if (sec >= 120) { $('progSub').textContent = PROG_SLOW; $('progStop').hidden = false; }\r\n    };\r\n    tick(); S.timer = setInterval(tick, 1000);\r\n  }\r\n  $('progStop').addEventListener('click', function(){ loadMine(); $('mineSec').scrollIntoView({ behavior:'smooth', block:'start' }); });\r\n  function pollProject(pid, scenario){\r\n    stopPoll();\r\n    setBusy(true); $('progTitle').textContent = 'Creating your design'; startTimer();\r\n    var started = Date.now(), poll = S.poll = { pid:pid, t:null };\r\n    (async function tick(){\r\n      if (S.poll !== poll) return;\r\n      try {\r\n        var r = await sb.from('visualizer_projects').select('id,status,stage,error').eq('id', pid).maybeSingle();\r\n        var p = r.data;\r\n        if (p) {\r\n          paintProgress(p.stage); stageToasts(p.stage);\r\n          if (p.status === 'complete') { stopPoll(); stageToasts('done'); await openProject(pid); toast('done', 'Your design is ready', 'ok'); loadQuota(); loadMine(); if (S.current && S.current.project.id === pid) emailPdf(S.current.project, S.current.urls, true); return; }\r\n          if (p.status === 'failed') {\r\n            stopPoll(); settleBusyToasts(); setBusy(false);\r\n            var m = (p.error || BUSY) + ' Your credits were given back.';\r\n            showErr(errFor(scenario), m); toast('fail', 'We are experiencing high demand. Your credits were given back.', 'err');\r\n            loadQuota(); loadMine(); return;\r\n          }\r\n        }\r\n      } catch(e){}\r\n      if (Date.now() - started > 8*60*1000) {\r\n        stopPoll(); settleBusyToasts(); setBusy(false);\r\n        showErr(errFor(scenario), 'This is taking longer than usual. Check My Designs below in a few minutes.');\r\n        loadQuota(); return;\r\n      }\r\n      poll.t = setTimeout(tick, 3000);\r\n    })();\r\n  }\r\n\r\n  \/* ================= result ================= *\/\r\n  async function signed(paths){\r\n    if (!paths.length) return {};\r\n    var r = await sb.storage.from('visualizer').createSignedUrls(paths, 3600), map = {};\r\n    (r.data || []).forEach(function(x){ if (x.signedUrl) map[x.path] = x.signedUrl; });\r\n    return map;\r\n  }\r\n\r\n  async function openProject(pid){\r\n    var r = await sb.from('visualizer_projects').select('*').eq('id', pid).maybeSingle();\r\n    var p = r.data;\r\n    if (!p) { setBusy(false); return; }\r\n    if (p.status === 'processing' || p.status === 'draft') { pollProject(p.id, p.scenario); return; }\r\n    if (p.status === 'failed') { setBusy(false); return; }\r\n    var urls = await signed([].concat(p.input_photos || [], p.output_images || []));\r\n    S.current = { project:p, urls:urls };\r\n    S.busy = false;\r\n    render();\r\n    renderResult(p, urls);\r\n    $('tool').scrollIntoView({ behavior:'smooth', block:'start' });\r\n  }\r\n\r\n  function listHtml(items){ return items && items.length ? '<ul class=\"vz-list\">' + items.map(function(x){ return '<li>' + E(x) + '<\/li>'; }).join('') + '<\/ul>' : ''; }\r\n\r\n  function renderResult(p, urls){\r\n    var plan = p.plan || {}, inp = p.input || {}, cur = p.currency || S.currency;\r\n    var before = urls[(p.input_photos || [])[0]], after = urls[conceptPath(p)];\r\n    var prop = !!(plan.areas && plan.areas.length && inp.flow === 'property');\r\n    var sub = p.scenario === 'room' ? (inp.room_label || 'Room') : (inp.site_type === 'stand' ? 'New home on your stand' : prop ? wzLabel('work_type', inp.work_type) : 'House design');\r\n    var h = '';\r\n    h += '<div class=\"vz-res-head\"><div><h3>' + E(p.title || 'Your design') + '<\/h3><p>' + E(sub) + ' \u2022 Created ' + E(dateShort(p.completed_at || p.created_at)) + ' \u2022 Saved to My Designs<\/p><\/div><\/div>';\r\n    h += '<div class=\"glass vz-box vz-acts\"><div class=\"vz-acts-main\"><button class=\"vz-btn p\" id=\"btnQuote\" type=\"button\">Request a quote<\/button>' +\r\n         '<div class=\"vz-next\">What happens next: your design and materials list are sent to contractors as a quote request. You can follow it in My Quotes.<\/div><\/div>' +\r\n         '<div class=\"vz-acts-sec\"><button class=\"vz-btn\" id=\"btnPdf\" type=\"button\">Download PDF<\/button>' +\r\n         '<button class=\"vz-btn ghost\" id=\"btnMail\" type=\"button\">Email me this PDF<\/button>' +\r\n         (after ? (prop ? '' : '<button class=\"vz-btn\" id=\"btnDl\" type=\"button\">Download AI image<\/button>') + '<button class=\"vz-btn ghost\" id=\"btnShare\" type=\"button\">Share on WhatsApp<\/button>' : '') +\r\n         '<button class=\"vz-btn ghost\" id=\"btnVar\" type=\"button\">Create a variation<\/button>' +\r\n         '<button class=\"vz-btn ghost\" id=\"btnClose\" type=\"button\">Save and close<\/button>' +\r\n         '<button class=\"vz-btn ghost\" id=\"btnNew\" type=\"button\">New design<\/button><\/div><\/div>';\r\n    h += '<div class=\"vz-err\" id=\"resErr\" role=\"alert\"><\/div>';\r\n\r\n    if (prop) {\r\n      var startAt = 0;\r\n      plan.areas.forEach(function(ar, i){\r\n        var cnt = ((inp.areas || [])[i] || {}).photo_count || 1, bf = urls[(p.input_photos || [])[startAt]], cp = ((plan.concepts || [])[i] || {}).path, af = cp ? urls[cp] : '';\r\n        startAt += cnt;\r\n        h += '<div class=\"glass vz-box vz-area\"><h4>' + E(ar.label) + '<\/h4>' +\r\n             '<p class=\"vz-areaitems\">' + E((ar.items || []).join(', ')) + (ar.area_m2 ? ' \u2022 about ' + E(ar.area_m2) + ' ' + E(ar.unit || 'm\u00b2') + (ar.area_assumed ? ' (estimated from your photo)' : '') : '') + '<\/p>' +\r\n             (ar.summary ? '<p style=\"margin-bottom:12px;\">' + E(ar.summary) + '<\/p>' : '') +\r\n             '<div class=\"vz-ba\"><figure><img decoding=\"async\" src=\"' + E(bf || '') + '\" alt=\"Your original photo of the ' + E(ar.label) + '. Tap to enlarge.\" data-zoom=\"1\"><figcaption>Before<\/figcaption><\/figure>' +\r\n             (af ? '<figure><img decoding=\"async\" src=\"' + E(af) + '\" alt=\"Your AI design for the ' + E(ar.label) + '. Tap to enlarge.\" data-zoom=\"1\"><figcaption>Your design (AI concept image)<\/figcaption><\/figure>'\r\n                 : '<figure><div class=\"vz-noimg\">The concept image for this area could not be created this time. Your plan and materials list are ready below.<\/div><figcaption>Your design (AI concept image)<\/figcaption><\/figure>') + '<\/div>' +\r\n             (af ? '<div style=\"margin-top:10px;\"><button class=\"vz-btn\" type=\"button\" data-dl=\"' + E(cp) + '\">Download this image<\/button><\/div>' : '') + '<\/div>';\r\n      });\r\n    } else {\r\n      h += '<div class=\"vz-ba\">' +\r\n         '<figure><img decoding=\"async\" src=\"' + E(before || '') + '\" alt=\"Your original photo. Tap to enlarge.\" data-zoom=\"1\"><figcaption>Before<\/figcaption><\/figure>' +\r\n         (after ? '<figure><img decoding=\"async\" src=\"' + E(after) + '\" alt=\"Your AI design. Tap to enlarge.\" data-zoom=\"1\"><figcaption>Your design (AI concept image)<\/figcaption><\/figure>'\r\n                : '<figure><div class=\"vz-noimg\">Your concept image could not be created this time. Your plan and materials list are ready below.<\/div><figcaption>Your design (AI concept image)<\/figcaption><\/figure>') + '<\/div>';\r\n    }\r\n\r\n    var d = plan.design || {};\r\n    h += '<div class=\"glass vz-box\"><h4>' + E(d.style_name || 'Design direction') + '<\/h4><p>' + E(plan.summary || '') + '<\/p>' +\r\n         (d.direction ? '<p style=\"margin-top:10px;\">' + E(d.direction) + '<\/p>' : '') +\r\n         (d.materials_family ? '<p style=\"margin-top:10px;\"><b>Materials family:<\/b> ' + E(d.materials_family) + '<\/p>' : '') +\r\n         ((d.palette || []).length ? '<div class=\"vz-pal\">' + d.palette.map(function(c){ return '<div class=\"vz-sw\" style=\"background:' + E(c) + ';\" title=\"' + E(c) + '\"><\/div>'; }).join('') + '<\/div>' : '') +\r\n         ((d.key_features || []).length ? '<div style=\"margin-top:14px;\">' + listHtml(d.key_features) + '<\/div>' : '') + '<\/div>';\r\n\r\n    if (plan.checks && plan.checks.length) {\r\n      h += '<div class=\"glass vz-box\"><h4>Does it fit your brief?<\/h4><ul class=\"vz-list vz-checks\">' +\r\n           plan.checks.map(function(c){ return '<li><span class=\"ic ' + (c.ok ? 'ok' : 'bad') + '\">' + (c.ok ? '\u2713' : '!') + '<\/span><span>' + E(c.text) + '<\/span><\/li>'; }).join('') + '<\/ul>';\r\n      if (plan.fit) {\r\n        var f = plan.fit, pct = Math.min(100, f.coverage_pct);\r\n        h += '<div class=\"vz-bar' + (f.coverage_pct > 100 ? ' over' : '') + '\"><i style=\"width:' + pct + '%\"><\/i><\/div>' +\r\n             '<div style=\"font-size:15px;font-weight:700;\">' + f.coverage_pct + '% of the stand covered by the building footprint (' + plan.storeys + (plan.storeys === 1 ? ' storey' : ' storeys') + ')<\/div>' +\r\n             '<div class=\"vz-kv\"><div><b>' + f.stand_area_m2 + ' m\u00b2<\/b><span>Stand<\/span><\/div><div><b>' + f.net_room_area_m2 + ' m\u00b2<\/b><span>Rooms<\/span><\/div><div><b>' + f.gross_floor_area_m2 + ' m\u00b2<\/b><span>Total floor area<\/span><\/div><div><b>' + f.footprint_m2 + ' m\u00b2<\/b><span>Footprint<\/span><\/div><\/div>' +\r\n             '<div class=\"vz-fine\">Total floor area adds ' + f.circulation_m2 + ' m\u00b2 for passages and stairs and ' + f.walls_m2 + ' m\u00b2 for walls (standard allowances).<\/div>';\r\n      }\r\n      h += '<\/div>';\r\n    }\r\n\r\n    if (plan.rooms && plan.rooms.length) {\r\n      h += '<div class=\"glass vz-box\"><h4>Schedule of accommodation<\/h4><div class=\"vz-scroll\"><table class=\"vz-table\"><thead><tr><th>Room<\/th><th class=\"r\">Area<\/th><th>Notes<\/th><\/tr><\/thead><tbody>' +\r\n           plan.rooms.map(function(r){ return '<tr><td>' + E(r.name) + '<\/td><td class=\"r\">' + r.area_m2 + ' m\u00b2<\/td><td>' + E(r.notes) + '<\/td><\/tr>'; }).join('') + '<\/tbody><\/table><\/div>' +\r\n           '<div class=\"vz-mat\">' + plan.rooms.map(function(r){ return '<div class=\"vz-mat-item\"><b>' + E(r.name) + '<\/b><div class=\"m\"><span>' + E(r.notes) + '<\/span><span>' + r.area_m2 + ' m\u00b2<\/span><\/div><\/div>'; }).join('') + '<\/div><\/div>';\r\n    }\r\n\r\n    var fpUrl = urls[floorPath(p)];\r\n    if (fpUrl) {\r\n      h += '<div class=\"glass vz-box\"><h4>Proposed floor plan<\/h4><img decoding=\"async\" class=\"vz-fp\" src=\"' + E(fpUrl) + '\" alt=\"Proposed floor plan (AI concept). Tap to enlarge.\" data-zoom=\"1\">' +\r\n           '<div class=\"vz-fine\">AI floor plan concept. Illustrative only, not to scale and not construction drawings. The schedule of accommodation above is the source for room sizes.<\/div><\/div>';\r\n    }\r\n\r\n    if ((plan.keep && plan.keep.length) || (plan.change && plan.change.length)) {\r\n      h += '<div class=\"vz-two\">' +\r\n           (plan.keep && plan.keep.length ? '<div class=\"glass vz-box\"><h4>Worth keeping<\/h4>' + listHtml(plan.keep) + '<\/div>' : '') +\r\n           (plan.change && plan.change.length ? '<div class=\"glass vz-box\"><h4>What changes<\/h4>' + listHtml(plan.change) + '<\/div>' : '') + '<\/div>';\r\n    }\r\n\r\n    (plan.notices || []).forEach(function(n){ h += '<div class=\"glass vz-box vz-notice\">' + E(n) + '<\/div>'; });\r\n\r\n    var mats = plan.materials || [];\r\n    h += '<div class=\"glass vz-box\"><h4>Materials list<\/h4>' + (plan.priced_for ? '<p style=\"margin-bottom:12px;\"><b>Priced for:<\/b> ' + E(plan.priced_for) + '<\/p>' : '') + '<div class=\"vz-tools\"><button class=\"vz-btn\" id=\"btnCopy\" type=\"button\">Copy list<\/button><button class=\"vz-btn\" id=\"btnCsv\" type=\"button\">Download CSV<\/button><\/div><div class=\"vz-scroll\"><table class=\"vz-table\"><thead><tr><th>Material<\/th><th>Where it is used<\/th><th class=\"r\">Quantity<\/th><th class=\"r\">Est. price<\/th><th class=\"r\">Est. total<\/th><th><\/th><\/tr><\/thead><tbody>' +\r\n         mats.map(function(m){ return '<tr><td><b>' + E(m.item) + '<\/b><br>' + E(m.category) + '<\/td><td>' + E(m.use) + '<\/td><td class=\"r\">' + (m.quantity === null ? '\u2014' : m.quantity + ' ' + E(m.unit)) + '<\/td><td class=\"r\">' + money(m.unit_price, cur, 2) + '<\/td><td class=\"r\">' + money(m.line_total, cur) + '<\/td><td class=\"r\"><a class=\"vz-shop\" href=\"https:\/\/app.builders24.net\/retail\/shop.html?search=' + encodeURIComponent(m.shopping_keyword || m.item) + '\">Shop &rsaquo;<\/a><\/td><\/tr>'; }).join('') +\r\n         '<\/tbody><\/table><\/div>' +\r\n         '<div class=\"vz-mat\">' + mats.map(function(m){ return '<div class=\"vz-mat-item\"><b>' + E(m.item) + '<\/b><div style=\"font-size:14px;margin-top:3px;\">' + E(m.use) + '<\/div><div class=\"m\"><span>' + (m.quantity === null ? '\u2014' : m.quantity + ' ' + E(m.unit)) + ' \u00d7 ' + money(m.unit_price, cur, 2) + '<\/span><b>' + money(m.line_total, cur) + '<\/b><\/div><a class=\"vz-shop\" href=\"https:\/\/app.builders24.net\/retail\/shop.html?search=' + encodeURIComponent(m.shopping_keyword || m.item) + '\">Shop this &rsaquo;<\/a><\/div>'; }).join('') + '<\/div>' +\r\n         '<div class=\"vz-total\"><span>Estimated materials (' + E(cur) + ')<\/span><span>' + money(plan.totals && plan.totals.materials_estimate, cur) + '<\/span><\/div>' +\r\n         '<div class=\"vz-fine\">Rough estimates in ' + E(cur) + ', materials only, before labour' + (plan.totals && plan.totals.items_without_price ? ' (' + plan.totals.items_without_price + ' item' + (plan.totals.items_without_price === 1 ? '' : 's') + ' without a price)' : '') + '. A contractor\u2019s quote will give real prices.<\/div><\/div>';\r\n\r\n    if (plan.assumptions && plan.assumptions.length) h += '<div class=\"glass vz-box\"><h4>What we assumed<\/h4>' + listHtml(plan.assumptions) + '<\/div>';\r\n    h += '<div class=\"vz-fine\">This is a concept for planning and quoting. The image is AI generated, sizes are standard reference figures unless you gave your own, and none of it replaces drawings from a professional or a site visit.<\/div>';\r\n\r\n    var el = $('result'); el.innerHTML = h;\r\n    $('btnNew').onclick = function(){ resetForms(); S.current = null; S.mode = null; render(); loadQuota(); };\r\n    $('btnClose').onclick = function(){ S.current = null; S.mode = null; render(); loadMine(); $('mineSec').scrollIntoView({ behavior:'smooth', block:'start' }); };\r\n    $('btnVar').onclick = function(){ prefillFrom(p); };\r\n    $('btnQuote').onclick = function(){ requestQuote(p, urls); };\r\n    $('btnPdf').onclick = function(){ downloadPdf(p, urls); };\r\n    $('btnMail').onclick = function(){ emailPdf(p, urls, false); };\r\n    $('btnCopy').onclick = function(){ copyList(p); };\r\n    $('btnCsv').onclick = function(){ csvList(p); };\r\n    if ($('btnDl')) $('btnDl').onclick = function(){ downloadImage(after, p); };\r\n    el.querySelectorAll('[data-dl]').forEach(function(b){ b.onclick = function(){ downloadImage(urls[b.getAttribute('data-dl')], p); }; });\r\n    if ($('btnShare')) $('btnShare').onclick = function(){ shareImage(p); };\r\n    el.querySelectorAll('[data-zoom]').forEach(function(im){ im.onclick = function(){ if (im.getAttribute('src')) showLightbox(im.getAttribute('src'), im.alt); }; });\r\n  }\r\n\r\n  \/* ---- materials list export, sharing, variations, resetting ---- *\/\r\n  function listRows(p){\r\n    var cur = p.currency || S.currency;\r\n    return [['Material','Category','Where it is used','Quantity','Unit','Unit price','Total','Currency']].concat(((p.plan || {}).materials || []).map(function(m){\r\n      return [m.item, m.category, m.use, m.quantity === null ? '' : m.quantity, m.unit || '', m.unit_price === null ? '' : m.unit_price, m.line_total === null ? '' : m.line_total, cur];\r\n    }));\r\n  }\r\n  async function copyList(p){\r\n    var txt = listRows(p).map(function(r){ return r.join('\\t'); }).join('\\n');\r\n    try { await navigator.clipboard.writeText(txt); toast('copy', 'Materials list copied', 'ok'); }\r\n    catch(e){ toast('copy', 'The list could not be copied. Please use Download CSV.', 'err'); }\r\n  }\r\n  function csvList(p){\r\n    var csv = listRows(p).map(function(r){ return r.map(function(c){ return '\"' + String(c === null || c === undefined ? '' : c).replace(\/\"\/g, '\"\"') + '\"'; }).join(','); }).join('\\r\\n');\r\n    var b = new Blob(['\\ufeff' + csv], { type:'text\/csv;charset=utf-8' }), a = document.createElement('a');\r\n    a.href = URL.createObjectURL(b); a.download = 'builders24-materials-' + String(p.id).slice(0, 8) + '.csv';\r\n    document.body.appendChild(a); a.click(); a.remove(); setTimeout(function(){ URL.revokeObjectURL(a.href); }, 2000);\r\n    toast('csv', 'Materials list downloaded', 'ok');\r\n  }\r\n  async function shareImage(p){\r\n    try {\r\n      var path = conceptPath(p);\r\n      var r = await sb.storage.from('visualizer').createSignedUrl(path, 7 * 24 * 3600);\r\n      if (r.error || !r.data) throw new Error('x');\r\n      var text = 'My Builders24 design: ' + (p.title || 'AI design') + '. The link to the image works for 7 days: ' + r.data.signedUrl;\r\n      window.open('https:\/\/wa.me\/?text=' + encodeURIComponent(text), '_blank', 'noopener');\r\n      toast('share', 'Sharing a link to your AI image, valid for 7 days', 'ok');\r\n    } catch(e){ toast('share', 'We could not create a share link. Please try again.', 'err'); }\r\n  }\r\n  function resetForms(){\r\n    $('formRoom').reset(); $('formBuild').reset();\r\n    S.pickers.room.clear(); S.pickers.build.clear(); wzReset();\r\n    applyStyle(''); selectRef('');\r\n    S.ensuite = false; document.querySelectorAll('#ensSeg button').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-v') === 'no'); });\r\n    resetRoomSize(); resetStandSize(); updateMustCount();\r\n    $('bBase').value = 'any'; syncBase(); fillSizes();\r\n    ['rErr','bErr','wzErr'].forEach(function(id){ $(id).textContent = ''; });\r\n  }\r\n  function prefillFrom(p){\r\n    var inp = p.input || {};\r\n    resetForms();\r\n    S.current = null;\r\n    var mode = p.scenario === 'room' ? 'room' : (inp.site_type === 'stand' ? 'stand' : 'house');\r\n    S.wzKeep = mode === 'house';\r\n    setMode(mode);\r\n    S.wzKeep = false;\r\n    if (mode === 'house') {\r\n      wzFrom(inp); wzRender();\r\n      toast('var', 'Your answers are filled in. Add your photos again, change anything you like, then create the variation.', 'ok');\r\n      return;\r\n    }\r\n    var setv = function(id, v){ if (v !== null && v !== undefined && v !== '') $(id).value = v; };\r\n    if (mode === 'room') {\r\n      setv('rType', inp.room_type); setv('rWants', inp.wants); setv('rKeep', inp.keep);\r\n      setv('rW', inp.room_width_m); setv('rD', inp.room_depth_m); setv('rA', inp.room_area_m2);\r\n      setv('rBMin', inp.budget_min); setv('rBMax', inp.budget_max);\r\n      if (inp.style_hint) applyStyle(inp.style_hint);\r\n    } else {\r\n      setv('bW', inp.stand_width_m); setv('bD', inp.stand_depth_m); setv('bA', inp.stand_area_m2);\r\n      setv('bBeds', inp.bedrooms); setv('bBaths', inp.bathrooms); setv('bMust', inp.must_haves); setv('bLoc', inp.location);\r\n      setv('bBMin', inp.budget_min); setv('bBMax', inp.budget_max);\r\n      if (inp.work_type && mode === 'house') setv('bWork', inp.work_type);\r\n      if (inp.base_design) setv('bBase', inp.base_design);\r\n      if (inp.orientation) Array.prototype.forEach.call($('bOrient').options, function(o){ if (o.textContent === inp.orientation) $('bOrient').value = o.value; });\r\n      S.ensuite = !!inp.ensuite; document.querySelectorAll('#ensSeg button').forEach(function(b){ b.classList.toggle('on', b.getAttribute('data-v') === (S.ensuite ? 'yes' : 'no')); });\r\n      if (inp.house_plan_id) selectRef(inp.house_plan_id);\r\n      Object.keys(inp.sizes || {}).forEach(function(k){ var el = $('sz_' + k); if (el) el.value = inp.sizes[k]; });\r\n      syncBase(); updateMustCount();\r\n    }\r\n    ['rW','rD','bW','bD'].forEach(function(id){ $(id).dispatchEvent(new Event('input')); });\r\n    toast('var', 'Your answers are filled in. Add your photos again, change anything you like, then create the variation.', 'ok');\r\n  }\r\n\r\n  async function downloadImage(url, p){\r\n    try {\r\n      var b = await (await fetch(url)).blob(), a = document.createElement('a');\r\n      a.href = URL.createObjectURL(b); a.download = 'builders24-design.' + (b.type.indexOf('jpeg') > -1 ? 'jpg' : 'png');\r\n      document.body.appendChild(a); a.click(); a.remove(); setTimeout(function(){ URL.revokeObjectURL(a.href); }, 2000);\r\n      toast('img', 'Image downloaded', 'ok');\r\n    } catch(e){ $('resErr').textContent = 'The image could not be downloaded. Please try again.'; toast('img', 'The image could not be downloaded', 'err'); }\r\n  }\r\n\r\n  \/* ================= PDF ================= *\/\r\n  \/*PDF_BEGIN*\/\r\n  function buildPdf(jsPDF, d){\r\n    var p = d.project, plan = p.plan || {}, inp = p.input || {}, cur = p.currency || d.currency || 'USD';\r\n    var NAVY = [13,27,62], ORANGE = [240,165,0], GREEN = [34,160,90], RED = [200,60,60], LIGHT = [238,242,250];\r\n    var refCode = String(p.id).slice(0,8).toUpperCase(), link = 'https:\/\/app.builders24.net\/retail\/visualize.html?design=' + p.id, cust = d.customer || {};\r\n    var doc = new jsPDF({ unit:'mm', format:'a4' });\r\n    var W = 210, H = 297, M = 14, CW = W - 2*M, y = 0;\r\n    var safe = function(t){ return String(t === null || t === undefined ? '' : t).replace(\/\\u00B2\/g, '2').replace(\/\\u00B3\/g, '3').replace(\/[^\\x20-\\x7E\u00a0-\u00ff\u2013\u2014\u2018\u2019\u201c\u201d\u2022\u20ac]\/g, ''); };\r\n    var fmt = function(n, dec){ return n === null || n === undefined || isNaN(n) ? '-' : cur + ' ' + Number(n).toLocaleString('en-US', { minimumFractionDigits: dec || 0, maximumFractionDigits: dec || 0 }); };\r\n    var hex = function(h){ return \/^#[0-9a-fA-F]{6}$\/.test(h) ? [parseInt(h.slice(1,3),16), parseInt(h.slice(3,5),16), parseInt(h.slice(5,7),16)] : [200,200,200]; };\r\n    var ensure = function(h){ if (y + h > H - 18) { doc.addPage(); y = 20; } };\r\n    var heading = function(t){ ensure(16); doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.setTextColor.apply(doc, NAVY); doc.text(safe(t), M, y); y += 2; doc.setDrawColor.apply(doc, ORANGE); doc.setLineWidth(0.8); doc.line(M, y, M + 22, y); y += 6; };\r\n    var para = function(t, size, bold, w){ doc.setFont('helvetica', bold ? 'bold' : 'normal'); doc.setFontSize(size || 10); doc.setTextColor.apply(doc, NAVY); var lines = doc.splitTextToSize(safe(t), w || CW); lines.forEach(function(l){ ensure(5.2); doc.text(l, M, y); y += 5; }); };\r\n    var fit = function(im, maxW, maxH){ var r = Math.min(maxW \/ im.w, maxH \/ im.h); return { w: im.w * r, h: im.h * r }; };\r\n    var fmtOf = function(u){ return \/^data:image\\\/png\/.test(u) ? 'PNG' : 'JPEG'; };\r\n\r\n    \/\/ header band\r\n    doc.setFillColor.apply(doc, NAVY); doc.rect(0, 0, W, 30, 'F');\r\n    doc.setFillColor.apply(doc, ORANGE); doc.rect(0, 30, W, 1.4, 'F');\r\n    doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(18); doc.text('BUILDERS24', M, 14);\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(10); doc.text('AI Design Concept and Estimated Quote', M, 21);\r\n    doc.setFontSize(9); doc.text('Ref ' + refCode, W - M, 14, { align:'right' });\r\n    doc.text(safe(d.dateText || ''), W - M, 21, { align:'right' });\r\n    y = 42;\r\n\r\n    doc.setFont('helvetica','bold'); doc.setFontSize(19); doc.setTextColor.apply(doc, NAVY);\r\n    doc.splitTextToSize(safe(p.title || 'Your design'), CW).slice(0,2).forEach(function(l){ doc.text(l, M, y); y += 8; });\r\n    var sub = p.scenario === 'room' ? (inp.room_label || 'Room') + ' redesign' : (inp.site_type === 'stand' ? 'New home on an empty stand' : (d.jobLabel || ('House ' + (inp.work_type || 'renovation'))));\r\n    if (plan.design && plan.design.style_name) sub += '  |  ' + plan.design.style_name;\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(10.5); doc.text(safe(sub), M, y); y += 7;\r\n\r\n    \/\/ summary box (first page): who it is for, the total, the reference and a link back to the design\r\n    doc.setDrawColor.apply(doc, NAVY); doc.setLineWidth(0.4); doc.setFillColor.apply(doc, LIGHT); doc.roundedRect(M, y, CW, 27, 2, 2, 'FD');\r\n    doc.setFont('helvetica','bold'); doc.setFontSize(9.5); doc.setTextColor.apply(doc, NAVY);\r\n    doc.text('Prepared for', M + 4, y + 6); doc.text('Estimated materials total', M + 74, y + 6);\r\n    doc.setFont('helvetica','normal');\r\n    doc.text(safe(cust.name || cust.email || '-'), M + 4, y + 11.5);\r\n    if (cust.name && cust.email) doc.text(safe(cust.email), M + 4, y + 16.5);\r\n    doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.text(fmt(plan.totals && plan.totals.materials_estimate, 0), M + 74, y + 12.5);\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(8.5);\r\n    doc.text('Design ref ' + refCode + '   |   Full ID ' + p.id, M + 4, y + 21.5);\r\n    doc.setTextColor(0, 0, 238); doc.textWithLink('Open this design: ' + link, M + 4, y + 25.2, { url:link }); doc.setTextColor.apply(doc, NAVY);\r\n    y += 33;\r\n\r\n    \/\/ existing property: one before\/after block per area, drawn on the customer's own photo\r\n    if (d.areas && d.areas.length) {\r\n      d.areas.forEach(function(ar){\r\n        heading(ar.label);\r\n        para((ar.items || []).join(', ') + (ar.area_m2 ? '  |  about ' + ar.area_m2 + (ar.unit === 'm' ? ' m' : ' m2') + (ar.area_assumed ? ' (estimated from the photo)' : '') : ''), 10, true);\r\n        if (ar.summary) para(ar.summary, 10);\r\n        if (ar.after) {\r\n          var aa = fit(ar.after, CW, 92); ensure(aa.h + 8);\r\n          doc.addImage(ar.after.dataUrl, fmtOf(ar.after.dataUrl), M + (CW - aa.w)\/2, y, aa.w, aa.h);\r\n          y += aa.h + 2; doc.setFont('helvetica','normal'); doc.setFontSize(8.5); doc.setTextColor.apply(doc, NAVY); doc.text('AI concept image on your own photo. Illustrative only.', M, y + 2); y += 7;\r\n        } else { para('The concept image for this area could not be created this time.', 9.5, true); }\r\n        if (ar.before) {\r\n          var bb = fit(ar.before, 56, 40); ensure(bb.h + 8);\r\n          doc.addImage(ar.before.dataUrl, fmtOf(ar.before.dataUrl), M, y, bb.w, bb.h);\r\n          doc.setFontSize(8.5); doc.text('Your original photo', M, y + bb.h + 4); y += bb.h + 9;\r\n        }\r\n      });\r\n      heading('Summary'); para(plan.summary || '', 10); y += 2;\r\n    }\r\n\r\n    \/\/ main image\r\n    if (d.after && !(d.areas && d.areas.length)) {\r\n      var a = fit(d.after, CW, 100);\r\n      doc.addImage(d.after.dataUrl, fmtOf(d.after.dataUrl), M + (CW - a.w)\/2, y, a.w, a.h);\r\n      y += a.h + 2; doc.setFontSize(8.5); doc.setTextColor.apply(doc, NAVY); doc.text('AI concept image. Illustrative only.', M, y + 2); y += 8;\r\n    }\r\n\r\n    \/\/ before + summary\r\n    var colY = y, leftW = 58;\r\n    if (d.before && !(d.areas && d.areas.length)) {\r\n      var b = fit(d.before, leftW, 44);\r\n      doc.addImage(d.before.dataUrl, fmtOf(d.before.dataUrl), M, colY, b.w, b.h);\r\n      doc.setFontSize(8.5); doc.setFont('helvetica','normal'); doc.setTextColor.apply(doc, NAVY); doc.text('Your original photo', M, colY + b.h + 4);\r\n      leftW = Math.max(leftW, b.w);\r\n    }\r\n    var rx = M + 64, rw = CW - 64, ry = colY + 3;\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(10); doc.setTextColor.apply(doc, NAVY);\r\n    var propPdf = !!(d.areas && d.areas.length);\r\n    if (!propPdf) doc.splitTextToSize(safe(plan.summary || ''), rw).slice(0, 9).forEach(function(l){ doc.text(l, rx, ry); ry += 4.8; });\r\n    var pal = ((plan.design || {}).palette || []).slice(0, 6);\r\n    ry += 2;\r\n    pal.forEach(function(c, i){ doc.setFillColor.apply(doc, hex(c)); doc.setDrawColor.apply(doc, NAVY); doc.setLineWidth(0.25); doc.roundedRect(rx + i * 10, ry, 8, 8, 1.5, 1.5, 'FD'); });\r\n    y = Math.max(colY + (d.before && !propPdf ? fit(d.before, 58, 44).h + 8 : 0), ry + (pal.length ? 12 : 0)) + 4;\r\n\r\n    if (plan.design && plan.design.direction) { heading('Design direction'); para(plan.design.direction, 10); if (plan.design.materials_family) para('Materials family: ' + plan.design.materials_family, 10, false); y += 2; }\r\n\r\n    \/\/ chosen reference design\r\n    if (d.ref) {\r\n      var rf = d.ref;\r\n      heading('Your chosen reference design');\r\n      if (rf.image) { var ri = fit(rf.image, CW, 70); ensure(ri.h + 4); doc.addImage(rf.image.dataUrl, 'JPEG', M + (CW - ri.w)\/2, y, ri.w, ri.h); y += ri.h + 3; }\r\n      para(rf.name, 11, true);\r\n      if (rf.specs.length) para(rf.specs.join('  |  '), 10);\r\n      if (rf.rooms.length) {\r\n        y += 2;\r\n        doc.autoTable({ startY:y, margin:{ top:16, left:M, right:M }, theme:'striped',\r\n          head:[['Room','Size (ft)']], body:rf.rooms.map(function(r){ return [safe(r.name), r.size]; }),\r\n          styles:{ font:'helvetica', fontSize:9, textColor:NAVY, cellPadding:1.8 }, headStyles:{ fillColor:NAVY, textColor:255 },\r\n          alternateRowStyles:{ fillColor:LIGHT }, columnStyles:{ 1:{ halign:'right', cellWidth:34 } } });\r\n        y = doc.lastAutoTable.finalY + 4;\r\n      }\r\n      para('For reference only. The design above was used as inspiration; your concept may differ from it.', 9, false);\r\n      y += 3;\r\n    }\r\n\r\n    \/\/ checks\r\n    if (plan.checks && plan.checks.length) {\r\n      heading('Does it fit your brief?');\r\n      plan.checks.forEach(function(c){\r\n        var lines = doc.splitTextToSize(safe(c.text), CW - 9); ensure(lines.length * 5 + 3);\r\n        doc.setFillColor.apply(doc, c.ok ? GREEN : RED); doc.circle(M + 2, y - 1.2, 1.7, 'F');\r\n        doc.setFont('helvetica','normal'); doc.setFontSize(10); doc.setTextColor.apply(doc, NAVY);\r\n        lines.forEach(function(l){ doc.text(l, M + 7, y); y += 5; }); y += 1.5;\r\n      });\r\n      if (plan.fit) {\r\n        var f = plan.fit;\r\n        doc.autoTable({ startY: y + 1, margin:{ top:16, left:M, right:M }, theme:'grid',\r\n          head:[['Stand','Rooms','Total floor area','Footprint','Coverage']],\r\n          body:[[f.stand_area_m2 + ' m2', f.net_room_area_m2 + ' m2', f.gross_floor_area_m2 + ' m2', f.footprint_m2 + ' m2', f.coverage_pct + '%  (' + plan.storeys + (plan.storeys === 1 ? ' storey' : ' storeys') + ')']],\r\n          styles:{ font:'helvetica', fontSize:9.5, textColor:NAVY, cellPadding:2.4 }, headStyles:{ fillColor:NAVY, textColor:255 } });\r\n        y = doc.lastAutoTable.finalY + 6;\r\n      }\r\n    }\r\n\r\n    \/\/ rooms\r\n    if (plan.rooms && plan.rooms.length) {\r\n      heading('Schedule of accommodation');\r\n      doc.autoTable({ startY:y, margin:{ top:16, left:M, right:M }, theme:'striped',\r\n        head:[['Room','Area','Notes']],\r\n        body:plan.rooms.map(function(r){ return [safe(r.name), r.area_m2 + ' m2', safe(r.notes)]; }),\r\n        styles:{ font:'helvetica', fontSize:9.5, textColor:NAVY, cellPadding:2.2 }, headStyles:{ fillColor:NAVY, textColor:255 },\r\n        alternateRowStyles:{ fillColor:LIGHT }, columnStyles:{ 1:{ halign:'right', cellWidth:24 } } });\r\n      y = doc.lastAutoTable.finalY + 8;\r\n    }\r\n\r\n    \/\/ proposed floor plan (AI concept)\r\n    if (d.floor) {\r\n      heading('Proposed floor plan');\r\n      var fpi = fit(d.floor, CW, 150); ensure(fpi.h + 12);\r\n      doc.setDrawColor.apply(doc, NAVY); doc.setLineWidth(0.3); doc.rect(M + (CW - fpi.w)\/2 - 0.5, y - 0.5, fpi.w + 1, fpi.h + 1);\r\n      doc.addImage(d.floor.dataUrl, fmtOf(d.floor.dataUrl), M + (CW - fpi.w)\/2, y, fpi.w, fpi.h);\r\n      y += fpi.h + 5; doc.setFont('helvetica','normal'); doc.setFontSize(8.5); doc.setTextColor.apply(doc, NAVY);\r\n      doc.text('AI floor plan concept. Illustrative only, not to scale and not construction drawings. Room sizes are in the schedule above.', M, y); y += 9;\r\n    }\r\n\r\n    \/\/ materials + quote\r\n    var mats = plan.materials || [];\r\n    heading('Building materials and estimated quote');\r\n    doc.autoTable({ startY:y, margin:{ top:16, left:M, right:M }, theme:'striped', rowPageBreak:'avoid',\r\n      head:[['Material','Where it is used','Qty','Unit price','Total']],\r\n      body:mats.map(function(m){ return [safe(m.item) + (m.category ? '\\n' + safe(m.category) : ''), safe(m.use), m.quantity === null ? '-' : m.quantity + ' ' + safe(m.unit), fmt(m.unit_price, 2), fmt(m.line_total, 0)]; }),\r\n      foot:[[ { content:'Estimated materials total', colSpan:4, styles:{ halign:'right' } }, fmt(plan.totals && plan.totals.materials_estimate, 0) ]],\r\n      showFoot:'lastPage',\r\n      styles:{ font:'helvetica', fontSize:8.8, textColor:NAVY, cellPadding:2 }, headStyles:{ fillColor:NAVY, textColor:255 },\r\n      footStyles:{ fillColor:ORANGE, textColor:NAVY, fontStyle:'bold', fontSize:10 }, alternateRowStyles:{ fillColor:LIGHT },\r\n      columnStyles:{ 0:{ cellWidth:52 }, 2:{ halign:'right', cellWidth:22 }, 3:{ halign:'right', cellWidth:26 }, 4:{ halign:'right', cellWidth:28 } } });\r\n    y = doc.lastAutoTable.finalY + 6;\r\n    var unp = plan.totals && plan.totals.items_without_price;\r\n    para('Rough estimates in ' + cur + ', materials only, before labour' + (unp ? ' (' + unp + ' item' + (unp === 1 ? '' : 's') + ' without a price)' : '') + '. A contractor quote will give real prices.', 9.5, true);\r\n    y += 3;\r\n    (plan.notices || []).forEach(function(t){ para(t, 10, true); y += 2; });\r\n\r\n    if (plan.assumptions && plan.assumptions.length) { heading('What we assumed'); plan.assumptions.forEach(function(t){ para('- ' + t, 9.5); }); y += 2; }\r\n    \/\/ referral to Builders24\r\n    ensure(68);\r\n    doc.setFillColor.apply(doc, NAVY); doc.roundedRect(M, y, CW, 62, 2, 2, 'F');\r\n    doc.setFillColor.apply(doc, ORANGE); doc.rect(M, y, 3, 62, 'F');\r\n    doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(13); doc.text('Referral to Builders24', M + 9, y + 9);\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(9.5);\r\n    var refLines = [\r\n      'Project: ' + (p.title || 'Your design'),\r\n      'Type: ' + sub,\r\n      d.ref ? 'Reference design: ' + d.ref.name : null,\r\n      'Estimated materials: ' + fmt(plan.totals && plan.totals.materials_estimate, 0),\r\n      'Reference: ' + refCode,\r\n      (cust.name || cust.email) ? 'Customer: ' + [cust.name, cust.email].filter(Boolean).join('  |  ') : null\r\n    ].filter(Boolean);\r\n    refLines.forEach(function(t, i){ doc.text(doc.splitTextToSize(safe(t), CW - 18)[0], M + 9, y + 17 + i * 5.2); });\r\n    var cy = y + 17 + refLines.length * 5.2 + 3;\r\n    doc.setFont('helvetica','bold'); doc.text('Take this design to Builders24 for a real quote and a vetted contractor.', M + 9, cy);\r\n    doc.setFont('helvetica','normal');\r\n    doc.text('Open your design in My Designs at app.builders24.net and choose Request a quote.', M + 9, cy + 5);\r\n    doc.text('support@builders24.net   |   builders24.net', M + 9, cy + 10);\r\n    y += 68;\r\n    ensure(24);\r\n    doc.setFillColor.apply(doc, LIGHT); doc.roundedRect(M, y, CW, 20, 2, 2, 'F');\r\n    doc.setFont('helvetica','normal'); doc.setFontSize(8.8); doc.setTextColor.apply(doc, NAVY);\r\n    doc.splitTextToSize('This is a concept for planning and quoting. The image is AI generated, sizes are standard reference figures unless you gave your own, and none of it replaces drawings from a professional or a site visit.', CW - 8).forEach(function(l, i){ doc.text(l, M + 4, y + 6 + i * 4.4); });\r\n\r\n    \/\/ footers\r\n    var n = doc.getNumberOfPages();\r\n    for (var i = 1; i <= n; i++) {\r\n      doc.setPage(i);\r\n      if (i > 1) {\r\n        doc.setFillColor.apply(doc, NAVY); doc.rect(0, 0, W, 11, 'F'); doc.setFillColor.apply(doc, ORANGE); doc.rect(0, 11, W, 0.8, 'F');\r\n        doc.setTextColor(255,255,255); doc.setFont('helvetica','bold'); doc.setFontSize(10); doc.text('BUILDERS24', M, 7.2);\r\n        doc.setFont('helvetica','normal'); doc.setFontSize(8.5); doc.text(safe(String(p.title || 'Your design')).slice(0, 60), W \/ 2, 7.2, { align:'center' }); doc.text('Ref ' + refCode, W - M, 7.2, { align:'right' });\r\n      }\r\n      doc.setDrawColor.apply(doc, ORANGE); doc.setLineWidth(0.5); doc.line(M, H - 12, W - M, H - 12);\r\n      doc.setFont('helvetica','normal'); doc.setFontSize(8); doc.setTextColor.apply(doc, NAVY);\r\n      doc.text('Builders24  |  app.builders24.net', M, H - 7); doc.text('Page ' + i + ' of ' + n, W - M, H - 7, { align:'right' });\r\n    }\r\n    return doc;\r\n  }\r\n  \/*PDF_END*\/\r\n\r\n  function loadScript(src){\r\n    return new Promise(function(res, rej){\r\n      var tries = 0;\r\n      (function go(){\r\n        var el = document.createElement('script'); el.src = src; el.onload = res;\r\n        el.onerror = function(){ el.remove(); if (++tries < 2) setTimeout(go, 800); else rej(new Error('Could not load the PDF tools. Please check your connection and try again.')); };\r\n        document.head.appendChild(el);\r\n      })();\r\n    });\r\n  }\r\n  async function pdfLibs(){\r\n    if (!(window.jspdf && window.jspdf.jsPDF)) await loadScript('https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf\/2.5.1\/jspdf.umd.min.js');\r\n    var probe = new window.jspdf.jsPDF();\r\n    if (typeof probe.autoTable !== 'function') await loadScript('https:\/\/cdnjs.cloudflare.com\/ajax\/libs\/jspdf-autotable\/3.8.2\/jspdf.plugin.autotable.min.js');\r\n    return window.jspdf.jsPDF;\r\n  }\r\n  function toJpeg(url, maxPx){\r\n    return new Promise(function(resolve, reject){\r\n      var im = new Image(); im.crossOrigin = 'anonymous';\r\n      im.onload = function(){\r\n        var w = im.naturalWidth, h = im.naturalHeight, k = Math.min(1, maxPx \/ Math.max(w, h));\r\n        var c = document.createElement('canvas'); c.width = Math.round(w*k); c.height = Math.round(h*k);\r\n        var x = c.getContext('2d'); x.fillStyle = '#fff'; x.fillRect(0, 0, c.width, c.height); x.drawImage(im, 0, 0, c.width, c.height);\r\n        resolve({ dataUrl:c.toDataURL('image\/jpeg', 0.88), w:c.width, h:c.height });\r\n      };\r\n      im.onerror = function(){ reject(new Error('An image could not be added to the PDF.')); };\r\n      im.src = url;\r\n    });\r\n  }\r\n  async function loadRefForPdf(p){\r\n    var id = (p.input || {}).house_plan_id; if (!id) return null;\r\n    try {\r\n      var r = await sb.from('house_plans').select('name,bedrooms,bathrooms,storeys,heated_area_sqft,total_under_roof_sqft,house_plan_rooms(name,width_in,depth_in,sort_order),house_plan_images(path,sort_order)').eq('id', id).maybeSingle();\r\n      var h = r.data; if (!h) return null;\r\n      var specs = [];\r\n      if (h.bedrooms !== null && h.bedrooms !== undefined) specs.push(h.bedrooms + ' bedrooms');\r\n      if (h.bathrooms !== null && h.bathrooms !== undefined) specs.push(Number(h.bathrooms) + ' bathrooms');\r\n      if (h.storeys) specs.push(h.storeys + (h.storeys === 1 ? ' storey' : ' storeys'));\r\n      if (h.heated_area_sqft) specs.push(Number(h.heated_area_sqft).toLocaleString('en-US') + ' sq ft heated');\r\n      var ft = function(n){ return Math.round(Number(n) \/ 12 * 10) \/ 10; };\r\n      var rooms = (p.input || {}).flow === 'property' ? [] : (h.house_plan_rooms || []).slice().sort(function(a, b){ return a.sort_order - b.sort_order; })\r\n        .map(function(x){ return { name:x.name, size: ft(x.width_in) + ' x ' + ft(x.depth_in) }; });\r\n      var imgs = (h.house_plan_images || []).slice().sort(function(a, b){ return a.sort_order - b.sort_order; });\r\n      var image = null;\r\n      if (imgs[0]) { try { image = await toJpeg(sb.storage.from('house-plans').getPublicUrl(imgs[0].path).data.publicUrl, 1200); } catch(e){ image = null; } }\r\n      return { name:h.name, specs:specs, rooms:rooms, image:image };\r\n    } catch(e){ return null; }\r\n  }\r\n  async function makePdf(p, urls){\r\n    var jsPDF = await pdfLibs();\r\n    var beforeUrl = urls[(p.input_photos || [])[0]], afterUrl = urls[conceptPath(p)], fpUrl = urls[floorPath(p)];\r\n    var inp0 = p.input || {}, plan0 = p.plan || {}, areasPdf = null;\r\n    if (inp0.flow === 'property' && plan0.areas && plan0.areas.length) {\r\n      var at = 0;\r\n      areasPdf = await Promise.all(plan0.areas.map(function(ar, i){\r\n        var cnt = ((inp0.areas || [])[i] || {}).photo_count || 1, bu = urls[(p.input_photos || [])[at]], cp = ((plan0.concepts || [])[i] || {}).path, au = cp ? urls[cp] : '';\r\n        at += cnt;\r\n        return Promise.all([ bu ? toJpeg(bu, 1000).catch(function(){ return null; }) : null, au ? toJpeg(au, 1600).catch(function(){ return null; }) : null ]).then(function(g){\r\n          return { label:ar.label, items:ar.items, area_m2:ar.area_m2, unit:ar.unit, area_assumed:ar.area_assumed, summary:ar.summary, before:g[0], after:g[1] };\r\n        });\r\n      }));\r\n    }\r\n    var got = await Promise.all([ areasPdf ? null : (beforeUrl ? toJpeg(beforeUrl, 1200) : null), areasPdf ? null : (afterUrl ? toJpeg(afterUrl, 1600) : null), loadRefForPdf(p), fpUrl ? toJpeg(fpUrl, 1800).catch(function(){ return null; }) : null ]);\r\n    var um = (S.user && S.user.user_metadata) || {};\r\n    return buildPdf(jsPDF, { project:p, before:got[0], after:got[1], ref:got[2], floor:got[3], areas:areasPdf, jobLabel: areasPdf ? wzLabel('work_type', inp0.work_type) : '', customer:{ name: um.full_name || um.name || '', email:(S.user && S.user.email) || '' }, currency:S.currency, dateText: dateShort(p.completed_at || p.created_at) });\r\n  }\r\n  async function downloadPdf(p, urls){\r\n    var btn = $('btnPdf'); btn.disabled = true; $('resErr').textContent = '';\r\n    try {\r\n      toast('pdf', 'Building your PDF', 'busy');\r\n      var doc = await makePdf(p, urls);\r\n      doc.save('builders24-design-' + String(p.id).slice(0, 8) + '.pdf');\r\n      toast('pdf', 'PDF downloaded', 'ok');\r\n    } catch(e){\r\n      $('resErr').textContent = (e && e.message) || 'The PDF could not be created. Please try again.';\r\n      toast('pdf', 'The PDF could not be created', 'err');\r\n    }\r\n    btn.disabled = false;\r\n  }\r\n  async function emailPdf(p, urls, auto){\r\n    var btn = $('btnMail'), id = 'mail-' + p.id;\r\n    if (btn) btn.disabled = true;\r\n    try {\r\n      toast(id, 'Preparing your PDF to email', 'busy');\r\n      var doc = await makePdf(p, urls);\r\n      var b64 = doc.output('datauristring').split(',')[1];\r\n      var r = await sb.functions.invoke('visualize-email', { body:{ project_id:p.id, pdf_base64:b64 } });\r\n      if (r.error || !r.data || !r.data.ok) throw new Error('send');\r\n      toast(id, 'PDF emailed to ' + (S.user.email || 'you'), 'ok');\r\n    } catch(e){\r\n      toast(id, auto ? 'We could not email your PDF yet. Use Email me this PDF to try again.' : 'We could not email your PDF. Please try again.', 'err');\r\n    }\r\n    if (btn) btn.disabled = false;\r\n  }\r\n\r\n  \/* ================= quote request ================= *\/\r\n  async function requestQuote(p, urls){\r\n    var btn = $('btnQuote'), err = $('resErr'); err.textContent = '';\r\n    var uid = S.user.id, inp = p.input || {}, plan = p.plan || {}, cur = p.currency || S.currency;\r\n    var lines = (plan.materials || []).map(function(m){\r\n      return '- ' + m.item + ' (' + m.category + '): ' + (m.quantity === null ? '' : m.quantity + ' ' + m.unit) + ' for ' + m.use + (m.line_total === null ? '' : ' \u2014 est. ' + money(m.line_total, cur));\r\n    });\r\n    var propQ = inp.flow === 'property' && plan.areas && plan.areas.length;\r\n    var head = propQ\r\n      ? 'AI Design Studio \u2014 ' + wzLabel('work_type', inp.work_type) + '.\\n' + plan.areas.map(function(a){ return a.label + ': ' + (a.items || []).join(', ') + (a.area_m2 ? ' (about ' + a.area_m2 + ' ' + (a.unit || 'm\u00b2') + (a.area_assumed ? ', estimated from the photo' : '') + ')' : ''); }).join('\\n') +\r\n        Object.keys(plan.answer_labels || {}).map(function(k){ return (plan.answer_labels[k] || []).length ? '\\n' + plan.answer_labels[k].join(', ') : ''; }).join('') +\r\n        (inp.wants ? '\\nNotes: ' + inp.wants : '') + (inp.location ? '\\nLocation: ' + inp.location : '') + (inp.stand_area_m2 ? '\\nStand: ' + inp.stand_area_m2 + ' m\u00b2' : '')\r\n      : p.scenario === 'room'\r\n      ? 'AI Design Studio \u2014 ' + (inp.room_label || 'room') + ' redesign.\\nWhat I want: ' + (inp.wants || '')\r\n      : 'AI Design Studio \u2014 ' + (inp.site_type === 'stand' ? 'new home on an empty stand' : 'house ' + (inp.work_type || 'renovation')) + '.\\nStand: ' + (plan.fit ? plan.fit.stand_area_m2 + ' m\u00b2' : '') +\r\n        '. Brief: ' + inp.bedrooms + ' bedrooms, ' + inp.bathrooms + ' bathrooms' + (inp.ensuite ? ' (with ensuite)' : '') + '.' + (inp.base_design_label && inp.base_design !== 'any' ? '\\nBase design: ' + inp.base_design_label : '') + (inp.must_haves ? '\\nMust-haves: ' + inp.must_haves : '') +\r\n        (inp.location ? '\\nLocation: ' + inp.location : '') + (inp.orientation ? '\\nStand position: ' + inp.orientation : '');\r\n    var description = head + '\\n\\nDesign: ' + ((plan.design && plan.design.style_name) || '') + '. ' + (plan.summary || '') + '\\n\\nSuggested materials (estimates only):\\n' + lines.join('\\n') +\r\n      '\\n\\nSource: AI Design Studio\\nDesign link: https:\/\/app.builders24.net\/retail\/visualize.html?design=' + p.id;\r\n\r\n    var yn = function(id, label, opts){ return '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"' + id + '\">' + E(label) + '<\/label><select class=\"vz-in\" id=\"' + id + '\"><option value=\"\">Not stated<\/option>' + opts.map(function(o){ return '<option>' + E(o) + '<\/option>'; }).join('') + '<\/select><\/div>'; };\r\n    var tx = function(id, label){ return '<div class=\"vz-field\"><label class=\"vz-lbl\" for=\"' + id + '\">' + E(label) + '<\/label><input class=\"vz-in\" id=\"' + id + '\" type=\"text\" maxlength=\"200\"><\/div>'; };\r\n    var siteHtml = propQ ? '<div style=\"margin-top:16px;\"><span class=\"vz-lbl\">Site details for the contractor (optional)<\/span>' +\r\n      (inp.work_type === 'renovate' || inp.work_type === 'extend' ? yn('qLive', 'Will you live in the house during the work?', ['Yes', 'No']) : '') +\r\n      yn('qDraw', 'Do you have drawings or a survey?', ['Yes', 'No', 'I need design support']) +\r\n      yn('qAppr', 'Do you need help with council approvals?', ['Yes', 'No', 'Not sure']) +\r\n      tx('qIssues', 'Known issues (damp, asbestos, old wiring, plumbing, drainage)') + tx('qAccess', 'Site access for machinery and materials') +\r\n      tx('qServ', 'Underground services in the work area (water, power, gas, sewer)') + tx('qTime', 'Target completion') + '<\/div>' : '';\r\n    var chk = await showModal({ title:'Check your quote request', text:'This is what contractors will see. You can edit it before sending.',\r\n      html:'<textarea class=\"vz-in\" id=\"qDesc\" maxlength=\"6000\">' + E(description) + '<\/textarea>' + siteHtml, read:true,\r\n      actions:[{ label:'Cancel', value:false }, { label:'Send quote request', primary:true, value:true }] });\r\n    if (!chk || !chk.value) return;\r\n    var edited = chk.box.querySelector('#qDesc').value.trim() || description;\r\n    if (propQ) {\r\n      var gv = function(id, label){ var el = chk.box.querySelector('#' + id), v = el ? el.value.trim() : ''; return v ? label + ': ' + v : ''; };\r\n      var extra = [gv('qLive', 'Living in the house during the work'), gv('qDraw', 'Drawings or survey'), gv('qAppr', 'Help with council approvals'), gv('qIssues', 'Known issues'), gv('qAccess', 'Site access'), gv('qServ', 'Underground services'), gv('qTime', 'Target completion')].filter(Boolean);\r\n      if (extra.length) edited += '\\n\\nSite details from the customer:\\n' + extra.join('\\n');\r\n    }\r\n\r\n    btn.disabled = true; btn.textContent = 'Sending\u2026';\r\n    try {\r\n      toast('quote', 'Preparing your photos for the quote', 'busy');\r\n      var cust = await sb.rpc('get_or_create_customer_ref', { p_entity_id:uid, p_type:'individual' });\r\n      if (cust.error || !cust.data) throw new Error('profile');\r\n\r\n      var files = [].concat((p.input_photos || []).slice(0, 3), p.output_images || []), photoUrls = [];\r\n      for (var i = 0; i < files.length; i++) {\r\n        var src = urls[files[i]]; if (!src) continue;\r\n        var blob = await (await fetch(src)).blob();\r\n        var ext = blob.type.indexOf('png') > -1 ? 'png' : blob.type.indexOf('webp') > -1 ? 'webp' : 'jpg';\r\n        var path = uid + '\/' + Date.now() + '-' + Math.random().toString(36).slice(2, 8) + '.' + ext;\r\n        var up = await sb.storage.from('quote-photos').upload(path, blob, { contentType:blob.type || 'image\/jpeg' });\r\n        if (up.error) throw new Error('upload');\r\n        photoUrls.push(sb.storage.from('quote-photos').getPublicUrl(path).data.publicUrl);\r\n      }\r\n      toast('quote', 'Sending your quote request', 'busy');\r\n\r\n      var category = p.scenario === 'room' ? (inp.room_type === 'kitchen' ? 'Kitchen' : 'Building & Renovation') : (inp.site_type === 'stand' ? 'Building Construction' : 'Building & Renovation');\r\n      var ref = 'Q-' + new Date().getFullYear() + '-' + Math.floor(1000 + Math.random()*9000);\r\n      var ins = await sb.from('quote_requests').insert({\r\n        title: 'AI Design: ' + (p.title || 'Visualizer design'), category: category, description: edited, photos: photoUrls,\r\n        budget_min: inp.budget_min, budget_max: inp.budget_max, currency: cur, status: 'open', customer_ref_id: cust.data,\r\n        delivery_required: true, reference_number: ref, expires_at: new Date(Date.now() + 14*86400000).toISOString(),\r\n        category_details: { visualizer_project_id: p.id, source: 'AI Visualizer' }\r\n      });\r\n      if (ins.error) throw ins.error;\r\n      btn.textContent = 'Quote requested \u2713';\r\n      toast('quote', 'Quote request sent', 'ok');\r\n      var go = await showModal({ title:'Quote request sent', text:'Your reference is ' + ref + '. Contractors can now see your design and materials list. You can follow it in My Quotes.',\r\n        actions:[{ label:'Stay here', value:false }, { label:'View my quote request', primary:true, value:true }] });\r\n      if (go) location.href = 'https:\/\/app.builders24.net\/retail\/my-quotes.html';\r\n    } catch(e){\r\n      btn.disabled = false; btn.textContent = 'Request a quote';\r\n      err.textContent = 'We could not send your quote request. Please try again later.';\r\n      toast('quote', 'The quote request could not be sent', 'err');\r\n      err.scrollIntoView({ behavior:'smooth', block:'center' });\r\n    }\r\n  }\r\n\r\n  \/* ================= my designs ================= *\/\r\n  var KIND_ICON = {\r\n    room:  '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"3\" y=\"3\" width=\"18\" height=\"18\" rx=\"2\"\/><path d=\"M3 15h18\"\/><\/svg>',\r\n    house: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 11 12 3l9 8\"\/><path d=\"M5 10v11h14V10\"\/><\/svg>',\r\n    stand: '<svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.9\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M3 21h18\"\/><path d=\"M5 21V9l7-5 7 5v12\" stroke-dasharray=\"3 3\"\/><\/svg>'\r\n  };\r\n  function kindOf(p){ var inp = p.input || {}; return p.scenario === 'room' ? 'room' : (inp.site_type === 'stand' ? 'stand' : 'house'); }\r\n  function modeLabel(p){\r\n    var inp = p.input || {}, k = kindOf(p);\r\n    if (k === 'room') return inp.room_label || 'Room';\r\n    if (k === 'stand') return 'New home';\r\n    var w = (S.opts.work_type || []).filter(function(o){ return o.value === inp.work_type; })[0];\r\n    return w ? w.label : 'House';\r\n  }\r\n  function closeMenus(){ document.querySelectorAll('.vz-menu').forEach(function(m){ m.remove(); }); }\r\n  async function loadMine(){\r\n    if (!S.user) return;\r\n    $('mineSec').hidden = false;\r\n    if (!$('mine').children.length) $('mine').innerHTML = '<div class=\"vz-skel\" style=\"aspect-ratio:4\/3\"><\/div><div class=\"vz-skel\" style=\"aspect-ratio:4\/3\"><\/div><div class=\"vz-skel\" style=\"aspect-ratio:4\/3\"><\/div><div class=\"vz-skel\" style=\"aspect-ratio:4\/3\"><\/div>';\r\n    var cols = 'id,scenario,status,stage,title,input,created_at,output_images,error';\r\n    try {\r\n      var q = sb.from('visualizer_projects').select(cols).eq('user_id', S.user.id).is('deleted_at', null).order('created_at', { ascending:false }).limit(S.mineLimit + 1);\r\n      var r = await q;\r\n      if (r.error) r = await sb.from('visualizer_projects').select(cols).eq('user_id', S.user.id).order('created_at', { ascending:false }).limit(S.mineLimit + 1);\r\n      S.projects = r.data || [];\r\n    } catch(e){ S.projects = []; }\r\n    S.mineMore = S.projects.length > S.mineLimit;\r\n    if (S.mineMore) S.projects = S.projects.slice(0, S.mineLimit);\r\n    var all = S.projects.filter(function(p){ return p.status !== 'draft'; });\r\n    var shown = all.filter(function(p){ return p.status === 'complete' || p.status === 'processing'; });\r\n    var failed = all.filter(function(p){ return !(p.status === 'complete' || p.status === 'processing'); });\r\n    $('mineFailed').innerHTML = failed.map(function(p){\r\n      return '<div class=\"vz-frow\"><div><b>' + E(p.title || 'Design') + '<\/b><span>Not completed \u00b7 ' + E(dateShort(p.created_at)) + '<\/span><\/div><button type=\"button\" data-del=\"' + E(p.id) + '\" aria-label=\"Delete ' + E(p.title || 'design') + '\">Delete<\/button><\/div>';\r\n    }).join('');\r\n    $('mineMore').hidden = !S.mineMore;\r\n    if (!shown.length) { $('mine').innerHTML = failed.length ? '' : '<div class=\"glass vz-empty\" style=\"grid-column:1\/-1;\">Your designs will appear here once you create one.<\/div>'; return; }\r\n    var urls = await signed(shown.filter(function(p){ return p.status === 'complete'; }).map(function(p){ return (p.output_images || [])[0]; }).filter(Boolean));\r\n    $('mine').innerHTML = shown.map(function(p){\r\n      var u = urls[(p.output_images || [])[0]], k = kindOf(p);\r\n      var badge = p.status === 'complete' ? ['ok', 'Complete'] : p.status === 'processing' ? ['busy', 'In progress'] : ['bad', 'Not completed'];\r\n      var label = p.status === 'complete' ? '' : p.status === 'processing' ? 'In progress\u2026' : 'Not completed';\r\n      return '<div class=\"vz-card\" data-card=\"' + E(p.id) + '\"><button class=\"open\" type=\"button\" data-open=\"' + E(p.id) + '\"' + (p.status === 'failed' ? ' disabled' : '') + '><div class=\"vz-thumb\"' + (u ? ' style=\"background-image:url(\\'' + E(u) + '\\')\"' : '') + '>' + E(label) + '<\/div>' +\r\n             '<div class=\"vz-card-b\"><b>' + E(p.title || 'Design') + '<\/b><span>' + E(modeLabel(p)) + ' \u00b7 ' + E(dateShort(p.created_at)) + '<\/span><\/div><\/button>' +\r\n             '<span class=\"vz-badge ' + badge[0] + '\">' + badge[1] + '<\/span><span class=\"vz-kind\" title=\"' + E(modeLabel(p)) + '\">' + KIND_ICON[k] + '<\/span>' +\r\n             '<button class=\"vz-dots\" type=\"button\" data-menu=\"' + E(p.id) + '\" aria-label=\"More actions for ' + E(p.title || 'design') + '\" aria-haspopup=\"true\">\u22ef<\/button><\/div>';\r\n    }).join('');\r\n  }\r\n  $('mineMore').addEventListener('click', function(){ S.mineLimit += 12; loadMine(); });\r\n  async function cardPdf(id){\r\n    try {\r\n      toast('cpdf', 'Building your PDF', 'busy');\r\n      var r = await sb.from('visualizer_projects').select('*').eq('id', id).maybeSingle();\r\n      var p = r.data; if (!p) throw new Error('x');\r\n      var urls = await signed([].concat(p.input_photos || [], p.output_images || []));\r\n      var doc = await makePdf(p, urls);\r\n      doc.save('builders24-design-' + String(p.id).slice(0, 8) + '.pdf');\r\n      toast('cpdf', 'PDF downloaded', 'ok');\r\n    } catch(e){ toast('cpdf', 'The PDF could not be created. Please try again.', 'err'); }\r\n  }\r\n  async function deleteDesign(id, title){\r\n    var ok = await showModal({ title:'Delete this design?', text:'\"' + (title || 'This design') + '\" will be removed from My Designs. Credits already used for it are not given back.', actions:[{ label:'Keep it', value:false }, { label:'Delete design', primary:true, value:true }] });\r\n    if (!ok) return;\r\n    try {\r\n      var r = await sb.rpc('visualizer_delete_project', { p_id:id });\r\n      if (r.error) throw r.error;\r\n      if (S.current && S.current.project.id === id) { S.current = null; S.mode = null; render(); }\r\n      toast('del', 'Design deleted', 'ok'); loadMine();\r\n    } catch(e){ toast('del', 'The design could not be deleted. Please try again.', 'err'); }\r\n  }\r\n  $('mineFailed').addEventListener('click', function(e){\r\n    var d = e.target.closest('[data-del]'); if (!d) return;\r\n    var pr = S.projects.find(function(x){ return x.id === d.getAttribute('data-del'); });\r\n    deleteDesign(d.getAttribute('data-del'), pr && pr.title);\r\n  });\r\n  $('mine').addEventListener('click', function(e){\r\n    var mb = e.target.closest('[data-menu]');\r\n    if (mb) {\r\n      e.stopPropagation();\r\n      var id = mb.getAttribute('data-menu'), card = mb.closest('.vz-card'), had = card.querySelector('.vz-menu');\r\n      closeMenus(); if (had) return;\r\n      var p = S.projects.find(function(x){ return x.id === id; }); if (!p) return;\r\n      var m = document.createElement('div'); m.className = 'vz-menu'; m.setAttribute('role', 'menu');\r\n      m.innerHTML = (p.status !== 'failed' ? '<button type=\"button\" role=\"menuitem\" data-act=\"open\">Open<\/button>' : '') +\r\n        (p.status === 'complete' ? '<button type=\"button\" role=\"menuitem\" data-act=\"pdf\">Download PDF<\/button>' : '') +\r\n        (p.status !== 'processing' ? '<button type=\"button\" role=\"menuitem\" data-act=\"del\">Delete<\/button>' : '');\r\n      card.appendChild(m);\r\n      m.addEventListener('click', function(ev){\r\n        var a = ev.target.closest('[data-act]'); if (!a) return; ev.stopPropagation(); closeMenus();\r\n        var act = a.getAttribute('data-act');\r\n        if (act === 'open') { S.busy = false; S.current = null; openProject(id); }\r\n        else if (act === 'pdf') cardPdf(id);\r\n        else if (act === 'del') deleteDesign(id, p.title);\r\n      });\r\n      var f = m.querySelector('button'); if (f) f.focus();\r\n      return;\r\n    }\r\n    var c = e.target.closest('[data-open]'); if (!c) return;\r\n    var pr = S.projects.find(function(x){ return x.id === c.getAttribute('data-open'); });\r\n    if (!pr || pr.status === 'failed') return;\r\n    S.busy = false; S.current = null;\r\n    openProject(pr.id);\r\n  });\r\n  document.addEventListener('click', closeMenus);\r\n\r\n  \/* ================= styles ================= *\/\r\n  function applyStyle(name){\r\n    S.style = name;\r\n    document.querySelectorAll('.style-chip').forEach(function(c){ c.classList.toggle('active', c.getAttribute('data-style') === name); });\r\n  }\r\n  document.querySelectorAll('.style-chip').forEach(function(c){\r\n    c.addEventListener('click', function(){ var n = c.getAttribute('data-style'); applyStyle(S.style === n ? '' : n); });\r\n  });\r\n\r\n  \/* ================= style questionnaire (optional helper) ================= *\/\r\n    var PREF_QUESTIONS = [\r\n    {\r\n      question: \"When you walk into your ideal room, what's the primary feeling you want it to live by?\",\r\n      options: [\r\n        { label: '\"Less is more.\" Absolute simplicity, hidden storage, zero clutter, stark lines.', style: 'Modern Minimalist' },\r\n        { label: 'Fluid, current, sleek and comfortable \u2014 adapts smoothly to today\\'s design trends.', style: 'Contemporary' },\r\n        { label: 'Hygge and \"less but better.\" Light-filled, cozy, functional, peaceful.', style: 'Scandinavian' },\r\n        { label: 'Raw, unpolished, structural \u2014 celebrating the skeleton of the building itself.', style: 'Industrial' },\r\n        { label: 'Grand, opulent, symmetrical and prestigious \u2014 rich heritage, high-end detailing.', style: 'Classic Luxury' },\r\n        { label: 'Blurring indoors and outside \u2014 fresh air, open skies, seamless nature.', style: 'Outdoor Living' },\r\n      ]\r\n    },\r\n    {\r\n      question: 'If you had to pick one combination of textures for your primary surfaces, what would it be?',\r\n      options: [\r\n        { label: 'Smooth drywall, polished micro-cement, stark white paint, handleless cabinets.', style: 'Modern Minimalist' },\r\n        { label: 'Mixed metals, glass, high-gloss accents, current-season trend textures.', style: 'Contemporary' },\r\n        { label: 'Pale white-washed or light ash woods, soft wools, woven cottons.', style: 'Scandinavian' },\r\n        { label: 'Exposed red brick, distressed dark leather, matte black iron, reclaimed timber.', style: 'Industrial' },\r\n        { label: 'Polished marble, deep dark walnut, velvet, silk, polished brass or gold.', style: 'Classic Luxury' },\r\n        { label: 'Teak wood, weather-resistant wicker, stone pavers, performance canvas, greenery.', style: 'Outdoor Living' },\r\n      ]\r\n    },\r\n    {\r\n      question: 'What type of furniture silhouettes do you naturally gravitate toward?',\r\n      options: [\r\n        { label: 'Ultra-clean geometric lines, low profiles, completely unadorned surfaces.', style: 'Modern Minimalist' },\r\n        { label: 'Smooth, curved, current shapes \u2014 high-end modern metropolitan feel.', style: 'Contemporary' },\r\n        { label: 'Functional, understated wooden frames, organic edges, clean shapes.', style: 'Scandinavian' },\r\n        { label: 'Heavy-duty utility furniture \u2014 metal frames, riveted joints, workbench styles.', style: 'Industrial' },\r\n        { label: 'Ornate detailing, tufted upholstery, rolled arms, grand statement pieces.', style: 'Classic Luxury' },\r\n        { label: 'Deep-seating lounge chairs, modular weather-ready sectionals, slatted wood loungers.', style: 'Outdoor Living' },\r\n      ]\r\n    },\r\n    {\r\n      question: 'Which color story sets the right mood for you?',\r\n      options: [\r\n        { label: 'Monochromatic strict neutrals \u2014 stark whites, charcoal grays, deep blacks.', style: 'Modern Minimalist' },\r\n        { label: 'Sophisticated neutrals punctuated by whatever color is currently trending.', style: 'Contemporary' },\r\n        { label: 'Airy pastels, soft creams, birch tones, muted earth and nature tones.', style: 'Scandinavian' },\r\n        { label: 'Rust, charcoal, gunmetal gray, burnt orange, deep earthy browns.', style: 'Industrial' },\r\n        { label: 'Ivory, champagne, deep jewel tones (emerald\/navy), rich metallic gold.', style: 'Classic Luxury' },\r\n        { label: 'Sun-bleached neutrals, olive greens, sandy beiges, sky blues.', style: 'Outdoor Living' },\r\n      ]\r\n    },\r\n    {\r\n      question: 'How do you envision the lighting and architectural structure of your space?',\r\n      options: [\r\n        { label: 'Hidden or flush architectural strip lighting, clean wall planes, open space.', style: 'Modern Minimalist' },\r\n        { label: 'Statement modern designer fixtures paired with layered mood lighting.', style: 'Contemporary' },\r\n        { label: 'Abundant natural daylight, uncurtained windows, warm simple pendants.', style: 'Scandinavian' },\r\n        { label: 'High ceilings, visible ductwork, black steel window frames, Edison bulb clusters.', style: 'Industrial' },\r\n        { label: 'Dramatic crystal chandeliers, coordinated sconces, symmetrical architectural lighting.', style: 'Classic Luxury' },\r\n        { label: 'Direct sunlight, open-air pergolas, string lights, seamless glass sliding doors.', style: 'Outdoor Living' },\r\n      ]\r\n    },\r\n  ];\r\n  var prefStep = -1, prefAnswers = [], prefTie = null, prefRelease = null;\r\n  var prefOverlay = $('pref-overlay'), prefProgress = $('pref-progress'), prefNext = $('pref-next');\r\n  function renderPref(){\r\n    var box = $('pref-options'); box.innerHTML = '';\r\n    var intro = prefStep < 0 && !prefTie;\r\n    $('pref-intro').hidden = !intro; prefProgress.hidden = intro || !!prefTie;\r\n    $('pref-back').disabled = intro || !!prefTie; $('pref-skipq').hidden = intro || !!prefTie;\r\n    prefNext.hidden = !!prefTie;\r\n    if (prefTie) {\r\n      $('pref-step-label').textContent = 'Your result';\r\n      $('pref-question-text').textContent = 'These styles scored the same. Which do you prefer?';\r\n      prefTie.forEach(function(st){\r\n        var b = document.createElement('button'); b.type = 'button'; b.className = 'pref-opt'; b.textContent = st;\r\n        b.addEventListener('click', function(){ applyStyle(st); closePref(); });\r\n        box.appendChild(b);\r\n      });\r\n      return;\r\n    }\r\n    if (intro) {\r\n      $('pref-step-label').textContent = 'Style quiz';\r\n      $('pref-question-text').textContent = 'Find your style in 1 minute';\r\n      $('pref-intro').textContent = 'Answer ' + PREF_QUESTIONS.length + ' quick questions and we will suggest a style for you. You can skip any question.';\r\n      prefNext.textContent = 'Start'; prefNext.disabled = false;\r\n      return;\r\n    }\r\n    var q = PREF_QUESTIONS[prefStep];\r\n    $('pref-step-label').textContent = 'Question ' + (prefStep + 1) + ' of ' + PREF_QUESTIONS.length;\r\n    $('pref-question-text').textContent = q.question;\r\n    prefProgress.innerHTML = '';\r\n    PREF_QUESTIONS.forEach(function(_, i){ var seg = document.createElement('div'); seg.className = 'pref-progress-seg' + (i <= prefStep ? ' done' : ''); prefProgress.appendChild(seg); });\r\n    q.options.forEach(function(opt){\r\n      var b = document.createElement('button'); b.type = 'button'; b.className = 'pref-opt'; b.textContent = opt.label;\r\n      if (prefAnswers[prefStep] === opt.style) b.classList.add('selected');\r\n      b.addEventListener('click', function(){\r\n        prefAnswers[prefStep] = opt.style;\r\n        box.querySelectorAll('.pref-opt').forEach(function(x){ x.classList.remove('selected'); });\r\n        b.classList.add('selected'); prefNext.disabled = false;\r\n      });\r\n      box.appendChild(b);\r\n    });\r\n    prefNext.disabled = !prefAnswers[prefStep];\r\n    prefNext.textContent = prefStep === PREF_QUESTIONS.length - 1 ? 'Finish' : 'Next';\r\n  }\r\n  function closePref(){ prefOverlay.classList.remove('open'); if (prefRelease) { prefRelease(); prefRelease = null; } }\r\n  function finishPref(){\r\n    var tally = {}, bestN = 0;\r\n    prefAnswers.forEach(function(st){ if (st) tally[st] = (tally[st] || 0) + 1; });\r\n    Object.keys(tally).forEach(function(st){ if (tally[st] > bestN) bestN = tally[st]; });\r\n    var top = Object.keys(tally).filter(function(st){ return tally[st] === bestN; });\r\n    if (!top.length) { closePref(); return; }\r\n    if (top.length === 1) { applyStyle(top[0]); closePref(); return; }\r\n    prefTie = top; renderPref();\r\n  }\r\n  function advancePref(){\r\n    if (prefStep < PREF_QUESTIONS.length - 1) { prefStep++; renderPref(); } else finishPref();\r\n  }\r\n  document.querySelectorAll('.quizBtn').forEach(function(b){ b.addEventListener('click', function(){\r\n    prefStep = -1; prefAnswers = []; prefTie = null; renderPref(); prefOverlay.classList.add('open');\r\n    prefRelease = trapFocus(prefOverlay, closePref);\r\n  }); });\r\n  prefNext.addEventListener('click', advancePref);\r\n  $('pref-skipq').addEventListener('click', advancePref);\r\n  $('pref-back').addEventListener('click', function(){ if (prefStep >= 0) { prefStep--; renderPref(); } });\r\n  $('pref-skip').addEventListener('click', closePref);\r\n  prefOverlay.addEventListener('click', function(e){ if (e.target === prefOverlay) closePref(); });\r\n\r\n  \/* ================= boot ================= *\/\r\n  (async function boot(){\r\n    S.pickers.room = makePicker('rPhotos', 'rErr');\r\n    S.pickers.build = makePicker('bPhotos', 'bErr');\r\n    $('gateLink').href = 'https:\/\/app.builders24.net\/login.html?redirect=' + encodeURIComponent('\/retail\/visualize.html');\r\n\r\n    var sess = null;\r\n    try { sess = (await sb.auth.getSession()).data.session; } catch(e){}\r\n    S.user = sess ? sess.user : null;\r\n    document.querySelectorAll('.style-chip .sw').forEach(function(sw){\r\n      var m = \/url\\(['\"]?([^'\")]+)\/.exec(sw.getAttribute('style') || ''); if (!m) return;\r\n      var t = new Image(); t.onerror = function(){ sw.classList.add('noimg'); }; t.src = m[1];\r\n    });\r\n    await loadLive();\r\n    S.pickers.room.repaint(); S.pickers.build.repaint();\r\n    if (!S.user) { $('toolLoad').hidden = true; $('gate').hidden = false; return; }\r\n\r\n    $('toolMain').hidden = false;\r\n    try { if (typeof getCustomerCurrency === 'function') S.currency = (await getCustomerCurrency()) || 'USD'; } catch(e){}\r\n    var cs = $('curSel');\r\n    if (!S.currencies.some(function(x){ return x.value === S.currency; })) S.currencies.unshift({ value:S.currency, label:S.currency });\r\n    fillSelect(cs, S.currencies); cs.value = S.currency;\r\n    paintCurrency(); updateMustCount();\r\n    render();\r\n    await loadQuota();\r\n    await loadMine();\r\n\r\n    \/\/ Accepted ?mode= values: room, house (also build, renovate, extend, rebuild), stand (also new)\r\n    var qs = new URLSearchParams(location.search), m = (qs.get('mode') || '').toLowerCase();\r\n    var ALIAS = { room:'room', house:'house', build:'house', renovate:'house', extend:'house', additions:'house', yard:'house', landscaping:'house', boundary:'house', rebuild:'house', stand:'stand', 'new':'stand' };\r\n    if (m && !ALIAS[m]) console.warn('visualize: unknown mode \"' + m + '\". Accepted: room, house, build, renovate, extend, additions, yard, landscaping, boundary, rebuild, stand, new.');\r\n    var running = S.projects.find(function(p){ return p.status === 'processing'; });\r\n    if (ALIAS[m]) {\r\n      setMode(ALIAS[m]);\r\n      if (ALIAS[m] === 'house' && jobReady(m)) { wzReset(); W.job = m; W.areas = [wzNewArea()]; W.skipPrompt = true; W.skipJob = true; W.pos = 0; wzRender(); }\r\n    }\r\n    var dz = qs.get('design');\r\n    if (dz && \/^[0-9a-f-]{36}$\/i.test(dz)) openProject(dz);\r\n    else if (running) pollProject(running.id, running.scenario);\r\n    setTimeout(function(){ pdfLibs().catch(function(){}); }, 2500);\r\n  })();\r\n})();\r\n<\/script>\r\n<\/body>\r\n<\/html>\t\t\t\t<\/div>\n\t\t\n<\/div>\n\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>AI Design Studio | Room, Renovation and New Home Design | Builders24 \/* Everything is scoped to #b24ds so nothing here can restyle the WordPress site around it. *\/ #b24ds{ &#8211;navy:#0D1B3E; &#8211;navy-2:#16295A; &#8211;navy-3:#091430; &#8211;orange:#F0A500; &#8211;border:rgba(255,255,255,0.18); &#8211;pad:20px; &#8211;green:#4ADE80; &#8211;red:#FF8080; } #b24ds, #b24ds *{box-sizing:border-box; margin:0; padding:0;} #b24ds{ background-color:#0a1530; } #b24ds{ font-family:&#8217;Archivo Narrow&#8217;, sans-serif; color:#fff; -webkit-font-smoothing:antialiased; background-image:linear-gradient(rgba(8,17,40,0.45), rgba(8,17,40,0.6)), url(&#8216;https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg&#8217;); background-size:cover; background-position:center; background-attachment:fixed; background-repeat:no-repeat; background-color:#0a1530; } #b24ds #app#app h1,#b24ds #app#app h2,#b24ds #app#app h3,#b24ds #app#app h4,#b24ds #app#app h5,#b24ds #app#app h6{ color:#fff !important; -webkit-text-fill-color:#fff !important; } #b24ds #app#app .orange{ color:var(&#8211;orange) !important; -webkit-text-fill-color:var(&#8211;orange) !important; } #b24ds a{ text-decoration:none; color:inherit; } #b24ds button{ font-family:inherit; border:none; cursor:pointer; background:none; color:inherit; } #b24ds input,#b24ds select,#b24ds textarea{ font-family:inherit; } #b24ds ::-webkit-scrollbar{ display:none; } #b24ds, #b24ds *{ scrollbar-width:none; } #b24ds [hidden]{ display:none !important; } #b24ds .page{ padding:36px var(&#8211;pad) 140px; background:var(&#8211;navy); position:relative; z-index:1; margin-top:6px; border-top:2px solid rgba(240,165,0,0.35); box-shadow:0 -20px 40px rgba(0,0,0,0.35); } @media(min-width:900px){#b24ds .page{ max-width:1200px; margin:6px auto 0; padding:56px 40px 60px; } } #b24ds .glass{ background:rgba(20,32,66,0.62); backdrop-filter:blur(18px); -webkit-backdrop-filter:blur(18px); border:1px solid var(&#8211;border); border-radius:16px; } #b24ds section{ margin-top:40px; } #b24ds section:first-child{ margin-top:0; } #b24ds .sec-head{ margin-bottom:18px; } #b24ds .sec-head h2{ font-size:22px; font-weight:700; margin-bottom:4px; } #b24ds .sec-head p{ font-size:14px; } #b24ds \/* ===== HERO ===== *\/ .hero-eyebrow{ color:var(&#8211;orange); font-size:13px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; margin-bottom:10px; } #b24ds .thero{ position:relative; min-height:0; display:flex; align-items:center; overflow:hidden; background:var(&#8211;navy-3); } @media(min-width:900px){#b24ds .thero{ border-radius:0 0 24px 24px; } } #b24ds .thero-bg{ position:absolute; inset:0; background-image:url(&#8216;https:\/\/app.builders24.net\/assets\/hero-home-decor.jpg&#8217;); background-size:cover; background-position:center; transform:scale(1.05); } @keyframes hero-pan{ 0%{ transform:scale(1.05) translateX(0); } 100%{ transform:scale(1.1) translateX(-1.5%); } } #b24ds .thero-overlay{ position:absolute; inset:0; background:linear-gradient(120deg, rgba(9,15,32,0.96) 10%, rgba(9,15,32,0.75) 55%, rgba(9,15,32,0.5) 100%); } #b24ds .thero-grid{ position:absolute; inset:0; background-image:linear-gradient(rgba(240,165,0,0.05) 1px,transparent 1px),linear-gradient(90deg,rgba(240,165,0,0.05) 1px,transparent 1px); background-size:52px 52px; pointer-events:none; } #b24ds .thero-orb{ position:absolute; top:-110px; right:-60px; width:380px; height:380px; border-radius:50%; background:radial-gradient(circle, rgba(240,165,0,0.14) 0%, transparent 70%); pointer-events:none; } @keyframes orb-float{ 0%,100%{ transform:translateY(0); } 50%{ transform:translateY(-20px); } } #b24ds .thero-inner{ position:relative; z-index:2; padding:26px var(&#8211;pad) 24px; width:100%; text-align:center; } @media(min-width:900px){#b24ds .thero-inner{ padding:34px 40px 30px; } } #b24ds .thero h1{ font-size:clamp(26px,4.4vw,38px); font-weight:700; line-height:1.15; margin:0 auto 10px; max-width:680px; } #b24ds .thero h1 .orange{ color:var(&#8211;orange); } #b24ds .thero p{ font-size:16px; max-width:560px; margin:0 auto 14px; line-height:1.5; } #b24ds .hero-trust{ display:flex; flex-wrap:wrap; justify-content:center; gap:8px 10px; max-width:760px; margin:0 auto; } #b24ds .hero-trust span{ display:inline-flex; align-items:center; gap:6px; padding:7px 12px; border-radius:999px; border:1px solid var(&#8211;border); background:rgba(20,32,66,0.62); font-size:13px; font-weight:700; } #b24ds .hero-trust span:before{ content:&#8221;; width:7px; height:7px; border-radius:50%; background:var(&#8211;orange); } #b24ds \/* ===== STEP CARDS ===== *\/ .tcard-grid{ display:grid; grid-template-columns:1fr 1fr; gap:12px; perspective:1200px; } @media(min-width:768px){#b24ds .tcard-grid{ grid-template-columns:repeat(4,1fr); } } #b24ds .tcard{ position:relative; border-radius:16px; padding:16px 14px; transition:border-color .3s; } #b24ds .tcard:hover{ border-color:rgba(240,165,0,0.4) !important; } #b24ds .tcard .n{ width:28px; height:28px; border-radius:50%; background:var(&#8211;orange); color:var(&#8211;navy-3); font-weight:700; font-size:13px; display:grid; place-items:center; margin-bottom:10px; } #b24ds .tcard h3,#b24ds .tcard h4{ font-size:15px; font-weight:700; margin-bottom:4px; } #b24ds .tcard p{ font-size:13px; line-height:1.45; } #b24ds \/* ===== TOOL ===== *\/ .vz-tool{ padding:0; overflow:hidden; } #b24ds .vz-q{ font-size:clamp(20px,3vw,26px); font-weight:700; margin-bottom:6px; } #b24ds .vz-qs{ font-size:15px; margin-bottom:18px; } #b24ds .vz-choice{ display:grid; grid-template-columns:1fr; gap:12px; } @media(min-width:700px){#b24ds .vz-choice{ grid-template-columns:repeat(3,1fr); } } #b24ds .vz-pick{ text-align:left; padding:20px 18px; border-radius:16px; border:1.5px solid var(&#8211;border); background:rgba(255,255,255,0.07); min-height:120px; display:flex; flex-direction:column; gap:6px; transition:border-color .2s, transform .2s, background .2s; } #b24ds .vz-pick:hover,#b24ds .vz-pick:focus-visible{ border-color:var(&#8211;orange); background:var(&#8211;navy-2); outline:none; } #b24ds .vz-pick svg{ color:var(&#8211;orange); margin-bottom:4px; } #b24ds .vz-pick b{ font-size:19px; font-weight:700; } #b24ds .vz-pick span{ font-size:14px; line-height:1.4; } #b24ds .vz-back{ display:inline-flex; align-items:center; gap:6px; min-height:44px; font-size:15px; font-weight:700; color:var(&#8211;orange); } #b24ds .vz-formhead{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:18px; } #b24ds .vz-formhead h3{ font-size:clamp(19px,3vw,24px); font-weight:700; } #b24ds .vz-reveal{ animation:vzreveal .35s ease both; } @keyframes vzreveal{ from{ opacity:0; transform:translateY(14px); } to{ opacity:1; transform:none; } } #b24ds .vz-credits{ padding:14px 16px 16px; border-radius:14px; background:rgba(255,255,255,0.07); border:1px solid var(&#8211;border); margin-bottom:24px; } #b24ds .vz-cr-sel{ display:flex; align-items:center; gap:8px; font-size:14px; font-weight:600; } #b24ds .vz-cr-sel select{ width:auto; min-height:42px; padding:8px 12px; font-size:15px; max-width:230px; } #b24ds .vz-cr-wrap{ display:flex; flex-wrap:wrap; align-items:center; gap:16px 22px; } #b24ds .vz-donut{ position:relative; width:116px; height:116px; flex:none; } #b24ds .vz-donut svg{ width:100%; height:100%; transform:rotate(-90deg); display:block; } #b24ds .vz-donut .trk{ fill:none; stroke:rgba(255,255,255,0.2); stroke-width:11; } #b24ds .vz-donut .arc{ fill:none; stroke:var(&#8211;orange); stroke-width:11; stroke-linecap:round; stroke-dasharray:0 263.9; transition:stroke-dasharray .8s ease; } #b24ds .vz-credits.out .vz-donut .arc{ stroke:var(&#8211;red); } #b24ds .vz-donut .mid{ position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; line-height:1.05; } #b24ds .vz-donut .mid b{ font-size:30px; font-weight:700; color:var(&#8211;orange); } #b24ds .vz-donut .mid span{ font-size:13px; font-weight:600; margin-top:2px; } #b24ds .vz-cr-info{ flex:1 1 240px; min-width:0; display:flex; flex-direction:column; gap:12px; } #b24ds .vz-cr-sub{ font-size:14px; line-height:1.45; } #b24ds \/* toasts *\/ .vz-toasts{ position:fixed; left:50%; transform:translateX(-50%); bottom:96px; z-index:1100; width:min(420px, calc(100% &#8211; 32px)); display:flex; flex-direction:column-reverse; gap:10px; pointer-events:none; } #b24ds .vz-toast{ display:flex; align-items:center; gap:12px; padding:13px 16px; border-radius:14px; background:rgba(9,20,48,0.96); border:1.5px solid var(&#8211;border); box-shadow:0 12px 32px rgba(0,0,0,0.45); font-size:15px; font-weight:600; animation:vztin .35s cubic-bezier(.2,.9,.3,1.2) both; } #b24ds .vz-toast[data-state=ok]{ border-color:var(&#8211;green); } #b24ds .vz-toast[data-state=err]{ border-color:var(&#8211;red); } #b24ds .vz-toast[data-state=busy]{ border-color:var(&#8211;orange); } #b24ds .vz-toast.out{ animation:vztout .3s ease both; } @keyframes vztin{ from{ opacity:0; transform:translateY(24px) scale(.95); } to{ opacity:1; transform:none; } } @keyframes vztout{ to{ opacity:0; transform:translateY(12px) scale(.96); } } #b24ds .vz-ti{ flex:none; width:26px; height:26px; display:grid; place-items:center; } #b24ds .vz-tspin{ width:22px; height:22px; border-radius:50%; border:3px solid rgba(255,255,255,0.25); border-top-color:var(&#8211;orange); animation:vzspin .8s linear infinite; } #b24ds .vz-ti svg{ width:26px; height:26px; } #b24ds .vz-ti .ring{ fill:none; stroke-width:2.5; stroke-dasharray:70; stroke-dashoffset:70; animation:vzdraw .45s ease forwards; } #b24ds .vz-ti .tick{ fill:none; stroke-width:3; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:30; stroke-dashoffset:30; animation:vzdraw .35s .25s ease forwards; } #b24ds [data-state=ok] .vz-ti .ring,#b24ds [data-state=ok] .vz-ti .tick{ stroke:var(&#8211;green); } #b24ds [data-state=err] .vz-ti .ring,#b24ds [data-state=err] .vz-ti .tick{ stroke:var(&#8211;red); } @keyframes vzdraw{ to{ stroke-dashoffset:0; } } #b24ds .vz-body{ padding:22px 18px 26px; } @media(min-width:700px){#b24ds .vz-body{ padding:30px 32px 34px; } } #b24ds .vz-field{ margin-bottom:20px; } #b24ds .vz-lbl{ display:block; font-size:15px; font-weight:700; margin-bottom:8px; } #b24ds .vz-lbl i{ font-style:normal; color:var(&#8211;red); } #b24ds .vz-hint{ font-size:13px; margin-bottom:10px; line-height:1.45; } #b24ds .vz-in{ width:100%; min-height:48px; background:rgba(255,255,255,0.08); border:1px solid var(&#8211;border); border-radius:10px; padding:12px 14px; font-size:16px; color:#fff; outline:none; } #b24ds .vz-in:focus{ border-color:var(&#8211;orange); } #b24ds textarea.vz-in{ min-height:110px; resize:vertical; line-height:1.5; } #b24ds select.vz-in option{ background:#10182f; color:#fff; } #b24ds .vz-in::placeholder{ color:#E6ECFA; opacity:1; } #b24ds .vz-grid2{ display:grid; grid-template-columns:1fr; gap:14px; } #b24ds .vz-grid3{ display:grid; grid-template-columns:1fr; gap:14px; } @media(min-width:600px){#b24ds .vz-grid2{ grid-template-columns:1fr 1fr; } #b24ds .vz-grid3{ grid-template-columns:repeat(3,1fr); } } #b24ds .vz-size{ display:grid; grid-template-columns:1fr; gap:12px; align-items:end; } @media(min-width:700px){#b24ds .vz-size{ grid-template-columns:1fr 1fr auto 1fr; } } #b24ds .vz-or{ text-align:center; font-weight:700; font-size:14px; padding-bottom:14px; } @media(max-width:699px){#b24ds .vz-or{ padding:0; } } #b24ds .vz-mini{ display:block; font-size:13px; font-weight:700; margin-bottom:6px; } #b24ds .vz-seg{ display:grid; grid-template-columns:1fr 1fr; gap:10px; } #b24ds .vz-seg button{ min-height:52px; border-radius:12px; border:1.5px solid var(&#8211;border); background:rgba(255,255,255,0.06); font-size:15px; font-weight:700; padding:10px; } #b24ds .vz-seg button.on{ border-color:var(&#8211;orange); background:rgba(240,165,0,0.2); color:#fff; box-shadow:inset 0 0 0 1px var(&#8211;orange); } #b24ds .vz-extras{ border:1px solid var(&#8211;border); border-radius:12px; padding:0; margin-bottom:20px; background:rgba(255,255,255,0.04); } #b24ds<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"elementor_canvas","meta":{"footnotes":""},"class_list":["post-153229","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153229","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/comments?post=153229"}],"version-history":[{"count":1,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153229\/revisions"}],"predecessor-version":[{"id":153431,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153229\/revisions\/153431"}],"wp:attachment":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/media?parent=153229"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}