/* Prospect Advisory Survey v2 */ (function () { 'use strict'; if (window.__PAS_SURVEY_LOADED__) return; window.__PAS_SURVEY_LOADED__ = true; // === DATA === // ========================================================================== // CONFIG // ========================================================================== // // WEBHOOK_URL is the Google Apps Script web app /exec URL. It must be a // deployment owned by austin.sandmeyer@sendoso.com, bound to the sheet // "Prospect Advisory Project: Survey Results (sendoso.com)" // (1ZvLY77wUcMLO_UIJDlkuWYKIrVrJmnMBSg0vkso74Xs), and deployed with // "Who has access: Anyone". // // Not Hannah's original sheet -- that one keeps the Netlify test data. // // build/build.py refuses to build while this is still the placeholder, // unless you pass --allow-placeholder for local testing. // // REPORT_WHEN is the single place the report timing is expressed. It is // written to slot into the sentence "...the experiments we're launching // ___." Set it to 'on September 15' when the date is real; leave it as // 'when it publishes' while the date is TBD. Do not hardcode a date // anywhere else. // -------------------------------------------------------------------------- var CONFIG = { WEBHOOK_URL: 'https://script.google.com/macros/s/AKfycbz_yJIuns-HWnQDzkjZeXDLaU8J1uWztHp3tgjsHRho-gDw2HJCsxLztFtdgCeWsx2F/exec', MOUNT_ID: 'pas-survey-root', REPORT_WHEN: 'when it publishes', MIN_SAMPLE: 5, MAX_OPEN_CHARS: 2000 }; // ========================================================================== // SURVEY MODEL // ========================================================================== // // DO NOT CHANGE the section `key` values or the option `letter` values. // They are the data model: they are what gets written to the sheet, what // the tally endpoint counts, and what any downstream Zap keys on. // Renaming a letter silently invalidates every response already collected. // // Option `label` and `text` are display strings and safe to edit. // -------------------------------------------------------------------------- var SECTIONS = [ { key: 'cta', title: 'THE CTA', intro: [ "Every outbound playbook ends with an ask. Too few earn it.", "So we're pressure-testing one of outbound's biggest assumptions: the CTA.", "Before we run the experiment, we want to hear from the people on the receiving end. What actually earns engagement today?" ], questions: [ { id: 'cta_choice', type: 'single', text: 'Which ask would be most likely to earn a reply from you?', options: [ { letter: 'A', label: 'Learn Something', text: '"I put together a gifting playbook for teams like yours. Want me to send it over?"' }, { letter: 'B', label: 'Give Feedback', text: '"Can I ask you an honest question? What\'s the biggest reason you haven\'t considered Sendoso?"' }, { letter: 'C', label: 'Try Something', text: '"We built a free tool that scores your gifting program in under 4 minutes. Curious how you\'d stack up?"' }, { letter: 'D', label: 'Receive Something', text: '"Saw this and thought of you. Happy to send it your way if you\'re interested."' } ] }, { id: 'cta_open', type: 'open', text: "Tell us about the last outbound CTA that actually made you stop and engage. What made it work?", placeholder: 'What was it, and why did it land?' } ] }, { key: 'personalization', title: 'EARNING ATTENTION', intro: [ "Anyone can personalize at scale. What we're trying to understand is what actually earns your attention." ], questions: [ { id: 'personalization_choice', type: 'single', text: 'What earns your attention more than personalization?', options: [ { letter: 'A', label: 'Relevance', text: "It speaks directly to a challenge I'm dealing with." }, { letter: 'B', label: 'Insight', text: "It teaches me something I didn't know." }, { letter: 'C', label: 'Effort', text: 'Someone clearly invested real time in creating it.' }, { letter: 'D', label: 'Generosity', text: 'It delivers value before asking me to do anything.' } ] }, { id: 'personalization_open', type: 'open', text: "What's the most thoughtful piece of outbound you've received in the last year? What made it stand out?", placeholder: 'What happened, and why did it stick with you?' } ] }, { key: 'events', title: 'EXPERIENCES', intro: [ "Everyone is fighting for the same limited calendar space.", "We're testing a simple question: What type of experience actually earns a \"yes\" from GTM leaders like yourself?" ], questions: [ { id: 'events_choice', type: 'single', text: 'Which experience would you be most likely to make time for?', options: [ { letter: 'A', label: 'Build Something', text: 'A hands-on workshop where I leave with something I can use.' }, { letter: 'B', label: 'Learn Something', text: "Exclusive research and data I haven't seen anywhere else." }, { letter: 'C', label: 'Meet Someone', text: 'A small dinner with peers facing similar challenges.' }, { letter: 'D', label: 'Debate Something', text: "A live discussion where GTM leaders vote on and debate today's biggest outbound assumptions." } ] }, { id: 'events_open', type: 'open', text: "Tell us about the last vendor event or experience that felt genuinely worth your time. What made it different?", placeholder: 'Company, format, whatever you remember...' } ] }, { key: 'content', title: 'CONTENT', intro: [ "Great content doesn't just get read. It gets shared.", "We're testing what actually earns a forward inside today's buying committee." ], questions: [ { id: 'content_choice', type: 'single', text: "When you forward vendor content internally, what's usually the reason?", options: [ { letter: 'A', label: null, text: "It supports a decision we're already considering." }, { letter: 'B', label: null, text: 'It introduces a new idea we should discuss.' }, { letter: 'C', label: null, text: 'It shows how another company solved a similar problem.' }, { letter: 'D', label: null, text: 'It gives us something we can immediately put into practice.' } ] }, { id: 'content_open', type: 'open', text: "What's one report, framework, or resource you'd immediately download if it existed?", placeholder: "This is your chance to ask for something that doesn't exist yet..." } ] } ]; // ========================================================================== // RESULTS PAGE CARD LABELS // ========================================================================== // // These are short restatements of the choice question in each section. // They must stay faithful to the question actually asked -- v1 shipped a // card labelled "What makes outbound feel like someone actually tried?" // above the tally for "What earns your attention more than // personalization?", which is a different question. // -------------------------------------------------------------------------- var RESULT_CARDS = [ { key: 'cta', question: 'Which ask would earn a reply?' }, { key: 'personalization', question: 'What earns attention more than personalization?' }, { key: 'events', question: 'Which experience would you make time for?' }, { key: 'content', question: 'Why do you forward vendor content internally?' } ]; // ========================================================================== // STATIC COPY // ========================================================================== // Every visitor-facing string that is not part of a question lives here. // The Webflow page owns no copy at all -- it is a mount container only. // -------------------------------------------------------------------------- var COPY = { intro: { eyebrow: 'From Kris Rudeegraap, CEO of Sendoso', headline: "OUTBOUND ISN'T DEAD.
IT JUST STOPPED EARNING ATTENTION.", body: [ "Everyone has an opinion about why outbound isn't working. Very few are asking buyers directly.", "This quarter, we're testing the assumptions behind today's GTM playbook, starting with the CTA and building our experiments from real executive feedback." ], commitment: "8 questions. About 8 minutes. Submit your responses to see live peer data, then get the full report and the experiments we're launching " + CONFIG.REPORT_WHEN + ".", cta: "Let's go, 8 minutes" }, contact: { eyebrow: 'Last thing (optional)', headline: 'WANT TO HEAR HOW THIS TURNS OUT?', body: "Leave your name and email if you'd like the full summary and a look at the experiments we're launching " + CONFIG.REPORT_WHEN + ". Either way, continue to see how your responses compare with everyone who's participated so far.", namePlaceholder: 'Name (optional)', emailPlaceholder: 'Email (optional)', cta: 'See how you compare' }, loading: { eyebrow: 'Submitting', headline: 'TALLYING YOUR ANSWERS...', note: "One second while we compare you to everyone else who's answered so far." }, results: { eyebrow: "Here's what your peers said", body: "Here's a live look at how everyone who's responded so far compares. Your selections are highlighted.", onListTitle: "You're on the list.", onList: "The full summary and what we're experimenting with lands in your inbox " + CONFIG.REPORT_WHEN + ".", gateHeading: 'Want the full results ' + CONFIG.REPORT_WHEN + '?', gateBody: 'Enter your email below to receive the complete summary and a look at the experiments we are launching.', gateCta: 'Add me', gateSuccess: "You're on the list.", gateError: "That didn't save. Try again, or let your Sendoso contact know.", earlyData: function (n) { return 'Early days: this is based on ' + n + (n === 1 ? ' response' : ' responses') + ' so far, so treat the percentages as directional.'; }, sampleNote: function (n) { return 'Based on ' + n + ' responses so far.'; }, saveFailed: "We couldn't save your response, so it isn't counted below and we don't have a record of it. Please let your Sendoso contact know.", talliesFailed: "Your response was saved. We couldn't load the live peer comparison right now." }, ribbon: { almost: 'Almost there', done: "You're in" } }; // The mount container is the scope for this bundle. The script is applied // to a single Webflow page, and gating on the container rather than on // location.pathname means a slug change degrades to a console warning // instead of a blank page. var mount = document.getElementById(CONFIG.MOUNT_ID); if (!mount) { console.warn('[pas-survey] No #' + CONFIG.MOUNT_ID + ' element on this page. Nothing rendered.'); return; } if (location.pathname.indexOf('advisory-report-26') === -1) { console.warn('[pas-survey] Mounted outside the expected path: ' + location.pathname); } // === STYLES === (function () { if (document.getElementById('pas-survey-styles')) return; var s = document.createElement('style'); s.id = 'pas-survey-styles'; s.textContent = `/* ========================================================================== Prospect Advisory Survey -- styles Every selector is scoped under #pas-survey-root and every class is prefixed pas-. This stylesheet is injected into on a live Webflow page, so an unscoped generic selector (.btn-primary, .option-card, .eyebrow) would restyle unrelated Webflow elements. v1 of this survey used exactly those names. Custom properties are prefixed --pas- for the same reason: the Sendoso site already defines --orange--main, --teal--main and friends. Colours are Sendoso brand tokens. Do not substitute approximations. ========================================================================== */ #pas-survey-root { --pas-teal-deep: #1B4250; --pas-teal: #015A64; --pas-orange: #F37021; --pas-orange-dark: #D4601A; --pas-peach: #F9B890; --pas-peach-light: #FBD4BC; --pas-warm-white: #FFFBF8; --pas-ice-teal: #E8F4F5; --pas-teal-wash: #E1F0F2; --pas-soft-teal: #80C0C7; --pas-charcoal: #1E2430; --pas-slate: #51596A; --pas-silver: #C2C5CB; --pas-white: #FFFFFF; font-family: 'Bricolage Grotesque', sans-serif; background: var(--pas-warm-white); color: var(--pas-charcoal); position: relative; overflow-x: hidden; } #pas-survey-root *, #pas-survey-root *::before, #pas-survey-root *::after { box-sizing: border-box; } #pas-survey-root .pas-stage { max-width: 680px; margin: 0 auto; padding: 48px 28px 80px; position: relative; } /* ---------- ribbon progress ---------- */ #pas-survey-root .pas-ribbon-wrap { max-width: 680px; margin: 0 auto; padding: 32px 28px 0; } #pas-survey-root .pas-ribbon-track { position: relative; height: 34px; display: flex; gap: 6px; } #pas-survey-root .pas-ribbon-seg { flex: 1; position: relative; height: 10px; align-self: center; border-radius: 6px; background: var(--pas-peach-light); overflow: visible; } #pas-survey-root .pas-ribbon-fill { position: absolute; inset: 0; background: var(--pas-orange); transform: scaleX(0); transform-origin: left; transition: transform 0.6s cubic-bezier(.4,0,.2,1); border-radius: 6px; } #pas-survey-root .pas-ribbon-bow { position: absolute; top: -8px; left: 50%; margin-left: -13px; width: 26px; height: 26px; transition: transform 0.4s ease, opacity 0.4s ease; opacity: 0.45; transform: scale(0.75); } #pas-survey-root .pas-ribbon-bow.pas-is-active { opacity: 1; transform: scale(1); } #pas-survey-root .pas-ribbon-label { text-align: center; font-size: 12px; font-weight: 700; letter-spacing: 0.12em; color: var(--pas-teal); margin-top: 14px; text-transform: uppercase; } /* ---------- typography ---------- */ #pas-survey-root .pas-eyebrow { font-size: 12px; font-weight: 700; letter-spacing: 0.14em; text-transform: uppercase; color: var(--pas-teal); margin-bottom: 14px; } #pas-survey-root .pas-headline { font-size: 38px; line-height: 1.12; font-weight: 800; color: var(--pas-teal-deep); text-transform: uppercase; margin: 0 0 22px; letter-spacing: -0.02em; } #pas-survey-root .pas-headline.pas-is-small { font-size: 30px; } #pas-survey-root .pas-body { font-size: 17px; line-height: 1.6; font-weight: 400; color: var(--pas-charcoal); margin: 0 0 16px; max-width: 62ch; } #pas-survey-root .pas-body.pas-is-lede { color: var(--pas-teal); font-weight: 600; } #pas-survey-root .pas-body.pas-is-commitment { color: var(--pas-teal-deep); font-weight: 700; } #pas-survey-root .pas-signature { display: flex; align-items: center; gap: 12px; margin: 32px 0 4px; } #pas-survey-root .pas-avatar { width: 44px; height: 44px; border-radius: 50%; background: var(--pas-teal-deep); display: flex; align-items: center; justify-content: center; color: var(--pas-white); font-weight: 800; font-size: 15px; flex-shrink: 0; border: 2px solid var(--pas-peach); } #pas-survey-root .pas-who { font-size: 14px; color: var(--pas-teal-deep); font-weight: 700; line-height: 1.4; } #pas-survey-root .pas-who span { display: block; color: var(--pas-slate); font-weight: 400; font-size: 13px; } /* ---------- buttons ---------- */ #pas-survey-root .pas-btn { background: var(--pas-orange); color: var(--pas-white); border: none; font-family: inherit; font-weight: 700; font-size: 17px; padding: 16px 30px; border-radius: 999px; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: background 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease; box-shadow: 0 4px 14px rgba(243, 112, 33, 0.26); } #pas-survey-root .pas-btn:hover { background: var(--pas-orange-dark); transform: translateY(-2px); box-shadow: 0 6px 18px rgba(243, 112, 33, 0.34); } #pas-survey-root .pas-btn:active { transform: translateY(0); } #pas-survey-root .pas-btn:disabled { background: var(--pas-silver); color: #3f434a; cursor: not-allowed; transform: none; box-shadow: none; } #pas-survey-root .pas-btn.pas-is-compact { font-size: 15px; padding: 13px 24px; } #pas-survey-root .pas-btn-ghost { background: none; border: none; color: var(--pas-teal); font-family: inherit; font-weight: 700; font-size: 15px; cursor: pointer; padding: 12px 10px; border-radius: 8px; text-decoration: underline; text-underline-offset: 3px; } #pas-survey-root .pas-btn-ghost:hover { color: var(--pas-teal-deep); } #pas-survey-root .pas-btn-row { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 40px; } #pas-survey-root .pas-btn-row.pas-is-start { justify-content: flex-start; margin-top: 32px; } /* Visible focus on every interactive element. Never remove without providing an alternative -- WCAG 2.2 AA. */ #pas-survey-root .pas-btn:focus-visible, #pas-survey-root .pas-btn-ghost:focus-visible, #pas-survey-root .pas-open:focus-visible, #pas-survey-root .pas-input:focus-visible { outline: 3px solid var(--pas-teal-deep); outline-offset: 3px; } #pas-survey-root .pas-option-input:focus-visible + .pas-option-body { outline: 3px solid var(--pas-teal-deep); outline-offset: 3px; } /* ---------- questions ---------- */ #pas-survey-root .pas-q-block { margin: 0 0 40px; padding: 0; border: 0; } #pas-survey-root .pas-q-legend { display: block; width: 100%; padding: 0; margin: 0 0 18px; } #pas-survey-root .pas-q-num { display: block; font-size: 12px; font-weight: 700; color: var(--pas-slate); letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 8px; } #pas-survey-root .pas-q-text { display: block; font-size: 20px; font-weight: 700; color: var(--pas-teal-deep); line-height: 1.35; } #pas-survey-root .pas-option { display: block; position: relative; margin-bottom: 10px; cursor: pointer; } #pas-survey-root .pas-option-input { position: absolute; width: 1px; height: 1px; opacity: 0; margin: 0; pointer-events: none; } #pas-survey-root .pas-option-body { display: flex; align-items: flex-start; gap: 12px; background: var(--pas-white); border: 2px solid var(--pas-ice-teal); border-radius: 14px; padding: 16px 18px; font-size: 16px; line-height: 1.45; color: var(--pas-charcoal); transition: border-color 0.15s ease, background 0.15s ease; } #pas-survey-root .pas-option:hover .pas-option-body { border-color: var(--pas-soft-teal); } #pas-survey-root .pas-option.pas-is-selected .pas-option-body, #pas-survey-root .pas-option:has(.pas-option-input:checked) .pas-option-body { border-color: var(--pas-orange); background: var(--pas-ice-teal); } #pas-survey-root .pas-option-letter { display: inline-flex; width: 24px; height: 24px; border-radius: 50%; background: var(--pas-teal-deep); color: var(--pas-white); font-size: 12px; font-weight: 800; align-items: center; justify-content: center; flex-shrink: 0; margin-top: 1px; } #pas-survey-root .pas-option.pas-is-selected .pas-option-letter, #pas-survey-root .pas-option:has(.pas-option-input:checked) .pas-option-letter { background: var(--pas-orange); } #pas-survey-root .pas-option-copy strong { font-weight: 700; } #pas-survey-root .pas-open { display: block; width: 100%; min-height: 96px; border: 2px solid var(--pas-ice-teal); border-radius: 14px; padding: 14px 16px; font-family: inherit; font-size: 16px; line-height: 1.5; color: var(--pas-charcoal); background: var(--pas-white); resize: vertical; outline: none; transition: border-color 0.15s ease; } #pas-survey-root .pas-open:focus { border-color: var(--pas-soft-teal); } #pas-survey-root .pas-open::placeholder { color: #7d8794; } #pas-survey-root .pas-input { display: block; width: 100%; border: 2px solid var(--pas-ice-teal); border-radius: 999px; padding: 15px 20px; font-family: inherit; font-size: 16px; color: var(--pas-charcoal); background: var(--pas-white); outline: none; transition: border-color 0.15s ease; margin-bottom: 12px; } #pas-survey-root .pas-input:focus { border-color: var(--pas-soft-teal); } #pas-survey-root .pas-input::placeholder { color: #7d8794; } #pas-survey-root .pas-field-label { display: block; font-size: 13px; font-weight: 700; color: var(--pas-teal); margin-bottom: 6px; letter-spacing: 0.02em; } /* Honeypot. Bots fill it, humans never see it. Not display:none -- some bots skip hidden fields. */ #pas-survey-root .pas-hp { position: absolute; left: -10000px; width: 1px; height: 1px; overflow: hidden; } /* ---------- results ---------- */ #pas-survey-root .pas-card { background: var(--pas-white); border-radius: 16px; padding: 22px 24px; margin-bottom: 16px; border: 2px solid var(--pas-ice-teal); } #pas-survey-root .pas-card-q { font-size: 15px; font-weight: 700; color: var(--pas-teal-deep); margin: 0 0 18px; } #pas-survey-root .pas-tally-row { margin-bottom: 18px; } #pas-survey-root .pas-tally-row:last-child { margin-bottom: 0; } #pas-survey-root .pas-tally-head { display: flex; align-items: flex-start; gap: 10px; margin-bottom: 8px; flex-wrap: wrap; } #pas-survey-root .pas-tally-copy { font-size: 14px; font-weight: 600; color: var(--pas-charcoal); line-height: 1.4; flex: 1; min-width: 55%; } #pas-survey-root .pas-tally-meter { display: flex; align-items: center; gap: 10px; } #pas-survey-root .pas-tally-track { flex: 1; height: 12px; background: var(--pas-teal-wash); border-radius: 8px; overflow: hidden; } #pas-survey-root .pas-tally-fill { height: 100%; background: var(--pas-soft-teal); border-radius: 8px; transition: width 0.8s cubic-bezier(.4,0,.2,1); } #pas-survey-root .pas-tally-fill.pas-is-mine { background: var(--pas-orange); } #pas-survey-root .pas-tally-pct { font-size: 13px; font-weight: 800; color: var(--pas-teal-deep); width: 44px; text-align: right; flex-shrink: 0; } #pas-survey-root .pas-mine-tag { font-size: 11px; font-weight: 800; color: #8a3a0e; letter-spacing: 0.06em; text-transform: uppercase; background: var(--pas-peach-light); padding: 3px 8px; border-radius: 6px; } /* ---------- orange CTA panel ---------- Matches the treatment on /score-my-gifting-program. This is the one place orange is used as a large background fill rather than an accent, which is a deliberate exception to the brand rule, following that page's precedent. Type is sized up so white-on-orange clears the WCAG large-text threshold: the title and body are both >=18.66px bold, where 3:1 applies rather than 4.5:1. Do not shrink them without darkening the background. */ #pas-survey-root .pas-cta-panel { background: linear-gradient(135deg, var(--pas-orange) 0%, #DE5F17 100%); border-radius: 24px; padding: 44px 40px; margin-top: 32px; text-align: center; color: var(--pas-white); } #pas-survey-root .pas-cta-title { font-size: 34px; line-height: 1.15; font-weight: 800; letter-spacing: -0.02em; color: var(--pas-white); margin: 0 0 14px; } #pas-survey-root .pas-cta-body { font-size: 19px; font-weight: 700; line-height: 1.5; color: var(--pas-white); margin: 0 auto 26px; max-width: 44ch; } #pas-survey-root .pas-cta-body.pas-is-last { margin-bottom: 0; } #pas-survey-root .pas-cta-form { display: flex; gap: 12px; justify-content: center; align-items: center; flex-wrap: wrap; max-width: 520px; margin: 0 auto; } #pas-survey-root .pas-cta-form .pas-input { flex: 1 1 240px; margin-bottom: 0; border-color: transparent; font-size: 17px; } #pas-survey-root .pas-btn-invert { background: var(--pas-white); color: var(--pas-orange); border: none; font-family: inherit; font-weight: 800; font-size: 18px; padding: 15px 30px; border-radius: 999px; cursor: pointer; white-space: nowrap; transition: transform 0.15s ease, box-shadow 0.15s ease; box-shadow: 0 4px 14px rgba(27, 66, 80, 0.18); } #pas-survey-root .pas-btn-invert:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(27, 66, 80, 0.24); } #pas-survey-root .pas-btn-invert:active { transform: translateY(0); } #pas-survey-root .pas-btn-invert:disabled { opacity: 0.55; cursor: not-allowed; transform: none; } #pas-survey-root .pas-btn-invert:focus-visible { outline: 3px solid var(--pas-white); outline-offset: 3px; } #pas-survey-root .pas-cta-panel .pas-inline-error, #pas-survey-root .pas-cta-panel .pas-inline-ok { color: var(--pas-white); font-size: 17px; font-weight: 700; text-align: center; margin: 16px 0 0; } /* Present for screen readers, invisible on screen. */ #pas-survey-root .pas-vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } #pas-survey-root .pas-note { font-size: 14px; line-height: 1.5; color: var(--pas-slate); margin: 0 0 20px; } #pas-survey-root .pas-note.pas-is-early { color: var(--pas-teal-deep); font-weight: 600; background: var(--pas-teal-wash); border-radius: 12px; padding: 14px 16px; } #pas-survey-root .pas-note.pas-is-error { color: #9a2c0b; font-weight: 700; background: var(--pas-peach-light); border-radius: 12px; padding: 14px 16px; } #pas-survey-root .pas-inline-error { font-size: 14px; color: #9a2c0b; font-weight: 700; margin: 10px 0 0; } #pas-survey-root .pas-inline-ok { font-size: 14px; color: var(--pas-teal); font-weight: 700; margin: 10px 0 0; } /* ---------- motion ---------- */ #pas-survey-root .pas-fade-in { animation: pasFadeIn 0.45s ease; } @keyframes pasFadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } @media (prefers-reduced-motion: reduce) { #pas-survey-root .pas-fade-in { animation: none; } #pas-survey-root .pas-ribbon-fill, #pas-survey-root .pas-ribbon-bow, #pas-survey-root .pas-tally-fill, #pas-survey-root .pas-btn { transition: none; } } /* ---------- breakpoints ---------- */ @media (max-width: 767px) { #pas-survey-root .pas-cta-panel { padding: 36px 26px; border-radius: 20px; } #pas-survey-root .pas-cta-title { font-size: 28px; } #pas-survey-root .pas-headline { font-size: 31px; } #pas-survey-root .pas-headline.pas-is-small { font-size: 26px; } #pas-survey-root .pas-stage { padding: 36px 20px 64px; } #pas-survey-root .pas-ribbon-wrap { padding: 24px 20px 0; } } @media (max-width: 478px) { #pas-survey-root .pas-cta-panel { padding: 30px 20px; } #pas-survey-root .pas-cta-title { font-size: 25px; } #pas-survey-root .pas-cta-body { font-size: 18px; } #pas-survey-root .pas-cta-form { flex-direction: column; } #pas-survey-root .pas-cta-form .pas-input { flex: 1 1 auto; width: 100%; } #pas-survey-root .pas-btn-invert { width: 100%; } #pas-survey-root .pas-headline { font-size: 27px; } #pas-survey-root .pas-headline.pas-is-small { font-size: 23px; } #pas-survey-root .pas-stage { padding: 28px 16px 56px; } #pas-survey-root .pas-ribbon-wrap { padding: 20px 16px 0; } #pas-survey-root .pas-btn { width: 100%; justify-content: center; } #pas-survey-root .pas-btn-row { flex-direction: column-reverse; align-items: stretch; } #pas-survey-root .pas-tally-copy { min-width: 100%; } } `; document.head.appendChild(s); })(); // === UI === // ========================================================================== // STATE // ========================================================================== // step 0 = intro, 1..N = sections, N+1 = contact, N+2 = results // -------------------------------------------------------------------------- var LAST_SECTION = SECTIONS.length; var STEP_CONTACT = SECTIONS.length + 1; var STEP_RESULTS = SECTIONS.length + 2; var step = 0; var answers = {}; var submissionId = makeId(); var firstPaint = true; // null until the submit round-trip finishes. // { tallies, total, saveOk, talliesOk } var result = null; // ========================================================================== // HELPERS // ========================================================================== function esc(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } function makeId() { try { if (window.crypto && window.crypto.randomUUID) return window.crypto.randomUUID(); } catch (e) { /* fall through */ } return 'pas-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 10); } function isEmail(value) { return /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(String(value || '').trim()); } function endpointReady() { return !!CONFIG.WEBHOOK_URL && CONFIG.WEBHOOK_URL.indexOf('REPLACE_WITH') === -1; } function post(body) { // text/plain keeps this a CORS "simple request". Apps Script web apps do // not answer preflight OPTIONS, so application/json would fail outright. return fetch(CONFIG.WEBHOOK_URL, { method: 'POST', headers: { 'Content-Type': 'text/plain;charset=utf-8' }, body: JSON.stringify(body) }).then(function (res) { return res.json(); }); } function sectionByKey(key) { for (var i = 0; i < SECTIONS.length; i++) { if (SECTIONS[i].key === key) return SECTIONS[i]; } return null; } function choiceQuestion(section) { for (var i = 0; i < section.questions.length; i++) { if (section.questions[i].type === 'single') return section.questions[i]; } return null; } function optionCopy(sectionKey, letter) { var section = sectionByKey(sectionKey); if (!section) return ''; var q = choiceQuestion(section); if (!q) return ''; for (var i = 0; i < q.options.length; i++) { if (q.options[i].letter === letter) { var o = q.options[i]; return o.label ? '' + esc(o.label) + ': ' + esc(o.text) : esc(o.text); } } return ''; } function letters(section) { var q = choiceQuestion(section); var out = []; if (!q) return out; for (var i = 0; i < q.options.length; i++) out.push(q.options[i].letter); return out; } // ========================================================================== // RIBBON // ========================================================================== function bowSvg(active) { // The completed bow sits on top of an orange filled segment, so its wings // are teal, not orange -- orange-on-orange made the bow invisible. var wing = active ? '#1B4250' : '#80C0C7'; var knot = active ? '#FFFBF8' : '#FFFBF8'; return ''; } function renderRibbon() { if (step === 0) return ''; var current = Math.min(step, LAST_SECTION); var past = step > LAST_SECTION; var segs = ''; for (var i = 1; i <= LAST_SECTION; i++) { var done = past || i < current; var isCurrent = !past && i === current; var scale = done ? 1 : (isCurrent ? 0.5 : 0); segs += '
' + '
' + bowSvg(done) + '
'; } var label = step <= LAST_SECTION ? 'Section ' + current + ' of ' + LAST_SECTION : (step === STEP_CONTACT ? COPY.ribbon.almost : COPY.ribbon.done); return '
' + '' + '

' + esc(label) + '

' + '
'; } // ========================================================================== // SCREENS // ========================================================================== function renderIntro() { var body = ''; for (var i = 0; i < COPY.intro.body.length; i++) { body += '

' + esc(COPY.intro.body[i]) + '

'; } return '
' + '

' + esc(COPY.intro.eyebrow) + '

' + '

' + COPY.intro.headline + '

' + body + '

' + esc(COPY.intro.commitment) + '

' + signature() + '
' + '' + '
' + '
'; } function signature() { return '
' + '' + '

Kris RudeegraapCEO, Sendoso

' + '
'; } function renderSection(idx) { var s = SECTIONS[idx]; var intro = ''; for (var i = 0; i < s.intro.length; i++) { intro += '

' + esc(s.intro[i]) + '

'; } var blocks = ''; for (var qi = 0; qi < s.questions.length; qi++) { var q = s.questions[qi]; var num = 'Question ' + (idx + 1) + '.' + (qi + 1); if (q.type === 'single') { var opts = ''; for (var oi = 0; oi < q.options.length; oi++) { var o = q.options[oi]; var inputId = 'pas-' + q.id + '-' + o.letter; var copy = o.label ? '' + esc(o.label) + ': ' + esc(o.text) : esc(o.text); opts += ''; } blocks += '
' + '' + '' + esc(num) + '' + '' + esc(q.text) + '' + '' + opts + '
'; } else { var taId = 'pas-' + q.id; blocks += '
' + '' + '' + '
'; } } // No section eyebrow: the ribbon label directly above already reads // "Section N of 4", and v1 printed it twice. var lastSection = idx === SECTIONS.length - 1; return '
' + '

' + esc(s.title) + '

' + intro + '
' + blocks + '
' + '
' + (idx === 0 ? '' : '') + '' + '
' + '
'; } function renderContact() { return '
' + '

' + esc(COPY.contact.eyebrow) + '

' + '

' + esc(COPY.contact.headline) + '

' + '

' + esc(COPY.contact.body) + '

' + '' + '' + '' + '' + '' + '' + '
' + '' + '' + '
' + '
'; } function renderLoading() { return '
' + '

' + esc(COPY.loading.eyebrow) + '

' + '

' + esc(COPY.loading.headline) + '

' + '

' + esc(COPY.loading.note) + '

' + '
'; } function renderTallyCards() { var cards = ''; for (var c = 0; c < RESULT_CARDS.length; c++) { var card = RESULT_CARDS[c]; var section = sectionByKey(card.key); if (!section) continue; var dist = result.tallies[card.key] || {}; var keys = letters(section); var total = 0; var k; for (k = 0; k < keys.length; k++) total += Number(dist[keys[k]] || 0); var mine = answers[card.key + '_choice']; var rows = ''; for (k = 0; k < keys.length; k++) { var letter = keys[k]; var count = Number(dist[letter] || 0); var pct = total > 0 ? Math.round(count / total * 100) : 0; var isMine = letter === mine; rows += '
' + '
' + '' + esc(letter) + '' + '' + optionCopy(card.key, letter) + '' + (isMine ? 'Your pick' : '') + '
' + '
' + '
' + '' + pct + '%' + '
' + '
'; } cards += '

' + esc(card.question) + '

' + rows + '
'; } return cards; } function renderResults() { var name = String(answers.name || '').trim(); var headline = name ? "YOU'RE IN, " + esc(name).toUpperCase() + '.' : "YOU'RE IN."; var head = '

' + esc(COPY.results.eyebrow) + '

' + '

' + headline + '

'; // The save failed: no bars. Showing an all-zero or partial chart here // would imply the response was counted when it was not. if (!result.saveOk) { return '
' + head + '

' + esc(COPY.results.saveFailed) + '

' + signature() + '
'; } if (!result.talliesOk) { return '
' + head + '

' + esc(COPY.results.talliesFailed) + '

' + emailBlock() + signature() + '
'; } var early = ''; if (result.total > 0 && result.total < CONFIG.MIN_SAMPLE) { early = '

' + esc(COPY.results.earlyData(result.total)) + '

'; } return '
' + head + '

' + esc(COPY.results.body) + '

' + early + renderTallyCards() + (result.total >= CONFIG.MIN_SAMPLE ? '

' + esc(COPY.results.sampleNote(result.total)) + '

' : '') + emailBlock() + signature() + '
'; } // The orange CTA panel, matching /score-my-gifting-program. Both states use // it so the results page does not change shape depending on whether an email // was already given. function emailBlock() { if (answers.email) { return '
' + onListPanel() + '
'; } return '
' + '

' + esc(COPY.results.gateHeading) + '

' + '

' + esc(COPY.results.gateBody) + '

' + '
' + '' + '' + '' + '
' + '' + '
'; } function onListPanel() { return '

' + esc(COPY.results.onListTitle) + '

' + '

' + esc(COPY.results.onList) + '

'; } // ========================================================================== // SUBMIT // ========================================================================== function submitAndTally() { step = STEP_RESULTS; result = null; render(); if (!endpointReady()) { result = { tallies: {}, total: 0, saveOk: false, talliesOk: false }; render(); return; } var payload = {}; for (var k in answers) { if (Object.prototype.hasOwnProperty.call(answers, k)) payload[k] = answers[k]; } payload.submissionId = submissionId; payload.submittedAt = new Date().toISOString(); post(payload).then(function (data) { result = { tallies: (data && data.tallies) || {}, total: (data && Number(data.total)) || 0, saveOk: !!(data && data.ok), talliesOk: !!(data && data.tallies) }; render(); }).catch(function () { result = { tallies: {}, total: 0, saveOk: false, talliesOk: false }; render(); }); } function addLateEmail() { var input = document.getElementById('pas-late-email'); var errEl = document.getElementById('pas-gate-error'); var btn = document.querySelector('[data-action="late-email"]'); if (!input || !errEl) return; var value = input.value.trim(); errEl.hidden = true; if (!isEmail(value)) { errEl.textContent = 'That does not look like an email address.'; errEl.hidden = false; return; } if (!endpointReady()) { errEl.textContent = COPY.results.gateError; errEl.hidden = false; return; } if (btn) btn.disabled = true; // A real CORS request, not mode:'no-cors'. v1 used no-cors, which makes // the response opaque -- a silent failure was indistinguishable from a // success, so nothing ever proved this path worked. post({ lateEmailAdd: true, submissionId: submissionId, email: value, name: answers.name || '', submittedAt: new Date().toISOString() }).then(function (data) { if (!(data && data.ok)) throw new Error('not ok'); answers.email = value; var gate = document.getElementById('pas-gate'); if (gate) { gate.innerHTML = onListPanel() + '

' + esc(COPY.results.gateSuccess) + '

'; } }).catch(function () { if (btn) btn.disabled = false; errEl.textContent = COPY.results.gateError; errEl.hidden = false; }); } // ========================================================================== // NAVIGATION // ========================================================================== function sectionComplete(idx) { var s = SECTIONS[idx]; for (var i = 0; i < s.questions.length; i++) { var q = s.questions[i]; if (q.type === 'single' && !answers[q.id]) return false; } return true; } function syncNextButton() { var btn = document.querySelector('[data-action="next"]'); if (!btn) return; var idx = Number(btn.getAttribute('data-section')); btn.disabled = !sectionComplete(idx); } function handleSubmitStep() { var nameEl = document.getElementById('pas-name'); var emailEl = document.getElementById('pas-email'); var hpEl = document.getElementById('pas-company-url'); var errEl = document.getElementById('pas-contact-error'); var email = emailEl ? emailEl.value.trim() : ''; if (email && !isEmail(email)) { if (errEl) { errEl.textContent = 'That does not look like an email address. Clear it to continue without one.'; errEl.hidden = false; } if (emailEl) emailEl.focus(); return; } answers.name = nameEl ? nameEl.value.trim() : ''; answers.email = email; answers.hp = hpEl ? hpEl.value.trim() : ''; submitAndTally(); } // ========================================================================== // RENDER + HYDRATE // ========================================================================== function screen() { if (step === 0) return renderIntro(); if (step >= 1 && step <= LAST_SECTION) return renderSection(step - 1); if (step === STEP_CONTACT) return renderContact(); return result ? renderResults() : renderLoading(); } function render() { mount.innerHTML = renderRibbon() + screen(); hydrate(); if (!firstPaint) { try { mount.scrollIntoView({ block: 'start', behavior: 'smooth' }); } catch (e) { window.scrollTo(0, mount.offsetTop || 0); } var heading = document.getElementById('pas-step-heading'); if (heading) heading.focus({ preventScroll: true }); } firstPaint = false; } function hydrate() { // Restore radio state from `answers` rather than interpolating a checked // attribute, and restore textarea content via .value rather than // innerHTML -- no user-supplied string is ever parsed as markup. var radios = mount.querySelectorAll('.pas-option-input'); var i; for (i = 0; i < radios.length; i++) { var radio = radios[i]; radio.checked = answers[radio.getAttribute('data-qid')] === radio.value; syncOptionClass(radio); } var areas = mount.querySelectorAll('.pas-open'); for (i = 0; i < areas.length; i++) { var area = areas[i]; area.value = answers[area.getAttribute('data-qid')] || ''; } var nameEl = document.getElementById('pas-name'); if (nameEl) nameEl.value = answers.name || ''; var emailEl = document.getElementById('pas-email'); if (emailEl) emailEl.value = answers.email || ''; syncNextButton(); } function syncOptionClass(radio) { var label = radio.closest ? radio.closest('.pas-option') : radio.parentNode; if (!label) return; if (radio.checked) label.classList.add('pas-is-selected'); else label.classList.remove('pas-is-selected'); } // ========================================================================== // EVENTS // ========================================================================== // Delegated, bound once. Webflow strips inline onclick attributes on // publish, so every handler has to be addEventListener. // -------------------------------------------------------------------------- mount.addEventListener('click', function (ev) { var el = ev.target.closest ? ev.target.closest('[data-action]') : null; if (!el || !mount.contains(el)) return; var action = el.getAttribute('data-action'); if (action === 'start') { step = 1; render(); return; } if (action === 'back') { step = Math.max(0, step - 1); render(); return; } if (action === 'next') { var idx = Number(el.getAttribute('data-section')); if (!sectionComplete(idx)) return; step = (idx === SECTIONS.length - 1) ? STEP_CONTACT : step + 1; render(); return; } if (action === 'submit') { handleSubmitStep(); return; } if (action === 'late-email') { addLateEmail(); return; } }); // Selecting an option updates state in place. v1 re-rendered the whole // screen on every option click, which wiped any open-text already typed in // that section and yanked the page back to the top. mount.addEventListener('change', function (ev) { var target = ev.target; if (!target.classList || !target.classList.contains('pas-option-input')) return; var qid = target.getAttribute('data-qid'); answers[qid] = target.value; var group = mount.querySelectorAll('.pas-option-input[data-qid="' + qid + '"]'); for (var i = 0; i < group.length; i++) syncOptionClass(group[i]); syncNextButton(); }); // Open text is written to state on every keystroke, so Back, Next and // option clicks can never lose it. mount.addEventListener('input', function (ev) { var target = ev.target; if (!target.classList || !target.classList.contains('pas-open')) return; answers[target.getAttribute('data-qid')] = target.value.slice(0, CONFIG.MAX_OPEN_CHARS); }); render(); })();