{"id":22935,"date":"2026-09-10T05:44:07","date_gmt":"2026-09-10T09:44:07","guid":{"rendered":"https:\/\/www.chilliprinting.com\/online-printing-blog\/?page_id=22935"},"modified":"2026-09-10T09:28:05","modified_gmt":"2026-09-10T13:28:05","slug":"print-campaign-planner","status":"publish","type":"page","link":"https:\/\/www.chilliprinting.com\/online-printing-blog\/print-campaign-planner\/","title":{"rendered":"Print Campaign Planner Tool"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"22935\" class=\"elementor elementor-22935\" data-elementor-post-type=\"page\">\n\t\t\t\t\t\t<section class=\"elementor-section elementor-top-section elementor-element elementor-element-7f512ba elementor-section-full_width elementor-section-height-default elementor-section-height-default\" data-id=\"7f512ba\" data-element_type=\"section\" data-e-type=\"section\">\n\t\t\t\t\t\t<div class=\"elementor-container elementor-column-gap-no\">\n\t\t\t\t\t<div class=\"elementor-column elementor-col-100 elementor-top-column elementor-element elementor-element-be452e3\" data-id=\"be452e3\" data-element_type=\"column\" data-e-type=\"column\">\n\t\t\t<div class=\"elementor-widget-wrap elementor-element-populated\">\n\t\t\t\t\t\t<div class=\"elementor-element elementor-element-c491b6c elementor-widget elementor-widget-shortcode\" data-id=\"c491b6c\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"shortcode.default\">\n\t\t\t\t\t\t\t<div class=\"elementor-shortcode\"><style>\r\n  .cpp-outer-title{\r\n    max-width:760px; margin:0 auto; padding:32px 16px 0; text-align:center;\r\n    font-family:'Raleway', 'Segoe UI', sans-serif; font-weight:800;\r\n    font-size:clamp(28px,5vw,42px); line-height:1.12; color:#271219;\r\n  }\r\n<\/style>\r\n<h1 class=\"cpp-outer-title\">Free Print Campaign Planner<\/h1>\r\n<div id=\"cpp-planner-holder\" style=\"width:100%;\"><\/div>\r\n<script>\r\n(function(){\r\n  var html = \"<!doctype html>\\n<html lang=\\\"en\\\">\\n<head>\\n<meta charset=\\\"utf-8\\\">\\n<meta name=\\\"viewport\\\" content=\\\"width=device-width, initial-scale=1\\\">\\n<link rel=\\\"preconnect\\\" href=\\\"https:\/\/fonts.googleapis.com\\\">\\n<link rel=\\\"preconnect\\\" href=\\\"https:\/\/fonts.gstatic.com\\\" crossorigin>\\n<link href=\\\"https:\/\/fonts.googleapis.com\/css2?family=Raleway:wght@600;700;800&family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@500;600&display=swap\\\" rel=\\\"stylesheet\\\">\\n<style>\\n  :root{\\n    --cp-primary:#8a1e50;\\n    --cp-primary-dark:#5c1638;\\n    --cp-primary-light:#b3286d;\\n    --cp-primary-tint:#f3e2ea;\\n    --ink:#271219;\\n    --ink-soft:#4a2c38;\\n    --paper:#faf6f4;\\n    --surface:#ffffff;\\n    --surface-2:#f6ecef;\\n    --border:#e6d5dc;\\n    --muted:#7c6670;\\n    --good:#3f7d5c;\\n    --good-tint:#e4f0e9;\\n    --warn:#a8701c;\\n    --warn-tint:#f6ebd8;\\n    --focus:#2f6fb0;\\n    --font-display:'Raleway', 'Segoe UI', sans-serif;\\n    --font-body:'Inter', 'Segoe UI', sans-serif;\\n    --font-mono:'JetBrains Mono', 'Courier New', monospace;\\n    --radius-lg:18px;\\n    --radius-md:12px;\\n    --radius-sm:8px;\\n    color-scheme: light;\\n  }\\n  @media (prefers-color-scheme: dark){\\n    :root:not([data-theme=\\\"light\\\"]){\\n      --cp-primary:#e0568f;\\n      --cp-primary-dark:#c9407c;\\n      --cp-primary-light:#f088b3;\\n      --cp-primary-tint:#3a2029;\\n      --ink:#f3e7ec;\\n      --ink-soft:#d9c2cd;\\n      --paper:#1b1114;\\n      --surface:#241820;\\n      --surface-2:#2c1c26;\\n      --border:#432d3a;\\n      --muted:#b79aa6;\\n      --good:#6cbd93;\\n      --good-tint:#1c2c24;\\n      --warn:#e0aa5c;\\n      --warn-tint:#332415;\\n      --focus:#6fa8dc;\\n      color-scheme: dark;\\n    }\\n  }\\n  :root[data-theme=\\\"dark\\\"]{\\n    --cp-primary:#e0568f;\\n    --cp-primary-dark:#c9407c;\\n    --cp-primary-light:#f088b3;\\n    --cp-primary-tint:#3a2029;\\n    --ink:#f3e7ec;\\n    --ink-soft:#d9c2cd;\\n    --paper:#1b1114;\\n    --surface:#241820;\\n    --surface-2:#2c1c26;\\n    --border:#432d3a;\\n    --muted:#b79aa6;\\n    --good:#6cbd93;\\n    --good-tint:#1c2c24;\\n    --warn:#e0aa5c;\\n    --warn-tint:#332415;\\n    --focus:#6fa8dc;\\n    color-scheme: dark;\\n  }\\n\\n  *{box-sizing:border-box;}\\n  body{\\n    margin:0;\\n    background:#ffffff;\\n    color:var(--ink);\\n    font-family:var(--font-body);\\n    padding:0 16px;\\n    -webkit-font-smoothing:antialiased;\\n  }\\n  h1,h2,h3{ font-family:var(--font-display); text-wrap:balance; margin:0; }\\n  p{ margin:0; }\\n  ::selection{ background:var(--cp-primary); color:#fff; }\\n  a{ color:var(--cp-primary); }\\n  button{ font-family:inherit; }\\n  :focus-visible{ outline:2px solid var(--focus); outline-offset:2px; }\\n\\n  .wrap{ max-width:760px; margin:0 auto; padding-block:4px 64px; }\\n\\n  \/* ---------- Hero ---------- *\/\\n  \/* No top padding here - the actual <h1> now lives outside this iframe\\n     (see the WordPress wrapper) so it's recognized as a real page heading\\n     by SEO tools\/crawlers, which can't see into an iframe's own document.\\n     This hero div just holds the subtitle now; the outer h1 is styled to\\n     match so visually it reads as one continuous title block. *\/\\n  .hero{ text-align:center; padding-block:0 28px; }\\n  h1{ font-size:clamp(28px,5vw,42px); font-weight:800; line-height:1.12; color:var(--ink); }\\n  .sub{ max-width:52ch; margin:14px auto 0; color:var(--ink-soft); font-size:16px; line-height:1.55; }\\n  .brand-link{ font-weight:600; text-decoration:none; border-bottom:1px solid currentColor; }\\n\\n  \/* ---------- Wizard card ---------- *\/\\n  .card{\\n    background:var(--surface); border:1px solid var(--border); border-radius:var(--radius-lg);\\n    box-shadow:0 1px 2px rgba(20,6,14,.04), 0 12px 32px -18px rgba(20,6,14,.18);\\n    overflow:hidden;\\n  }\\n  .progress-track{ height:5px; background:var(--surface-2); }\\n  .progress-fill{ height:100%; background:linear-gradient(90deg,var(--cp-primary-dark),var(--cp-primary)); transition:width .35s ease; }\\n  .card-body{ padding:30px 26px 26px; }\\n  @media (min-width:560px){ .card-body{ padding:38px 40px 34px; } }\\n\\n  .step-meta{ display:flex; justify-content:space-between; align-items:baseline; margin-bottom:6px; gap:10px; }\\n  .step-count{ font-family:var(--font-mono); font-size:12px; font-weight:600; letter-spacing:.05em; color:var(--muted); white-space:nowrap; }\\n  .step-tag{ font-family:var(--font-mono); font-size:11px; font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--cp-primary); }\\n  .step-q{ font-size:clamp(19px,3.2vw,24px); font-weight:700; margin:6px 0 4px; }\\n  .step-hint{ color:var(--muted); font-size:14px; margin-bottom:20px; }\\n\\n  .options{ display:grid; grid-template-columns:1fr 1fr; gap:10px; }\\n  @media (max-width:480px){ .options{ grid-template-columns:1fr; } }\\n  .opt{\\n    text-align:left; cursor:pointer; border:1.5px solid var(--border); background:var(--surface);\\n    border-radius:var(--radius-md); padding:13px 15px; font-size:14.5px; font-weight:600; color:var(--ink);\\n    transition:border-color .15s, background .15s, transform .1s;\\n    display:flex; align-items:center; justify-content:space-between; gap:8px;\\n  }\\n  .opt:hover{ border-color:var(--cp-primary-light); }\\n  .opt:active{ transform:scale(.99); }\\n  .opt .check{ width:17px; height:17px; border-radius:5px; border:1.5px solid var(--border); flex:none; position:relative; }\\n  .opt[data-multi=\\\"true\\\"] .check{ border-radius:5px; }\\n  .opt:not([data-multi=\\\"true\\\"]) .check{ border-radius:999px; }\\n  .opt.selected{ border-color:var(--cp-primary); background:var(--cp-primary-tint); }\\n  .opt.selected .check{ border-color:var(--cp-primary); background:var(--cp-primary); }\\n  .opt.selected .check::after{\\n    content:\\\"\\\"; position:absolute; inset:0; margin:auto; width:8px; height:8px;\\n    background:var(--surface); mask:radial-gradient(circle, #000 60%, transparent 61%);\\n  }\\n  .opt.selected:not([data-multi=\\\"true\\\"]) .check::after{\\n    background:transparent; border-radius:3px; width:7px;height:7px;\\n    box-shadow:none; mask:none;\\n  }\\n  .opt.selected:not([data-multi=\\\"true\\\"]) .check{\\n    display:flex; align-items:center; justify-content:center;\\n  }\\n  .opt.selected:not([data-multi=\\\"true\\\"]) .check::before{\\n    content:\\\"\\\"; width:7px; height:7px; border-radius:50%; background:var(--surface);\\n  }\\n\\n  .field-inline{ margin-top:18px; }\\n  .field-inline label{ display:block; font-size:12.5px; font-weight:600; color:var(--muted); margin-bottom:6px; }\\n  .field-inline input[type=\\\"text\\\"], .field-inline input[type=\\\"number\\\"]{\\n    width:100%; padding:11px 13px; border:1.5px solid var(--border); border-radius:var(--radius-sm);\\n    background:var(--surface); color:var(--ink); font-size:14.5px; font-family:inherit;\\n  }\\n\\n  .nav-row{ display:flex; justify-content:space-between; align-items:center; margin-top:26px; gap:12px; }\\n  .btn{\\n    font-weight:700; font-size:14.5px; border-radius:999px; padding:12px 22px; border:1.5px solid transparent;\\n    cursor:pointer; transition:background .15s, border-color .15s, opacity .15s;\\n  }\\n  .btn-primary{ background:var(--cp-primary); color:#fff; }\\n  .btn-primary:hover{ background:var(--cp-primary-dark); }\\n  .btn-primary:disabled{ opacity:.4; cursor:not-allowed; }\\n  .btn-ghost{ background:transparent; color:var(--ink-soft); border-color:var(--border); }\\n  .btn-ghost:hover{ border-color:var(--cp-primary-light); color:var(--cp-primary); }\\n  .btn-ghost:disabled{ opacity:0; pointer-events:none; }\\n\\n  \/* ---------- Results: job ticket ---------- *\/\\n  .ticket-head{\\n    background:linear-gradient(135deg,var(--cp-primary-dark),var(--cp-primary));\\n    color:#fff; padding:26px 26px 22px;\\n  }\\n  @media (min-width:560px){ .ticket-head{ padding:32px 40px 26px; } }\\n  .ticket-title{ font-size:clamp(21px,4vw,27px); font-weight:800; margin-top:0; }\\n  .chip-row{ display:flex; flex-wrap:wrap; gap:8px; margin-top:16px; }\\n  .chip{\\n    font-size:12.5px; font-weight:600; background:rgba(255,255,255,.16); border:1px solid rgba(255,255,255,.3);\\n    border-radius:999px; padding:5px 12px;\\n  }\\n\\n  .ticket-body{ padding:26px; }\\n  @media (min-width:560px){ .ticket-body{ padding:32px 40px; } }\\n  .section-title{\\n    font-family:var(--font-mono); font-size:11.5px; font-weight:700; letter-spacing:.1em; text-transform:uppercase;\\n    color:var(--cp-primary); margin-bottom:14px; padding-bottom:8px; border-bottom:1px solid var(--border);\\n  }\\n  .section{ margin-bottom:32px; }\\n\\n  .products{ display:flex; flex-direction:column; gap:10px; }\\n  .product{\\n    display:flex; gap:14px; align-items:flex-start; position:relative;\\n    border:1px solid var(--border); border-radius:var(--radius-md); padding:14px 16px; background:var(--surface-2);\\n  }\\n  .product-image{\\n    width:60px; height:60px; flex:none; border-radius:10px; object-fit:cover;\\n    background:var(--surface); border:1px solid var(--border);\\n  }\\n  .product-body{ display:grid; grid-template-columns:1fr auto; gap:4px 16px; align-items:start; flex:1; min-width:0; }\\n  .product-remove{\\n    position:absolute; top:8px; right:8px; width:22px; height:22px; border-radius:50%; flex:none;\\n    border:1.5px solid var(--border); background:var(--surface); color:var(--muted); cursor:pointer;\\n    font-size:15px; line-height:1; display:flex; align-items:center; justify-content:center; padding:0;\\n    transition:background .15s, color .15s, border-color .15s;\\n  }\\n  .product-remove:hover{ background:var(--warn); border-color:var(--warn); color:#fff; }\\n  .product-name{ font-weight:700; font-size:15px; padding-right:22px; }\\n  .product-spec{ font-size:12.5px; color:var(--muted); grid-column:1\/2; }\\n  .product-why{ font-size:13px; color:var(--ink-soft); grid-column:1\/-1; margin-top:6px; line-height:1.5; }\\n  .product-qty{ font-family:var(--font-mono); font-weight:700; font-size:16px; text-align:right; white-space:nowrap; font-variant-numeric:tabular-nums; }\\n  .product-cost{ font-family:var(--font-mono); font-size:12.5px; color:var(--muted); text-align:right; font-variant-numeric:tabular-nums; }\\n  .configure-link{\\n    display:inline-flex; align-items:center; gap:5px; text-align:right; white-space:nowrap;\\n    font-family:var(--font-mono); font-size:11.5px; font-weight:700; letter-spacing:.02em;\\n    color:var(--cp-primary); background:var(--cp-primary-tint); border-radius:999px; padding:6px 12px;\\n    text-decoration:none; transition:background .15s;\\n  }\\n  .configure-link:hover{ background:var(--cp-primary); color:#fff; }\\n  .configure-link svg{ width:11px; height:11px; flex:none; }\\n  .price-col{ display:flex; flex-direction:column; align-items:flex-end; gap:6px; }\\n  .starting-price{ font-family:var(--font-mono); font-size:12px; color:var(--muted); text-align:right; line-height:1.4; }\\n  .starting-price b{ color:var(--ink-soft); font-weight:700; }\\n\\n  \/* ---------- Add more products ---------- *\/\\n  .add-more-row{ display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-top:14px; }\\n  .add-more-row .btn{ flex:none; }\\n  .product-select{\\n    flex:1; min-width:180px; padding:10px 13px; border:1.5px solid var(--border); border-radius:var(--radius-sm);\\n    background:var(--surface); color:var(--ink); font-size:13.5px; font-family:inherit;\\n  }\\n  .add-cards{ display:flex; flex-direction:column; gap:8px; margin-top:10px; }\\n  .add-card{\\n    display:flex; align-items:center; justify-content:space-between; gap:10px;\\n    border:1px dashed var(--cp-primary-light); border-radius:var(--radius-md); padding:10px 14px; background:var(--cp-primary-tint);\\n  }\\n  .add-card .add-card-name{ font-weight:700; font-size:13.5px; color:var(--ink); }\\n  .add-card-actions{ display:flex; align-items:center; gap:8px; }\\n\\n  \/* ---------- Budget allocation: donut + summary ---------- *\/\\n  .budget-grid{ display:grid; grid-template-columns:auto 1fr; gap:28px; align-items:center; margin-bottom:18px; }\\n  @media (max-width:560px){ .budget-grid{ grid-template-columns:1fr; justify-items:center; text-align:center; } }\\n  .donut-wrap{ position:relative; width:180px; height:180px; flex:none; }\\n  .donut-wrap svg{ width:100%; height:100%; display:block; }\\n  .donut-seg{ cursor:pointer; transition:opacity .15s; }\\n  .donut-seg:hover{ opacity:.82; }\\n  .donut-center{\\n    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;\\n    flex-direction:column; text-align:center; pointer-events:none;\\n  }\\n  .donut-center .n{ font-family:var(--font-mono); font-weight:700; font-size:20px; color:var(--ink); }\\n  .donut-center .l{ font-size:10.5px; color:var(--muted); text-transform:uppercase; letter-spacing:.06em; margin-top:1px; }\\n  .donut-tooltip{\\n    position:absolute; pointer-events:none; z-index:5; background:var(--ink); color:#fff;\\n    font-size:12px; font-weight:600; padding:6px 10px; border-radius:8px; white-space:nowrap;\\n    transform:translate(-50%, -130%); box-shadow:0 6px 18px rgba(0,0,0,.2); opacity:0; transition:opacity .1s;\\n  }\\n  .donut-tooltip.show{ opacity:1; }\\n  .budget-summary{ font-size:14.5px; color:var(--ink-soft); line-height:1.6; }\\n  .budget-summary p{ margin:0 0 10px; }\\n  .budget-summary p:last-child{ margin-bottom:0; }\\n  .budget-summary .lede{ color:var(--ink); font-weight:600; }\\n\\n  .budget-legend{ display:flex; flex-direction:column; gap:8px; margin-top:14px; }\\n  .budget-row{ display:flex; align-items:center; gap:10px; font-size:13.5px; }\\n  .swatch{ width:10px; height:10px; border-radius:3px; flex:none; }\\n  .budget-row .label{ flex:1; color:var(--ink-soft); }\\n  .budget-row .amount{ font-family:var(--font-mono); font-weight:600; font-variant-numeric:tabular-nums; }\\n  .budget-total{ display:flex; justify-content:space-between; margin-top:14px; padding-top:12px; border-top:1px dashed var(--border); font-weight:700; }\\n  .budget-total .amount{ font-family:var(--font-mono); font-variant-numeric:tabular-nums; }\\n\\n  .timeline{ list-style:none; margin:0; padding:0; display:flex; flex-direction:column; }\\n  .tl-item{ display:grid; grid-template-columns:22px 1fr; gap:14px; padding-bottom:18px; position:relative; }\\n  .tl-item:last-child{ padding-bottom:0; }\\n  .tl-dot{ width:22px; height:22px; border-radius:50%; background:var(--cp-primary-tint); border:1.5px solid var(--cp-primary); position:relative; z-index:1; }\\n  .tl-item:not(:last-child)::before{\\n    content:\\\"\\\"; position:absolute; left:10px; top:22px; bottom:0; width:1.5px; background:var(--border);\\n  }\\n  .tl-phase{ font-family:var(--font-mono); font-size:11px; font-weight:700; letter-spacing:.07em; text-transform:uppercase; color:var(--cp-primary); }\\n  .tl-title{ font-weight:700; font-size:14.5px; margin-top:2px; }\\n  .tl-detail{ font-size:13.5px; color:var(--ink-soft); margin-top:3px; line-height:1.5; }\\n\\n  .notes{ background:var(--surface-2); border:1px solid var(--border); border-radius:var(--radius-md); padding:16px 18px; font-size:14px; color:var(--ink-soft); line-height:1.6; }\\n\\n  .disclaimer{ font-size:12px; color:var(--muted); margin-top:6px; }\\n\\n  .cta-row{ display:flex; flex-wrap:wrap; gap:10px; margin-top:6px; }\\n  .cta-row .btn-primary, .cta-row a.btn-ghost{ text-decoration:none; display:inline-flex; align-items:center; }\\n\\n  .footer-note{ text-align:center; color:var(--muted); font-size:12.5px; margin-top:26px; line-height:1.6; }\\n\\n  [hidden]{ display:none !important; }\\n  @media (prefers-reduced-motion:reduce){ *{ transition:none !important; } }\\n<\\\/style>\\n\\n<div class=\\\"wrap\\\">\\n\\n  <div class=\\\"hero\\\">\\n    <p class=\\\"sub\\\">Answer a few questions about your business and get a press-ready plan - products, quantities, budget split, and a production timeline. Built by <a class=\\\"brand-link\\\" href=\\\"https:\/\/www.chilliprinting.com\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">ChilliPrinting<\\\/a>.<\\\/p>\\n  <\\\/div>\\n\\n  <div class=\\\"card\\\">\\n    <div class=\\\"progress-track\\\"><div class=\\\"progress-fill\\\" id=\\\"progressFill\\\" style=\\\"width:0%\\\"><\\\/div><\\\/div>\\n    <div class=\\\"card-body\\\">\\n\\n      <div id=\\\"wizard\\\">\\n        <div class=\\\"step-meta\\\">\\n          <span class=\\\"step-tag\\\" id=\\\"stepTag\\\">Job spec<\\\/span>\\n          <span class=\\\"step-count\\\" id=\\\"stepCount\\\">Step 1 \/ 7<\\\/span>\\n        <\\\/div>\\n        <h2 class=\\\"step-q\\\" id=\\\"stepQ\\\"><\\\/h2>\\n        <p class=\\\"step-hint\\\" id=\\\"stepHint\\\"><\\\/p>\\n        <div class=\\\"options\\\" id=\\\"optionsGrid\\\"><\\\/div>\\n        <div class=\\\"field-inline\\\" id=\\\"textFieldWrap\\\" hidden>\\n          <label for=\\\"cityInput\\\" id=\\\"textFieldLabel\\\"><\\\/label>\\n          <input type=\\\"text\\\" id=\\\"cityInput\\\" placeholder=\\\"e.g. Riverside, CA\\\" autocomplete=\\\"off\\\">\\n        <\\\/div>\\n        <div class=\\\"field-inline\\\" id=\\\"numberFieldWrap\\\" hidden>\\n          <label for=\\\"attendeesInput\\\" id=\\\"numberFieldLabel\\\"><\\\/label>\\n          <input type=\\\"number\\\" id=\\\"attendeesInput\\\" min=\\\"1\\\" step=\\\"1\\\" inputmode=\\\"numeric\\\" autocomplete=\\\"off\\\">\\n        <\\\/div>\\n        <div class=\\\"nav-row\\\">\\n          <button class=\\\"btn btn-ghost\\\" id=\\\"backBtn\\\">Back<\\\/button>\\n          <button class=\\\"btn btn-primary\\\" id=\\\"nextBtn\\\" disabled>Next<\\\/button>\\n        <\\\/div>\\n      <\\\/div>\\n\\n      <div id=\\\"results\\\" hidden><\\\/div>\\n\\n    <\\\/div>\\n  <\\\/div>\\n\\n  <p class=\\\"footer-note\\\">This planner gives ballpark quantities and budget splits based on common campaign patterns - not a formal quote. Paper stock, finishing, and rush timing all move the final price.<\\\/p>\\n<\\\/div>\\n\\n<script>\\n(function(){\\n  \\\"use strict\\\";\\n\\n  \/* ---------------- Config: steps ---------------- *\/\\n  var STEPS = [\\n    {\\n      key:\\\"businessType\\\", tag:\\\"Business type\\\", type:\\\"single\\\",\\n      q:\\\"What kind of business is this campaign for?\\\",\\n      hint:\\\"This shapes which print products actually make sense.\\\",\\n      options:[\\n        {v:\\\"restaurant\\\", l:\\\"Restaurant or caf\\u00e9\\\"},\\n        {v:\\\"retail\\\", l:\\\"Retail or ecommerce\\\"},\\n        {v:\\\"realestate\\\", l:\\\"Real estate\\\"},\\n        {v:\\\"healthcare\\\", l:\\\"Healthcare or wellness\\\"},\\n        {v:\\\"professional\\\", l:\\\"Professional services\\\"},\\n        {v:\\\"fitness\\\", l:\\\"Fitness or studio\\\"},\\n        {v:\\\"events\\\", l:\\\"Events or nonprofit\\\"},\\n        {v:\\\"trades\\\", l:\\\"Construction or trades\\\"},\\n        {v:\\\"beauty\\\", l:\\\"Beauty or salon\\\"},\\n        {v:\\\"other\\\", l:\\\"Other \/ general business\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"objective\\\", tag:\\\"Objective\\\", type:\\\"single\\\",\\n      q:\\\"What's the main goal of this campaign?\\\",\\n      hint:\\\"We'll weight recommendations toward this outcome.\\\",\\n      options:[\\n        {v:\\\"brand-awareness\\\", l:\\\"Build brand awareness\\\"},\\n        {v:\\\"foot-traffic\\\", l:\\\"Drive foot traffic\\\"},\\n        {v:\\\"lead-gen\\\", l:\\\"Generate leads or inquiries\\\"},\\n        {v:\\\"promo-sale\\\", l:\\\"Promote a sale or event\\\"},\\n        {v:\\\"retention\\\", l:\\\"Retain existing customers\\\"},\\n        {v:\\\"grand-opening\\\", l:\\\"Announce a launch or opening\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"audience\\\", tag:\\\"Audience\\\", type:\\\"single\\\",\\n      q:\\\"Who are you trying to reach?\\\",\\n      hint:\\\"\\\",\\n      options:[\\n        {v:\\\"neighborhood\\\", l:\\\"Local neighborhood\\\"},\\n        {v:\\\"city\\\", l:\\\"City-wide\\\"},\\n        {v:\\\"regional\\\", l:\\\"Multi-city \/ regional\\\"},\\n        {v:\\\"national\\\", l:\\\"National\\\"},\\n        {v:\\\"b2b\\\", l:\\\"Other businesses (B2B)\\\"},\\n        {v:\\\"existing\\\", l:\\\"Mostly existing customers\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"reach\\\", tag:\\\"Reach\\\", type:\\\"single\\\",\\n      q:\\\"How wide should distribution run?\\\",\\n      hint:\\\"Tell us the city or area too, if you'd like - it'll show up in your plan.\\\",\\n      options:[\\n        {v:\\\"tight\\\", l:\\\"Tight radius (1-3 miles)\\\"},\\n        {v:\\\"city\\\", l:\\\"City-wide\\\"},\\n        {v:\\\"multi\\\", l:\\\"Multi-city \/ regional\\\"},\\n        {v:\\\"national\\\", l:\\\"National \/ shipped\\\"}\\n      ],\\n      textField:{id:\\\"cityInput\\\", label:\\\"City or area (optional)\\\"}\\n    },\\n    {\\n      key:\\\"budget\\\", tag:\\\"Budget\\\", type:\\\"single\\\",\\n      q:\\\"What's the total budget for this campaign?\\\",\\n      hint:\\\"Print + design only - not ad spend.\\\",\\n      options:[\\n        {v:\\\"under500\\\", l:\\\"Under $500\\\"},\\n        {v:\\\"500-1500\\\", l:\\\"$500 - $1,500\\\"},\\n        {v:\\\"1500-5000\\\", l:\\\"$1,500 - $5,000\\\"},\\n        {v:\\\"5000-15000\\\", l:\\\"$5,000 - $15,000\\\"},\\n        {v:\\\"15000plus\\\", l:\\\"$15,000+\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"duration\\\", tag:\\\"Duration\\\", type:\\\"single\\\",\\n      q:\\\"How long will the campaign run?\\\",\\n      hint:\\\"\\\",\\n      options:[\\n        {v:\\\"oneoff\\\", l:\\\"One-time event\\\"},\\n        {v:\\\"short\\\", l:\\\"2-4 weeks\\\"},\\n        {v:\\\"medium\\\", l:\\\"1-3 months\\\"},\\n        {v:\\\"long\\\", l:\\\"3-6 months\\\"},\\n        {v:\\\"ongoing\\\", l:\\\"Always-on \/ ongoing\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"distribution\\\", tag:\\\"Distribution\\\", type:\\\"multi\\\",\\n      q:\\\"How will materials reach people?\\\",\\n      hint:\\\"Pick as many as apply.\\\",\\n      options:[\\n        {v:\\\"mail\\\", l:\\\"Direct mail\\\"},\\n        {v:\\\"handout\\\", l:\\\"In-store \/ hand-to-hand\\\"},\\n        {v:\\\"doortodoor\\\", l:\\\"Door-to-door\\\"},\\n        {v:\\\"event\\\", l:\\\"Events or trade shows\\\"},\\n        {v:\\\"partner\\\", l:\\\"Partner locations \/ co-op racks\\\"}\\n      ]\\n    },\\n    {\\n      key:\\\"attendees\\\", tag:\\\"Event size\\\", type:\\\"number\\\", options:[],\\n      q:\\\"About how many people will be at the event?\\\",\\n      hint:\\\"Optional - helps size flyer, catalog, and handout quantities against your actual turnout. Leave it blank to skip.\\\",\\n      showIf:function(){ return state.distribution.indexOf(\\\"event\\\") > -1; },\\n      numberField:{ id:\\\"attendeesInput\\\", label:\\\"Expected attendees\\\", placeholder:\\\"e.g. 200\\\" }\\n    }\\n  ];\\n\\n  var LABELS = {};\\n  STEPS.forEach(function(s){\\n    LABELS[s.key] = {};\\n    s.options.forEach(function(o){ LABELS[s.key][o.v] = o.l; });\\n  });\\n\\n  \/* ---------------- Config: product catalog ----------------\\n     Mirrors ChilliPrinting's actual printing-products lineup. startingPrice \/\\n     startingQty are REAL figures pulled directly from each product's own\\n     configurator (\\\"Total - incl. shipping\\\" at that product's own default,\\n     out-of-the-box configuration) - not estimates. They're shown to the\\n     visitor as a starting reference, clearly tied to their own quantity,\\n     since that quantity is very likely different from this plan's\\n     recommended quantity. unitCost\/minQty\/step\/cap are internal planning\\n     weights only (used to size quantities and split the stated budget\\n     across a realistic mix); where real per-unit data exists at a\\n     meaningful bulk quantity, unitCost is derived directly from it\\n     (startingPrice \/ startingQty) rather than guessed. perAttendee (where\\n     set) is an internal heuristic for sizing quantities against an\\n     expected event headcount - different product types get different\\n     take-rates. Every product still links out to its own configurator for\\n     an exact, final price at the visitor's chosen quantity and options. *\/\\n  var PRODUCTS = {\\n    flyers:{ name:\\\"Flyers\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\", 70lb gloss text stock, full color - minimum order 100\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/flyer-printing\/\\\",\\n      startingPrice:105.51, startingQty:100, unitCost:1.0551, minQty:100, step:100, cap:5000, perAttendee:0.6, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Cheap-Restaurant-Flyers.jpg\\\", color:\\\"#C2452F\\\",\\n      business:[\\\"events\\\",\\\"fitness\\\",\\\"restaurant\\\",\\\"retail\\\",\\\"trades\\\",\\\"other\\\"], objective:[\\\"foot-traffic\\\",\\\"lead-gen\\\",\\\"grand-opening\\\",\\\"promo-sale\\\"], distribution:[\\\"handout\\\",\\\"doortodoor\\\",\\\"event\\\"], audience:[\\\"neighborhood\\\",\\\"city\\\"] },\\n    postcards:{ name:\\\"Postcards\\\", spec:\\\"6\\\\\\\" \\u00d7 4\\\\\\\", 16pt matte card stock, full color both sides - minimum order 50\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/custom-postcards\/\\\",\\n      startingPrice:62.33, startingQty:50, unitCost:1.2466, minQty:100, step:100, cap:5000, perAttendee:0.4, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Business-Postcards.jpg\\\", color:\\\"#D98E3B\\\",\\n      business:[\\\"retail\\\",\\\"restaurant\\\",\\\"realestate\\\",\\\"beauty\\\",\\\"fitness\\\",\\\"other\\\"], objective:[\\\"promo-sale\\\",\\\"brand-awareness\\\",\\\"grand-opening\\\"], distribution:[\\\"mail\\\",\\\"partner\\\",\\\"handout\\\"], audience:[\\\"neighborhood\\\",\\\"city\\\",\\\"regional\\\",\\\"existing\\\"] },\\n    brochures:{ name:\\\"Brochures\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\" trifold (or other fold), gloss or matte text\/card stock - minimum order 100\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/brochure-printing\/\\\",\\n      startingPrice:110.03, startingQty:100, unitCost:1.1003, minQty:100, step:100, cap:5000, perAttendee:0.35, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/69e21fd45678e92f21bcf338_1cb230bf562f92a55971d89cda80314a_NYC-real-estate-agency-brochure.avif\\\", color:\\\"#3F6E5B\\\",\\n      business:[\\\"professional\\\",\\\"healthcare\\\",\\\"realestate\\\",\\\"events\\\"], objective:[\\\"lead-gen\\\",\\\"brand-awareness\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"event\\\",\\\"mail\\\"], audience:[\\\"b2b\\\",\\\"city\\\",\\\"regional\\\"] },\\n    catalogs:{ name:\\\"Catalogs\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\", multi-page, saddle-stitched, gloss text stock - minimum order 10\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/catalogs\/\\\",\\n      startingPrice:286.49, startingQty:50, unitCost:5.7298, minQty:50, step:50, cap:3000, perAttendee:0.3, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/69de6b43e21822529ce14b19_7f8bf7ad2416ad61a03f4ddca4793d0f_Furniture-catalog-design-premium-quality.avif\\\", color:\\\"#8A1E50\\\",\\n      business:[\\\"retail\\\",\\\"professional\\\",\\\"other\\\"], objective:[\\\"lead-gen\\\",\\\"brand-awareness\\\",\\\"promo-sale\\\"], distribution:[\\\"mail\\\",\\\"handout\\\",\\\"partner\\\",\\\"event\\\"], audience:[\\\"b2b\\\",\\\"city\\\",\\\"regional\\\",\\\"existing\\\"] },\\n    booklets:{ name:\\\"Booklets\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\", 8-320 pages, saddle-stitch or perfect binding - minimum order 10\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/booklets\/\\\",\\n      startingPrice:81.73, startingQty:1, unitCost:2.10, minQty:25, step:25, cap:2000, perAttendee:0.25, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/69b9d2c9d496274ae710aba4_cda992ecbcde351cd17a05f04b9ebe03_printed-cheap-custom-booklets.avif\\\", color:\\\"#5B4B8A\\\",\\n      business:[\\\"professional\\\",\\\"healthcare\\\",\\\"realestate\\\",\\\"events\\\",\\\"other\\\"], objective:[\\\"lead-gen\\\",\\\"brand-awareness\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"event\\\",\\\"mail\\\"], audience:[\\\"b2b\\\",\\\"city\\\",\\\"regional\\\"] },\\n    businesscards:{ name:\\\"Business Cards\\\", spec:\\\"3.5\\\\\\\" \\u00d7 2\\\\\\\", 14-16pt matte or gloss - minimum order 50\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/business-cards\/\\\",\\n      hidePrice:true, unitCost:1.2102, minQty:50, step:50, cap:2000, perAttendee:0.4, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/categories\/Standard-Business-Cards.jpg\\\", color:\\\"#2E6E8E\\\",\\n      business:[\\\"professional\\\",\\\"realestate\\\",\\\"beauty\\\",\\\"trades\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"retention\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"event\\\"], audience:[\\\"existing\\\",\\\"b2b\\\",\\\"neighborhood\\\"] },\\n    bookmarks:{ name:\\\"Bookmarks\\\", spec:\\\"2.5\\\\\\\" \\u00d7 9\\\\\\\", 14pt matte card stock, full color front & back - minimum order 250\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/bookmark-printing\/\\\",\\n      startingPrice:132.12, startingQty:250, unitCost:0.5285, minQty:250, step:250, cap:5000, perAttendee:0.3, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Custom-Bookmarks.jpg\\\", color:\\\"#B0563F\\\",\\n      business:[\\\"retail\\\",\\\"professional\\\",\\\"events\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"retention\\\"], distribution:[\\\"handout\\\",\\\"partner\\\"], audience:[\\\"existing\\\",\\\"neighborhood\\\",\\\"city\\\"] },\\n    stickers:{ name:\\\"Stickers & Labels\\\", spec:\\\"Custom die-cut or rectangle, 60lb gloss label stock - minimum order 100\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/stickers\/\\\",\\n      startingPrice:128.90, startingQty:100, unitCost:1.289, minQty:100, step:100, cap:5000, perAttendee:0.5, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Custom-Stickers.jpg\\\", color:\\\"#4C7A3E\\\",\\n      business:[\\\"retail\\\",\\\"restaurant\\\",\\\"events\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"retention\\\",\\\"promo-sale\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"event\\\"], audience:[\\\"existing\\\",\\\"neighborhood\\\"] },\\n    hangtags:{ name:\\\"Hang Tags\\\", spec:\\\"3.5\\\\\\\" \\u00d7 5.5\\\\\\\", 14pt matte card stock, pre-drilled hole - minimum order 250\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/custom-hang-tags-printing\/\\\",\\n      startingPrice:138.02, startingQty:250, unitCost:0.5521, minQty:250, step:250, cap:5000, perAttendee:0.3, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Cheap-Hang-Tags.jpg\\\", color:\\\"#A66B2E\\\",\\n      business:[\\\"retail\\\",\\\"beauty\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"promo-sale\\\"], distribution:[\\\"handout\\\",\\\"partner\\\"], audience:[\\\"existing\\\",\\\"neighborhood\\\"] },\\n    letterhead:{ name:\\\"Letterhead\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\", 70lb uncoated offset, full color - minimum order 250\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/letterhead-printing\/\\\",\\n      startingPrice:135.54, startingQty:250, unitCost:0.5422, minQty:250, step:250, cap:5000, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Custom-Letterhead.jpg\\\", color:\\\"#4A5A73\\\",\\n      business:[\\\"professional\\\",\\\"healthcare\\\",\\\"realestate\\\",\\\"trades\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"retention\\\"], distribution:[\\\"handout\\\",\\\"mail\\\",\\\"partner\\\"], audience:[\\\"b2b\\\",\\\"existing\\\"] },\\n    notepads:{ name:\\\"Notepads\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\", 50 sheets per pad, 70lb uncoated offset - minimum order 50 pads\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/notepad-printing\/\\\",\\n      startingPrice:366.84, startingQty:50, unitCost:7.3368, minQty:50, step:50, cap:1000, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Notepads-Printed.jpg\\\", color:\\\"#8C5B8A\\\",\\n      business:[\\\"professional\\\",\\\"healthcare\\\",\\\"realestate\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\",\\\"retention\\\"], distribution:[\\\"handout\\\",\\\"event\\\",\\\"partner\\\"], audience:[\\\"existing\\\",\\\"b2b\\\"] },\\n    calendars:{ name:\\\"Calendars\\\", spec:\\\"Wall or desk style, 13-14 page wire-O bound, coated stock - minimum order 10\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/custom-calendars\/\\\",\\n      startingPrice:150.62, startingQty:1, unitCost:4.50, minQty:25, step:25, cap:1000, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Custom-Wall-Calendars.jpg\\\", color:\\\"#2F7A72\\\",\\n      business:[\\\"retail\\\",\\\"professional\\\",\\\"realestate\\\",\\\"other\\\"], objective:[\\\"retention\\\",\\\"brand-awareness\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"mail\\\"], audience:[\\\"existing\\\",\\\"b2b\\\"] },\\n    carbonlessforms:{ name:\\\"Carbonless Form Sets\\\", spec:\\\"8.5\\\\\\\" \\u00d7 11\\\\\\\" or 5.5\\\\\\\" \\u00d7 8.5\\\\\\\", 2-4 part NCR sets - minimum order varies by configuration\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/ncr-carbonless-forms\/\\\",\\n      unitCost:0.35, minQty:100, step:100, cap:5000, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/Print-Professional-Carbonless-Forms.jpg\\\", color:\\\"#6B5B45\\\",\\n      business:[\\\"trades\\\",\\\"professional\\\",\\\"retail\\\",\\\"healthcare\\\",\\\"other\\\"], objective:[\\\"retention\\\"], distribution:[\\\"handout\\\"], audience:[\\\"existing\\\",\\\"b2b\\\"] },\\n    posters:{ name:\\\"Bulk Posters\\\", spec:\\\"18\\\\\\\" \\u00d7 24\\\\\\\" and up, offset printing for the lowest per-unit cost at volume - minimum order 25\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/poster-printing\/bulk-poster-printing\/\\\",\\n      startingPrice:197.16, startingQty:25, unitCost:7.8864, minQty:25, step:25, cap:500, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/products\/6a2806431bd78ef5a66ccc50_3ba95933bdae33cf1fed3c6f01b95ab0_Big-sales-bulk-posters.avif\\\", color:\\\"#C0392B\\\",\\n      business:[\\\"events\\\",\\\"retail\\\",\\\"restaurant\\\",\\\"fitness\\\",\\\"other\\\"], objective:[\\\"grand-opening\\\",\\\"foot-traffic\\\",\\\"promo-sale\\\",\\\"brand-awareness\\\"], distribution:[\\\"event\\\",\\\"handout\\\",\\\"partner\\\"], audience:[\\\"neighborhood\\\",\\\"city\\\",\\\"regional\\\"] },\\n    cdcovers:{ name:\\\"CD Booklets & Covers\\\", spec:\\\"Folded inserts or saddle-stitched booklets sized for standard jewel cases - minimum order 50 (stapled) \/ 100 (folded)\\\", url:\\\"https:\/\/www.chilliprinting.com\/printing-products\/cd-covers\/\\\",\\n      startingPrice:207.90, startingQty:100, unitCost:2.079, minQty:50, step:50, cap:2000, img:\\\"https:\/\/cdn.chilliprinting.com\/pics\/1\/media\/banners\/6a4281b5d0e9d55de3c83b1c_cd-booklets-covers-inserts-category-banner.avif\\\", color:\\\"#5C6B73\\\",\\n      business:[\\\"events\\\",\\\"other\\\"], objective:[\\\"brand-awareness\\\"], distribution:[\\\"handout\\\",\\\"partner\\\",\\\"event\\\"], audience:[\\\"existing\\\",\\\"city\\\"] }\\n  };\\n\\n  var BUDGET_MAX = { \\\"under500\\\":500, \\\"500-1500\\\":1500, \\\"1500-5000\\\":5000, \\\"5000-15000\\\":15000, \\\"15000plus\\\":25000 };\\n  var BUDGET_TOPN = { \\\"under500\\\":2, \\\"500-1500\\\":3, \\\"1500-5000\\\":4, \\\"5000-15000\\\":5, \\\"15000plus\\\":6 };\\n\\n  \/* ---------------- State ---------------- *\/\\n  var state = { businessType:null, objective:null, audience:null, reach:null, city:\\\"\\\", budget:null, duration:null, distribution:[], attendees:null, manualExclude:{}, manualAdds:[] };\\n  var current = 0;\\n\\n  var els = {\\n    progressFill: document.getElementById(\\\"progressFill\\\"),\\n    stepTag: document.getElementById(\\\"stepTag\\\"),\\n    stepCount: document.getElementById(\\\"stepCount\\\"),\\n    stepQ: document.getElementById(\\\"stepQ\\\"),\\n    stepHint: document.getElementById(\\\"stepHint\\\"),\\n    optionsGrid: document.getElementById(\\\"optionsGrid\\\"),\\n    textFieldWrap: document.getElementById(\\\"textFieldWrap\\\"),\\n    textFieldLabel: document.getElementById(\\\"textFieldLabel\\\"),\\n    cityInput: document.getElementById(\\\"cityInput\\\"),\\n    numberFieldWrap: document.getElementById(\\\"numberFieldWrap\\\"),\\n    numberFieldLabel: document.getElementById(\\\"numberFieldLabel\\\"),\\n    attendeesInput: document.getElementById(\\\"attendeesInput\\\"),\\n    backBtn: document.getElementById(\\\"backBtn\\\"),\\n    nextBtn: document.getElementById(\\\"nextBtn\\\"),\\n    wizard: document.getElementById(\\\"wizard\\\"),\\n    results: document.getElementById(\\\"results\\\")\\n  };\\n\\n  function isVisible(s){ return !s.showIf || s.showIf(); }\\n  function visibleSteps(){ return STEPS.filter(isVisible); }\\n  function visiblePosition(idx){\\n    var pos = 0;\\n    for (var i = 0; i <= idx; i++){ if (isVisible(STEPS[i])) pos++; }\\n    return pos;\\n  }\\n\\n  function renderStep(){\\n    var s = STEPS[current];\\n    var total = visibleSteps().length;\\n    var pos = visiblePosition(current);\\n    els.progressFill.style.width = ((pos-1)\/total*100) + \\\"%\\\";\\n    els.stepTag.textContent = s.tag;\\n    els.stepCount.textContent = \\\"Step \\\" + pos + \\\" \/ \\\" + total;\\n    els.stepQ.textContent = s.q;\\n    els.stepHint.textContent = s.hint || \\\"\\\";\\n    els.optionsGrid.innerHTML = \\\"\\\";\\n\\n    s.options.forEach(function(o){\\n      var btn = document.createElement(\\\"button\\\");\\n      btn.type = \\\"button\\\";\\n      btn.className = \\\"opt\\\";\\n      btn.setAttribute(\\\"data-multi\\\", s.type === \\\"multi\\\" ? \\\"true\\\" : \\\"false\\\");\\n      var selected = s.type === \\\"multi\\\" ? state[s.key].indexOf(o.v) > -1 : state[s.key] === o.v;\\n      if (selected) btn.classList.add(\\\"selected\\\");\\n      btn.innerHTML = '<span>'+o.l+'<\\\/span><span class=\\\"check\\\" aria-hidden=\\\"true\\\"><\\\/span>';\\n      btn.addEventListener(\\\"click\\\", function(){\\n        if (s.type === \\\"multi\\\"){\\n          var idx = state[s.key].indexOf(o.v);\\n          if (idx > -1) state[s.key].splice(idx,1); else state[s.key].push(o.v);\\n        } else {\\n          state[s.key] = o.v;\\n        }\\n        renderStep();\\n      });\\n      els.optionsGrid.appendChild(btn);\\n    });\\n\\n    if (s.textField){\\n      els.textFieldWrap.hidden = false;\\n      els.textFieldLabel.textContent = s.textField.label;\\n      els.cityInput.value = state.city || \\\"\\\";\\n    } else {\\n      els.textFieldWrap.hidden = true;\\n    }\\n\\n    if (s.numberField){\\n      els.numberFieldWrap.hidden = false;\\n      els.numberFieldLabel.textContent = s.numberField.label;\\n      els.attendeesInput.placeholder = s.numberField.placeholder || \\\"\\\";\\n      els.attendeesInput.value = state[s.key] || \\\"\\\";\\n    } else {\\n      els.numberFieldWrap.hidden = true;\\n    }\\n\\n    els.backBtn.disabled = pos === 1;\\n    var valid;\\n    if (s.type === \\\"multi\\\") valid = state[s.key].length > 0;\\n    else if (s.type === \\\"number\\\") valid = true; \/* optional field *\/\\n    else valid = !!state[s.key];\\n    els.nextBtn.disabled = !valid;\\n    els.nextBtn.textContent = pos === total ? \\\"Build my plan\\\" : \\\"Next\\\";\\n  }\\n\\n  els.cityInput.addEventListener(\\\"input\\\", function(){ state.city = els.cityInput.value; });\\n  els.attendeesInput.addEventListener(\\\"input\\\", function(){\\n    var n = parseInt(els.attendeesInput.value, 10);\\n    state.attendees = (n > 0) ? n : null;\\n  });\\n\\n  els.backBtn.addEventListener(\\\"click\\\", function(){\\n    var idx = current;\\n    do { idx--; } while (idx >= 0 && !isVisible(STEPS[idx]));\\n    if (idx >= 0){ current = idx; renderStep(); }\\n  });\\n\\n  els.nextBtn.addEventListener(\\\"click\\\", function(){\\n    var idx = current;\\n    do { idx++; } while (idx < STEPS.length && !isVisible(STEPS[idx]));\\n    if (idx < STEPS.length){ current = idx; renderStep(); }\\n    else { showResults(); }\\n  });\\n\\n  \/* ---------------- Plan generation ---------------- *\/\\n  function scoreProduct(key, p){\\n    var score = 0;\\n    if (p.business.indexOf(state.businessType) > -1) score += 3;\\n    if (p.objective.indexOf(state.objective) > -1) score += 3;\\n    if (p.audience.indexOf(state.audience) > -1) score += 2;\\n    state.distribution.forEach(function(d){ if (p.distribution.indexOf(d) > -1) score += 3; });\\n    return score;\\n  }\\n\\n  function money(n){\\n    return \\\"$\\\" + n.toLocaleString(\\\"en-US\\\", {minimumFractionDigits:0, maximumFractionDigits:0});\\n  }\\n\\n  function generatePlan(){\\n    \/\/ Products the visitor has manually removed (the \\\"-\\\" button on a\\n    \/\/ recommended product) are dropped from candidate scoring entirely, so\\n    \/\/ the next-best-scoring product naturally slides into their place -\\n    \/\/ rather than just leaving a gap.\\n    var scored = Object.keys(PRODUCTS).filter(function(key){\\n      return !state.manualExclude[key];\\n    }).map(function(key){\\n      return { key:key, p:PRODUCTS[key], score:scoreProduct(key, PRODUCTS[key]) };\\n    }).sort(function(a,b){ return b.score - a.score; });\\n\\n    var topN = BUDGET_TOPN[state.budget] || 3;\\n    var chosen = scored.filter(function(s){ return s.score > 0; }).slice(0, topN);\\n    if (chosen.length === 0) chosen = scored.slice(0, topN);\\n\\n    var budget = BUDGET_MAX[state.budget];\\n    var designFee = Math.max(60, Math.min(400, Math.round(budget * 0.12)));\\n    var remaining = budget - designFee;\\n\\n    var mailingFee = 0;\\n    var needsMailPrep = state.distribution.indexOf(\\\"mail\\\") > -1 && !chosen.some(function(c){ return c.key === \\\"eddm\\\"; });\\n    if (needsMailPrep){\\n      mailingFee = Math.max(80, Math.min(600, Math.round(remaining * 0.15)));\\n      remaining -= mailingFee;\\n    }\\n\\n    var allocated = [];\\n    var pool = remaining;\\n    chosen.forEach(function(c){\\n      var minCost = c.p.flat ? c.p.unitCost : c.p.unitCost * c.p.minQty;\\n      if (pool >= minCost){\\n        allocated.push({ key:c.key, p:c.p, score:c.score, qty: c.p.flat ? 1 : c.p.minQty, cost: minCost });\\n        pool -= minCost;\\n      }\\n    });\\n\\n    if (allocated.length === 0 && chosen.length > 0){\\n      var cheapest = chosen.slice().sort(function(a,b){\\n        return (a.p.flat ? a.p.unitCost : a.p.unitCost*a.p.minQty) - (b.p.flat ? b.p.unitCost : b.p.unitCost*b.p.minQty);\\n      })[0];\\n      var c = cheapest.p.flat ? cheapest.p.unitCost : cheapest.p.unitCost * cheapest.p.minQty;\\n      allocated.push({ key:cheapest.key, p:cheapest.p, score:cheapest.score, qty: cheapest.p.flat ? 1 : cheapest.p.minQty, cost:c });\\n      pool -= c;\\n    }\\n\\n    \/* If this is an in-person event with a headcount, prioritize bringing\\n       attendee-facing products (flyers, catalogs, brochures, booklets, etc.)\\n       up toward a realistic per-attendee quantity before spreading whatever\\n       budget is left proportionally. Take-rates differ by product - a flyer\\n       handout rate is much higher than a full catalog or booklet, which is\\n       why each product carries its own perAttendee figure rather than one\\n       shared ratio. *\/\\n    if (state.attendees && state.distribution.indexOf(\\\"event\\\") > -1){\\n      allocated.forEach(function(a){\\n        if (!a.p.perAttendee) return;\\n        var idealQty = Math.min(a.p.cap, Math.ceil((state.attendees * a.p.perAttendee) \/ a.p.step) * a.p.step);\\n        a.idealQty = idealQty;\\n        if (idealQty <= a.qty) return;\\n        var extraQty = idealQty - a.qty;\\n        var extraCost = extraQty * a.p.unitCost;\\n        if (extraCost > pool){\\n          var affordableSteps = Math.floor(pool \/ (a.p.unitCost * a.p.step));\\n          extraQty = affordableSteps * a.p.step;\\n          extraCost = extraQty * a.p.unitCost;\\n        }\\n        if (extraQty > 0){ a.qty += extraQty; a.cost += extraCost; pool -= extraCost; }\\n      });\\n    }\\n\\n    var totalScore = allocated.reduce(function(s,a){ return s + a.score; }, 0) || 1;\\n    if (pool > 0){\\n      allocated.forEach(function(a){\\n        var share = pool * (a.score \/ totalScore);\\n        if (a.p.flat){\\n          var extraUnits = Math.min(a.p.cap - a.qty, Math.floor(share \/ a.p.unitCost));\\n          if (extraUnits > 0){ a.qty += extraUnits; a.cost += extraUnits * a.p.unitCost; }\\n        } else {\\n          var extraSteps = Math.floor(share \/ (a.p.unitCost * a.p.step));\\n          var extraQty = Math.min(a.p.cap - a.qty, extraSteps * a.p.step);\\n          if (extraQty > 0){ a.qty += extraQty; a.cost += extraQty * a.p.unitCost; }\\n        }\\n      });\\n    }\\n\\n    var spentOnProducts = allocated.reduce(function(s,a){ return s + a.cost; }, 0);\\n    var contingency = Math.max(0, remaining - spentOnProducts);\\n\\n    return {\\n      allocated: allocated,\\n      designFee: designFee,\\n      mailingFee: mailingFee,\\n      contingency: contingency,\\n      budget: budget\\n    };\\n  }\\n\\n  function reasonFor(key, p){\\n    var bits = [];\\n    if (p.business.indexOf(state.businessType) > -1) bits.push(\\\"fits a \\\" + LABELS.businessType[state.businessType].toLowerCase() + \\\" business\\\");\\n    if (p.objective.indexOf(state.objective) > -1) bits.push(\\\"supports \\\" + LABELS.objective[state.objective].toLowerCase());\\n    var distMatches = state.distribution.filter(function(d){ return p.distribution.indexOf(d) > -1; });\\n    if (distMatches.length) bits.push(\\\"works for \\\" + distMatches.map(function(d){ return LABELS.distribution[d].toLowerCase(); }).join(\\\" and \\\"));\\n    var reason = bits.length ? (bits.join(\\\", \\\").charAt(0).toUpperCase() + bits.join(\\\", \\\").slice(1) + \\\".\\\") : \\\"A solid all-around addition for this budget tier.\\\";\\n    if (state.attendees && state.distribution.indexOf(\\\"event\\\") > -1 && p.perAttendee){\\n      reason += \\\" Quantity scales with your expected \\\" + state.attendees.toLocaleString(\\\"en-US\\\") + \\\" attendees (~\\\" + p.perAttendee + \\\" per person).\\\";\\n    }\\n    return reason;\\n  }\\n\\n  \/\/ Neutral (non-product) segment colors for the budget donut\/legend - kept\\n  \/\/ apart from the per-product brand colors on PRODUCTS[key].color.\\n  var NEUTRALS = { design:\\\"#8C7B70\\\", mailing:\\\"#A98CA0\\\", contingency:\\\"#D8CFC9\\\" };\\n\\n  \/\/ Builds a thick-ring donut chart as an SVG string, plus a center label and\\n  \/\/ a tooltip element used by the mouseover handlers wired up after render.\\n  function buildDonut(segs, budget, materialCount){\\n    var size = 168, r = 50, cx = size\/2, cy = size\/2, sw = 36; \/\/ thick ring relative to radius\\n    var circumference = 2 * Math.PI * r;\\n    var cumulative = 0;\\n    var circles = \\\"\\\";\\n    segs.forEach(function(s){\\n      var frac = budget > 0 ? (s.amount \/ budget) : 0;\\n      var segLen = frac * circumference;\\n      if (segLen <= 0.001) return;\\n      var dasharray = segLen.toFixed(2) + \\\" \\\" + (circumference - segLen).toFixed(2);\\n      var dashoffset = (-cumulative).toFixed(2);\\n      cumulative += segLen;\\n      var pct = Math.round(frac * 100);\\n      circles += '<circle class=\\\"donut-seg\\\" cx=\\\"' + cx + '\\\" cy=\\\"' + cy + '\\\" r=\\\"' + r + '\\\" fill=\\\"none\\\" stroke=\\\"' +\\n        s.color + '\\\" stroke-width=\\\"' + sw + '\\\" stroke-dasharray=\\\"' + dasharray + '\\\" stroke-dashoffset=\\\"' + dashoffset +\\n        '\\\" pointer-events=\\\"stroke\\\" data-name=\\\"' + s.label.replace(\/\\\"\/g,\\\"&quot;\\\") + '\\\" data-pct=\\\"' + pct + '\\\"><title>' +\\n        s.label + \\\" - \\\" + pct + '%<\\\/title><\\\/circle>';\\n    });\\n    var svg = '<svg viewBox=\\\"0 0 ' + size + ' ' + size + '\\\" role=\\\"img\\\" aria-label=\\\"Budget allocation\\\">' +\\n      '<circle cx=\\\"' + cx + '\\\" cy=\\\"' + cy + '\\\" r=\\\"' + r + '\\\" fill=\\\"none\\\" stroke=\\\"var(--surface-2)\\\" stroke-width=\\\"' + sw + '\\\"><\\\/circle>' +\\n      '<g transform=\\\"rotate(-90 ' + cx + ' ' + cy + ')\\\">' + circles + '<\\\/g>' +\\n      '<\\\/svg>';\\n    return '<div class=\\\"donut-wrap\\\" id=\\\"donutWrap\\\">' + svg +\\n      '<div class=\\\"donut-center\\\"><span class=\\\"n\\\">' + materialCount + '<\\\/span><span class=\\\"l\\\">material' + (materialCount === 1 ? \\\"\\\" : \\\"s\\\") + '<\\\/span><\\\/div>' +\\n      '<div class=\\\"donut-tooltip\\\" id=\\\"donutTooltip\\\"><\\\/div>' +\\n      '<\\\/div>';\\n  }\\n\\n  function showResults(){\\n    els.wizard.hidden = true;\\n    els.progressFill.style.width = \\\"100%\\\";\\n    var plan = generatePlan();\\n    var cityText = state.city && state.city.trim() ? state.city.trim() : null;\\n\\n    var html = \\\"\\\";\\n    html += '<div style=\\\"margin:-30px -26px -26px;\\\">';\\n    html += '  <div class=\\\"ticket-head\\\">';\\n    html += '    <h2 class=\\\"ticket-title\\\">Your print campaign plan<\\\/h2>';\\n    html += '    <div class=\\\"chip-row\\\">';\\n    html += '      <span class=\\\"chip\\\">' + LABELS.businessType[state.businessType] + '<\\\/span>';\\n    html += '      <span class=\\\"chip\\\">' + LABELS.objective[state.objective] + '<\\\/span>';\\n    html += '      <span class=\\\"chip\\\">' + LABELS.audience[state.audience] + (cityText ? \\\" \\u00b7 \\\" + cityText : \\\"\\\") + '<\\\/span>';\\n    html += '      <span class=\\\"chip\\\">' + LABELS.budget[state.budget] + '<\\\/span>';\\n    html += '      <span class=\\\"chip\\\">' + LABELS.duration[state.duration] + '<\\\/span>';\\n    html += '    <\\\/div>';\\n    html += '  <\\\/div>';\\n    html += '  <div class=\\\"ticket-body\\\">';\\n\\n    html += '  <div class=\\\"section\\\">';\\n    html += '    <div class=\\\"section-title\\\">Recommended products<\\\/div>';\\n    html += '    <p class=\\\"disclaimer\\\" style=\\\"margin-top:-8px; margin-bottom:14px;\\\">\\\"From\\\" prices are ChilliPrinting\\\\'s own starting rate at their default quantity - not this plan\\\\'s recommended quantity. Configure each product to price it exactly.<\\\/p>';\\n    html += '    <div class=\\\"products\\\">';\\n    plan.allocated.forEach(function(a){\\n      var qtyLabel = a.p.flat ? (a.qty + (a.qty === 1 ? \\\" unit\\\" : \\\" units\\\")) : (a.qty.toLocaleString(\\\"en-US\\\") + \\\" items\\\");\\n      html += '    <div class=\\\"product\\\">';\\n      if (a.p.img){\\n        html += '      <img class=\\\"product-image\\\" src=\\\"' + a.p.img + '\\\" alt=\\\"' + a.p.name + '\\\" loading=\\\"lazy\\\">';\\n      }\\n      html += '      <button type=\\\"button\\\" class=\\\"product-remove\\\" data-remove-key=\\\"' + a.key + '\\\" title=\\\"Remove ' + a.p.name + '\\\" aria-label=\\\"Remove ' + a.p.name + '\\\">&times;<\\\/button>';\\n      html += '      <div class=\\\"product-body\\\">';\\n      html += '        <div class=\\\"product-name\\\">' + a.p.name + '<\\\/div>';\\n      html += '        <div class=\\\"product-qty\\\">' + qtyLabel + '<\\\/div>';\\n      html += '        <div class=\\\"product-spec\\\">' + a.p.spec + '<\\\/div>';\\n      html += '        <div class=\\\"price-col\\\">';\\n      if (a.p.startingPrice && !a.p.hidePrice){\\n        html += '          <div class=\\\"starting-price\\\">From <b>$' + a.p.startingPrice.toFixed(2) + '<\\\/b> incl. shipping<br>(chilliprinting.com, qty ' + a.p.startingQty + ')<\\\/div>';\\n      }\\n      html += '          <a class=\\\"configure-link\\\" href=\\\"' + a.p.url + '\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Configure to see prices<svg viewBox=\\\"0 0 24 24\\\" fill=\\\"none\\\" stroke=\\\"currentColor\\\" stroke-width=\\\"2.5\\\"><path d=\\\"M7 17L17 7M8 7h9v9\\\"\/><\\\/svg><\\\/a>';\\n      html += '        <\\\/div>';\\n      html += '        <div class=\\\"product-why\\\">' + reasonFor(a.key, a.p) + '<\\\/div>';\\n      html += '      <\\\/div>';\\n      html += '    <\\\/div>';\\n    });\\n    html += '    <\\\/div>';\\n\\n    var allocatedKeys = plan.allocated.map(function(a){ return a.key; });\\n    var addable = Object.keys(PRODUCTS).filter(function(key){\\n      return allocatedKeys.indexOf(key) === -1 && state.manualAdds.indexOf(key) === -1;\\n    });\\n    html += '    <div class=\\\"add-more-row\\\">';\\n    html += '      <button class=\\\"btn btn-ghost\\\" id=\\\"addMoreBtn\\\" type=\\\"button\\\">+ Add more products<\\\/button>';\\n    if (addable.length){\\n      html += '      <select class=\\\"product-select\\\" id=\\\"addProductSelect\\\">';\\n      addable.forEach(function(key){\\n        html += '        <option value=\\\"' + key + '\\\">' + PRODUCTS[key].name + '<\\\/option>';\\n      });\\n      html += '      <\\\/select>';\\n    } else {\\n      html += '      <span class=\\\"disclaimer\\\" style=\\\"margin:0;\\\">Every product is already in your plan.<\\\/span>';\\n    }\\n    html += '    <\\\/div>';\\n    if (state.manualAdds.length){\\n      html += '    <div class=\\\"add-cards\\\">';\\n      state.manualAdds.forEach(function(key){\\n        var p = PRODUCTS[key];\\n        if (!p) return;\\n        html += '      <div class=\\\"add-card\\\">';\\n        html += '        <span class=\\\"add-card-name\\\">' + p.name + '<\\\/span>';\\n        html += '        <div class=\\\"add-card-actions\\\">';\\n        html += '          <a class=\\\"btn btn-primary\\\" style=\\\"padding:8px 16px; font-size:12.5px;\\\" href=\\\"' + p.url + '\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Configure the product<\\\/a>';\\n        html += '          <button type=\\\"button\\\" class=\\\"product-remove\\\" style=\\\"position:static;\\\" data-remove-add=\\\"' + key + '\\\" title=\\\"Remove\\\" aria-label=\\\"Remove ' + p.name + '\\\">&times;<\\\/button>';\\n        html += '        <\\\/div>';\\n        html += '      <\\\/div>';\\n      });\\n      html += '    <\\\/div>';\\n    }\\n    html += '  <\\\/div>';\\n\\n    var attendeeScaled = plan.allocated.filter(function(a){ return typeof a.idealQty === \\\"number\\\"; });\\n    var underBudget = attendeeScaled.filter(function(a){ return a.idealQty > a.qty; });\\n    var idealList = underBudget.length ? attendeeScaled.map(function(a){ return a.idealQty.toLocaleString(\\\"en-US\\\") + \\\" \\\" + a.p.name.toLowerCase(); }).join(\\\", \\\") : \\\"\\\";\\n\\n    var segs = [];\\n    segs.push({label:\\\"Design & setup\\\", amount:plan.designFee, color:NEUTRALS.design});\\n    if (plan.mailingFee > 0) segs.push({label:\\\"Mailing \/ postage prep\\\", amount:plan.mailingFee, color:NEUTRALS.mailing});\\n    plan.allocated.forEach(function(a){ segs.push({label:a.p.name, amount:Math.round(a.cost), color:a.p.color || \\\"var(--cp-primary)\\\", url:a.p.url}); });\\n    if (plan.contingency > 5) segs.push({label:\\\"Contingency \/ rounding\\\", amount:Math.round(plan.contingency), color:NEUTRALS.contingency});\\n\\n    html += '  <div class=\\\"section\\\">';\\n    html += '    <div class=\\\"section-title\\\">Budget allocation<\\\/div>';\\n    html += '    <div class=\\\"budget-grid\\\">';\\n    html += '      ' + buildDonut(segs, plan.budget, plan.allocated.length);\\n    html += '      <div class=\\\"budget-summary\\\">';\\n    html += '        <p class=\\\"lede\\\">Based on your chosen budget, you can print ' + plan.allocated.length + ' printed material' + (plan.allocated.length === 1 ? \\\"\\\" : \\\"s\\\") + '.<\\\/p>';\\n    if (underBudget.length){\\n      html += '        <p>However, based on your expected ' + state.attendees.toLocaleString(\\\"en-US\\\") + ' attendees, we\\\\'d recommend printing ' + idealList + ' for the best results - that may mean stretching the budget a bit further.<\\\/p>';\\n    }\\n    html += '      <\\\/div>';\\n    html += '    <\\\/div>';\\n    html += '    <div class=\\\"budget-legend\\\">';\\n    segs.forEach(function(s){\\n      var pct = Math.round(s.amount \/ plan.budget * 100);\\n      var right = s.url\\n        ? '<a class=\\\"configure-link\\\" href=\\\"' + s.url + '\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Configure to see prices<\\\/a>'\\n        : '<span class=\\\"amount\\\">' + pct + '%<\\\/span>';\\n      html += '  <div class=\\\"budget-row\\\"><span class=\\\"swatch\\\" style=\\\"background:' + s.color + ';\\\"><\\\/span><span class=\\\"label\\\">' + s.label + ' - ' + pct + '% of budget<\\\/span>' + right + '<\\\/div>';\\n    });\\n    html += '    <\\\/div>';\\n    html += '    <div class=\\\"budget-total\\\"><span>Total<\\\/span><span class=\\\"amount\\\">Configure to see prices<\\\/span><\\\/div>';\\n    html += '    <p class=\\\"disclaimer\\\">The final cost depends on the chosen features and delivery options.<\\\/p>';\\n    html += '  <\\\/div>';\\n\\n    html += '  <div class=\\\"section\\\">';\\n    html += '    <div class=\\\"section-title\\\">Printing Process<\\\/div>';\\n    html += '    <ul class=\\\"timeline\\\">';\\n    var topProduct = plan.allocated.length ? plan.allocated[0].p.name.toLowerCase() : \\\"product\\\";\\n    var processSteps = [\\n      { phase:\\\"Step 01\\\", title:\\\"Choose Your Specifications\\\", detail:\\\"Select your \\\" + topProduct + \\\"'s preferred size, paper, and finishing options in our configurator, then review production time and the estimated delivery date.\\\" },\\n      { phase:\\\"Step 02\\\", title:\\\"Add to Cart & Upload Your File\\\", detail:\\\"Click \\u201cAdd to Cart & Upload Your File\\u201d to submit your artwork. For multiple versions, duplicate the item and edit details.\\\" },\\n      { phase:\\\"Step 03\\\", title:\\\"Review Digital Proof & Go to Checkout\\\", detail:\\\"Download your free PDF proof to make sure your print looks exactly as expected, then complete the checkout process.\\\" }\\n    ];\\n    processSteps.forEach(function(t){\\n      html += '  <li class=\\\"tl-item\\\"><div class=\\\"tl-dot\\\"><\\\/div><div><div class=\\\"tl-phase\\\">' + t.phase + '<\\\/div><div class=\\\"tl-title\\\">' + t.title + '<\\\/div><div class=\\\"tl-detail\\\">' + t.detail + '<\\\/div><\\\/div><\\\/li>';\\n    });\\n    html += '    <\\\/ul>';\\n    html += '  <\\\/div>';\\n\\n    html += '  <div class=\\\"section\\\" style=\\\"margin-bottom:0;\\\">';\\n    html += '    <div class=\\\"cta-row\\\">';\\n    html += '      <a class=\\\"btn btn-primary\\\" href=\\\"https:\/\/www.chilliprinting.com\/printing-products\/\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Explore All Products<\\\/a>';\\n    html += '      <a class=\\\"btn btn-ghost\\\" href=\\\"https:\/\/www.chilliprinting.com\/contact-us\/\\\" target=\\\"_blank\\\" rel=\\\"noopener\\\">Contact Us<\\\/a>';\\n    html += '      <button class=\\\"btn btn-ghost\\\" id=\\\"restartBtn\\\" type=\\\"button\\\">Start over<\\\/button>';\\n    html += '    <\\\/div>';\\n    html += '  <\\\/div>';\\n\\n    html += '  <\\\/div>';\\n    html += '<\\\/div>';\\n\\n    els.results.innerHTML = html;\\n    els.results.hidden = false;\\n\\n    document.getElementById(\\\"restartBtn\\\").addEventListener(\\\"click\\\", function(){\\n      state = { businessType:null, objective:null, audience:null, reach:null, city:\\\"\\\", budget:null, duration:null, distribution:[], attendees:null, manualExclude:{}, manualAdds:[] };\\n      current = 0;\\n      els.results.hidden = true;\\n      els.wizard.hidden = false;\\n      renderStep();\\n      window.scrollTo({top:0, behavior:\\\"smooth\\\"});\\n    });\\n\\n    \/\/ Remove a recommended product -> drop it from scoring and re-render;\\n    \/\/ the next-best-scoring product naturally fills the freed budget.\\n    var removeButtons = document.querySelectorAll(\\\".product-remove[data-remove-key]\\\");\\n    for (var ri = 0; ri < removeButtons.length; ri++){\\n      removeButtons[ri].addEventListener(\\\"click\\\", function(e){\\n        state.manualExclude[e.currentTarget.getAttribute(\\\"data-remove-key\\\")] = true;\\n        showResults();\\n      });\\n    }\\n\\n    \/\/ \\\"Add more products\\\" - reveals a standalone Configure card for a\\n    \/\/ product outside the algorithm's picks, without touching the budget.\\n    var addMoreBtn = document.getElementById(\\\"addMoreBtn\\\");\\n    var addSelect = document.getElementById(\\\"addProductSelect\\\");\\n    if (addMoreBtn && addSelect){\\n      addMoreBtn.addEventListener(\\\"click\\\", function(){\\n        var key = addSelect.value;\\n        if (!key) return;\\n        if (state.manualAdds.indexOf(key) === -1) state.manualAdds.push(key);\\n        showResults();\\n      });\\n    }\\n    var addRemoveButtons = document.querySelectorAll(\\\"[data-remove-add]\\\");\\n    for (var ai = 0; ai < addRemoveButtons.length; ai++){\\n      addRemoveButtons[ai].addEventListener(\\\"click\\\", function(e){\\n        var key = e.currentTarget.getAttribute(\\\"data-remove-add\\\");\\n        var idx = state.manualAdds.indexOf(key);\\n        if (idx > -1) state.manualAdds.splice(idx, 1);\\n        showResults();\\n      });\\n    }\\n\\n    \/\/ Donut hover tooltip: one shared floating label, positioned relative\\n    \/\/ to the chart, showing \\\"<name> - <pct>%\\\" for whichever ring segment\\n    \/\/ the pointer is over.\\n    var donutWrap = document.getElementById(\\\"donutWrap\\\");\\n    var donutTooltip = document.getElementById(\\\"donutTooltip\\\");\\n    if (donutWrap && donutTooltip){\\n      var donutSegs = donutWrap.querySelectorAll(\\\".donut-seg\\\");\\n      for (var di = 0; di < donutSegs.length; di++){\\n        donutSegs[di].addEventListener(\\\"mousemove\\\", function(e){\\n          var rect = donutWrap.getBoundingClientRect();\\n          donutTooltip.style.left = (e.clientX - rect.left) + \\\"px\\\";\\n          donutTooltip.style.top = (e.clientY - rect.top) + \\\"px\\\";\\n          donutTooltip.textContent = e.currentTarget.getAttribute(\\\"data-name\\\") + \\\" - \\\" + e.currentTarget.getAttribute(\\\"data-pct\\\") + \\\"%\\\";\\n          donutTooltip.classList.add(\\\"show\\\");\\n        });\\n        donutSegs[di].addEventListener(\\\"mouseleave\\\", function(){\\n          donutTooltip.classList.remove(\\\"show\\\");\\n        });\\n      }\\n    }\\n  }\\n\\n  state.distribution = [];\\n  renderStep();\\n\\n  var start = function(data){ if (data && data.state) { \/* no persisted state to restore in this tool *\/ } };\\n  if (window.claude && window.claude.hot){\\n    window.claude.hot.ready ? window.claude.hot.ready(function(){}) : null;\\n  }\\n})();\\n<\\\/script>\\n\\n<\\\/body>\\n<\\\/html>\";\r\n\r\n  \/\/ WordPress\/Elementor themes auto-render the page's own title as an H1\r\n  \/\/ above the content (this page's title is set to match ours). Having two\r\n  \/\/ identical H1s on the page is a duplicate-heading SEO issue, so hide any\r\n  \/\/ other H1 whose text matches ours, found by text match rather than a\r\n  \/\/ guessed theme selector (works across any theme markup, and keeps\r\n  \/\/ working if the page title text changes later).\r\n  function hideDuplicatePageTitle(){\r\n    try {\r\n      var holderEl = document.getElementById('cpp-planner-holder');\r\n      var ownH1 = holderEl && holderEl.previousElementSibling;\r\n      var ownText = ownH1 ? ownH1.textContent.trim().toLowerCase() : '';\r\n      var allH1s = document.querySelectorAll('h1');\r\n      for (var i = 0; i < allH1s.length; i++) {\r\n        var h = allH1s[i];\r\n        if (h === ownH1) continue;\r\n        var t = h.textContent ? h.textContent.trim().toLowerCase() : '';\r\n        if (t === 'tool idea' || (ownText && t === ownText)) {\r\n          h.style.setProperty('display', 'none', 'important');\r\n        }\r\n      }\r\n    } catch(e){}\r\n  }\r\n  \/\/ Run now, again after full load (in case a theme script swaps the title\r\n  \/\/ element in late), and keep watching in case anything re-shows it.\r\n  hideDuplicatePageTitle();\r\n  window.addEventListener('load', hideDuplicatePageTitle);\r\n  setTimeout(hideDuplicatePageTitle, 500);\r\n  setTimeout(hideDuplicatePageTitle, 1500);\r\n  try {\r\n    if (window.MutationObserver){\r\n      new MutationObserver(hideDuplicatePageTitle).observe(document.body, {childList:true, subtree:true});\r\n    }\r\n  } catch(e){}\r\n\r\n  var holder = document.getElementById('cpp-planner-holder');\r\n  var iframe = document.createElement('iframe');\r\n  iframe.title = 'Print Campaign Planner';\r\n  iframe.style.width = '100%';\r\n  iframe.style.border = '0';\r\n  iframe.style.display = 'block';\r\n  iframe.style.minHeight = '760px';\r\n  holder.appendChild(iframe);\r\n\r\n  var doc = iframe.contentWindow.document;\r\n  doc.open();\r\n  doc.write(html);\r\n  doc.close();\r\n\r\n  function resizeFrame(){\r\n    try {\r\n      var h = doc.documentElement.scrollHeight;\r\n      if (h) iframe.style.height = (h + 24) + 'px';\r\n    } catch(e){}\r\n  }\r\n\r\n  iframe.addEventListener('load', function(){\r\n    resizeFrame();\r\n    setTimeout(resizeFrame, 250);\r\n    setTimeout(resizeFrame, 1000);\r\n    try {\r\n      if (window.ResizeObserver) {\r\n        new ResizeObserver(resizeFrame).observe(doc.body);\r\n      }\r\n    } catch(e){}\r\n  });\r\n\r\n  window.addEventListener('resize', resizeFrame);\r\n})();\r\n<\/script><\/div>\n\t\t\t\t\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/div>\n\t\t\t\t\t<\/div>\n\t\t<\/section>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"","protected":false},"author":5,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_acf_changed":false,"_uag_custom_page_level_css":"","footnotes":""},"class_list":["post-22935","page","type-page","status-publish"],"acf":[],"uagb_featured_image_src":{"full":false,"thumbnail":false,"medium":false,"medium_large":false,"large":false,"1536x1536":false,"2048x2048":false,"bunyad-small":false,"bunyad-medium":false,"bunyad-full":false,"bunyad-viewport":false,"bunyad-768":false},"uagb_author_info":{"display_name":"Albert Badalyan","author_link":"https:\/\/www.chilliprinting.com\/online-printing-blog\/author\/albert-badalyan\/"},"uagb_comment_info":0,"uagb_excerpt":null,"jetpack_sharing_enabled":true,"jetpack_shortlink":"https:\/\/wp.me\/P7uAlw-5XV","_links":{"self":[{"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/pages\/22935","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/users\/5"}],"replies":[{"embeddable":true,"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/comments?post=22935"}],"version-history":[{"count":14,"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/pages\/22935\/revisions"}],"predecessor-version":[{"id":22951,"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/pages\/22935\/revisions\/22951"}],"wp:attachment":[{"href":"https:\/\/www.chilliprinting.com\/online-printing-blog\/wp-json\/wp\/v2\/media?parent=22935"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}