{"id":153221,"date":"2026-10-19T10:41:29","date_gmt":"2026-10-19T08:41:29","guid":{"rendered":"https:\/\/builders24.net\/?page_id=153221"},"modified":"2026-10-20T08:41:23","modified_gmt":"2026-10-20T08:41:23","slug":"request-a-quote","status":"publish","type":"page","link":"https:\/\/builders24.net\/zw\/request-a-quote\/","title":{"rendered":"Request A Quote"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"153221\" class=\"elementor elementor-153221\">\n\t\t\t\t<div class=\"elementor-element elementor-element-6ee5eff e-con e-atomic-element e-flexbox-base e-default-div e-cb0ceb1 \" data-id=\"6ee5eff\" data-element_type=\"e-flexbox\" data-e-type=\"e-flexbox\" data-interaction-id=\"6ee5eff\">\n    \t\t<div class=\"elementor-element elementor-element-ac3c5a5 elementor-widget elementor-widget-html\" data-id=\"ac3c5a5\" 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\">\r\n<title>Request a Quote \u2014 Builders24<\/title>\r\n<meta name=\"description\" content=\"Get a quote for building materials, hardware, equipment, vehicles, services and more \u2014 across Africa. Choose products, services, vehicles or shipping. Builders24 connects you with verified suppliers.\">\r\n<meta name=\"robots\" content=\"index, follow\">\r\n\r\n<meta property=\"og:type\" content=\"website\">\r\n<meta property=\"og:title\" content=\"Request a Quote \u2014 Builders24\">\r\n<meta property=\"og:description\" content=\"Get a quote for building materials, hardware, equipment, vehicles, services and more \u2014 across Africa. Verified suppliers, compared prices, one request.\">\r\n<meta property=\"og:image\" content=\"https:\/\/app.builders24.net\/assets\/support.png\">\r\n<meta property=\"og:site_name\" content=\"Builders24\">\r\n\r\n<meta name=\"twitter:card\" content=\"summary_large_image\">\r\n<meta name=\"twitter:title\" content=\"Request a Quote \u2014 Builders24\">\r\n<meta name=\"twitter:description\" content=\"Get a quote for building materials, hardware, equipment, vehicles, services and more \u2014 across Africa.\">\r\n<meta name=\"twitter:image\" content=\"https:\/\/app.builders24.net\/assets\/support.png\">\r\n\r\n<script type=\"application\/ld+json\">\r\n{\r\n  \"@context\": \"https:\/\/schema.org\",\r\n  \"@type\": \"WebPage\",\r\n  \"name\": \"Request a Quote \u2014 Builders24\",\r\n  \"description\": \"Get a quote for building materials, hardware, equipment, vehicles, services and more \u2014 across Africa.\",\r\n  \"isPartOf\": { \"@type\": \"WebSite\", \"name\": \"Builders24\", \"url\": \"https:\/\/app.builders24.net\" },\r\n  \"provider\": { \"@type\": \"Organization\", \"name\": \"Builders24\" }\r\n}\r\n<\/script>\r\n\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;800&display=swap\" rel=\"stylesheet\">\r\n<style>\r\n\/* Scoped to #b24rq so nothing here can restyle the WordPress site around it. *\/\r\n#b24rq{ --navy:#14213d; --orange:#f5901f; --orange-dark:#d97a0d; --page:#f4f6f9; --card:#fff; --line:#dfe4ea; --soft:#eef1f5; --body:#4f5a6b; --muted:#7c8696; --ok:#16a34a; --err:#d93b32; }\r\n#b24rq, #b24rq *{ box-sizing:border-box; margin:0; padding:0; }\r\n#b24rq{ font-family:'Archivo Narrow',sans-serif; background:var(--page); color:var(--navy); -webkit-font-smoothing:antialiased; overflow-x:hidden; }\r\n#b24rq button{ font-family:inherit; border:none; cursor:pointer; background:none; color:inherit; }\r\n#b24rq input,#b24rq select,#b24rq textarea{ font-family:inherit; color:var(--navy); }\r\n#b24rq :focus-visible{ outline:2px solid var(--orange); outline-offset:2px; }\r\n#b24rq .qhead{ background:#fff; color:var(--navy); padding:26px 20px 22px; border-bottom:1px solid var(--line); }\r\n#b24rq .qhead-in{ max-width:1080px; margin:0 auto; }\r\n#b24rq .qhead h1{ font-size:30px; font-weight:800; line-height:1.1; color:var(--navy); }\r\n#b24rq .qhead p{ color:var(--body); font-size:15px; margin-top:8px; max-width:560px; line-height:1.45; }\r\n#b24rq .wrap{ max-width:1080px; margin:0 auto; padding:20px 20px 120px; }\r\n@media(max-width:600px){#b24rq .wrap{ padding:16px 14px 130px; }\r\n#b24rq .qhead{ padding:20px 14px 18px; }\r\n#b24rq .qhead h1{ font-size:24px; }\r\n }\r\n#b24rq .prog{ display:flex; gap:6px; margin-bottom:18px; }\r\n#b24rq .prog-i{ flex:1; font-size:12.5px; font-weight:700; color:var(--muted); border-top:3px solid var(--line); padding-top:7px; }\r\n#b24rq .prog-i.on{ color:var(--navy); border-color:var(--orange); }\r\n#b24rq .prog-i.done{ color:var(--ok); border-color:var(--ok); }\r\n#b24rq .types{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; }\r\n@media(max-width:900px){#b24rq .types{ grid-template-columns:repeat(2,1fr); }\r\n }\r\n#b24rq .tcard{ background:var(--card); border:1.5px solid var(--line); border-radius:14px; padding:20px 16px 16px; text-align:left; display:flex; flex-direction:column; gap:8px; min-height:190px; transition:transform .15s ease, box-shadow .2s ease, border-color .2s ease; }\r\n#b24rq .tcard:hover{ transform:translateY(-3px); box-shadow:0 14px 30px rgba(20,33,61,.12); border-color:var(--orange); }\r\n#b24rq .tcard:active{ transform:scale(.98); }\r\n#b24rq .tcard svg{ width:34px; height:34px; stroke:var(--orange); fill:none; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; }\r\n#b24rq .tcard h3{ font-size:19px; font-weight:800; }\r\n#b24rq .tcard p{ font-size:13.5px; color:var(--body); line-height:1.4; flex:1; }\r\n#b24rq .tcard .cov{ font-size:12.5px; font-weight:700; color:var(--ok); }\r\n#b24rq .layout{ display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:18px; align-items:start; }\r\n@media(max-width:900px){#b24rq .layout{ grid-template-columns:1fr; }\r\n }\r\n#b24rq .card{ background:var(--card); border:1px solid var(--line); border-radius:14px; padding:20px; }\r\n@media(max-width:600px){#b24rq .card{ padding:14px; }\r\n }\r\n#b24rq .card h2{ font-size:21px; font-weight:800; margin-bottom:4px; }\r\n#b24rq .sub{ color:var(--body); font-size:14px; line-height:1.45; margin-bottom:16px; }\r\n#b24rq .back{ font-size:14px; font-weight:700; color:var(--body); margin-bottom:12px; display:inline-flex; gap:6px; padding:4px 0; }\r\n#b24rq .field{ margin-bottom:14px; min-width:0; }\r\n#b24rq .lab{ display:block; font-size:13px; font-weight:700; margin-bottom:6px; }\r\n#b24rq .req{ color:var(--err); }\r\n#b24rq .in,#b24rq .sel,#b24rq .ta{ width:100%; background:#fff; border:1px solid var(--line); border-radius:9px; padding:11px 12px; font-size:14px; }\r\n#b24rq .in,#b24rq .sel{ min-height:44px; }\r\n#b24rq .ta{ min-height:90px; resize:vertical; }\r\n#b24rq .grid2{ display:grid; grid-template-columns:1fr 1fr; gap:12px; }\r\n@media(max-width:520px){#b24rq .grid2{ grid-template-columns:1fr; }\r\n }\r\n#b24rq .searchbar{ display:flex; gap:10px; margin-bottom:12px; }\r\n#b24rq .searchbar .in{ flex:1; }\r\n#b24rq .chips{ display:flex; gap:8px; overflow-x:auto; padding:2px 0 10px; margin-bottom:6px; scrollbar-width:thin; }\r\n#b24rq .chip{ flex:none; min-height:38px; padding:0 14px; border-radius:999px; border:1px solid var(--line); background:#fff; font-size:13.5px; font-weight:700; white-space:nowrap; }\r\n#b24rq .chip.on{ background:var(--navy); border-color:var(--navy); color:#fff; }\r\n#b24rq .rows{ border:1px solid var(--line); border-radius:12px; overflow:hidden; }\r\n#b24rq .row{ display:flex; align-items:center; gap:12px; padding:12px 14px; border-top:1px solid var(--soft); background:#fff; }\r\n#b24rq .row:first-child{ border-top:none; }\r\n#b24rq .row.on{ background:#fff8ee; }\r\n#b24rq .tick{ width:24px; height:24px; flex:none; accent-color:var(--orange); }\r\n#b24rq .row-main{ flex:1; min-width:0; cursor:pointer; }\r\n#b24rq .row-name{ font-size:15px; font-weight:700; line-height:1.25; overflow-wrap:anywhere; }\r\n#b24rq .row-sub{ font-size:12.5px; color:var(--muted); margin-top:2px; }\r\n#b24rq .row-price{ font-size:13px; font-weight:700; color:var(--navy); white-space:nowrap; }\r\n#b24rq .qty{ width:84px; flex:none; min-height:40px; border:1px solid var(--line); border-radius:8px; padding:0 8px; font-size:14px; }\r\n#b24rq .fam{ background:var(--soft); padding:9px 14px; font-size:12.5px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:var(--body); border-top:1px solid var(--line); }\r\n#b24rq .fam:first-child{ border-top:none; }\r\n@media(max-width:520px){#b24rq .row{ flex-wrap:wrap; }\r\n#b24rq .row-price{ order:3; }\r\n#b24rq .qty{ margin-left:36px; }\r\n }\r\n#b24rq .notice{ background:#fff8ee; border:1px solid #f3d9b0; border-radius:10px; padding:12px 14px; font-size:13.5px; line-height:1.45; margin:12px 0; }\r\n#b24rq .notice.info{ background:#eef4ff; border-color:#cfe0fb; }\r\n#b24rq .estimate{ background:#eefaf1; border:1px solid #bfe6c9; border-radius:10px; padding:12px 14px; margin:12px 0; }\r\n#b24rq .estimate b{ font-size:18px; }\r\n#b24rq .estimate small{ display:block; color:var(--body); margin-top:3px; font-size:12.5px; }\r\n#b24rq .more{ margin-top:14px; border:1px dashed var(--line); border-radius:12px; padding:14px; }\r\n#b24rq .more h4{ font-size:15px; margin-bottom:4px; }\r\n#b24rq .btn{ min-height:46px; padding:0 20px; border-radius:10px; font-size:15px; font-weight:800; background:var(--orange); color:#fff; transition:transform .1s ease, background .15s ease; }\r\n#b24rq .btn:hover{ background:var(--orange-dark); }\r\n#b24rq .btn:active{ transform:scale(.97); }\r\n#b24rq .btn:disabled{ opacity:.55; cursor:not-allowed; }\r\n#b24rq .btn.ghost{ background:#fff; color:var(--navy); border:1px solid var(--line); }\r\n#b24rq .btn.ghost:hover{ background:var(--soft); }\r\n#b24rq .actions{ display:flex; gap:10px; justify-content:space-between; margin-top:18px; flex-wrap:wrap; }\r\n#b24rq .err{ color:var(--err); font-size:13.5px; margin:10px 0 0; min-height:18px; }\r\n#b24rq .side{ position:sticky; top:12px; }\r\n#b24rq .side h3{ font-size:16px; font-weight:800; margin-bottom:10px; display:flex; justify-content:space-between; }\r\n#b24rq .line{ display:flex; gap:8px; align-items:flex-start; padding:9px 0; border-top:1px solid var(--soft); }\r\n#b24rq .line:first-of-type{ border-top:none; }\r\n#b24rq .line-name{ flex:1; font-size:13.5px; font-weight:700; line-height:1.25; min-width:0; overflow-wrap:anywhere; }\r\n#b24rq .line-name small{ display:block; font-weight:500; color:var(--muted); font-size:12px; }\r\n#b24rq .tagreq{ display:inline-block; background:#fde7c8; color:#8a4b00; font-size:10.5px; font-weight:800; border-radius:999px; padding:1px 7px; margin-left:4px; text-transform:uppercase; }\r\n#b24rq .x{ width:32px; height:32px; flex:none; border-radius:8px; color:var(--muted); font-size:18px; }\r\n#b24rq .x:hover{ background:var(--soft); color:var(--err); }\r\n#b24rq .empty{ color:var(--muted); font-size:13.5px; padding:6px 0; }\r\n#b24rq .bar{ display:none; }\r\n@media(max-width:900px){#b24rq .side{ display:none; position:static; margin-top:14px; }\r\n#b24rq .side.open{ display:block; }\r\n#b24rq .bar{ display:flex; position:fixed; left:0; right:0; bottom:0; z-index:40; background:var(--navy); color:#fff; padding:10px 14px calc(10px + env(safe-area-inset-bottom)); align-items:center; justify-content:space-between; gap:10px; }\r\n#b24rq .bar .btn{ min-height:42px; }\r\n\r\n  }\r\n#b24rq .done-box{ text-align:center; padding:30px 10px; }\r\n#b24rq .done-box .ref{ font-size:22px; font-weight:800; color:var(--orange); margin:8px 0 14px; }\r\n#b24rq .loading{ color:var(--muted); padding:14px 0; font-size:14px; }\r\n#b24rq .subpills{ display:flex; gap:8px; overflow-x:auto; padding:2px 0 8px; margin-bottom:4px; scrollbar-width:none; }\r\n#b24rq .subpills::-webkit-scrollbar,#b24rq .chips::-webkit-scrollbar{ display:none; }\r\n#b24rq .chips{ scrollbar-width:none; }\r\n#b24rq .chip.sub{ min-height:36px; font-size:13px; background:var(--soft); }\r\n#b24rq .chip.sub.on{ background:var(--orange); border-color:var(--orange); color:#fff; }\r\n#b24rq .vgrid{ display:grid; gap:8px 10px; grid-template-columns:repeat(2,minmax(0,1fr)); margin-bottom:4px; }\r\n@media(min-width:760px){#b24rq .vgrid{ grid-template-columns:repeat(3,minmax(0,1fr)); }\r\n }\r\n#b24rq .vgrid .field{ margin-bottom:0; }\r\n#b24rq .chips.wrap{ flex-wrap:wrap; overflow:visible; padding-bottom:4px; margin-bottom:10px; }\r\n#b24rq .sstep{ margin-bottom:16px; }\r\n#b24rq .slab{ font-size:14px; font-weight:800; margin-bottom:8px; display:flex; align-items:center; gap:8px; }\r\n#b24rq .snum{ width:22px; height:22px; border-radius:50%; background:var(--navy); color:#fff; font-size:12px; display:inline-flex; align-items:center; justify-content:center; flex:none; }\r\n#b24rq .scopes{ display:grid; gap:8px; grid-template-columns:repeat(2,minmax(0,1fr)); }\r\n@media(min-width:760px){#b24rq .scopes{ grid-template-columns:repeat(4,minmax(0,1fr)); }\r\n }\r\n#b24rq .scope{ text-align:left; background:#fff; border:1.5px solid var(--line); border-radius:12px; padding:10px 12px; min-height:68px; }\r\n#b24rq .scope b{ display:block; font-size:13.5px; line-height:1.2; }\r\n#b24rq .scope small{ display:block; font-size:12px; color:var(--muted); margin-top:3px; line-height:1.25; }\r\n#b24rq .scope.on{ border-color:var(--orange); background:#fffaf2; }\r\n#b24rq .ccnt{ display:inline-block; margin-left:6px; font-size:11px; font-weight:800; opacity:.65; }\r\n#b24rq .toolbar{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin:6px 0 10px; }\r\n#b24rq .toolbar .cnt{ font-size:13.5px; color:var(--body); }\r\n#b24rq .seg{ display:flex; border:1px solid var(--line); border-radius:10px; overflow:hidden; }\r\n#b24rq .seg button{ white-space:nowrap; min-height:38px; padding:0 12px; font-size:13px; font-weight:800; background:#fff; }\r\n#b24rq .seg button.on{ background:var(--navy); color:#fff; }\r\n#b24rq .pgrid{ display:grid; gap:10px; grid-template-columns:repeat(var(--cols,2),minmax(0,1fr)); }\r\n@media(min-width:760px){#b24rq .pgrid{ grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); }\r\n#b24rq .seg{ display:none; }\r\n }\r\n#b24rq .pc{ position:relative; background:#fff; border:1.5px solid var(--line); border-radius:12px; padding:10px; display:grid; grid-template-columns:44px minmax(0,1fr) auto; grid-template-areas:\"im nm tg\" \"im mt tg\"; column-gap:10px; row-gap:3px; align-items:center; text-align:left; min-height:68px; cursor:pointer; transition:transform .12s ease, border-color .2s; }\r\n#b24rq .pc:active{ transform:scale(.98); }\r\n#b24rq .pc.on{ border-color:var(--orange); background:#fffaf2; }\r\n#b24rq .pc .im{ grid-area:im; width:44px; height:44px; border-radius:10px; background:linear-gradient(135deg,#e9edf3,#dfe5ee); overflow:hidden; display:flex; align-items:center; justify-content:center; color:var(--muted); font-size:17px; }\r\n#b24rq .pc .im img{ width:100%; height:100%; object-fit:cover; }\r\n#b24rq .pc .nm{ grid-area:nm; font-size:13.5px; font-weight:700; line-height:1.2; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; overflow:hidden; align-self:end; }\r\n#b24rq .pc .mt{ grid-area:mt; font-size:12px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; align-self:start; }\r\n#b24rq .pc .mt b{ color:var(--navy); }\r\n#b24rq .pc .add{ grid-area:tg; width:34px; height:34px; border-radius:50%; background:#fff; border:1.5px solid var(--line); font-size:19px; font-weight:800; display:flex; align-items:center; justify-content:center; }\r\n#b24rq .pc.on .add{ background:var(--orange); border-color:var(--orange); color:#fff; }\r\n#b24rq .rtag{ display:inline-block; background:var(--soft); font-size:10.5px; font-weight:800; padding:1px 6px; border-radius:999px; margin-left:4px; color:var(--body); }\r\n@media(max-width:759px){#b24rq .pc{ grid-template-columns:minmax(0,1fr) auto; grid-template-areas:\"im tg\" \"nm nm\" \"mt mt\"; row-gap:7px; align-items:start; min-height:122px; }\r\n#b24rq .pc .add{ align-self:center; }\r\n#b24rq .pc .nm{ align-self:start; font-size:13px; }\r\n#b24rq .pgrid.c3 .pc{ padding:8px; min-height:104px; }\r\n#b24rq .pgrid.c3 .im{ width:36px; height:36px; }\r\n#b24rq .pgrid.c3 .add{ width:30px; height:30px; font-size:17px; }\r\n#b24rq .pgrid.c3 .nm{ font-size:11.5px; }\r\n#b24rq .pgrid.c3 .mt{ font-size:11px; }\r\n\r\n  }\r\n#b24rq .sheet-bg{ position:fixed; inset:0; background:rgba(10,18,36,.55); display:none; z-index:60; align-items:flex-end; justify-content:center; }\r\n#b24rq .sheet-bg.open{ display:flex; }\r\n#b24rq .sheet{ background:#fff; width:100%; max-width:480px; max-height:80vh; overflow:auto; border-radius:18px 18px 0 0; padding-bottom:16px; }\r\n@media(min-width:760px){#b24rq .sheet-bg{ align-items:center; }\r\n#b24rq .sheet{ border-radius:18px; }\r\n }\r\n#b24rq .sh-top{ display:flex; gap:12px; padding:16px 16px 4px; position:relative; }\r\n#b24rq .sh-im{ flex:none; width:96px; height:96px; border-radius:12px; background:linear-gradient(135deg,#e9edf3,#dfe5ee); overflow:hidden; display:flex; align-items:center; justify-content:center; color:var(--muted); }\r\n#b24rq .sh-im img{ width:100%; height:100%; object-fit:cover; }\r\n#b24rq .sh-close{ position:absolute; top:8px; right:8px; width:36px; height:36px; border-radius:50%; background:var(--soft); font-size:20px; }\r\n#b24rq .sh-thumbs{ display:flex; gap:6px; padding:6px 16px 0; overflow-x:auto; }\r\n#b24rq .sh-thumbs .t{ flex:none; width:40px; height:40px; border-radius:8px; border:2px solid var(--line); overflow:hidden; background:var(--soft); }\r\n#b24rq .sh-thumbs .t.on{ border-color:var(--orange); }\r\n#b24rq .sh-thumbs img{ width:100%; height:100%; object-fit:cover; }\r\n#b24rq .sh-body{ padding:6px 16px 0; }\r\n#b24rq .sh-body .optlab{ font-size:12px; font-weight:800; letter-spacing:.05em; text-transform:uppercase; color:var(--muted); margin:12px 0 6px; }\r\n#b24rq .optpills{ display:flex; gap:8px; flex-wrap:wrap; }\r\n#b24rq .optpill{ min-height:36px; padding:0 14px; border-radius:999px; border:1px solid var(--line); background:var(--soft); font-size:13px; font-weight:700; }\r\n#b24rq .optpill.on{ background:var(--orange); border-color:var(--orange); color:#fff; }\r\n#b24rq .sh-desc{ font-size:14px; line-height:1.5; color:var(--body); margin:12px 0; }\r\n#b24rq .stepper{ display:flex; align-items:center; border:1px solid var(--line); border-radius:10px; overflow:hidden; }\r\n#b24rq .stepper button{ width:44px; height:46px; font-size:22px; font-weight:800; background:var(--soft); }\r\n#b24rq .stepper input{ width:60px; height:46px; border:none; text-align:center; font-size:16px; font-weight:700; }\r\n#b24rq .sh-row{ display:flex; gap:10px; align-items:center; margin-top:12px; }\r\n#b24rq .sh-row .btn{ flex:1; }\r\n\r\n\r\n<\/style>\r\n<\/head>\r\n<body>\r\n<div id=\"b24rq\">\r\n\r\n<div class=\"qhead\"><div class=\"qhead-in\">\r\n  <h1>Request a Quote<\/h1>\r\n  <p>Choose what you need a quote for. Everything you pick comes straight from what Builders24 supplies, so your request reaches the right people.<\/p>\r\n<\/div><\/div>\r\n\r\n<div class=\"wrap\">\r\n  <div class=\"prog\" id=\"prog\"><\/div>\r\n  <div id=\"view\"><\/div>\r\n<\/div>\r\n\r\n<div class=\"bar\" id=\"bar\"><\/div>\r\n<div class=\"sheet-bg\" id=\"sheetBg\"><div class=\"sheet\" id=\"sheet\"><\/div><\/div>\r\n<\/div><!-- \/#b24rq -->\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(async function(){\r\n  const $ = (id) => document.getElementById(id);\r\n  const esc = (s) => (s == null ? '' : String(s)).replace(\/[&<>\"']\/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;',\"'\":'&#39;'}[c]));\r\n  const clean = (s) => String(s || '').replace(\/[,%()*\\\\]\/g, ' ').trim();\r\n  const money = (n, cur) => { try { return new Intl.NumberFormat('en', { style:'currency', currency: cur || 'USD', maximumFractionDigits:0 }).format(n); } catch(e){ return (cur||'') + ' ' + Math.round(n); } };\r\n\r\n  const TYPES = {\r\n    products: { label:'Products', icon:'<path d=\"M21 8 12 3 3 8v8l9 5 9-5V8Z\"\/><path d=\"m3 8 9 5 9-5M12 13v8\"\/>',\r\n                blurb:'Building materials, hardware, tools, electronics, equipment and machinery from our shop.' },\r\n    services: { label:'Services', icon:'<path d=\"M14.7 6.3a4 4 0 0 0-5.4 5.4L3 18l3 3 6.3-6.3a4 4 0 0 0 5.4-5.4l-2.4 2.4-2.1-.5-.5-2.1 2.4-2.4Z\"\/>',\r\n                blurb:'Construction, renovation and trade services from our verified contractors.' },\r\n    vehicles: { label:'Vehicles', icon:'<path d=\"M3 16V11l2-5h14l2 5v5M3 16h18M3 16v2h3v-2M18 16v2h3v-2\"\/><circle cx=\"7.5\" cy=\"13\" r=\"1\"\/><circle cx=\"16.5\" cy=\"13\" r=\"1\"\/>',\r\n                blurb:'Buy or import a vehicle from Builders24 Auto.' },\r\n    shipping: { label:'Shipping', icon:'<path d=\"M2 18c2 0 2 1.5 4 1.5S8 18 10 18s2 1.5 4 1.5 2-1.5 4-1.5 2 1.5 4 1.5\"\/><path d=\"M4 15 3 10h18l-1 5M8 10V6h8v4M12 6V3\"\/>',\r\n                blurb:'Freight on the routes we cover: sea, air and road.' },\r\n  };\r\n\r\n  const S = {\r\n    view:'type', type:null, lines:[], tick:{}, \/\/ tick: product id -> line index\r\n    details:{ needBy:'', delivery:true, city:'', suburb:'', budgetMin:'', budgetMax:'', notes:'', name:'', email:'', phone:'' },\r\n    ship:{ scope:'', oc:'', dc:'', origin:'', dest:'', mode:'', cargo:'', weight:'' },\r\n    session:null, customerRefId:null, profile:null, counts:{},\r\n    cats:null, catChildren:null, roots:[], prodCats:null,\r\n    svc:null, svcCats:null, veh:null, freight:null,\r\n    q:'', root:'', sub:'', cols:2, shown:[], svcCat:'', ref:'',\r\n  };\r\n\r\n  \/\/ ---------- data loaders (all straight from the database) ----------\r\n  async function loadCounts(){\r\n    const head = (t, f) => { let q = sb.from(t).select('id', { count:'exact', head:true }); return f ? f(q) : q; };\r\n    const [p, s, v, r] = await Promise.all([\r\n      head('products', q => q.eq('is_published', true).eq('is_archived', false)),\r\n      head('service_catalog', q => q.contains('channels', ['project']).eq('is_active', true)),\r\n      head('vehicle_listings', q => q.eq('is_published', true)),\r\n      head('freight_routes', q => q.eq('status', 'active')),\r\n    ]);\r\n    S.counts = { products:p.count||0, services:s.count||0, vehicles:v.count||0, shipping:r.count||0 };\r\n  }\r\n  async function loadCats(){\r\n    if (S.cats) return;\r\n    const { data } = await sb.from('categories').select('id,name,parent_id,sort_order').eq('is_active', true).order('sort_order', { nullsFirst:false }).order('name').range(0, 4999);\r\n    S.cats = data || [];\r\n    S.catChildren = {}; S.cats.forEach(c => { (S.catChildren[c.parent_id || 'root'] = S.catChildren[c.parent_id || 'root'] || []).push(c); });\r\n    S.roots = S.catChildren['root'] || [];\r\n    const byId = {}; S.cats.forEach(c => byId[c.id] = c); S.catById = byId;\r\n    const { data: pc } = await sb.from('products').select('category_id').eq('is_published', true).eq('is_archived', false).range(0, 9999);\r\n    S.prodCats = pc || [];\r\n  }\r\n  function rootOf(id){ let c = S.catById[id], guard = 0; while (c && c.parent_id && guard++ < 10) c = S.catById[c.parent_id]; return c ? c.id : null; }\r\n  function descendants(id){ const out = [id]; (S.catChildren[id] || []).forEach(c => out.push(...descendants(c.id))); return out; }\r\n\r\n  async function loadServices(){\r\n    if (S.svc) return;\r\n    const [{ data: cats }, { data: svc }] = await Promise.all([\r\n      sb.from('service_categories').select('id,name,sort_order').order('sort_order', { nullsFirst:false }).order('name').range(0, 999),\r\n      sb.from('service_catalog').select('id,name,category_id,service_types').contains('channels', ['project']).eq('is_active', true).order('name').range(0, 1999),\r\n    ]);\r\n    S.svcCatsAll = cats || [];\r\n    S.svcCats = cats || [];\r\n    const nm = {}; (cats || []).forEach(c => nm[c.id] = c.name);\r\n    S.svc = (svc || []).map(s => ({ ...s, cat: nm[s.category_id] || '' }));\r\n  }\r\n\r\n  async function loadVehicles(){\r\n    if (S.veh) return;\r\n    const [{ data: ls }, { data: cats }, { data: models }, { data: makes }] = await Promise.all([\r\n      sb.from('vehicle_listings').select('id,slug,vehicle_model_id,year,price,display_price,currency,fob_price,fob_currency,mileage_km,fuel_type,transmission,steering,engine_cc,trim,location_country,images,status').eq('is_published', true).range(0, 9999),\r\n      sb.from('vehicle_categories').select('id,parent_id,name,slug,sort_order').eq('is_active', true).order('sort_order').range(0, 999),\r\n      sb.from('vehicle_models').select('id,make_id,name,category_id').eq('is_active', true).range(0, 9999),\r\n      sb.from('vehicle_makes').select('id,name').eq('is_active', true).order('name').range(0, 9999),\r\n    ]);\r\n    const hist = ls || [];\r\n    const mm = {}; (models || []).forEach(m => mm[m.id] = m); const mk = {}; (makes || []).forEach(m => mk[m.id] = m);\r\n    hist.forEach(l => { const m = mm[l.vehicle_model_id]; l._modelName = m ? m.name : ''; l._make = m ? m.make_id : ''; l._cat = m ? m.category_id : null; l._makeName = m && mk[m.make_id] ? mk[m.make_id].name : ''; });\r\n    const list = hist.filter(l => l.status === 'available' && l._make && l._makeName);\r\n    const kids = {}; const byId = {}; (cats || []).forEach(c => { byId[c.id] = c; (kids[c.parent_id || ''] = kids[c.parent_id || ''] || []).push(c); });\r\n    S.veh = { hist, list, cats: cats || [], kids, byId, models: models || [], makes: makes || [], f:{ grp:'', sub:'', leaf:'', q:'', make:'', model:'', year:'', fuel:'', trans:'', steer:'', country:'' }, shown: VPAGE, draft:{ node:'', cat:'', make:'', model:'', year:'', qty:1, steering:'', colour:'', spec:'' } };\r\n  }\r\n\r\n  async function loadFreight(){\r\n    if (S.freight) return;\r\n    const [{ data: routes }, { data: places }, { data: countries }] = await Promise.all([\r\n      sb.from('freight_routes').select('origin_place_id,dest_place_id,mode,transit_days_min,transit_days_max').eq('status', 'active').range(0, 4999),\r\n      sb.from('freight_places').select('id,country_code,kind,name').eq('is_active', true).range(0, 4999),\r\n      sb.from('freight_countries').select('code,name,is_africa').range(0, 999),\r\n    ]);\r\n    const co = {}; (countries || []).forEach(c => co[c.code] = c);\r\n    const pl = {}; (places || []).forEach(p => pl[p.id] = { ...p, country: co[p.country_code] ? co[p.country_code].name : p.country_code });\r\n    (routes || []).forEach(r => {\r\n      const o = pl[r.origin_place_id], d = pl[r.dest_place_id];\r\n      if (!o || !d) return;\r\n      const oa = !!(co[o.country_code] && co[o.country_code].is_africa), da = !!(co[d.country_code] && co[d.country_code].is_africa);\r\n      r._oc = o.country_code; r._dc = d.country_code;\r\n      r._scope = oa && da ? (o.country_code === d.country_code ? 'domestic' : 'cross') : (oa ? 'out' : (da ? 'into' : ''));\r\n    });\r\n    S.freight = { routes: routes || [], pl, co };\r\n  }\r\n\r\n  \/\/ ---------- helpers ----------\r\n  const lineKey = (l) => l.kind + ':' + (l.product_id || l.service_id || l.listing_id || l.vehicle_model_id || l.name);\r\n  function addLine(l){ S.lines.push(l); }\r\n  function removeLine(i){ const l = S.lines[i]; if (l && l.product_id) delete S.tick[l.product_id]; S.lines.splice(i, 1); }\r\n  function linesOfKind(k){ return S.lines.filter(l => l.kind === k); }\r\n\r\n  \/\/ ---------- chrome ----------\r\n  function renderProg(){\r\n    const steps = ['Type', 'Your request', 'Details', 'Send'];\r\n    const idx = { type:0, flow:1, details:2, done:3 }[S.view];\r\n    $('prog').innerHTML = steps.map((s, i) => `<div class=\"prog-i ${i < idx ? 'done' : (i === idx ? 'on' : '')}\">${i + 1}. ${s}<\/div>`).join('');\r\n  }\r\n  function summaryLines(){\r\n    if (!S.lines.length) return '<div class=\"empty\">Nothing added yet.<\/div>';\r\n    return S.lines.map((l, i) => `<div class=\"line\">\r\n      <div class=\"line-name\">${esc(l.name)}${l.match_status === 'requested' ? '<span class=\"tagreq\">Requested<\/span>' : ''}<small>${esc(l.sub || '')}<\/small><\/div>\r\n      ${l.noqty ? '' : `<input class=\"qty\" type=\"number\" min=\"1\" value=\"${esc(l.qty)}\" aria-label=\"Quantity\" data-qi=\"${i}\">`}\r\n      <button class=\"x\" data-rm=\"${i}\" aria-label=\"Remove\">\u00d7<\/button><\/div>`).join('');\r\n  }\r\n  function sidePanel(){\r\n    const title = S.type === 'products' ? 'Your quote list' : (S.type === 'services' ? 'Selected services' : 'Your request');\r\n    return `<aside class=\"side card\" id=\"side\"><h3><span>${title}<\/span><span>${S.lines.length}<\/span><\/h3><div id=\"sideLines\">${summaryLines()}<\/div><\/aside>`;\r\n  }\r\n  function renderBar(){\r\n    const bar = $('bar');\r\n    if (S.view !== 'flow' && S.view !== 'details') { bar.style.display = 'none'; return; }\r\n    bar.style.display = '';\r\n    const n = S.type === 'shipping' ? (S.ship.dest ? 1 : 0) : S.lines.length;\r\n    bar.innerHTML = `<button class=\"btn ghost\" id=\"barToggle\">${S.type === 'shipping' ? 'Route' : 'List'} (${n})<\/button><button class=\"btn\" id=\"barNext\">${S.view === 'flow' ? 'Continue' : 'Review below'}<\/button>`;\r\n    $('barToggle').onclick = () => { const s = $('side'); if (s) s.classList.toggle('open'); };\r\n    $('barNext').onclick = () => { if (S.view === 'flow') goDetails(); else $('submitBtn') && $('submitBtn').scrollIntoView({ behavior:'smooth', block:'center' }); };\r\n  }\r\n  function refreshSide(){ const el = $('sideLines'); if (el) { el.innerHTML = summaryLines(); const h = document.querySelector('#side h3 span:last-child'); if (h) h.textContent = S.lines.length; } renderBar(); }\r\n\r\n  function bindSide(){\r\n    const root = $('view');\r\n    root.querySelectorAll('[data-rm]').forEach(b => b.onclick = () => { removeLine(Number(b.dataset.rm)); render(); });\r\n    root.querySelectorAll('[data-qi]').forEach(i => i.oninput = () => { S.lines[Number(i.dataset.qi)].qty = i.value; });\r\n  }\r\n\r\n  \/\/ ---------- views ----------\r\n  function viewType(){\r\n    $('view').innerHTML = `<div class=\"types\">${Object.keys(TYPES).map(k => `\r\n      <button class=\"tcard\" data-type=\"${k}\">\r\n        <svg viewBox=\"0 0 24 24\">${TYPES[k].icon}<\/svg>\r\n        <h3>${TYPES[k].label}<\/h3><p>${TYPES[k].blurb}<\/p>\r\n        <span class=\"cov\">${S.counts[k] ? S.counts[k].toLocaleString() + ({products:' products', services:' services', vehicles:' vehicles in stock', shipping:' routes'}[k]) : ''}<\/span>\r\n      <\/button>`).join('')}<\/div>\r\n      <div class=\"notice info\" style=\"margin-top:18px;\">We only quote what Builders24 supplies. If we don't deal in it, we'll tell you straight away instead of leaving you waiting.<\/div>`;\r\n    $('view').querySelectorAll('[data-type]').forEach(b => b.onclick = () => startFlow(b.dataset.type));\r\n  }\r\n\r\n  async function startFlow(type){\r\n    S.type = type; S.view = 'flow'; S.lines = []; S.tick = {}; S.q = ''; S.root = ''; S.sub = ''; S.svcCat = ''; S.svcSetting = ''; S.svcShown = 48;\r\n    $('view').innerHTML = '<div class=\"loading\">Loading\u2026<\/div>'; renderProg(); renderBar();\r\n    try {\r\n      if (type === 'products') await loadCats();\r\n      if (type === 'services') await loadServices();\r\n      if (type === 'vehicles') await loadVehicles();\r\n      if (type === 'shipping') await loadFreight();\r\n    } catch (e) { console.error(e); }\r\n    render();\r\n  }\r\n\r\n  function render(){\r\n    renderProg();\r\n    if (S.view === 'type') viewType();\r\n    else if (S.view === 'flow') ({ products:viewProducts, services:viewServices, vehicles:viewVehicles, shipping:viewShipping })[S.type]();\r\n    else if (S.view === 'details') viewDetails();\r\n    else if (S.view === 'done') viewDone();\r\n    renderBar();\r\n  }\r\n  function wrap(inner, withSide = true){\r\n    return `<button class=\"back\" id=\"backBtn\">\u2190 ${S.view === 'flow' ? 'Change quote type' : 'Back'}<\/button>\r\n      <div class=\"${withSide ? 'layout' : ''}\"><div class=\"card\">${inner}<\/div>${withSide ? sidePanel() : ''}<\/div>`;\r\n  }\r\n  function bindBack(){ const b = $('backBtn'); if (b) b.onclick = () => { if (S.view === 'details') { S.view = 'flow'; render(); } else { S.view = 'type'; S.type = null; render(); } }; }\r\n\r\n  \/\/ ===== PRODUCTS =====\r\n  function hasProducts(catId){ \/\/ does this category (or a descendant) hold published products?\r\n    const ids = new Set(descendants(catId)); return S.prodCats.some(p => p.category_id && ids.has(p.category_id));\r\n  }\r\n  function rootChips(){\r\n    let other = S.prodCats.some(p => !p.category_id);\r\n    const items = S.roots.filter(r => hasProducts(r.id));\r\n    return `<div class=\"chips\"><button class=\"chip ${S.root === '' ? 'on' : ''}\" data-root=\"\">All<\/button>\r\n      ${items.map(r => `<button class=\"chip ${S.root === r.id ? 'on' : ''}\" data-root=\"${r.id}\">${esc(r.name)}<\/button>`).join('')}\r\n      ${other ? `<button class=\"chip ${S.root === 'other' ? 'on' : ''}\" data-root=\"other\">Other<\/button>` : ''}<\/div>`;\r\n  }\r\n  function subChips(){\r\n    if (!S.root || S.root === 'other') return '';\r\n    const kids = (S.catChildren[S.root] || []).filter(k => hasProducts(k.id));\r\n    if (!kids.length) return '';\r\n    return `<div class=\"subpills\"><button class=\"chip sub ${S.sub === '' ? 'on' : ''}\" data-sub=\"\">All<\/button>${kids.map(k => `<button class=\"chip sub ${S.sub === k.id ? 'on' : ''}\" data-sub=\"${k.id}\">${esc(k.name)}<\/button>`).join('')}<\/div>`;\r\n  }\r\n  function viewProducts(){\r\n    $('view').innerHTML = wrap(`\r\n      <h2>What products do you need?<\/h2>\r\n      <p class=\"sub\">Tap + to add a product to your quote list. Tap a product to see it in full.<\/p>\r\n      <div class=\"searchbar\"><input class=\"in\" id=\"pq\" type=\"search\" placeholder=\"Search products, brands or ranges\" value=\"${esc(S.q)}\" autocomplete=\"off\"><\/div>\r\n      ${rootChips()}${subChips()}\r\n      <div class=\"toolbar\"><span class=\"cnt\" id=\"pcnt\"><\/span><div class=\"seg\" id=\"seg\"><button data-cols=\"2\" class=\"${S.cols === 2 ? 'on' : ''}\">2 \u00d7 2<\/button><button data-cols=\"3\" class=\"${S.cols === 3 ? 'on' : ''}\">3 \u00d7 3<\/button><\/div><\/div>\r\n      <div id=\"presults\"><div class=\"loading\">Searching\u2026<\/div><\/div>\r\n      <div class=\"more\" id=\"moreBox\">\r\n        <h4>Can't find the exact item?<\/h4>\r\n        <p class=\"sub\" style=\"margin-bottom:10px;\">If we stock the category but not that exact product or model, request it by name. Pick the category, type the exact product, and we'll still source a quote.<\/p>\r\n        <div class=\"grid2\">\r\n          <div class=\"field\"><label class=\"lab\">Category <span class=\"req\">*<\/span><\/label><select class=\"sel\" id=\"rq1\"><option value=\"\">Select a category\u2026<\/option>${S.roots.map(r => `<option value=\"${r.id}\">${esc(r.name)}<\/option>`).join('')}<\/select><\/div>\r\n          <div class=\"field\"><label class=\"lab\">Sub-category<\/label><select class=\"sel\" id=\"rq2\"><option value=\"\">Any<\/option><\/select><\/div>\r\n        <\/div>\r\n        <div class=\"grid2\">\r\n          <div class=\"field\"><label class=\"lab\">Exact product name <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"rqn\" placeholder=\"e.g. brand, model, size\"><\/div>\r\n          <div class=\"field\"><label class=\"lab\">Quantity <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"rqq\" type=\"number\" min=\"1\" value=\"1\"><\/div>\r\n        <\/div>\r\n        <div class=\"err\" id=\"rqerr\"><\/div>\r\n        <button class=\"btn ghost\" id=\"rqadd\">Add requested item<\/button>\r\n      <\/div>\r\n      <div class=\"actions\"><span><\/span><button class=\"btn\" id=\"contBtn\">Continue<\/button><\/div>\r\n      <div class=\"err\" id=\"flowErr\"><\/div>`);\r\n    bindBack(); bindSide();\r\n    $('pq').oninput = debounce(() => { S.q = $('pq').value; runProductSearch(); }, 300);\r\n    $('view').querySelectorAll('[data-root]').forEach(b => b.onclick = () => { S.root = b.dataset.root; S.sub = ''; viewProducts(); });\r\n    $('view').querySelectorAll('[data-sub]').forEach(b => b.onclick = () => { S.sub = b.dataset.sub; viewProducts(); });\r\n    $('view').querySelectorAll('[data-cols]').forEach(b => b.onclick = () => { S.cols = Number(b.dataset.cols); viewProducts(); });\r\n    $('rq1').onchange = () => { const kids = S.catChildren[$('rq1').value] || []; $('rq2').innerHTML = '<option value=\"\">Any<\/option>' + kids.map(k => `<option value=\"${k.id}\">${esc(k.name)}<\/option>`).join(''); };\r\n    $('rqadd').onclick = addRequested;\r\n    $('contBtn').onclick = goDetails;\r\n    runProductSearch();\r\n  }\r\n  function debounce(fn, ms){ let t; return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); }; }\r\n\r\n  const imgUrl = (x) => !x ? '' : (typeof x === 'string' ? x : (x.url || x.src || ''));\r\n  const PCOLS = 'id,name,brand,price,currency,category_id,product_family,variant,image_url';\r\n  async function runProductSearch(){\r\n    const box = $('presults'); if (!box) return;\r\n    let q = sb.from('products').select(PCOLS).eq('is_published', true).eq('is_archived', false).order('product_family', { nullsFirst:false }).order('name').limit(60);\r\n    const t = clean(S.q);\r\n    if (t) q = q.or(`name.ilike.%${t}%,brand.ilike.%${t}%,product_family.ilike.%${t}%`);\r\n    if (S.root === 'other') q = q.is('category_id', null);\r\n    else if (S.sub) q = q.in('category_id', descendants(S.sub));\r\n    else if (S.root) q = q.in('category_id', descendants(S.root));\r\n    const { data, error } = await q;\r\n    if (!$('presults')) return;\r\n    if (error) { box.innerHTML = '<div class=\"notice\">Could not load products. Please try again.<\/div>'; return; }\r\n    S.shown = data;\r\n    if ($('pcnt')) $('pcnt').textContent = data.length + (data.length === 60 ? '+' : '') + ' product' + (data.length === 1 ? '' : 's');\r\n    if (!data.length) {\r\n      box.innerHTML = t\r\n        ? `<div class=\"notice\"><b>We don't have \u201c${esc(S.q)}\u201d in our shop.<\/b> We only quote products we supply. Try another name, browse a category above, or request a specific item from one of our categories below.<\/div>`\r\n        : '<div class=\"notice\">No products here yet.<\/div>';\r\n      return;\r\n    }\r\n    box.innerHTML = `<div class=\"pgrid ${S.cols === 3 ? 'c3' : ''}\" style=\"--cols:${S.cols}\">${data.map(prodCard).join('')}<\/div>`;\r\n    box.querySelectorAll('[data-add]').forEach(b => b.onclick = (e) => { e.stopPropagation(); toggleProduct(b.dataset.add); });\r\n    box.querySelectorAll('.pc').forEach(c => c.onclick = () => openProduct(c.dataset.id));\r\n  }\r\n  function prodCard(p){\r\n    const on = S.tick[p.id] != null;\r\n    return `<div class=\"pc ${on ? 'on' : ''}\" data-id=\"${p.id}\" role=\"button\" tabindex=\"0\">\r\n      <div class=\"im\">${p.image_url ? `<img decoding=\"async\" src=\"${esc(p.image_url)}\" alt=\"\" loading=\"lazy\" onerror=\"this.remove()\">` : '\u25a3'}<\/div>\r\n      <button class=\"add\" data-add=\"${p.id}\" aria-label=\"${on ? 'Remove from' : 'Add to'} quote list\">${on ? '\u2713' : '+'}<\/button>\r\n      <div class=\"nm\">${esc(p.name)}<\/div>\r\n      <div class=\"mt\">${p.price ? `<b>${money(p.price, p.currency)}<\/b>` : ''}${p.brand ? ' \u00b7 ' + esc(p.brand) : ''}${p.product_family ? `<span class=\"rtag\">${esc(p.product_family)}<\/span>` : ''}<\/div><\/div>`;\r\n  }\r\n  function lineFromProduct(p, qty){\r\n    const v = p.variant || {};\r\n    return { kind:'product', product_id:p.id, category_id:p.category_id, name:p.name, brand:p.brand || '', sub:[p.brand, v.grade].filter(Boolean).join(' \u00b7 '), qty:String(qty || 1), unit: v.size_kg ? 'Bags' : 'Units', price:p.price, currency:p.currency, match_status:'catalogue' };\r\n  }\r\n  function reindex(){ S.tick = {}; S.lines.forEach((l, i) => { if (l.product_id) S.tick[l.product_id] = i; }); }\r\n  function toggleProduct(id, p, qty){\r\n    p = p || (S.shown || []).find(x => x.id === id) || (S.cache && S.cache[id]);\r\n    if (!p) return;\r\n    if (S.tick[id] != null) { S.lines.splice(S.tick[id], 1); reindex(); }\r\n    else { S.tick[id] = S.lines.length; S.lines.push(lineFromProduct(p, qty)); }\r\n    runProductSearch(); refreshSide(); bindSide();\r\n  }\r\n\r\n  \/\/ ---- full product view (small bottom sheet) ----\r\n  S.cache = {};\r\n  async function openProduct(id){\r\n    const bg = $('sheetBg'), sh = $('sheet');\r\n    sh.innerHTML = '<div class=\"loading\" style=\"padding:30px 16px;\">Loading\u2026<\/div>'; bg.classList.add('open');\r\n    const { data: p } = await sb.from('products').select(PCOLS + ',images,description,seller_name').eq('id', id).maybeSingle();\r\n    if (!p) { bg.classList.remove('open'); return; }\r\n    S.cache[id] = p;\r\n    let sibs = [p];\r\n    if (p.product_family) {\r\n      const { data } = await sb.from('products').select(PCOLS).eq('product_family', p.product_family).eq('is_published', true).eq('is_archived', false);\r\n      if (data && data.length) { sibs = data; data.forEach(x => S.cache[x.id] = x); }\r\n    }\r\n    const imgs = (Array.isArray(p.images) ? p.images.map(imgUrl) : []).filter(Boolean);\r\n    if (p.image_url && !imgs.includes(p.image_url)) imgs.unshift(p.image_url);\r\n    const sel = { id: p.id, img: 0, qty: S.tick[id] != null ? Number(S.lines[S.tick[id]].qty) || 1 : 1 };\r\n    const groups = () => {\r\n      if (sibs.length < 2) return [];\r\n      const g = [['Brand', x => x.brand], ['Grade', x => x.variant && x.variant.grade], ['Size', x => x.variant && x.variant.size_kg ? x.variant.size_kg + ' kg' : '']];\r\n      return g.map(([label, f]) => [label, [...new Set(sibs.map(f).filter(Boolean))], f]).filter(x => x[1].length > 1);\r\n    };\r\n    const draw = () => {\r\n      const cur = sibs.find(x => x.id === sel.id) || p;\r\n      const gs = groups();\r\n      sh.innerHTML = `<div class=\"sh-top\"><div class=\"sh-im\">${imgs[sel.img] ? `<img decoding=\"async\" src=\"${esc(imgs[sel.img])}\" alt=\"\" onerror=\"this.remove()\">` : '\u25a3'}<\/div>\r\n        <div style=\"min-width:0;padding-right:34px;\"><h2 style=\"font-size:17px;font-weight:800;line-height:1.2;\">${esc(cur.name)}<\/h2>\r\n        <div style=\"font-size:13px;color:var(--muted);margin:4px 0;\">${esc([cur.brand, p.seller_name].filter(Boolean).join(' \u00b7 '))}<\/div>\r\n        ${cur.price ? `<div style=\"font-size:18px;font-weight:800;\">${money(cur.price, cur.currency)} <small style=\"font-size:12px;font-weight:600;color:var(--muted);\">indicative, supplier confirms<\/small><\/div>` : ''}<\/div>\r\n        <button class=\"sh-close\" id=\"shx\" aria-label=\"Close\">\u00d7<\/button><\/div>\r\n        ${imgs.length > 1 ? `<div class=\"sh-thumbs\">${imgs.slice(0, 8).map((u, i) => `<button class=\"t ${i === sel.img ? 'on' : ''}\" data-th=\"${i}\"><img decoding=\"async\" src=\"${esc(u)}\" alt=\"\" onerror=\"this.remove()\"><\/button>`).join('')}<\/div>` : ''}\r\n        <div class=\"sh-body\">\r\n          ${gs.map(([label, vals, f]) => `<div class=\"optlab\">${label}<\/div><div class=\"optpills\">${vals.map(v => `<button class=\"optpill ${f(cur) === v ? 'on' : ''}\" data-og=\"${label}\" data-ov=\"${esc(v)}\">${esc(v)}<\/button>`).join('')}<\/div>`).join('')}\r\n          ${p.description ? `<div class=\"sh-desc\">${esc(p.description.length > 360 ? p.description.slice(0, 360) + '\u2026' : p.description)}<\/div>` : ''}\r\n          <div class=\"sh-row\"><div class=\"stepper\"><button id=\"shm\" aria-label=\"Less\">\u2212<\/button><input id=\"shq\" value=\"${sel.qty}\" inputmode=\"numeric\" aria-label=\"Quantity\"><button id=\"shp\" aria-label=\"More\">+<\/button><\/div>\r\n          <button class=\"btn\" id=\"shadd\">${S.tick[cur.id] != null ? 'Update quote list' : 'Add to quote list'}<\/button><\/div>\r\n        <\/div>`;\r\n      $('shx').onclick = closeSheet;\r\n      sh.querySelectorAll('[data-th]').forEach(b => b.onclick = () => { sel.img = Number(b.dataset.th); draw(); });\r\n      sh.querySelectorAll('[data-og]').forEach(b => b.onclick = () => {\r\n        const want = {}; gs.forEach(([label, vals, f]) => { want[label] = label === b.dataset.og ? b.dataset.ov : f(cur); });\r\n        const match = sibs.find(x => gs.every(([label, vals, f]) => !want[label] || f(x) === want[label])) || sibs.find(x => gs.find(g => g[0] === b.dataset.og)[2](x) === b.dataset.ov);\r\n        if (match) { sel.id = match.id; } draw();\r\n      });\r\n      const q = () => Math.max(1, Math.floor(Number($('shq').value) || 1));\r\n      $('shm').onclick = () => { sel.qty = Math.max(1, q() - 1); $('shq').value = sel.qty; };\r\n      $('shp').onclick = () => { sel.qty = q() + 1; $('shq').value = sel.qty; };\r\n      $('shq').oninput = () => { sel.qty = q(); };\r\n      $('shadd').onclick = () => {\r\n        const prod = S.cache[sel.id] || cur;\r\n        if (S.tick[prod.id] != null) S.lines[S.tick[prod.id]].qty = String(q());\r\n        else { S.tick[prod.id] = S.lines.length; S.lines.push(lineFromProduct(prod, q())); }\r\n        closeSheet(); runProductSearch(); refreshSide(); bindSide();\r\n      };\r\n    };\r\n    draw();\r\n  }\r\n  function closeSheet(){ $('sheetBg').classList.remove('open'); }\r\n  $('sheetBg').onclick = (e) => { if (e.target.id === 'sheetBg') closeSheet(); };\r\n\r\n  function addRequested(){\r\n    const root = $('rq1').value, sub = $('rq2').value, name = $('rqn').value.trim(), qty = $('rqq').value, err = $('rqerr');\r\n    if (!root) { err.textContent = 'Choose the category this item belongs to.'; return; }\r\n    if (!name) { err.textContent = 'Type the exact product name.'; return; }\r\n    if (!(Number(qty) > 0)) { err.textContent = 'Enter a quantity.'; return; }\r\n    err.textContent = '';\r\n    addLine({ kind:'product', product_id:null, category_id: sub || root, name, sub: S.catById[sub || root].name, qty, unit:'Units', match_status:'requested' });\r\n    $('rqn').value = ''; $('rqq').value = 1; refreshSide(); bindSide();\r\n    $('sideLines').scrollIntoView({ block:'nearest' });\r\n  }\r\n\r\n  \/\/ ===== SERVICES =====\r\n  const SETTING = { residential:'Residential', commercial:'Commercial', industrial:'Industrial' };\r\n  const SVCPAGE = 48;\r\n  function viewServices(){\r\n    const t = S.q.toLowerCase();\r\n    const settings = [...new Set(S.svc.flatMap(s => s.service_types || []))].sort((a, b) => Object.keys(SETTING).indexOf(a) - Object.keys(SETTING).indexOf(b));\r\n    const inSetting = (s) => !S.svcSetting || (s.service_types || []).includes(S.svcSetting);\r\n    const inSearch = (s) => !t || (s.name + ' ' + s.cat).toLowerCase().includes(t);\r\n    const catCount = (id) => S.svc.filter(s => s.category_id === id && inSetting(s) && inSearch(s)).length;\r\n    const list = S.svc.filter(s => (!S.svcCat || s.category_id === S.svcCat) && inSetting(s) && inSearch(s));\r\n    const picked = new Set(S.lines.filter(l => l.service_id).map(l => l.service_id));\r\n    const page = list.slice(0, S.svcShown);\r\n    const curCat = S.svcCats.find(c => c.id === S.svcCat);\r\n    $('view').innerHTML = wrap(`\r\n      <h2>Which services do you need?<\/h2>\r\n      <p class=\"sub\">Tap + on every service you want quoted. Businesses that offer them will respond.<\/p>\r\n      <div class=\"searchbar\"><input class=\"in\" id=\"sq\" type=\"search\" placeholder=\"Search services\" value=\"${esc(S.q)}\" autocomplete=\"off\"><\/div>\r\n      ${settings.length > 1 ? `<div class=\"chips wrap\"><button class=\"chip ${!S.svcSetting ? 'on' : ''}\" data-ss=\"\">All types<\/button>${settings.map(k => `<button class=\"chip ${S.svcSetting === k ? 'on' : ''}\" data-ss=\"${k}\">${esc(SETTING[k] || k.replace(\/^\\w\/, c => c.toUpperCase()))}<\/button>`).join('')}<\/div>` : ''}\r\n      <div class=\"chips wrap\"><button class=\"chip sub ${S.svcCat === '' ? 'on' : ''}\" data-sc=\"\">All categories<\/button>${S.svcCats.map(c => `<button class=\"chip sub ${S.svcCat === c.id ? 'on' : ''}\" data-sc=\"${c.id}\">${esc(c.name)}<span class=\"ccnt\">${catCount(c.id)}<\/span><\/button>`).join('')}<\/div>\r\n      <div class=\"toolbar\"><span class=\"cnt\">${list.length} service${list.length === 1 ? '' : 's'}${curCat ? ' in ' + esc(curCat.name) : ''}<\/span><div class=\"seg\" id=\"seg\"><button data-cols=\"2\" class=\"${S.cols === 2 ? 'on' : ''}\">2 \u00d7 2<\/button><button data-cols=\"3\" class=\"${S.cols === 3 ? 'on' : ''}\">3 \u00d7 3<\/button><\/div><\/div>\r\n      ${list.length ? `<div class=\"pgrid ${S.cols === 3 ? 'c3' : ''}\" style=\"--cols:${S.cols}\">${page.map(s => { const on = picked.has(s.id); return `<div class=\"pc ${on ? 'on' : ''}\" data-id=\"${s.id}\" role=\"button\" tabindex=\"0\"><div class=\"im\">${esc((s.cat || '?').charAt(0))}<\/div><button class=\"add\" data-add=\"${s.id}\" aria-label=\"${on ? 'Remove' : 'Add'} ${esc(s.name)}\">${on ? '\u2713' : '+'}<\/button><div class=\"nm\">${esc(s.name)}<\/div><div class=\"mt\">${esc(s.cat)}<\/div><\/div>`; }).join('')}<\/div>${list.length > page.length ? `<div style=\"text-align:center;margin-top:12px;\"><button class=\"btn ghost\" id=\"svmore\">Show more (${list.length - page.length})<\/button><\/div>` : ''}`\r\n        : `<div class=\"notice\"><b>${curCat ? 'No ' + esc(curCat.name) + ' services listed' : 'No service matches'}${S.q ? ' for \u201c' + esc(S.q) + '\u201d' : ''}.<\/b> We can still find a business for you. Request it below.<\/div>`}\r\n      <div class=\"more\" id=\"svreq\">\r\n        <h4>Can't find your service?<\/h4>\r\n        <p class=\"sub\" style=\"margin-bottom:10px;\">Pick the category and describe the service. It goes to businesses in that category.<\/p>\r\n        <div class=\"grid2\">\r\n          <div class=\"field\"><label class=\"lab\">Category <span class=\"req\">*<\/span><\/label><select class=\"sel\" id=\"srcat\"><option value=\"\">Select a category\u2026<\/option>${S.svcCatsAll.map(c => `<option value=\"${c.id}\" ${S.svcCat === c.id ? 'selected' : ''}>${esc(c.name)}<\/option>`).join('')}<\/select><\/div>\r\n          <div class=\"field\"><label class=\"lab\">What do you need? <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"srnm\" placeholder=\"e.g. replace 40 m of roof sheeting\"><\/div>\r\n        <\/div>\r\n        <div class=\"err\" id=\"srerr\"><\/div>\r\n        <button class=\"btn ghost\" id=\"sradd\">Add requested service<\/button>\r\n      <\/div>\r\n      <div class=\"actions\"><span><\/span><button class=\"btn\" id=\"contBtn\">Continue<\/button><\/div>\r\n      <div class=\"err\" id=\"flowErr\"><\/div>`);\r\n    bindBack(); bindSide();\r\n    $('sq').oninput = debounce(() => { S.q = $('sq').value; S.svcShown = SVCPAGE; viewServices(); const e = $('sq'); e.focus(); e.setSelectionRange(e.value.length, e.value.length); }, 250);\r\n    $('view').querySelectorAll('[data-sc]').forEach(b => b.onclick = () => { S.svcCat = b.dataset.sc; S.svcShown = SVCPAGE; viewServices(); });\r\n    $('view').querySelectorAll('[data-ss]').forEach(b => b.onclick = () => { S.svcSetting = b.dataset.ss; S.svcShown = SVCPAGE; viewServices(); });\r\n    $('view').querySelectorAll('[data-cols]').forEach(b => b.onclick = () => { S.cols = Number(b.dataset.cols); viewServices(); });\r\n    if ($('svmore')) $('svmore').onclick = () => { S.svcShown += SVCPAGE; viewServices(); };\r\n    const tog = (id) => {\r\n      const s = S.svc.find(x => x.id === id); if (!s) return;\r\n      const i = S.lines.findIndex(l => l.service_id === id);\r\n      if (i >= 0) S.lines.splice(i, 1);\r\n      else S.lines.push({ kind:'service', service_id:id, service_category_id:s.category_id, name:s.name, sub:s.cat, cat:s.cat, qty:1, noqty:true, match_status:'catalogue' });\r\n      viewServices();\r\n    };\r\n    $('view').querySelectorAll('.pc').forEach(c => c.onclick = () => tog(c.dataset.id));\r\n    $('view').querySelectorAll('[data-add]').forEach(b => b.onclick = (e) => { e.stopPropagation(); tog(b.dataset.add); });\r\n    $('sradd').onclick = () => {\r\n      const cid = $('srcat').value, nm = $('srnm').value.trim();\r\n      if (!cid) { $('srerr').textContent = 'Choose a category.'; return; }\r\n      if (!nm) { $('srerr').textContent = 'Describe the service you need.'; return; }\r\n      const c = S.svcCatsAll.find(x => x.id === cid);\r\n      S.lines.push({ kind:'service', service_id:null, service_category_id:cid, name:nm, sub:c.name, cat:c.name, qty:1, noqty:true, match_status:'requested' });\r\n      viewServices();\r\n    };\r\n    $('contBtn').onclick = goDetails;\r\n  }\r\n\r\n  \/\/ ===== VEHICLES =====\r\n  const VPAGE = 24;\r\n  const FUEL = { diesel:'Diesel', petrol:'Petrol', hybrid:'Hybrid', electric:'Electric' };\r\n  const TRANS = { automatic:'Automatic', manual:'Manual', cvt:'CVT', fat:'Automatic (FAT)' };\r\n  const STEER = { lhd:'Left-hand drive', rhd:'Right-hand drive' };\r\n  const lab = (map, v) => map[v] || (v ? String(v).replace(\/^\\w\/, c => c.toUpperCase()) : '');\r\n  const kmFmt = (n) => n == null || n === '' ? '' : (Number(n) >= 1000 ? Math.round(Number(n) \/ 1000).toLocaleString('en-GB') + 'k km' : Number(n) + ' km');\r\n  const vFilterKeys = ['make','model','year','fuel','trans','steer','country'];\r\n  const vMatch = (l, f, skip) => vFilterKeys.every(k => k === skip || !f[k] || String(l[k === 'trans' ? 'transmission' : k === 'fuel' ? 'fuel_type' : k === 'steer' ? 'steering' : k === 'country' ? 'location_country' : k === 'make' ? '_make' : k === 'model' ? 'vehicle_model_id' : 'year']) === String(f[k]));\r\n  function vTitle(l){ return `${l.year || ''} ${l._makeName} ${l._modelName}`.trim(); }\r\n  function estimate(modelId, year){\r\n    let ls = S.veh.hist.filter(l => l.vehicle_model_id === modelId);\r\n    let scoped = year ? ls.filter(l => l.year === Number(year)) : ls;\r\n    let note = '';\r\n    if (scoped.length) ls = scoped; else if (year) note = 'No listings for that year, so this shows all years.';\r\n    const vals = ls.map(l => ({ p: Number(l.fob_price || l.price), c: l.fob_price ? (l.fob_currency || l.currency) : l.currency })).filter(v => v.p > 0);\r\n    if (!vals.length) return null;\r\n    const cur = vals[0].c; const same = vals.filter(v => v.c === cur).map(v => v.p);\r\n    return { lo: Math.min(...same), hi: Math.max(...same), n: same.length, cur, note };\r\n  }\r\n  function vehTicked(){ const t = {}; S.lines.forEach((l, i) => { if (l.listing_id) t[l.listing_id] = i; }); return t; }\r\n  function vehCard(l, t){\r\n    const on = t[l.id] != null; const img = imgUrl(Array.isArray(l.images) ? l.images[0] : '');\r\n    return `<div class=\"pc ${on ? 'on' : ''}\" data-id=\"${l.id}\" role=\"button\" tabindex=\"0\">\r\n      <div class=\"im\">${img ? `<img decoding=\"async\" src=\"${esc(img)}\" alt=\"\" loading=\"lazy\" onerror=\"this.remove()\">` : '\u25a3'}<\/div>\r\n      <button class=\"add\" data-add=\"${l.id}\" aria-label=\"${on ? 'Remove from' : 'Add to'} quote list\">${on ? '\u2713' : '+'}<\/button>\r\n      <div class=\"nm\">${esc(vTitle(l))}<\/div>\r\n      <div class=\"mt\">${l.price ? `<b>${money(l.display_price || l.price, l.currency)}<\/b>` : ''}${l.mileage_km != null && l.mileage_km !== '' ? ' \u00b7 ' + kmFmt(l.mileage_km) : ''}${l.location_country ? ' \u00b7 ' + esc(l.location_country) : ''}<\/div><\/div>`;\r\n  }\r\n  function toggleVehicle(id){\r\n    const l = S.veh.list.find(x => x.id === id); if (!l) return;\r\n    const t = vehTicked();\r\n    if (t[id] != null) S.lines.splice(t[id], 1);\r\n    else S.lines.push({ kind:'vehicle', listing_id:id, vehicle_model_id:l.vehicle_model_id, vehicle_category_id:l._cat || null, name:vTitle(l), make:l._makeName, model:l._modelName,\r\n      sub:[l.price ? money(l.display_price || l.price, l.currency) : '', l.location_country].filter(Boolean).join(' \u00b7 '), yMin:l.year, yMax:l.year,\r\n      spec:'Listing ' + (l.slug || l.id), steering: l.steering === 'lhd' ? 'left' : (l.steering === 'rhd' ? 'right' : ''), qty:'1', unit:'Units', price:l.price, currency:l.currency, match_status:'catalogue' });\r\n    drawVeh(); refreshSide(); bindSide();\r\n  }\r\n  function openVehicle(id){\r\n    const l = S.veh.list.find(x => x.id === id); if (!l) return;\r\n    const bg = $('sheetBg'), sh = $('sheet');\r\n    const imgs = (Array.isArray(l.images) ? l.images.map(imgUrl) : []).filter(Boolean);\r\n    const sel = { img:0 };\r\n    const est = estimate(l.vehicle_model_id, l.year);\r\n    const draw = () => {\r\n      const t = vehTicked(); const on = t[id] != null;\r\n      const spec = [['Year', l.year], ['Mileage', l.mileage_km != null && l.mileage_km !== '' ? Number(l.mileage_km).toLocaleString('en-GB') + ' km' : ''], ['Fuel', lab(FUEL, l.fuel_type)], ['Gearbox', lab(TRANS, l.transmission)], ['Steering', lab(STEER, l.steering)], ['Engine', l.engine_cc ? Number(l.engine_cc).toLocaleString('en-GB') + ' cc' : ''], ['Located in', l.location_country]].filter(r => r[1]);\r\n      sh.innerHTML = `<div class=\"sh-top\"><div class=\"sh-im\">${imgs[sel.img] ? `<img decoding=\"async\" src=\"${esc(imgs[sel.img])}\" alt=\"\" onerror=\"this.remove()\">` : '\u25a3'}<\/div>\r\n        <div style=\"min-width:0;padding-right:34px;\"><h2 style=\"font-size:17px;font-weight:800;line-height:1.2;\">${esc(vTitle(l))}<\/h2>\r\n        ${l.trim ? `<div style=\"font-size:13px;color:var(--muted);margin:4px 0;\">${esc(l.trim)}<\/div>` : ''}\r\n        ${l.price ? `<div style=\"font-size:18px;font-weight:800;\">${money(l.display_price || l.price, l.currency)} <small style=\"font-size:12px;font-weight:600;color:var(--muted);\">before shipping, duties and VAT<\/small><\/div>` : ''}<\/div>\r\n        <button class=\"sh-close\" id=\"shx\" aria-label=\"Close\">\u00d7<\/button><\/div>\r\n        ${imgs.length > 1 ? `<div class=\"sh-thumbs\">${imgs.slice(0, 8).map((u, i) => `<button class=\"t ${i === sel.img ? 'on' : ''}\" data-th=\"${i}\"><img decoding=\"async\" src=\"${esc(u)}\" alt=\"\" onerror=\"this.remove()\"><\/button>`).join('')}<\/div>` : ''}\r\n        <div class=\"sh-body\">\r\n          <div class=\"sh-desc\" style=\"display:grid;grid-template-columns:1fr 1fr;gap:6px 14px;\">${spec.map(r => `<div><small style=\"color:var(--muted);display:block;\">${r[0]}<\/small><b>${esc(r[1])}<\/b><\/div>`).join('')}<\/div>\r\n          ${est ? `<div class=\"estimate\" style=\"margin-top:10px;\"><b>Similar ${esc(l.year)} ${esc(l._modelName)}: ${money(est.lo, est.cur)} to ${money(est.hi, est.cur)}<\/b><small>Based on ${est.n} listed. Indicative.<\/small><\/div>` : ''}\r\n          <div class=\"sh-row\"><span><\/span><button class=\"btn\" id=\"shadd\">${on ? 'Remove from quote list' : 'Add to quote list'}<\/button><\/div>\r\n        <\/div>`;\r\n      $('shx').onclick = closeSheet;\r\n      sh.querySelectorAll('[data-th]').forEach(b => b.onclick = () => { sel.img = Number(b.dataset.th); draw(); });\r\n      $('shadd').onclick = () => { toggleVehicle(id); closeSheet(); };\r\n    };\r\n    draw(); bg.classList.add('open');\r\n  }\r\n  function viewVehicles(){\r\n    const V = S.veh;\r\n    $('view').innerHTML = wrap(`\r\n      <h2>Which vehicle are you looking for?<\/h2>\r\n      <p class=\"sub\">These are the vehicles Builders24 Auto has available now. Tap + to add one to your quote, or tap a vehicle to see it in full. If you want something similar that isn't listed, use the request box below.<\/p>\r\n      ${V.list.length ? '<div id=\"vfil\"><\/div><div class=\"toolbar\"><span class=\"cnt\" id=\"vcnt\"><\/span><div class=\"seg\" id=\"seg\"><button data-cols=\"2\">2 \u00d7 2<\/button><button data-cols=\"3\">3 \u00d7 3<\/button><\/div><\/div><div id=\"vres\"><\/div>' : '<div class=\"notice\">No vehicles are available right now. You can still request one below.<\/div>'}\r\n      <div class=\"more\" id=\"vreq\"><\/div>\r\n      <div class=\"actions\"><span><\/span><button class=\"btn\" id=\"contBtn\">Continue<\/button><\/div>\r\n      <div class=\"err\" id=\"flowErr\"><\/div>`);\r\n    bindBack(); bindSide();\r\n    $('contBtn').onclick = goDetails;\r\n    drawVeh();\r\n  }\r\n  function vdesc(id){\r\n    const V = S.veh; const out = new Set([id]); const walk = (p) => (V.kids[p] || []).forEach(k => { out.add(k.id); walk(k.id); });\r\n    walk(id); return out;\r\n  }\r\n  const vNode = (f) => f.leaf || f.sub || f.grp || '';\r\n  const vLeaves = () => S.veh.cats.filter(c => !(S.veh.kids[c.id] || []).length && c.parent_id);\r\n  const vPath = (c) => { const V = S.veh; const parts = []; let x = c; while (x) { parts.unshift(x.name); x = V.byId[x.parent_id]; } return parts; };\r\n  function vStock(id){ const ids = vdesc(id); return S.veh.list.filter(l => ids.has(l._cat)).length; }\r\n  function drawVeh(){\r\n    const V = S.veh; const f = V.f; const d = V.draft;\r\n    if (!$('vreq')) return;\r\n    const node = vNode(f);\r\n    const ids = node ? vdesc(node) : null;\r\n    const inCat = (l) => !ids || ids.has(l._cat);\r\n    const inQ = (l) => !f.q || (vTitle(l) + ' ' + (l.trim || '')).toLowerCase().includes(f.q.toLowerCase());\r\n    const pool = (skip) => V.list.filter(l => inCat(l) && inQ(l) && vMatch(l, f, skip));\r\n    if ($('vfil')) {\r\n      const opts = (key, getter, fmt, sortFn) => {\r\n        const vals = [...new Set(pool(key).map(getter).filter(v => v != null && v !== ''))];\r\n        vals.sort(sortFn || ((a, b) => String(fmt(a)).localeCompare(String(fmt(b)))));\r\n        return vals.map(v => `<option value=\"${esc(v)}\" ${String(f[key]) === String(v) ? 'selected' : ''}>${esc(fmt(v))}<\/option>`).join('');\r\n      };\r\n      const makeVals = [...new Map(pool('make').map(l => [l._make, l._makeName])).entries()].sort((a, b) => a[1].localeCompare(b[1]));\r\n      const modelName = {}; V.list.forEach(l => modelName[l.vehicle_model_id] = l._modelName);\r\n      const sel = (id, label, body, any) => `<div class=\"field\"><label class=\"lab\">${label}<\/label><select class=\"sel\" id=\"${id}\"><option value=\"\">${any}<\/option>${body}<\/select><\/div>`;\r\n      const roots = V.kids[''] || [];\r\n      const subs = f.grp ? (V.kids[f.grp] || []) : [];\r\n      const leaves = f.sub ? (V.kids[f.sub] || []) : [];\r\n      const cnt = (id) => `<span class=\"ccnt\">${vStock(id)}<\/span>`;\r\n      $('vfil').innerHTML = `<div class=\"searchbar\"><input class=\"in\" id=\"vq\" type=\"search\" placeholder=\"Search make, model or name\" value=\"${esc(f.q)}\" autocomplete=\"off\"><\/div>\r\n        <div class=\"chips wrap\"><button class=\"chip ${!f.grp ? 'on' : ''}\" data-vg=\"\">All<\/button>${roots.map(c => `<button class=\"chip ${f.grp === c.id ? 'on' : ''}\" data-vg=\"${c.id}\">${esc(c.name)}${cnt(c.id)}<\/button>`).join('')}<\/div>\r\n        ${subs.length ? `<div class=\"chips wrap\">${subs.map(c => `<button class=\"chip sub ${f.sub === c.id ? 'on' : ''}\" data-vs=\"${c.id}\">${esc(c.name)}${cnt(c.id)}<\/button>`).join('')}<\/div>` : ''}\r\n        ${leaves.length ? `<div class=\"field\" style=\"max-width:320px;\"><label class=\"lab\">${esc((V.byId[f.sub] || {}).name || '')}<\/label><select class=\"sel\" id=\"fl\"><option value=\"\">All ${esc(((V.byId[f.sub] || {}).name || '').toLowerCase())}<\/option>${leaves.map(c => `<option value=\"${c.id}\" ${f.leaf === c.id ? 'selected' : ''}>${esc(c.name)} (${vStock(c.id)})<\/option>`).join('')}<\/select><\/div>` : ''}\r\n        <div class=\"vgrid\">\r\n          ${sel('fk', 'Make', makeVals.map(([id, n]) => `<option value=\"${id}\" ${f.make === id ? 'selected' : ''}>${esc(n)}<\/option>`).join(''), 'All makes')}\r\n          ${sel('fm', 'Model', opts('model', l => l.vehicle_model_id, id => modelName[id]), 'All models')}\r\n          ${sel('fy', 'Year', opts('year', l => l.year, y => y, (a, b) => b - a), 'Any year')}\r\n          ${sel('ff', 'Fuel', opts('fuel', l => l.fuel_type, v => lab(FUEL, v)), 'Any fuel')}\r\n          ${sel('ft', 'Gearbox', opts('trans', l => l.transmission, v => lab(TRANS, v)), 'Any gearbox')}\r\n          ${sel('fs', 'Steering', opts('steer', l => l.steering, v => lab(STEER, v)), 'Any steering')}\r\n          ${sel('fc', 'Located in', opts('country', l => l.location_country, v => v), 'Any country')}\r\n        <\/div>`;\r\n      const bind = (id, key) => { $(id).onchange = () => { f[key] = $(id).value; if (key === 'make') f.model = ''; V.shown = VPAGE; drawVeh(); }; };\r\n      bind('fk', 'make'); bind('fm', 'model'); bind('fy', 'year'); bind('ff', 'fuel'); bind('ft', 'trans'); bind('fs', 'steer'); bind('fc', 'country');\r\n      $('vq').oninput = debounce(() => { f.q = $('vq').value; V.shown = VPAGE; drawVeh(); const e = $('vq'); if (e) { e.focus(); e.setSelectionRange(e.value.length, e.value.length); } }, 250);\r\n      $('vfil').querySelectorAll('[data-vg]').forEach(b => b.onclick = () => { f.grp = b.dataset.vg; f.sub = f.leaf = ''; V.shown = VPAGE; drawVeh(); });\r\n      $('vfil').querySelectorAll('[data-vs]').forEach(b => b.onclick = () => { f.sub = f.sub === b.dataset.vs ? '' : b.dataset.vs; f.leaf = ''; V.shown = VPAGE; drawVeh(); });\r\n      if ($('fl')) $('fl').onchange = () => { f.leaf = $('fl').value; V.shown = VPAGE; drawVeh(); };\r\n      $('seg').querySelectorAll('button').forEach(b => { b.classList.toggle('on', Number(b.dataset.cols) === S.cols); b.onclick = () => { S.cols = Number(b.dataset.cols); drawVeh(); }; });\r\n      const res = pool().sort((a, b) => (b.year || 0) - (a.year || 0) || Number(a.price) - Number(b.price));\r\n      const nodeName = node && V.byId[node] ? V.byId[node].name : '';\r\n      $('vcnt').textContent = res.length + ' vehicle' + (res.length === 1 ? '' : 's') + ' available' + (nodeName ? ' in ' + nodeName : '');\r\n      const t = vehTicked(); const page = res.slice(0, V.shown);\r\n      let empty = '';\r\n      if (!res.length) {\r\n        const anyFilter = f.q || f.make || f.model || f.year || f.fuel || f.trans || f.steer || f.country;\r\n        if (node && !vStock(node)) empty = `<div class=\"notice\"><b>No ${esc(nodeName)} in stock right now.<\/b> We can still source one for you.<div style=\"margin-top:10px;\"><button class=\"btn ghost\" id=\"vreqgo\">Request ${esc(nodeName)}<\/button><\/div><\/div>`;\r\n        else if (anyFilter) empty = '<div class=\"notice\">No available vehicle matches those filters.<div style=\"margin-top:10px;\"><button class=\"btn ghost\" id=\"vclear\">Clear filters<\/button><\/div><\/div>';\r\n        else empty = '<div class=\"notice\">No vehicles in stock right now. You can still request one below.<\/div>';\r\n      }\r\n      $('vres').innerHTML = res.length\r\n        ? `<div class=\"pgrid ${S.cols === 3 ? 'c3' : ''}\" style=\"--cols:${S.cols}\">${page.map(l => vehCard(l, t)).join('')}<\/div>${res.length > page.length ? `<div style=\"text-align:center;margin-top:12px;\"><button class=\"btn ghost\" id=\"vmore\">Show more (${res.length - page.length})<\/button><\/div>` : ''}`\r\n        : empty;\r\n      $('vres').querySelectorAll('[data-add]').forEach(b => b.onclick = (e) => { e.stopPropagation(); toggleVehicle(b.dataset.add); });\r\n      $('vres').querySelectorAll('.pc').forEach(c => c.onclick = () => openVehicle(c.dataset.id));\r\n      if ($('vmore')) $('vmore').onclick = () => { V.shown += VPAGE; drawVeh(); };\r\n      if ($('vclear')) $('vclear').onclick = () => { Object.keys(f).forEach(k => { if (!['grp', 'sub', 'leaf'].includes(k)) f[k] = ''; }); drawVeh(); };\r\n      if ($('vreqgo')) $('vreqgo').onclick = () => { d.node = node; d.cat = vLeaves().some(c => c.id === node) ? node : ''; d.make = d.model = d.year = ''; drawVeh(); $('vreq').scrollIntoView({ behavior:'smooth', block:'start' }); };\r\n    }\r\n    \/\/ ---- request a vehicle (not listed \/ not exact \/ category empty) ----\r\n    const leaves = vLeaves().filter(c => !d.node || vdesc(d.node).has(c.id));\r\n    const groups = {}; leaves.forEach(c => { const p = vPath(c); const g = p.slice(0, -1).join(' \u203a '); (groups[g] = groups[g] || []).push(c); });\r\n    const cmodels = d.cat ? V.models.filter(m => m.category_id === d.cat) : [];\r\n    const mkIds = [...new Set(cmodels.map(m => m.make_id))];\r\n    const mks = V.makes.filter(m => mkIds.includes(m.id));\r\n    const models = cmodels.filter(m => m.make_id === d.make).sort((a, b) => a.name.localeCompare(b.name));\r\n    const years = [...new Set(V.hist.filter(l => d.model && l.vehicle_model_id === d.model).map(l => l.year).filter(Boolean))].sort((a, b) => b - a);\r\n    const est = d.model ? estimate(d.model, d.year) : null;\r\n    $('vreq').innerHTML = `<h4>Can't find it? Request a vehicle<\/h4>\r\n      <p class=\"sub\" style=\"margin-bottom:10px;\">Choose the category, then the closest make and model if we list them. Tell us the details and we'll still quote.<\/p>\r\n      <div class=\"field\"><label class=\"lab\">Category <span class=\"req\">*<\/span><\/label><select class=\"sel\" id=\"vcat\"><option value=\"\">Select a category\u2026<\/option>${Object.keys(groups).map(g => `<optgroup label=\"${esc(g)}\">${groups[g].map(c => `<option value=\"${c.id}\" ${d.cat === c.id ? 'selected' : ''}>${esc(c.name)}<\/option>`).join('')}<\/optgroup>`).join('')}<\/select><\/div>\r\n      <div class=\"vgrid\">\r\n        <div class=\"field\"><label class=\"lab\">Make<\/label><select class=\"sel\" id=\"vmk\" ${mks.length ? '' : 'disabled'}><option value=\"\">${d.cat && !mks.length ? 'None listed' : 'Select make\u2026'}<\/option>${mks.map(m => `<option value=\"${m.id}\" ${d.make === m.id ? 'selected' : ''}>${esc(m.name)}<\/option>`).join('')}<\/select><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Model<\/label><select class=\"sel\" id=\"vmd\" ${d.make ? '' : 'disabled'}><option value=\"\">Select model\u2026<\/option>${models.map(m => `<option value=\"${m.id}\" ${d.model === m.id ? 'selected' : ''}>${esc(m.name)}<\/option>`).join('')}<\/select><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Year<\/label><select class=\"sel\" id=\"vyr\" ${years.length ? '' : 'disabled'}><option value=\"\">Any year<\/option>${years.map(y => `<option value=\"${y}\" ${String(d.year) === String(y) ? 'selected' : ''}>${y}<\/option>`).join('')}<\/select><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Steering<\/label><select class=\"sel\" id=\"vst\"><option value=\"\">No preference<\/option><option value=\"left\" ${d.steering === 'left' ? 'selected' : ''}>Left-hand drive<\/option><option value=\"right\" ${d.steering === 'right' ? 'selected' : ''}>Right-hand drive<\/option><\/select><\/div>\r\n      <\/div>\r\n      ${est ? `<div class=\"estimate\"><b>Around ${money(est.lo, est.cur)} to ${money(est.hi, est.cur)}<\/b><small>Based on ${est.n} vehicle${est.n === 1 ? '' : 's'} we have listed. Indicative, before shipping, duties and VAT.${est.note ? ' ' + esc(est.note) : ''}<\/small><\/div>` : (d.model ? '<div class=\"notice info\">We have no price history for this model yet. You can still request a quote.<\/div>' : '')}\r\n      <div class=\"grid2\">\r\n        <div class=\"field\"><label class=\"lab\">${d.model ? 'Exact trim \/ engine \/ spec' : 'Describe the vehicle'} ${d.model ? '' : '<span class=\"req\">*<\/span>'}<\/label><input class=\"in\" id=\"vsp\" placeholder=\"${d.model ? 'e.g. trim, engine, hybrid, options' : 'e.g. make, model, size, capacity, use'}\" value=\"${esc(d.spec)}\"><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Colour<\/label><input class=\"in\" id=\"vcl\" value=\"${esc(d.colour)}\" placeholder=\"Any\"><\/div>\r\n      <\/div>\r\n      <div class=\"field\" style=\"max-width:160px;\"><label class=\"lab\">Quantity <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"vqt\" type=\"number\" min=\"1\" value=\"${esc(d.qty)}\"><\/div>\r\n      <div class=\"err\" id=\"verr\"><\/div>\r\n      <button class=\"btn ghost\" id=\"vadd\">Add requested vehicle<\/button>`;\r\n    const keep = () => { d.spec = $('vsp').value; d.steering = $('vst').value; d.colour = $('vcl').value; d.qty = $('vqt').value; d.year = $('vyr').value; };\r\n    $('vcat').onchange = () => { keep(); d.cat = $('vcat').value; d.make = d.model = d.year = ''; drawVeh(); };\r\n    $('vmk').onchange = () => { keep(); d.make = $('vmk').value; d.model = d.year = ''; drawVeh(); };\r\n    $('vmd').onchange = () => { keep(); d.model = $('vmd').value; d.year = ''; drawVeh(); };\r\n    $('vyr').onchange = () => { keep(); drawVeh(); };\r\n    $('vadd').onclick = () => {\r\n      keep();\r\n      if (!d.cat) { $('verr').textContent = 'Choose a category.'; return; }\r\n      if (!d.model && !d.spec.trim()) { $('verr').textContent = 'Pick a make and model, or describe the vehicle.'; return; }\r\n      if (!(Number(d.qty) > 0)) { $('verr').textContent = 'Enter a quantity.'; return; }\r\n      const mk = V.makes.find(m => m.id === d.make), md = V.models.find(m => m.id === d.model), cat = V.byId[d.cat];\r\n      S.lines.push({ kind:'vehicle', vehicle_category_id:d.cat, vehicle_model_id:d.model || null, name: md ? `${mk.name} ${md.name}` : `${cat.name}: ${d.spec.trim()}`, make: mk ? mk.name : '', model: md ? md.name : '',\r\n        sub:[cat.name, d.year, md ? d.spec : ''].filter(Boolean).join(' \u00b7 '), yMin:d.year, yMax:d.year, spec:d.spec, steering:d.steering, colour:d.colour, qty:d.qty, unit:'Units', match_status:'requested' });\r\n      V.draft = { node:'', cat:'', make:'', model:'', year:'', qty:1, steering:'', colour:'', spec:'' };\r\n      drawVeh(); refreshSide(); bindSide();\r\n    };\r\n  }\r\n\r\n  \/\/ ===== SHIPPING =====\r\n  const SCOPES = {\r\n    domestic:{ label:'Within a country', hint:'Moving goods inside one African country' },\r\n    cross:{ label:'Across borders in Africa', hint:'From one African country to another' },\r\n    out:{ label:'Out of Africa', hint:'From Africa to the rest of the world' },\r\n    into:{ label:'Into Africa', hint:'From the rest of the world into Africa' },\r\n  };\r\n  const MODE_LABEL = { road:'Road', air:'Air', sea:'Sea' };\r\n  const days = (r) => r.transit_days_min ? r.transit_days_min + (r.transit_days_max && r.transit_days_max !== r.transit_days_min ? '\u2013' + r.transit_days_max : '') + ' days' : '';\r\n  function viewShipping(){\r\n    const F = S.freight; const sh = S.ship;\r\n    const cname = (c) => F.co[c] ? F.co[c].name : c;\r\n    const chip = (attr, val, text, on) => `<button class=\"chip ${on ? 'on' : ''}\" ${attr}=\"${esc(val)}\">${esc(text)}<\/button>`;\r\n    const uniq = (a) => [...new Set(a)];\r\n    const inScope = F.routes.filter(r => r._scope && r._scope === sh.scope);\r\n    const scopes = Object.keys(SCOPES).filter(k => F.routes.some(r => r._scope === k));\r\n    const ocs = uniq(inScope.map(r => r._oc)).sort((a, b) => cname(a).localeCompare(cname(b)));\r\n    const fromO = sh.oc ? inScope.filter(r => r._oc === sh.oc) : [];\r\n    const dcs = uniq(fromO.map(r => r._dc)).sort((a, b) => cname(a).localeCompare(cname(b)));\r\n    const fromOD = sh.dc ? fromO.filter(r => r._dc === sh.dc) : [];\r\n    const modes = uniq(fromOD.map(r => r.mode));\r\n    const byMode = sh.mode ? fromOD.filter(r => r.mode === sh.mode) : [];\r\n    const pn = (id) => F.pl[id] ? `${F.pl[id].name}` : '';\r\n    const kindOf = (id) => F.pl[id] ? F.pl[id].kind : '';\r\n    const placeSort = (a, b) => pn(a).localeCompare(pn(b));\r\n    const opl = uniq(byMode.map(r => r.origin_place_id)).filter(id => F.pl[id]).sort(placeSort);\r\n    const dpl = uniq((sh.origin ? byMode.filter(r => r.origin_place_id === sh.origin) : byMode).map(r => r.dest_place_id)).filter(id => F.pl[id]).sort(placeSort);\r\n    const rts = (sh.origin && sh.dest) ? byMode.filter(r => r.origin_place_id === sh.origin && r.dest_place_id === sh.dest) : [];\r\n    const step = (n, title, body) => `<div class=\"sstep\"><div class=\"slab\"><span class=\"snum\">${n}<\/span>${title}<\/div>${body}<\/div>`;\r\n    const placeChips = (attr, list, cur) => `<div class=\"chips wrap\">${list.map(id => chip(attr, id, `${pn(id)}${kindOf(id) ? ' (' + kindOf(id) + ')' : ''}`, cur === id)).join('')}<\/div>`;\r\n    let html = `<h2>Where are you shipping?<\/h2>\r\n      <p class=\"sub\">We only quote routes we operate. Choose the kind of move first, and we'll show only the countries and modes we cover.<\/p>`;\r\n    html += step(1, 'What kind of move is it?', `<div class=\"scopes\">${scopes.map(k => `<button class=\"scope ${sh.scope === k ? 'on' : ''}\" data-scope=\"${k}\"><b>${SCOPES[k].label}<\/b><small>${SCOPES[k].hint}<\/small><\/button>`).join('')}<\/div>`);\r\n    if (sh.scope) html += step(2, sh.scope === 'domestic' ? 'Which country?' : (sh.scope === 'into' ? 'Shipping from which country?' : 'From which country?'), `<div class=\"chips wrap\">${ocs.map(c => chip('data-oc', c, cname(c), sh.oc === c)).join('')}<\/div>`);\r\n    if (sh.oc && sh.scope !== 'domestic') html += step(3, sh.scope === 'out' ? 'Delivered to which country?' : 'To which country?', dcs.length ? `<div class=\"chips wrap\">${dcs.map(c => chip('data-dc', c, cname(c), sh.dc === c)).join('')}<\/div>` : '<div class=\"notice\">No destinations from here.<\/div>');\r\n    if (sh.dc) html += step(sh.scope === 'domestic' ? 3 : 4, 'How should it travel?', `<div class=\"chips wrap\">${modes.map(m => chip('data-mode', m, MODE_LABEL[m] || m, sh.mode === m)).join('')}<\/div>`);\r\n    if (sh.mode) {\r\n      html += step(sh.scope === 'domestic' ? 4 : 5, `Where from, in ${esc(cname(sh.oc))}?`, placeChips('data-op', opl, sh.origin));\r\n      if (sh.origin) html += step(sh.scope === 'domestic' ? 5 : 6, `Where to, in ${esc(cname(sh.dc))}?`, dpl.length ? placeChips('data-dp', dpl, sh.dest) : '<div class=\"notice\">No destinations from there.<\/div>');\r\n    }\r\n    if (rts.length) html += `<div class=\"estimate\"><b>We cover this route<\/b><small>${esc(pn(sh.origin))} to ${esc(pn(sh.dest))} \u00b7 ${esc(MODE_LABEL[sh.mode] || sh.mode)}${rts.map(days).filter(Boolean).length ? ' \u00b7 ' + uniq(rts.map(days).filter(Boolean)).join(' \/ ') : ''}<\/small><\/div>\r\n      <div class=\"field\"><label class=\"lab\">What are you shipping? <span class=\"req\">*<\/span><\/label><textarea class=\"ta\" id=\"sfc\" placeholder=\"Goods, packaging, quantity\">${esc(sh.cargo)}<\/textarea><\/div>\r\n      <div class=\"field\" style=\"max-width:220px;\"><label class=\"lab\">Approx. total weight (kg)<\/label><input class=\"in\" id=\"sfw\" type=\"number\" min=\"0\" value=\"${esc(sh.weight)}\"><\/div>`;\r\n    html += `<div class=\"actions\"><span><\/span><button class=\"btn\" id=\"contBtn\">Continue<\/button><\/div><div class=\"err\" id=\"flowErr\"><\/div>`;\r\n    $('view').innerHTML = wrap(html, true);\r\n    bindBack();\r\n    $('sideLines').innerHTML = sh.dest ? `<div class=\"line\"><div class=\"line-name\">${esc(pn(sh.origin))}, ${esc(cname(sh.oc))}<small>to ${esc(pn(sh.dest))}, ${esc(cname(sh.dc))} \u00b7 ${esc(MODE_LABEL[sh.mode] || sh.mode)}<\/small><\/div><\/div>` : '<div class=\"empty\">Choose a route.<\/div>';\r\n    const keep = () => { if ($('sfc')) { sh.cargo = $('sfc').value; sh.weight = $('sfw').value; } };\r\n    const on = (attr, fn) => $('view').querySelectorAll(`[${attr}]`).forEach(b => b.onclick = () => { keep(); fn(b.getAttribute(attr)); autoPick(); viewShipping(); });\r\n    \/\/ when only one choice remains at a step, take it so nobody has to tap the obvious\r\n    const autoPick = () => {\r\n      const g = (x) => x;\r\n      if (sh.scope === 'domestic' && sh.oc) sh.dc = sh.oc;\r\n      const f1 = F.routes.filter(r => r._scope === sh.scope && r._oc === sh.oc);\r\n      if (sh.oc && !sh.dc) { const d = uniq(f1.map(r => r._dc)); if (d.length === 1) sh.dc = d[0]; }\r\n      const f2 = f1.filter(r => r._dc === sh.dc);\r\n      if (sh.dc && !sh.mode) { const m = uniq(f2.map(r => r.mode)); if (m.length === 1) sh.mode = m[0]; }\r\n      const f3 = f2.filter(r => r.mode === sh.mode);\r\n      if (sh.mode && !sh.origin) { const o = uniq(f3.map(r => r.origin_place_id)); if (o.length === 1) sh.origin = o[0]; }\r\n      if (sh.origin && !sh.dest) { const d = uniq(f3.filter(r => r.origin_place_id === sh.origin).map(r => r.dest_place_id)); if (d.length === 1) sh.dest = d[0]; }\r\n    };\r\n    on('data-scope', v => { sh.scope = v; sh.oc = sh.dc = sh.mode = sh.origin = sh.dest = ''; });\r\n    on('data-oc', v => { sh.oc = v; sh.dc = sh.mode = sh.origin = sh.dest = ''; });\r\n    on('data-dc', v => { sh.dc = v; sh.mode = sh.origin = sh.dest = ''; });\r\n    on('data-mode', v => { sh.mode = v; sh.origin = sh.dest = ''; });\r\n    on('data-op', v => { sh.origin = v; sh.dest = ''; });\r\n    on('data-dp', v => { sh.dest = v; });\r\n    $('contBtn').onclick = () => { keep(); goDetails(); };\r\n  }\r\n\r\n  \/\/ ===== validation + details =====\r\n  function goDetails(){\r\n    const err = $('flowErr'); const fail = (m) => { if (err) err.textContent = m; };\r\n    if (S.type === 'shipping') {\r\n      if (!(S.ship.origin && S.ship.dest && S.ship.mode)) return fail('Choose a route we cover.');\r\n      if (!S.ship.cargo.trim()) return fail('Tell us what you are shipping.');\r\n    } else {\r\n      if (!S.lines.length) return fail(S.type === 'products' ? 'Tick at least one product, or request a specific item.' : (S.type === 'services' ? 'Tick at least one service.' : 'Add at least one vehicle.'));\r\n      if (S.lines.some(l => !l.noqty && !(Number(l.qty) > 0))) return fail('Enter a quantity for every item in your list.');\r\n    }\r\n    S.view = 'details'; render(); window.scrollTo({ top:0, behavior:'smooth' });\r\n  }\r\n\r\n  function viewDetails(){\r\n    const D = S.details; const goods = S.type === 'products' || S.type === 'vehicles';\r\n    const logged = !!S.session;\r\n    $('view').innerHTML = wrap(`\r\n      <h2>Details<\/h2><p class=\"sub\">A few details so suppliers can respond properly.<\/p>\r\n      <div class=\"grid2\">\r\n        <div class=\"field\"><label class=\"lab\">Needed by <span class=\"req\">*<\/span><\/label><input class=\"in\" type=\"date\" id=\"dnb\" value=\"${esc(D.needBy)}\"><\/div>\r\n        ${goods ? `<div class=\"field\"><label class=\"lab\">Delivery required?<\/label><select class=\"sel\" id=\"ddl\"><option value=\"1\" ${D.delivery ? 'selected' : ''}>Yes<\/option><option value=\"0\" ${!D.delivery ? 'selected' : ''}>No, I'll collect<\/option><\/select><\/div>` : ''}\r\n      <\/div>\r\n      ${S.type !== 'shipping' ? `<div class=\"grid2\">\r\n        <div class=\"field\"><label class=\"lab\">${S.type === 'services' ? 'Job location' : 'Delivery location'} (city) <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"dct\" value=\"${esc(D.city)}\"><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Suburb<\/label><input class=\"in\" id=\"dsb\" value=\"${esc(D.suburb)}\"><\/div><\/div>` : ''}\r\n      ${S.type === 'products' || S.type === 'services' ? `<div class=\"grid2\">\r\n        <div class=\"field\"><label class=\"lab\">Budget min (optional)<\/label><input class=\"in\" type=\"number\" min=\"0\" id=\"dbn\" value=\"${esc(D.budgetMin)}\"><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Budget max (optional)<\/label><input class=\"in\" type=\"number\" min=\"0\" id=\"dbx\" value=\"${esc(D.budgetMax)}\"><\/div><\/div>` : ''}\r\n      <div class=\"field\"><label class=\"lab\">${S.type === 'services' ? 'Describe the work' : 'Anything else suppliers should know'}<\/label><textarea class=\"ta\" id=\"dnt\">${esc(D.notes)}<\/textarea><\/div>\r\n      <h3 style=\"font-size:16px;margin:6px 0 10px;\">Your contact details<\/h3>\r\n      ${logged ? `<div class=\"notice info\">Sending as ${esc((S.profile && (S.profile.full_name || S.profile.email)) || 'your account')}.<\/div>` : `\r\n        <div class=\"grid2\"><div class=\"field\"><label class=\"lab\">Name <span class=\"req\">*<\/span><\/label><input class=\"in\" id=\"dnm\" value=\"${esc(D.name)}\"><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Email <span class=\"req\">*<\/span><\/label><input class=\"in\" type=\"email\" id=\"dem\" value=\"${esc(D.email)}\"><\/div><\/div>\r\n        <div class=\"field\"><label class=\"lab\">Phone<\/label><input class=\"in\" id=\"dph\" value=\"${esc(D.phone)}\"><\/div>`}\r\n      <div class=\"err\" id=\"derr\"><\/div>\r\n      <div class=\"actions\"><button class=\"btn ghost\" id=\"dback\">Back<\/button><button class=\"btn\" id=\"submitBtn\">Send request<\/button><\/div>`);\r\n    bindBack(); bindSide();\r\n    $('dback').onclick = () => { collect(); S.view = 'flow'; render(); };\r\n    $('submitBtn').onclick = submit;\r\n  }\r\n  function collect(){\r\n    const D = S.details; const g = (id) => $(id) ? $(id).value : null;\r\n    if (g('dnb') != null) D.needBy = g('dnb'); if (g('ddl') != null) D.delivery = g('ddl') === '1';\r\n    if (g('dct') != null) D.city = g('dct'); if (g('dsb') != null) D.suburb = g('dsb');\r\n    if (g('dbn') != null) D.budgetMin = g('dbn'); if (g('dbx') != null) D.budgetMax = g('dbx');\r\n    if (g('dnt') != null) D.notes = g('dnt');\r\n    if (g('dnm') != null) D.name = g('dnm'); if (g('dem') != null) D.email = g('dem'); if (g('dph') != null) D.phone = g('dph');\r\n  }\r\n\r\n  \/\/ ===== submit =====\r\n  async function submit(){\r\n    collect(); const D = S.details; const err = $('derr'); err.textContent = '';\r\n    if (!D.needBy) { err.textContent = 'Choose when you need this by.'; return; }\r\n    if (S.type !== 'shipping' && !D.city.trim()) { err.textContent = 'Enter the city.'; return; }\r\n    if (!S.session) {\r\n      if (!D.name.trim()) { err.textContent = 'Enter your name.'; return; }\r\n      if (!D.email.trim() || !D.email.includes('@')) { err.textContent = 'Enter a valid email.'; return; }\r\n    } else if (!S.customerRefId) { err.textContent = 'Could not find your customer profile. Please refresh and try again.'; return; }\r\n\r\n    const btn = $('submitBtn'); btn.disabled = true; btn.textContent = 'Sending\u2026';\r\n    try {\r\n      const T = S.type; const first = S.lines[0];\r\n      let title, category, description, service_id = null, category_details = null, origin_place_id = null, dest_place_id = null;\r\n      if (T === 'shipping') {\r\n        const F = S.freight, o = F.pl[S.ship.origin], d = F.pl[S.ship.dest];\r\n        title = `Shipment: ${o.name} \u2192 ${d.name}`; category = 'Shipping';\r\n        description = `From: ${o.name}, ${o.country}\\nTo: ${d.name}, ${d.country}\\nMode: ${S.ship.mode}\\nCargo: ${S.ship.cargo}${S.ship.weight ? `\\nWeight: ${S.ship.weight} kg` : ''}`;\r\n        category_details = { mode:S.ship.mode, cargo:S.ship.cargo, weight_kg:S.ship.weight || null };\r\n        origin_place_id = S.ship.origin; dest_place_id = S.ship.dest;\r\n      } else {\r\n        title = S.lines.length > 1 ? `${first.name} + ${S.lines.length - 1} more` : first.name;\r\n        category = T === 'vehicles' ? 'Vehicles' : (T === 'services' ? (first.cat || 'Services') : 'Products');\r\n        description = S.lines.map((l, i) => `${i + 1}. ${l.name}${l.noqty ? '' : ` \u2014 Qty: ${l.qty} ${l.unit || ''}`}${l.match_status === 'requested' ? ' (requested item)' : ''}${l.sub ? ` \u2014 ${l.sub}` : ''}`).join('\\n');\r\n        if (T === 'services') service_id = first.service_id;\r\n      }\r\n      if (D.notes.trim()) description += `\\n\\nNotes: ${D.notes.trim()}`;\r\n      const ref = 'Q-' + new Date().getFullYear() + '-' + Math.floor(1000 + Math.random() * 9000);\r\n      const goods = T === 'products' || T === 'vehicles';\r\n      const { data: req, error } = await sb.from('quote_requests').insert({\r\n        quote_type:T, customer_ref_id: S.session ? S.customerRefId : null,\r\n        guest_name: S.session ? null : D.name.trim(), guest_email: S.session ? null : D.email.trim(), guest_phone: S.session ? null : (D.phone.trim() || null),\r\n        title, description, category, service_id, category_details, origin_place_id, dest_place_id,\r\n        quantity: T === 'shipping' ? 1 : S.lines.length, quantity_unit: T === 'shipping' ? 'Request' : 'Various',\r\n        delivery_required: goods ? D.delivery : false, timeline: D.needBy,\r\n        city: D.city.trim() || null, suburb: D.suburb.trim() || null,\r\n        budget_min: (T === 'products' || T === 'services') && D.budgetMin ? D.budgetMin : null,\r\n        budget_max: (T === 'products' || T === 'services') && D.budgetMax ? D.budgetMax : null,\r\n        photos: [], status:'open', reference_number: ref, expires_at: new Date(Date.now() + 14 * 86400000).toISOString(),\r\n      }).select('id').single();\r\n      if (error) throw error;\r\n\r\n      if (T !== 'shipping') {\r\n        const rows = S.lines.map((l, i) => ({\r\n          quote_request_id: req.id, sort_order:i, item_name: l.name, quantity: l.noqty ? 1 : (Number(l.qty) || 1), unit: l.unit || null,\r\n          brand: l.brand || null, product_id: l.product_id || null, category_id: l.category_id || null,\r\n          service_id: l.service_id || null, ...(l.service_category_id ? { service_category_id: l.service_category_id } : {}), vehicle_model_id: l.vehicle_model_id || null, vehicle_category_id: l.vehicle_category_id || null, match_status: l.match_status || 'catalogue',\r\n          vehicle_make: l.make || null, vehicle_model: l.model || null, vehicle_engine: l.spec || null, vehicle_colour: l.colour || null,\r\n          vehicle_steering_side: l.steering || null,\r\n          vehicle_year_min: l.yMin ? Number(l.yMin) : null, vehicle_year_max: l.yMax ? Number(l.yMax) : null,\r\n        }));\r\n        const { error: ie } = await sb.from('quote_request_items').insert(rows);\r\n        if (ie) throw ie;\r\n      }\r\n      S.ref = ref; S.view = 'done'; render(); window.scrollTo({ top:0, behavior:'smooth' });\r\n    } catch (e) {\r\n      console.error(e); $('derr').textContent = e.message || 'Could not send your request. Please try again.';\r\n      const b = $('submitBtn'); if (b) { b.disabled = false; b.textContent = 'Send request'; }\r\n    }\r\n  }\r\n  function viewDone(){\r\n    $('view').innerHTML = `<div class=\"card done-box\"><h2>Request sent<\/h2><div class=\"ref\">${esc(S.ref)}<\/div>\r\n      <p class=\"sub\" style=\"max-width:420px;margin:0 auto 18px;\">Suppliers who can fulfil this have been notified. You'll see quotes in My Quotes as they arrive.<\/p>\r\n      <a class=\"btn\" href=\"https:\/\/app.builders24.net\/retail\/my-quotes.html\" style=\"display:inline-flex;align-items:center;text-decoration:none;\">View my quotes<\/a><\/div>`;\r\n  }\r\n\r\n  \/\/ ---------- boot ----------\r\n  try {\r\n    const { data: { session } } = await sb.auth.getSession();\r\n    S.session = session;\r\n    if (typeof updateCartBadge === 'function') updateCartBadge();\r\n    if (session) {\r\n      const { data: row } = await sb.from('customers').select('id').eq('individual_id', session.user.id).eq('customer_type', 'individual').maybeSingle();\r\n      S.customerRefId = row ? row.id : null;\r\n      if (!S.customerRefId) {\r\n        const { data: created } = await sb.from('customers').insert({ customer_type:'individual', individual_id: session.user.id }).select('id').single();\r\n        S.customerRefId = created ? created.id : null;\r\n      }\r\n      const { data: profile } = await sb.from('app_users').select('full_name,email,phone').eq('id', session.user.id).maybeSingle();\r\n      S.profile = profile || null;\r\n    }\r\n  } catch (e) { console.warn('session:', e.message); }\r\n  try { await loadCounts(); } catch (e) { console.warn(e); }\r\n  render();\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>Request a Quote \u2014 Builders24 \/* Scoped to #b24rq so nothing here can restyle the WordPress site around it. *\/ #b24rq{ &#8211;navy:#14213d; &#8211;orange:#f5901f; &#8211;orange-dark:#d97a0d; &#8211;page:#f4f6f9; &#8211;card:#fff; &#8211;line:#dfe4ea; &#8211;soft:#eef1f5; &#8211;body:#4f5a6b; &#8211;muted:#7c8696; &#8211;ok:#16a34a; &#8211;err:#d93b32; } #b24rq, #b24rq *{ box-sizing:border-box; margin:0; padding:0; } #b24rq{ font-family:&#8217;Archivo Narrow&#8217;,sans-serif; background:var(&#8211;page); color:var(&#8211;navy); -webkit-font-smoothing:antialiased; overflow-x:hidden; } #b24rq button{ font-family:inherit; border:none; cursor:pointer; background:none; color:inherit; } #b24rq input,#b24rq select,#b24rq textarea{ font-family:inherit; color:var(&#8211;navy); } #b24rq :focus-visible{ outline:2px solid var(&#8211;orange); outline-offset:2px; } #b24rq .qhead{ background:#fff; color:var(&#8211;navy); padding:26px 20px 22px; border-bottom:1px solid var(&#8211;line); } #b24rq .qhead-in{ max-width:1080px; margin:0 auto; } #b24rq .qhead h1{ font-size:30px; font-weight:800; line-height:1.1; color:var(&#8211;navy); } #b24rq .qhead p{ color:var(&#8211;body); font-size:15px; margin-top:8px; max-width:560px; line-height:1.45; } #b24rq .wrap{ max-width:1080px; margin:0 auto; padding:20px 20px 120px; } @media(max-width:600px){#b24rq .wrap{ padding:16px 14px 130px; } #b24rq .qhead{ padding:20px 14px 18px; } #b24rq .qhead h1{ font-size:24px; } } #b24rq .prog{ display:flex; gap:6px; margin-bottom:18px; } #b24rq .prog-i{ flex:1; font-size:12.5px; font-weight:700; color:var(&#8211;muted); border-top:3px solid var(&#8211;line); padding-top:7px; } #b24rq .prog-i.on{ color:var(&#8211;navy); border-color:var(&#8211;orange); } #b24rq .prog-i.done{ color:var(&#8211;ok); border-color:var(&#8211;ok); } #b24rq .types{ display:grid; grid-template-columns:repeat(4,1fr); gap:14px; } @media(max-width:900px){#b24rq .types{ grid-template-columns:repeat(2,1fr); } } #b24rq .tcard{ background:var(&#8211;card); border:1.5px solid var(&#8211;line); border-radius:14px; padding:20px 16px 16px; text-align:left; display:flex; flex-direction:column; gap:8px; min-height:190px; transition:transform .15s ease, box-shadow .2s ease, border-color .2s ease; } #b24rq .tcard:hover{ transform:translateY(-3px); box-shadow:0 14px 30px rgba(20,33,61,.12); border-color:var(&#8211;orange); } #b24rq .tcard:active{ transform:scale(.98); } #b24rq .tcard svg{ width:34px; height:34px; stroke:var(&#8211;orange); fill:none; stroke-width:1.8; stroke-linecap:round; stroke-linejoin:round; } #b24rq .tcard h3{ font-size:19px; font-weight:800; } #b24rq .tcard p{ font-size:13.5px; color:var(&#8211;body); line-height:1.4; flex:1; } #b24rq .tcard .cov{ font-size:12.5px; font-weight:700; color:var(&#8211;ok); } #b24rq .layout{ display:grid; grid-template-columns:minmax(0,1fr) 320px; gap:18px; align-items:start; } @media(max-width:900px){#b24rq .layout{ grid-template-columns:1fr; } } #b24rq .card{ background:var(&#8211;card); border:1px solid var(&#8211;line); border-radius:14px; padding:20px; } @media(max-width:600px){#b24rq .card{ padding:14px; } } #b24rq .card h2{ font-size:21px; font-weight:800; margin-bottom:4px; } #b24rq .sub{ color:var(&#8211;body); font-size:14px; line-height:1.45; margin-bottom:16px; } #b24rq .back{ font-size:14px; font-weight:700; color:var(&#8211;body); margin-bottom:12px; display:inline-flex; gap:6px; padding:4px 0; } #b24rq .field{ margin-bottom:14px; min-width:0; } #b24rq .lab{ display:block; font-size:13px; font-weight:700; margin-bottom:6px; } #b24rq .req{ color:var(&#8211;err); } #b24rq .in,#b24rq .sel,#b24rq .ta{ width:100%; background:#fff; border:1px solid var(&#8211;line); border-radius:9px; padding:11px 12px; font-size:14px; } #b24rq .in,#b24rq .sel{ min-height:44px; } #b24rq .ta{ min-height:90px; resize:vertical; } #b24rq .grid2{ display:grid; grid-template-columns:1fr 1fr; gap:12px; } @media(max-width:520px){#b24rq .grid2{ grid-template-columns:1fr; } } #b24rq .searchbar{ display:flex; gap:10px; margin-bottom:12px; } #b24rq .searchbar .in{ flex:1; } #b24rq .chips{ display:flex; gap:8px; overflow-x:auto; padding:2px 0 10px; margin-bottom:6px; scrollbar-width:thin; } #b24rq .chip{ flex:none; min-height:38px; padding:0 14px; border-radius:999px; border:1px solid var(&#8211;line); background:#fff; font-size:13.5px; font-weight:700; white-space:nowrap; } #b24rq .chip.on{ background:var(&#8211;navy); border-color:var(&#8211;navy); color:#fff; } #b24rq .rows{ border:1px solid var(&#8211;line); border-radius:12px; overflow:hidden; } #b24rq .row{ display:flex; align-items:center; gap:12px; padding:12px 14px; border-top:1px solid var(&#8211;soft); background:#fff; } #b24rq .row:first-child{ border-top:none; } #b24rq .row.on{ background:#fff8ee; } #b24rq .tick{ width:24px; height:24px; flex:none; accent-color:var(&#8211;orange); } #b24rq .row-main{ flex:1; min-width:0; cursor:pointer; } #b24rq .row-name{ font-size:15px; font-weight:700; line-height:1.25; overflow-wrap:anywhere; } #b24rq .row-sub{ font-size:12.5px; color:var(&#8211;muted); margin-top:2px; } #b24rq .row-price{ font-size:13px; font-weight:700; color:var(&#8211;navy); white-space:nowrap; } #b24rq .qty{ width:84px; flex:none; min-height:40px; border:1px solid var(&#8211;line); border-radius:8px; padding:0 8px; font-size:14px; } #b24rq .fam{ background:var(&#8211;soft); padding:9px 14px; font-size:12.5px; font-weight:800; letter-spacing:.04em; text-transform:uppercase; color:var(&#8211;body); border-top:1px solid var(&#8211;line); } #b24rq .fam:first-child{ border-top:none; } @media(max-width:520px){#b24rq .row{ flex-wrap:wrap; } #b24rq .row-price{ order:3; } #b24rq .qty{ margin-left:36px; } } #b24rq .notice{ background:#fff8ee; border:1px solid #f3d9b0; border-radius:10px; padding:12px 14px; font-size:13.5px; line-height:1.45; margin:12px 0; } #b24rq .notice.info{ background:#eef4ff; border-color:#cfe0fb; } #b24rq .estimate{ background:#eefaf1; border:1px solid #bfe6c9; border-radius:10px; padding:12px 14px; margin:12px 0; } #b24rq .estimate b{ font-size:18px; } #b24rq .estimate small{ display:block; color:var(&#8211;body); margin-top:3px; font-size:12.5px; } #b24rq .more{ margin-top:14px; border:1px dashed var(&#8211;line); border-radius:12px; padding:14px; } #b24rq .more h4{ font-size:15px; margin-bottom:4px; } #b24rq .btn{ min-height:46px; padding:0 20px; border-radius:10px; font-size:15px; font-weight:800; background:var(&#8211;orange); color:#fff; transition:transform .1s ease, background .15s ease; } #b24rq .btn:hover{ background:var(&#8211;orange-dark); } #b24rq .btn:active{ transform:scale(.97); } #b24rq .btn:disabled{ opacity:.55; cursor:not-allowed; } #b24rq .btn.ghost{ background:#fff; color:var(&#8211;navy); border:1px solid var(&#8211;line); } #b24rq .btn.ghost:hover{ background:var(&#8211;soft); } #b24rq .actions{ display:flex; gap:10px; justify-content:space-between; margin-top:18px; flex-wrap:wrap; } #b24rq .err{ color:var(&#8211;err); font-size:13.5px; margin:10px 0 0; min-height:18px; } #b24rq .side{ position:sticky; top:12px; } #b24rq .side h3{ font-size:16px; font-weight:800; margin-bottom:10px; display:flex; justify-content:space-between; } #b24rq .line{ display:flex; gap:8px; align-items:flex-start; padding:9px 0; border-top:1px solid var(&#8211;soft); } #b24rq .line:first-of-type{ border-top:none; } #b24rq .line-name{ flex:1; font-size:13.5px; font-weight:700; line-height:1.25; min-width:0; overflow-wrap:anywhere; } #b24rq .line-name small{ display:block; font-weight:500; color:var(&#8211;muted); font-size:12px; } #b24rq .tagreq{ display:inline-block; background:#fde7c8; color:#8a4b00; font-size:10.5px; font-weight:800; border-radius:999px; padding:1px 7px; margin-left:4px; text-transform:uppercase; } #b24rq .x{ width:32px; height:32px; flex:none; border-radius:8px; color:var(&#8211;muted); font-size:18px; } #b24rq .x:hover{ background:var(&#8211;soft); color:var(&#8211;err); } #b24rq .empty{ color:var(&#8211;muted); font-size:13.5px; padding:6px 0; } #b24rq .bar{ display:none; } @media(max-width:900px){#b24rq .side{ display:none; position:static; margin-top:14px; } #b24rq .side.open{ display:block; } #b24rq .bar{ display:flex; position:fixed; left:0; right:0; bottom:0; z-index:40; background:var(&#8211;navy); color:#fff; padding:10px 14px calc(10px + env(safe-area-inset-bottom)); align-items:center; justify-content:space-between; gap:10px; } #b24rq .bar .btn{ min-height:42px; } } #b24rq .done-box{ text-align:center; padding:30px 10px; } #b24rq .done-box .ref{ font-size:22px; font-weight:800; color:var(&#8211;orange); margin:8px 0 14px; } #b24rq .loading{ color:var(&#8211;muted); padding:14px 0; font-size:14px; } #b24rq .subpills{ display:flex; gap:8px; overflow-x:auto; padding:2px 0 8px; margin-bottom:4px; scrollbar-width:none; } #b24rq .subpills::-webkit-scrollbar,#b24rq .chips::-webkit-scrollbar{ display:none; } #b24rq .chips{ scrollbar-width:none; } #b24rq .chip.sub{ min-height:36px; font-size:13px; background:var(&#8211;soft); } #b24rq .chip.sub.on{ background:var(&#8211;orange); border-color:var(&#8211;orange); color:#fff; } #b24rq .vgrid{ display:grid; gap:8px 10px; grid-template-columns:repeat(2,minmax(0,1fr)); margin-bottom:4px; } @media(min-width:760px){#b24rq .vgrid{ grid-template-columns:repeat(3,minmax(0,1fr)); } } #b24rq .vgrid .field{ margin-bottom:0; } #b24rq .chips.wrap{ flex-wrap:wrap; overflow:visible; padding-bottom:4px; margin-bottom:10px; } #b24rq .sstep{ margin-bottom:16px; } #b24rq .slab{ font-size:14px; font-weight:800; margin-bottom:8px; display:flex; align-items:center; gap:8px; } #b24rq .snum{ width:22px; height:22px; border-radius:50%; background:var(&#8211;navy); color:#fff; font-size:12px; display:inline-flex; align-items:center; justify-content:center; flex:none; } #b24rq .scopes{ display:grid; gap:8px; grid-template-columns:repeat(2,minmax(0,1fr)); } @media(min-width:760px){#b24rq .scopes{ grid-template-columns:repeat(4,minmax(0,1fr)); } } #b24rq .scope{ text-align:left; background:#fff; border:1.5px solid var(&#8211;line); border-radius:12px; padding:10px 12px; min-height:68px; } #b24rq .scope b{ display:block; font-size:13.5px; line-height:1.2; } #b24rq .scope small{ display:block; font-size:12px; color:var(&#8211;muted); margin-top:3px; line-height:1.25; } #b24rq .scope.on{ border-color:var(&#8211;orange); background:#fffaf2; } #b24rq .ccnt{ display:inline-block; margin-left:6px; font-size:11px; font-weight:800; opacity:.65; } #b24rq .toolbar{ display:flex; align-items:center; justify-content:space-between; gap:10px; margin:6px 0 10px; } #b24rq .toolbar .cnt{ font-size:13.5px; color:var(&#8211;body); } #b24rq .seg{ display:flex; border:1px solid var(&#8211;line); border-radius:10px; overflow:hidden; } #b24rq .seg button{ white-space:nowrap; min-height:38px; padding:0 12px; font-size:13px; font-weight:800; background:#fff; } #b24rq .seg button.on{ background:var(&#8211;navy); color:#fff; } #b24rq .pgrid{ display:grid; gap:10px; grid-template-columns:repeat(var(&#8211;cols,2),minmax(0,1fr)); } @media(min-width:760px){#b24rq .pgrid{ grid-template-columns:repeat(auto-fill,minmax(210px,1fr)); } #b24rq .seg{ display:none; } } #b24rq .pc{ position:relative; background:#fff; border:1.5px solid var(&#8211;line); border-radius:12px; padding:10px; display:grid; grid-template-columns:44px minmax(0,1fr) auto; grid-template-areas:&#8221;im nm tg&#8221; &#8220;im mt tg&#8221;; column-gap:10px; row-gap:3px; align-items:center; text-align:left; min-height:68px; cursor:pointer; transition:transform .12s ease, border-color .2s; } #b24rq .pc:active{ transform:scale(.98); } #b24rq .pc.on{ border-color:var(&#8211;orange); background:#fffaf2; } #b24rq .pc .im{ grid-area:im; width:44px; height:44px; border-radius:10px; background:linear-gradient(135deg,#e9edf3,#dfe5ee); overflow:hidden; display:flex; align-items:center; justify-content:center; color:var(&#8211;muted); font-size:17px; } #b24rq<\/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-153221","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153221","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=153221"}],"version-history":[{"count":1,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153221\/revisions"}],"predecessor-version":[{"id":153484,"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/pages\/153221\/revisions\/153484"}],"wp:attachment":[{"href":"https:\/\/builders24.net\/zw\/wp-json\/wp\/v2\/media?parent=153221"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}