- Filter by
- All 09
- / < 100 кв.м² 03
- / 100-200 кв.м² 06
Назад
Сайт использует cookies
Узнать подробнее
<!— Studio ZEN — Калькулятор стоимости дизайн-проекта (тёмный + золото) | “Что включено” СРАЗУ под “Формат проекта” —>
<div id=»zenCalc» class=»zen-calc zen-zenTheme»></div>
<style>
.zen-zenTheme{
—bg:#0f1012;
—field:#1f2024;
—field2:#1b1c20;
—text:#f2f2f2;
—muted:#b7b7b7;
—muted2:#9b9b9b;
—line:rgba(255,255,255,.10);
—gold:#caa24b;
—gold2:#e1c06a;
—glow:0 0 0 1px rgba(202,162,75,.35), 0 0 24px rgba(202,162,75,.20);
—r:18px;
—r2:14px;
}
.zen-calc{
max-width:860px;
margin:0 auto;
padding:26px;
border-radius:var(—r);
background:radial-gradient(1200px 500px at 20% 0%, rgba(202,162,75,.10) 0%, rgba(15,16,18,1) 45%), var(—bg);
color:var(—text);
border:1px solid rgba(202,162,75,.35);
box-shadow:0 20px 60px rgba(0,0,0,.65);
font-family:inherit;
}
.zen-calc *{ box-sizing:border-box; }
.zen-toplabel{
font-size:12px;
letter-spacing:.12em;
text-transform:uppercase;
color:var(—muted2);
margin:0 0 10px 0;
}
.zen-title{
margin:0;
font-family:Georgia,»Times New Roman»,Times,serif;
font-size:27px;
line-height:1.18;
letter-spacing:-0.01em;
color:var(—text);
}
.zen-subtitle{
margin:10px 0 0 0;
color:var(—muted);
font-size:14px;
max-width:90ch;
}
.zen-stack{ margin-top:18px; display:flex; flex-direction:column; gap:14px; }
.zen-panel{
border-radius:var(—r);
background:
radial-gradient(900px 260px at 18% 0%, rgba(202,162,75,.10) 0%, rgba(0,0,0,0) 55%),
linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));
border:1px solid var(—line);
padding:16px;
}
.zen-field{ display:flex; flex-direction:column; gap:8px; }
.zen-label{
font-size:12px;
color:var(—muted2);
letter-spacing:.02em;
}
.zen-input, .zen-select{
width:100%;
padding:12px 12px;
border-radius:var(—r2);
border:1px solid rgba(255,255,255,.10);
background:linear-gradient(180deg, var(—field), var(—field2));
color:var(—text);
font:inherit;
font-size:14px;
outline:none;
}
.zen-input::placeholder{ color:rgba(255,255,255,.35); }
.zen-input:focus, .zen-select:focus{
border-color:rgba(202,162,75,.55);
box-shadow:var(—glow);
}
.zen-select{
appearance:none;
-webkit-appearance:none;
-moz-appearance:none;
background-image:
linear-gradient(180deg, var(—field), var(—field2)),
linear-gradient(45deg, transparent 50%, rgba(255,255,255,.65) 50%),
linear-gradient(135deg, rgba(255,255,255,.65) 50%, transparent 50%);
background-repeat:no-repeat;
background-size:100% 100%, 8px 8px, 8px 8px;
background-position:0 0, calc(100% — 18px) 55%, calc(100% — 12px) 55%;
padding-right:38px;
}
.zen-select option{ background:#151619; color:#f2f2f2; }
.zen-input[type=»number»]::-webkit-outer-spin-button,
.zen-input[type=»number»]::-webkit-inner-spin-button{ -webkit-appearance:none; margin:0; }
.zen-input[type=»number»]{ -moz-appearance:textfield; appearance:textfield; }
.zen-hint{
margin:0;
font-size:12px;
color:rgba(255,255,255,.55);
line-height:1.4;
}
.zen-noteWarn{
margin-top:6px;
padding:10px 12px;
border-radius:var(—r2);
border:1px solid rgba(202,162,75,.40);
background:rgba(202,162,75,.08);
color:rgba(255,255,255,.88);
font-size:12px;
line-height:1.35;
}
.zen-blockTitle{
margin:0 0 10px 0;
font-family:Georgia,»Times New Roman»,Times,serif;
font-size:18px;
font-weight:800;
color:var(—text);
}
.zen-tag{
display:inline-flex;
align-items:center;
padding:6px 10px;
border-radius:999px;
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.25);
color:rgba(255,255,255,.70);
font-size:12px;
width:fit-content;
}
.zen-total{
margin:10px 0 0 0;
font-family:Georgia,»Times New Roman»,Times,serif;
font-size:42px;
font-weight:900;
letter-spacing:-0.02em;
color:var(—text);
}
.zen-muted{ margin:6px 0 0 0; font-size:12px; color:rgba(255,255,255,.55); line-height:1.4; }
.zen-details{
border:1px solid rgba(255,255,255,.10);
border-radius:var(—r2);
padding:10px 12px;
background:rgba(0,0,0,.18);
}
.zen-details summary{
cursor:pointer;
font-size:13px;
font-weight:800;
color:var(—text);
list-style:none;
}
.zen-details summary::-webkit-details-marker{ display:none; }
.zen-detailsBody{
margin-top:10px;
font-size:13px;
color:rgba(255,255,255,.85);
line-height:1.45;
}
.zen-kpiRow{ display:flex; flex-direction:column; gap:10px; margin-top:10px; }
.zen-kpi{
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.18);
border-radius:var(—r2);
padding:12px 12px;
display:flex;
justify-content:space-between;
gap:12px;
align-items:center;
}
.zen-kpiLeft{ display:flex; flex-direction:column; gap:3px; }
.zen-kpiTop{ display:flex; align-items:center; gap:8px; }
.zen-pill{
display:inline-flex;
padding:4px 8px;
border-radius:999px;
border:1px solid rgba(202,162,75,.35);
color:rgba(255,255,255,.75);
font-size:12px;
background:rgba(202,162,75,.08);
}
.zen-kpiSub{ font-size:12px; color:rgba(255,255,255,.55); }
.zen-kpi strong{ font-weight:900; color:var(—text); }
.zen-tabs{
display:grid;
grid-template-columns:1fr 1fr;
gap:10px;
margin-top:10px;
}
@media (max-width:520px){ .zen-tabs{ grid-template-columns:1fr; } }
.zen-tab{
border:1px solid rgba(255,255,255,.10);
border-radius:var(—r2);
padding:12px;
background:linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,.01));
cursor:pointer;
display:flex;
gap:10px;
align-items:center;
transition:transform .05s ease, box-shadow .15s ease, border-color .15s ease;
}
.zen-tab:active{ transform:translateY(1px); }
.zen-dot{
width:10px; height:10px; border-radius:999px;
border:2px solid rgba(255,255,255,.25);
background:transparent;
flex:0 0 auto;
}
.zen-tabTitle{ font-size:14px; font-weight:700; color:var(—text); line-height:1.25; }
.zen-tab.is-active{ border-color:rgba(202,162,75,.55); box-shadow:var(—glow); }
.zen-tab.is-active .zen-dot{ border-color:var(—gold); background:var(—gold); }
.zen-list{ margin:0; padding-left:18px; color:rgba(255,255,255,.82); font-size:13px; line-height:1.5; }
.zen-list li{ margin:4px 0; }
.zen-miniGrid{
margin-top:12px;
display:grid;
grid-template-columns:1fr 1fr;
gap:10px;
}
@media (max-width:520px){ .zen-miniGrid{ grid-template-columns:1fr; } }
.zen-miniCard{
border:1px solid rgba(255,255,255,.10);
background:rgba(0,0,0,.18);
border-radius:var(—r2);
padding:12px;
}
.zen-miniTitle{ font-size:12px; color:rgba(255,255,255,.55); margin:0; }
.zen-miniBig{ font-size:18px; font-weight:900; margin:6px 0 0 0; color:var(—text); }
.zen-lead{
border:1px solid rgba(202,162,75,.55);
border-radius:var(—r);
padding:16px;
background:
radial-gradient(900px 240px at 20% 0%, rgba(202,162,75,.18) 0%, rgba(0,0,0,0) 55%),
rgba(0,0,0,.18);
box-shadow:var(—glow);
}
.zen-leadTitle{
margin:0;
font-family:Georgia,»Times New Roman»,Times,serif;
font-size:16px;
font-weight:900;
color:var(—text);
}
.zen-badges{ display:flex; gap:8px; flex-wrap:wrap; margin-top:10px; }
.zen-badge{
display:inline-flex;
padding:6px 10px;
border-radius:999px;
border:1px solid rgba(255,255,255,.12);
background:rgba(0,0,0,.25);
color:rgba(255,255,255,.80);
font-size:12px;
}
.zen-leadSub{ margin:10px 0 0 0; font-size:13px; color:rgba(255,255,255,.78); line-height:1.45; }
.zen-messengers{ display:flex; gap:10px; flex-wrap:wrap; margin-top:12px; }
.zen-mbtn{
border:1px solid rgba(255,255,255,.12);
border-radius:var(—r2);
padding:10px 12px;
background:rgba(0,0,0,.18);
cursor:pointer;
display:inline-flex;
gap:10px;
align-items:center;
font-size:13px;
font-weight:800;
color:var(—text);
transition:box-shadow .15s ease, border-color .15s ease, transform .05s ease;
}
.zen-mbtn:active{ transform:translateY(1px); }
.zen-mbtn .zen-dot{ width:10px; height:10px; }
.zen-mbtn.is-active{ border-color:rgba(202,162,75,.55); box-shadow:var(—glow); }
.zen-mbtn.is-active .zen-dot{ border-color:var(—gold); background:var(—gold); }
.zen-actions{ display:flex; flex-direction:column; gap:10px; margin-top:12px; }
.zen-inline{ display:flex; gap:10px; }
@media (max-width:540px){ .zen-inline{ flex-direction:column; } }
.zen-btn{
border-radius:var(—r2);
padding:12px 14px;
cursor:pointer;
font:inherit;
font-size:13px;
letter-spacing:.10em;
text-transform:uppercase;
font-weight:900;
transition:transform .05s ease, opacity .15s ease, box-shadow .15s ease, border-color .15s ease;
}
.zen-btn:active{ transform:translateY(1px); }
.zen-btnPrimary{
background:rgba(0,0,0,.35);
color:var(—text);
border:1px solid rgba(202,162,75,.70);
box-shadow:var(—glow);
}
.zen-btnPrimary:hover{
border-color:rgba(225,192,106,.85);
box-shadow:0 0 0 1px rgba(202,162,75,.40), 0 0 30px rgba(202,162,75,.28);
}
.zen-btn:disabled{ opacity:.65; cursor:not-allowed; }
.zen-consent{
display:flex; gap:8px; align-items:flex-start;
font-size:12px; color:rgba(255,255,255,.65);
line-height:1.35;
}
.zen-consent input{ margin-top:2px; }
.zen-success{
padding:10px 12px;
border:1px solid rgba(90, 230, 140, .35);
background:rgba(20, 60, 35, .35);
border-radius:var(—r2);
color:#bff5cf;
font-size:13px;
}
.zen-error{
padding:10px 12px;
border:1px solid rgba(255, 120, 120, .35);
background:rgba(80, 20, 20, .35);
border-radius:var(—r2);
color:#ffd0d0;
font-size:13px;
}
</style>
<script>
(() => {
const CONFIG = {
roundingStep: 1000,
minAreaLimit: 35,
packages: {
sketch: { title: «Эскизный», rate: 1450, stages: [0.50, 0.50] },
full: { title: «Полный проект», rate: 3500, stages: [0.40, 0.30, 0.30] },
full_spec: { title: «Проект + комплектация», rate: 3800, stages: [0.37, 0.28, 0.28, 0.07] },
full_supervision: { title: «Проект + авторский надзор», rate: 3500, stages: [0.40, 0.30, 0.30], monthly: 35000 }
},
styles: {
scandi: { title: «Скандинавский», k: 1.00 },
loft: { title: «Лофт», k: 1.05 },
modern: { title: «Современный», k: 1.05 },
japandi: { title: «Джапанди», k: 1.10 },
neoclassic: { title: «Неоклассика», k: 1.15 }
},
novelty: {
new: { title: «Новостройка», k: 1.00, hint: «Для новостройки применяем базовую сложность без дополнительных поправок.» },
secondary: { title: «Вторичное», k: 1.08, hint: «Во вторичном чаще больше нюансов по коммуникациям — учитываем это в расчёте.» }
},
objectType: {
house: { title: «Дом», k: 1.03, hint: «Сложность из-за дополнительных узлов объекта» },
apartment: { title: «Квартира», k: 1.00, hint: «Базовая сложность объекта» },
commercial: { title: «Коммерческое», k: 0.95, hint: «Упрощенный тип объекта» }
},
stageLabels: {
sketch: [«Планировки», «Концепция»],
full: [«Планировки + концепция», «Визуализации», «Чертежи + спецификация»],
full_spec: [«Планировки + концепция», «Визуализации», «Чертежи + спецификация», «Запуск комплектации»],
full_supervision: [«Планировки + концепция», «Визуализации», «Чертежи + спецификация»]
},
includes: {
sketch: [«Планировочные решения»,»Стилистическая концепция»,»Ключевые рекомендации по отделке»],
full: [«Планировочные решения»,»3D-визуализации помещений»,»Рабочие чертежи для строителей»,»Спецификация отделки»],
full_spec: [«Всё из полного проекта»,»Подбор материалов/света/сантехники»,»Согласования и КП»,»Сопровождение комплектации»],
full_supervision: [«Всё из полного проекта»,»Авторский надзор на стройке»,»Контроль соответствия проекту»,»Консультации и выезды (по договору)»]
},
calmReasonsBase: [
«Полный цикл: от концепции до заселения»,
«Собственное производство корпусной мебели»,
«Бесплатное хранение товаров на нашем складе»,
«Прозрачный процесс и контроль реализации»
],
supervision_monthly: 35000,
SUBMIT_URL: «»
};
const fmt = (n) => Math.round(n).toString().replace(/\B(?=(\d{3})+(?!\d))/g, » «);
const roundTo = (n, step) => Math.round(n / step) * step;
const clamp = (v, min, max) => Math.max(min, Math.min(max, v));
// анимация цифр
const motion = { raf:null, from:new Map(), to:new Map(), start:0, duration:420 };
function easeOutCubic(t){ return 1 — Math.pow(1 — t, 3); }
function readDisplayedNumber(el){
const raw = (el.textContent || «»).replace(/[^\d]/g,»»);
return raw ? Number(raw) : 0;
}
function setAnimatedNumber(el, target){
const current = motion.to.has(el) ? motion.to.get(el) : readDisplayedNumber(el);
motion.from.set(el, current);
motion.to.set(el, target);
kickAnimation();
}
function kickAnimation(){
if (motion.raf) cancelAnimationFrame(motion.raf);
motion.start = performance.now();
const step = (now) => {
const t = clamp((now — motion.start) / motion.duration, 0, 1);
const k = easeOutCubic(t);
motion.to.forEach((target, el) => {
const from = motion.from.get(el) ?? 0;
const val = from + (target — from) * k;
el.textContent = `${fmt(val)} ₽`;
});
if (t < 1) motion.raf = requestAnimationFrame(step);
else motion.raf = null;
};
motion.raf = requestAnimationFrame(step);
}
function areaDiscountNote(area){
if (area >= 250) return «Для данной площади действует скидка 7%»;
if (area >= 170) return «Для данной площади действует скидка 5%»;
if (area > 100) return «Для данной площади действует скидка 3%»;
return «»;
}
function areaFactor(area){
if (area < 50) return 1.10;
if (area >= 250) return 0.93;
if (area >= 170) return 0.95;
if (area > 100) return 0.97;
return 1.00;
}
function calcProjectTotal(area, pkgKey, styleKey, noveltyKey, objectKey) {
const pkg = CONFIG.packages[pkgKey];
const style = CONFIG.styles[styleKey];
const nov = CONFIG.novelty[noveltyKey];
const obj = CONFIG.objectType[objectKey];
const kArea = areaFactor(area);
const raw = area * pkg.rate * style.k * nov.k * obj.k * kArea;
const total = roundTo(raw, CONFIG.roundingStep);
return { total, pkg, style, nov, obj, kArea };
}
function calcStages(total, pkgKey) {
const pkg = CONFIG.packages[pkgKey];
const parts = pkg.stages || [];
const rounded = parts.map(p => roundTo(total * p, CONFIG.roundingStep));
const sum = rounded.reduce((a,b)=>a+b,0);
const diff = total — sum;
if (rounded.length && diff !== 0) rounded[rounded.length — 1] += diff;
return rounded;
}
function digitsOnly(s){ return (s||»»).replace(/\D/g,»»); }
function normalizePhone(raw){
const d = digitsOnly(raw);
if (d.startsWith(«8») && d.length === 11) return «7» + d.slice(1);
return d;
}
function isValidRuPhone(raw){
const d = normalizePhone(raw);
return d.length === 11 && d.startsWith(«7»);
}
function maskRuPhone(inputEl){
let d = digitsOnly(inputEl.value);
if (d.startsWith(«8»)) d = «7» + d.slice(1);
if (d.length === 0){ inputEl.value = «»; return; }
if (!d.startsWith(«7»)) d = «7» + d;
d = d.slice(0, 11);
const p = d.slice(1);
const a = p.slice(0,3);
const b = p.slice(3,6);
const c = p.slice(6,8);
const e = p.slice(8,10);
let out = «+7″;
if (a.length) out += » (» + a;
if (a.length === 3) out += «)»;
if (b.length) out += » » + b;
if (c.length) out += «-» + c;
if (e.length) out += «-» + e;
inputEl.value = out;
}
const root = document.getElementById(«zenCalc»);
if (!root) return;
const state = {
area: 60,
pkgKey: «full»,
styleKey: «modern»,
noveltyKey: «new»,
objectKey: «apartment»,
months: 3,
messenger: «whatsapp»
};
const packageTabs = Object.entries(CONFIG.packages).map(([k,p]) => `
<button class=»zen-tab ${state.pkgKey===k ? «is-active» : «»}» type=»button» data-pkg=»${k}»>
<span class=»zen-dot» aria-hidden=»true»></span>
<span class=»zen-tabTitle»>${p.title}</span>
</button>
`).join(«»);
root.innerHTML = `
<p class=»zen-toplabel»>01 // расчёт</p>
<h2 class=»zen-title»>Рассчитайте инвестицию в дизайн интерьера за 1 минуту</h2>
<p class=»zen-subtitle»>Понятный расчёт и поэтапная оплата. После рассчета отправим подробную спецификацию по материалам для площади, близкой к вашей.</p>
<div class=»zen-stack»>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Площадь объекта (м²)</h3>
<div class=»zen-field»>
<input class=»zen-input» id=»zenArea» type=»number» inputmode=»numeric» min=»0″ max=»1000″ step=»1″ value=»${state.area}»>
<p class=»zen-hint»>Общая площадь по плану БТИ/ДДУ.</p>
<div id=»zenMinAreaNote» style=»display:none» class=»zen-noteWarn»>К сожалению мы не берем в работу объекты меньше 35 м2</div>
</div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Примечание по площади</h3>
<div class=»zen-details»>
<div class=»zen-detailsBody» style=»margin-top:0;»>
<span id=»zenAreaRuleText»></span>
</div>
</div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Новизна объекта</h3>
<div class=»zen-field»>
<select class=»zen-select» id=»zenNovelty»>
${Object.entries(CONFIG.novelty).map(([k,v]) => `<option value=»${k}» ${state.noveltyKey===k?»selected»:»»}>${v.title}</option>`).join(«»)}
</select>
<p class=»zen-hint» id=»zenNoveltyHint»></p>
</div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Тип объекта</h3>
<div class=»zen-field»>
<select class=»zen-select» id=»zenObjectType»>
${Object.entries(CONFIG.objectType).map(([k,v]) => `<option value=»${k}» ${state.objectKey===k?»selected»:»»}>${v.title}</option>`).join(«»)}
</select>
<p class=»zen-hint» id=»zenObjectHint»></p>
</div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Стиль интерьера</h3>
<div class=»zen-field»>
<select class=»zen-select» id=»zenStyle»>
${Object.entries(CONFIG.styles).map(([k,v]) => `<option value=»${k}» ${state.styleKey===k?»selected»:»»}>${v.title}</option>`).join(«»)}
</select>
</div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Формат проекта</h3>
<div class=»zen-tabs» id=»zenPkgTabs»>${packageTabs}</div>
</div>
<!— ✅ “Что включено” сразу под “Формат проекта” —>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Что включено</h3>
<ul class=»zen-list» id=»zenIncluded»></ul>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Общая стоимость</h3>
<div class=»zen-tag» id=»zenTag»></div>
<div class=»zen-total» id=»zenTotal»>0 ₽</div>
<p class=»zen-muted»>Расчёт предварительный. Финальная стоимость — после брифа и плана.</p>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Показать как считается</h3>
<details class=»zen-details»>
<summary>Открыть формулу расчёта</summary>
<div class=»zen-detailsBody»>
<div><strong>Формула:</strong> <span id=»zenFormula»></span></div>
</div>
</details>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>График оплаты по этапам</h3>
<div class=»zen-kpiRow» id=»zenStages»></div>
<p class=»zen-muted»>Вы оплачиваете по мере готовности этапов — это даёт контроль бюджета и процесса.</p>
<div id=»zenMonthlyWrap»></div>
</div>
<div class=»zen-panel»>
<h3 class=»zen-blockTitle»>Почему выбирают нас?</h3>
<ul class=»zen-list» id=»zenCalm»></ul>
</div>
<div class=»zen-lead»>
<h3 class=»zen-leadTitle»>Пока вы считали стоимость, мы подготовили полную спецификацию проекта под вашу площадь</h3>
<div class=»zen-badges»>
<span class=»zen-badge»>PDF</span>
<span class=»zen-badge»>ориентир бюджета</span>
<span class=»zen-badge»>5 минут</span>
<span class=»zen-badge»>без звонков</span>
</div>
<p class=»zen-leadSub» id=»zenOfferText»></p>
<div class=»zen-messengers» id=»zenMessengerBtns»>
<button type=»button» class=»zen-mbtn is-active» data-m=»whatsapp»><span class=»zen-dot» aria-hidden=»true»></span> WhatsApp</button>
<button type=»button» class=»zen-mbtn» data-m=»telegram»><span class=»zen-dot» aria-hidden=»true»></span> Telegram</button>
</div>
<div class=»zen-actions»>
<div class=»zen-inline»>
<input class=»zen-input» id=»zenPhone» type=»tel» inputmode=»tel» placeholder=»+7 (___) ___-__-__»>
<button class=»zen-btn zen-btnPrimary» id=»zenSend»>Получить спецификацию</button>
</div>
<label class=»zen-consent»>
<input type=»checkbox» id=»zenConsent» checked>
<span>Я согласен(а) на обработку персональных данных и получение материалов.</span>
</label>
<div id=»zenMsg»></div>
</div>
</div>
</div>
`;
const el = {
area: root.querySelector(«#zenArea»),
minAreaNote: root.querySelector(«#zenMinAreaNote»),
novelty: root.querySelector(«#zenNovelty»),
noveltyHint: root.querySelector(«#zenNoveltyHint»),
objectType: root.querySelector(«#zenObjectType»),
objectHint: root.querySelector(«#zenObjectHint»),
style: root.querySelector(«#zenStyle»),
pkgTabs: root.querySelector(«#zenPkgTabs»),
areaRuleText: root.querySelector(«#zenAreaRuleText»),
tag: root.querySelector(«#zenTag»),
total: root.querySelector(«#zenTotal»),
formula: root.querySelector(«#zenFormula»),
included: root.querySelector(«#zenIncluded»),
stages: root.querySelector(«#zenStages»),
monthlyWrap: root.querySelector(«#zenMonthlyWrap»),
calm: root.querySelector(«#zenCalm»),
offerText: root.querySelector(«#zenOfferText»),
messengerBtns: root.querySelector(«#zenMessengerBtns»),
phone: root.querySelector(«#zenPhone»),
consent: root.querySelector(«#zenConsent»),
send: root.querySelector(«#zenSend»),
msg: root.querySelector(«#zenMsg»)
};
function updatePackageTabs(){
[…el.pkgTabs.querySelectorAll(«.zen-tab»)].forEach(btn => {
btn.classList.toggle(«is-active», btn.dataset.pkg === state.pkgKey);
});
}
function updateNoveltyHint(){
el.noveltyHint.textContent = CONFIG.novelty[el.novelty.value]?.hint || «»;
}
function updateObjectHint(){
el.objectHint.textContent = CONFIG.objectType[el.objectType.value]?.hint || «»;
}
function updateCalmList(){
const base = […CONFIG.calmReasonsBase];
const items = (state.pkgKey === «sketch»)
? base.filter(x => x !== «Полный цикл: от концепции до заселения»)
: base;
el.calm.innerHTML = items.map(x => `<li>${x}</li>`).join(«»);
}
function updateMessengerBtns(){
[…el.messengerBtns.querySelectorAll(«.zen-mbtn»)].forEach(btn => {
btn.classList.toggle(«is-active», btn.dataset.m === state.messenger);
});
}
function updateUI(){
const areaRaw = Number(el.area.value);
const area = clamp(isFinite(areaRaw) ? areaRaw : 0, 0, 1000);
state.area = area;
state.noveltyKey = el.novelty.value;
state.objectKey = el.objectType.value;
state.styleKey = el.style.value;
updateNoveltyHint();
updateObjectHint();
updateCalmList();
const tooSmall = area > 0 && area < CONFIG.minAreaLimit;
el.minAreaNote.style.display = tooSmall ? «block» : «none»;
el.areaRuleText.textContent = areaDiscountNote(area);
const from = Math.max(10, area — 10);
const to = area + 10;
el.offerText.textContent =
`Состав материалов, света и сантехники, примеры позиций и ориентиры по бюджету — для площади (${from}–${to} м²). Отправим в выбранный мессенджер.`;
el.included.innerHTML = (CONFIG.includes[state.pkgKey] || []).map(x => `<li>${x}</li>`).join(«»);
if (tooSmall){
el.tag.textContent = `Ваш проект: ${area} м²`;
el.formula.textContent = `—`;
setAnimatedNumber(el.total, 0);
const parts = (CONFIG.packages[state.pkgKey].stages || []);
const stageNames = CONFIG.stageLabels[state.pkgKey] || [];
el.stages.innerHTML = parts.map((p, idx) => `
<div class=»zen-kpi»>
<div class=»zen-kpiLeft»>
<div class=»zen-kpiTop»>
<strong>${idx+1} этап</strong>
<span class=»zen-pill»>${Math.round(p*100)}%</span>
</div>
<div class=»zen-kpiSub»>${stageNames[idx] || «Этап проекта»}</div>
</div>
<strong class=»zen-stageAmt» data-i=»${idx}»>0 ₽</strong>
</div>
`).join(«»);
[…el.stages.querySelectorAll(«.zen-stageAmt»)].forEach((node)=> setAnimatedNumber(node, 0));
el.monthlyWrap.innerHTML = «»;
return;
}
const { total, pkg, style, nov, obj, kArea } = calcProjectTotal(
area, state.pkgKey, state.styleKey, state.noveltyKey, state.objectKey
);
el.tag.textContent = `Ваш проект: ${area} м² · ${nov.title} · ${obj.title} · ${style.title}`;
setAnimatedNumber(el.total, total);
el.formula.textContent =
`${area} × ${fmt(pkg.rate)} × ${style.k.toFixed(2)} × ${nov.k.toFixed(2)} × ${obj.k.toFixed(2)} × ${kArea.toFixed(2)}`;
const stages = calcStages(total, state.pkgKey);
const stagePcts = (pkg.stages || []).map(p => Math.round(p * 100));
const stageNames = CONFIG.stageLabels[state.pkgKey] || [];
el.stages.innerHTML = stages.map((amt, idx) => `
<div class=»zen-kpi»>
<div class=»zen-kpiLeft»>
<div class=»zen-kpiTop»>
<strong>${idx+1} этап</strong>
<span class=»zen-pill»>${stagePcts[idx] || 0}%</span>
</div>
<div class=»zen-kpiSub»>${stageNames[idx] || «Этап проекта»}</div>
</div>
<strong class=»zen-stageAmt» data-i=»${idx}»>${fmt(amt)} ₽</strong>
</div>
`).join(«»);
[…el.stages.querySelectorAll(«.zen-stageAmt»)].forEach((node) => {
const i = Number(node.dataset.i);
setAnimatedNumber(node, stages[i] || 0);
});
const hasMonthly = typeof pkg.monthly === «number» && pkg.monthly > 0;
if (!hasMonthly){
el.monthlyWrap.innerHTML = «»;
return;
}
const months = clamp(Number(state.months) || 3, 1, 24);
const supervisionTotal = roundTo(CONFIG.supervision_monthly * months, CONFIG.roundingStep);
const grandTotal = total + supervisionTotal;
el.monthlyWrap.innerHTML = `
<div class=»zen-miniGrid»>
<div class=»zen-miniCard»>
<p class=»zen-miniTitle»>Авторский надзор</p>
<p class=»zen-miniBig»>${fmt(CONFIG.supervision_monthly)} ₽ / мес</p>
</div>
<div class=»zen-miniCard»>
<label class=»zen-label»>Срок надзора (мес)</label>
<input class=»zen-input» id=»zenMonths» type=»number» min=»1″ max=»24″ step=»1″ value=»${months}»>
<p class=»zen-hint» style=»margin-top:8px;»>Надзор итого: <strong id=»zenSupTotal»>${fmt(supervisionTotal)} ₽</strong></p>
</div>
</div>
<div class=»zen-kpi» style=»margin-top:10px;»>
<span>Итого (проект + надзор)</span><strong id=»zenGrandTotal»>${fmt(grandTotal)} ₽</strong>
</div>
`;
const monthsInput = root.querySelector(«#zenMonths»);
const supTotalEl = root.querySelector(«#zenSupTotal»);
const grandTotalEl = root.querySelector(«#zenGrandTotal»);
if (monthsInput){
monthsInput.addEventListener(«input», (e) => {
state.months = e.target.value;
const m = clamp(Number(state.months)||3, 1, 24);
const sup = roundTo(CONFIG.supervision_monthly * m, CONFIG.roundingStep);
const grand = total + sup;
setAnimatedNumber(supTotalEl, sup);
setAnimatedNumber(grandTotalEl, grand);
}, { passive:true });
}
}
// Events
el.area.addEventListener(«input», updateUI, { passive:true });
el.area.addEventListener(«change», updateUI);
el.novelty.addEventListener(«change», updateUI);
el.objectType.addEventListener(«change», updateUI);
el.style.addEventListener(«change», updateUI);
el.pkgTabs.addEventListener(«click», (e) => {
const btn = e.target.closest(«.zen-tab»);
if (!btn) return;
state.pkgKey = btn.dataset.pkg;
updatePackageTabs();
updateUI();
});
el.messengerBtns.addEventListener(«click», (e) => {
const btn = e.target.closest(«.zen-mbtn»);
if (!btn) return;
state.messenger = btn.dataset.m;
updateMessengerBtns();
});
el.phone.addEventListener(«input», () => maskRuPhone(el.phone), { passive:true });
el.send.addEventListener(«click», async () => {
el.msg.innerHTML = «»;
if (!el.consent.checked){
el.msg.innerHTML = `<div class=»zen-error»>Подтвердите согласие на обработку данных.</div>`;
return;
}
const phoneRaw = (el.phone.value || «»).trim();
if (!isValidRuPhone(phoneRaw)){
el.msg.innerHTML = `<div class=»zen-error»>Введите корректный номер телефона в формате +7 (___) ___-__-__.</div>`;
return;
}
try{
el.send.disabled = true;
el.send.textContent = «Отправляем…»;
if (CONFIG.SUBMIT_URL){
await fetch(CONFIG.SUBMIT_URL, {
method: «POST»,
headers: { «Content-Type»:»application/json» },
body: JSON.stringify({ phone: normalizePhone(phoneRaw), messenger: state.messenger })
});
}
const mText = state.messenger === «telegram» ? «Telegram» : «WhatsApp»;
el.msg.innerHTML = `<div class=»zen-success»>Готово! Отправим спецификацию в <strong>${mText}</strong> на номер <strong>${phoneRaw}</strong>.</div>`;
} catch (e){
el.msg.innerHTML = `<div class=»zen-error»>Не удалось отправить. Попробуйте ещё раз или напишите нам в мессенджер.</div>`;
} finally {
el.send.disabled = false;
el.send.textContent = «Получить спецификацию»;
}
});
// Init
updatePackageTabs();
updateMessengerBtns();
updateUI();
})();
</script>