/* SAB Eligibility Checker v6 — full-screen Apple-style takeover. v6 (2026-10-19) = Mohamed's locked design, built after the freeze lifted: nine tap-first questions → REVEAL → "claim your guide" finish. (1) NEEDS chips (live "kinds of help found" counter in the chrome) → (2) ZIP → (3) HOUSEHOLD number taps → (4) SITUATIONS in the home → (5) ALREADY GETTING HELP → (6) AGE range → (7) MONEY SOURCE → (8) INCOME range (the engine's existing brackets/values, "I'm not sure") → (9) HOME SITUATION → REVEAL (kinds count + top matches, problem-first, before any personal info) → CLAIM (a) name+birthday (b) address with autocomplete, city/state/ZIP pre-filled from their ZIP (c) email AND phone, both required, trust line "We never sell your information." → tap screens: job situation, citizenship, language, best time, how they heard + the optional "anything else going on?" box → RESULTS. The engine (match) is untouched from v4; its situation inputs are DERIVED by deriveSit(): senior = 65+ chip or age range; youngchild = kids-under-5 or pregnant chip; kids = youngchild or the child-care need; unemployed = "none right now" money source or the unemployed job situation. Extra answers (already-getting, money source, tenure, bill help) ride the lead's situations string as letter-only flag keys for the owner alert + guides. Server contract (sab-tracker) honored: valid email AND phone, real DOB, street, city, a named job situation + citizenship, >=1 need; an "I'm not sure" income is sent as amount 0/month (not stated) so leads never bounce. Funnel beacons keep the tracker's stage meanings (household = zip done, situations = household done, home = income done, reveal = front done; completion event on submit) vs the 44 starts → 19 finishes baseline. Returning-visitor memory + on-device in-progress resume kept. Deploy: upload AS eligibility-checker-js-v6.txt to the Media Library (PHP/JS uploads are blocked) and point the loader snippet's enqueue URL at it. */ (function(){ "use strict"; /* ---------- verified program data (from programs_db.py, research 2026-09-26/27) ---------- */ var PROGRAMS = [ {id:"snap", name:"SNAP Food Benefits (Texas HHSC)", what:"Monthly grocery money on a Lone Star Card — up to $994/mo for a family of 4 (FY2026 max).", phone:"2-1-1 or 877-541-7905", apply:"https://www.yourtexasbenefits.com/Learn/Home", est:"snap"}, {id:"wic", name:"WIC — Women, Infants & Children", what:"Healthy food, nutrition classes and breastfeeding support for pregnant women and kids under 5. No citizenship requirement.", phone:"800-942-3678", apply:"https://texaswic.org/apply"}, {id:"medicaid", name:"Texas Medicaid", what:"FREE health coverage for kids, pregnant women, seniors and people with disabilities.", phone:"2-1-1 or 1-877-541-7905", apply:"https://www.yourtexasbenefits.com/Learn/Home"}, {id:"chip", name:"Texas CHIP — Children's Health Insurance", what:"Low-cost health coverage for kids 18 & under — enrollment $50 or less per year, co-pays $3–$35.", phone:"2-1-1", apply:"https://www.yourtexasbenefits.com/Learn/Home"}, {id:"tanf", name:"TANF Cash Help", what:"Monthly cash for families with kids and little to no income (family of 3 ≈ $382/mo).", phone:"2-1-1 or 877-541-7905", apply:"https://www.yourtexasbenefits.com/Learn/Home", est:"tanf"}, {id:"opportunity_home", name:"Opportunity Home — Public Housing & Section 8", what:"Rent based on income (around 30%). Apply the moment a waitlist opens — they fill fast.", phone:"210-477-6000", apply:"https://homesa.org/housing/find-a-home/apply-for-housing/"}, {id:"hap", name:"City of San Antonio Housing Assistance (HAP)", what:"Up to 3 months of rent (max ~$3,500) + up to $1,500 in utility help, paid straight to your landlord. Time-sensitive: enrollment windows fill fast.", phone:"210-207-6459", apply:"https://www.sa.gov/Directory/Departments/NHSD/Housing-Support/Renter-Support/About-HAP", hot:true}, {id:"trla_rtc", name:"Right to Counsel — Free Eviction Lawyer", what:"A FREE attorney if you're facing eviction. Call immediately if you have court papers. Time-sensitive.", phone:"210-212-3703", apply:"https://www.trla.org/", hot:true}, {id:"cps_discount", name:"CPS Energy Affordability Discount", what:"Up to 25% off your electric bill every month (35% if you're 60+).", phone:"1-888-207-7434", apply:"https://www.sa.gov/Directory/Departments/DHS/Financial-Assistance/Utility-Assistance", est:"utility"}, {id:"sa_utility", name:"City of San Antonio Utility Assistance", what:"Help paying energy bills — priority for households with kids, disabilities, or adults 60+.", phone:"210-207-8198", apply:"https://www.sa.gov/Directory/Departments/DHS/Financial-Assistance/Utility-Assistance"}, {id:"vita", name:"VITA — Free Tax Preparation", what:"IRS-certified volunteers do your taxes FREE (Jan–Apr) — including EITC and Child Tax Credit.", phone:"210-207-6575", apply:"https://vitasa.org/"}, {id:"twc_unemp", name:"Texas Unemployment Benefits", what:"Partial income replacement if you lost your job or had hours cut — roughly $75–$605/week.", phone:"800-939-6631", apply:"https://www.twc.texas.gov/services/apply-benefits"}, {id:"wioa", name:"WIOA — Free Career Training (Workforce Solutions Alamo)", what:"FREE career counseling, job training and certifications at 16 Alamo-area career centers.", phone:"210-438-0581", apply:"https://www.workforcesolutionsalamo.org/information-sessions/"}, {id:"ccs", name:"Child Care Scholarships (CCS)", what:"Helps working parents pay for child care. Apply early — there can be a waitlist.", phone:"1-888-265-6461", apply:"https://childcare.twc.texas.gov/find/register?mode=signup"}, {id:"va", name:"VA Benefits", what:"Health care, disability compensation, GI Bill, home loans and more for veterans.", phone:"1-800-827-1005", apply:"https://benefits.va.gov/benefits/applying.asp"}, {id:"tvc", name:"Texas Veterans Commission", what:"FREE help filing VA disability claims, job placement and education benefits.", phone:"1-800-252-8387", apply:"https://tvc.texas.gov/"}, {id:"hud_vash", name:"HUD-VASH — Housing for Homeless Veterans", what:"Rental voucher + VA case management for veterans facing homelessness. Time-sensitive.", phone:"877-424-3838", apply:"https://www.va.gov/south-texas-health-care/health-services/homeless-veteran-care/", hot:true}, {id:"foodbank", name:"San Antonio Food Bank — Free Food + Application Help", what:"Free groceries AND free one-on-one help applying for SNAP, Medicaid and TANF. No citizenship requirement for food.", phone:"210-431-8326", apply:"https://safoodbank.org/our-programs/benefits-assistance/"}, {id:"trla", name:"Texas RioGrande Legal Aid", what:"FREE civil legal help — housing, family, benefits denials. English and Spanish.", phone:"(833) 329-8752", apply:"https://www.trla.org/"}, {id:"via_link", name:"VIA Link — Rides On Demand + VIA Buses", what:"VIA's on-demand vans pick you up near your home and take you anywhere inside your zone for $1.30 a ride (65¢ with a reduced fare). Book up to 7 days ahead — good for job interviews and appointments. Regular VIA buses cover the rest of the city.", phone:"210-655-5465", apply:"https://www.viainfo.net/Link/"}, {id:"lifeline", name:"Lifeline — Discount Phone & Internet", what:"A federal program that takes $9.25 a month off your phone or internet bill — some companies turn it into a completely free phone plan. You qualify with SNAP, Medicaid or SSI, or income at or below 135% of the federal poverty line.", phone:"1-800-234-9473", apply:"https://www.lifelinesupport.org/"}, {id:"211", name:"2-1-1 Texas Helpline", what:"One free call connects you to thousands of local services. 24/7, 100+ languages.", phone:"2-1-1", apply:"https://www.211texas.org/"} ]; /* Inline SVG icons (no emoji anywhere in the UI) */ var SVG_PHONE = ''; var SVG_CLOCK = ''; /* 2025 federal poverty guidelines (48 states), monthly */ var FPL_M = {1:1304.17, 2:1762.50, 3:2220.83, 4:2679.17, 5:3137.50, 6:3595.83, 7:4054.17, 8:4512.50}; var SNAP_MAX = {1:298, 2:548, 3:783, 4:994, 5:1158, 6:1390, 7:1536, 8:1755}; /* FY2026 max allotments */ function fpl(hh, pct){ var b = FPL_M[Math.min(hh,8)] || 4512.50; if(hh>8) b += (hh-8)*478.33; return b*pct; } function initChecker(root){ if(!root || root.dataset.init) return; root.dataset.init = "1"; var $ = function(s){ return root.querySelector(s); }; var $$ = function(s){ return Array.prototype.slice.call(root.querySelectorAll(s)); }; var REDUCED = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches; var leadSent = false; /* guard: never send the same lead twice (e.g. back-nav + resubmit) */ var state = { zip:"", hh:3, income:3000, incomeUnknown:false, sit:{}, sitRaw:{}, flags:{}, ageRange:"", moneySrc:"", tenure:"", focus:null, name:"", phone:"", email:"", pref:"", consent:false, dob:"", street:"", city:"", employment:"", income_amount:"", income_period:"", citizenship:"", top_needs:[], language:"en", contact_time:"", hear_about:"", notes:"" }; /* v6: the engine's situation inputs are derived from the answers. sitRaw holds the Q4 chip states (youngchild/pregnant/senior/disabled/ veteran); state.sit is the engine map match() has always read; state.flags holds the extra letter-only keys (already-getting, money source, tenure, bill help) that ride the lead's situations string for the owner + guides. */ var FLAG_KEYS = ["getsnap","getmedicaid","gethousing","srcjob","srcbenefits","srcmix","srcnone","renting","ownhome","withfamily","utilbills"]; function deriveSit(){ var raw = state.sitRaw || {}; state.sit.veteran = !!raw.veteran; state.sit.disabled = !!raw.disabled; state.sit.youngchild = !!raw.pregnant || !!raw.youngchild; state.sit.senior = !!raw.senior || state.ageRange === "65plus"; state.sit.kids = state.sit.youngchild || state.top_needs.indexOf("childcare") !== -1; state.sit.unemployed = state.moneySrc === "srcnone" || state.employment === "unemployed"; } try { var sc = root.closest('[data-sabck-focus]'); if (sc) state.focus = sc.getAttribute('data-sabck-focus'); } catch(e){} /* ---------- returning-visitor memory (v4.3 — device-only, nothing sent anywhere) ---------- */ var WB_KEY = "sabck_saved_v1"; var quizLive = false; /* a quiz run is underway in this page session */ function wbLoad(){ try{ var raw = window.localStorage.getItem(WB_KEY); if(!raw) return null; var d = JSON.parse(raw); if(!d || typeof d !== "object" || !d.zip || !d.matched || !d.matched.length) return null; return d; }catch(e){ return null; } } function wbSave(res){ try{ var sit = {}; Object.keys(state.sit).forEach(function(k){ if(state.sit[k]) sit[k] = true; }); Object.keys(state.flags).forEach(function(k){ if(state.flags[k]) sit[k] = true; }); var d = { v:2, zip: state.zip, hh: state.hh, income: state.income, incomeUnknown: !!state.incomeUnknown, sit: sit, top_needs: state.top_needs.slice(), matched: res.map(function(r){ return r.p.id; }), ts: Date.now() }; window.localStorage.setItem(WB_KEY, JSON.stringify(d)); }catch(e){} } function wbForget(){ try{ window.localStorage.removeItem(WB_KEY); }catch(e){} } /* v6.8.5 (Mohamed's order 2026-10-24): "I applied for this" marks. Stored on the device only, like the saved check. A marked program leaves the visitor's displayed lists (results + Welcome Back) and stays gone across visits; the emailed guide, the saved lead and the tracker always keep the full matched set. Start over clears them. */ var APPLIED_KEY = "sabck_applied_v1"; function appliedLoad(){ try{ var a = JSON.parse(window.localStorage.getItem(APPLIED_KEY) || "[]"); return (a && a.length !== undefined) ? a : []; }catch(e){ return []; } } function appliedHas(id){ return appliedLoad().indexOf(id) !== -1; } function appliedAdd(id){ try{ var a = appliedLoad(); if(a.indexOf(id) === -1){ a.push(id); window.localStorage.setItem(APPLIED_KEY, JSON.stringify(a)); } }catch(e){} } function appliedClear(){ try{ window.localStorage.removeItem(APPLIED_KEY); }catch(e){} } function wbResetQuiz(){ state.zip = ""; state.hh = 3; state.income = null; state.incomeUnknown = false; state.sit = {}; state.sitRaw = {}; state.flags = {}; state.ageRange = ""; state.moneySrc = ""; state.tenure = ""; state.top_needs = []; state.notes = ""; state.name = ""; state.email = ""; state.phone = ""; state.dob = ""; state.street = ""; state.city = ""; state.employment = ""; state.citizenship = ""; state.pref = ""; state.contact_time = ""; state.hear_about = ""; state.consent = false; progClear(); var z = $("#sabck-zip"); if(z) z.value = ""; $$("#sabck-hhgrid button").forEach(function(b){ b.classList.toggle("sel", b.getAttribute("data-hh") === "3"); }); $$(".sabck-checks input").forEach(function(c){ c.checked = false; }); $$(".sabck-options button").forEach(function(b){ b.classList.remove("sel"); }); $$(".sabck-prefchip").forEach(function(b){ b.classList.remove("sel"); }); ["sabck-name","sabck-phone","sabck-notes","sabck-street","sabck-city","sabck-state","sabck-zip2"].forEach(function(id){ var el = document.getElementById(id); if(el) el.value = ""; }); var db = $("#sabck-dob"); if(db) db.value = ""; var cs = $("#sabck-consent"); if(cs) cs.checked = false; var ab = $("#sabck-alerts"); if(ab) ab.checked = false; addrPrefilled = false; updatePill(); } /* ---------- in-progress save (v5: leaving mid-quiz resumes where they stopped) ---------- */ var PROG_KEY = "sabck_prog_v1"; function progSave(){ try{ if(!QUIZ_SCREENS[current]) return; var raw = {}; Object.keys(state.sitRaw).forEach(function(k){ if(state.sitRaw[k]) raw[k] = true; }); var fl = {}; Object.keys(state.flags).forEach(function(k){ if(state.flags[k]) fl[k] = true; }); window.localStorage.setItem(PROG_KEY, JSON.stringify({ zip: state.zip, hh: state.hh, income: state.income, incomeUnknown: !!state.incomeUnknown, sitRaw: raw, flags: fl, ageRange: state.ageRange, moneySrc: state.moneySrc, tenure: state.tenure, top_needs: state.top_needs.slice(), screen: current, ts: Date.now() })); }catch(e){} } function progLoad(){ try{ var raw = window.localStorage.getItem(PROG_KEY); if(!raw) return null; var d = JSON.parse(raw); if(d && (d.screen === "claimname" || d.screen === "claimcontact")) d.screen = "personal"; /* v6.9.2: pre-consolidation saves resume on the merged step */ if(!d || typeof d !== "object" || !d.screen || !QUIZ_SCREENS[d.screen]) return null; return d; }catch(e){ return null; } } function progClear(){ try{ window.localStorage.removeItem(PROG_KEY); }catch(e){} } function progApply(d){ state.zip = d.zip || ""; state.hh = d.hh || 3; state.incomeUnknown = !!d.incomeUnknown; state.income = d.incomeUnknown ? null : (typeof d.income === "number" ? d.income : null); state.sitRaw = {}; if(d.sitRaw){ Object.keys(d.sitRaw).forEach(function(k){ if(d.sitRaw[k]) state.sitRaw[k] = true; }); } state.flags = {}; if(d.flags){ Object.keys(d.flags).forEach(function(k){ if(d.flags[k]) state.flags[k] = true; }); } state.ageRange = d.ageRange || ""; state.moneySrc = d.moneySrc || ""; state.tenure = d.tenure || ""; state.top_needs = (d.top_needs || []).slice(); deriveSit(); var z = $("#sabck-zip"); if(z) z.value = state.zip; $$("#sabck-hhgrid button").forEach(function(b){ b.classList.toggle("sel", parseInt(b.getAttribute("data-hh"),10) === state.hh); }); $$("#sabck-sits input").forEach(function(c){ c.checked = !!state.sitRaw[c.value]; }); $$("#sabck-needs input").forEach(function(c){ c.checked = state.top_needs.indexOf(c.value) !== -1; }); $$("#sabck-getting input").forEach(function(c){ c.checked = !!state.flags[c.value]; }); $$("#sabck-tenure-opts button").forEach(function(b){ b.classList.toggle("sel", b.getAttribute("data-tenure") === state.tenure); }); var ub = $("#sabck-utilbills"); if(ub) ub.checked = !!state.flags.utilbills; $$("#sabck-money-opts button").forEach(function(b){ b.classList.toggle("sel", b.getAttribute("data-src") === state.moneySrc); }); updatePill(); } /* Welcome-back screen: built here so the markup snippet stays untouched. Reuses the existing screen/card/button classes, so it looks native. */ var wbScreen = document.createElement("div"); wbScreen.className = "sabck-screen"; wbScreen.setAttribute("data-screen", "welcome"); wbScreen.innerHTML = '

Welcome back

' + '

' + '' + '
' + '
' + '' + '' + '

We remember your answers on this phone only, so you don\'t have to answer again. Nothing extra is sent to us.

'; var wbAnchor = root.querySelector(".sabck-screen"); if(wbAnchor && wbAnchor.parentNode) wbAnchor.parentNode.insertBefore(wbScreen, wbAnchor); function wbRender(){ var d = wbLoad(); if(!d) return; /* restore the saved answers so the matcher re-runs against today's data */ state.zip = d.zip || ""; state.hh = d.hh || 3; state.incomeUnknown = !!d.incomeUnknown; state.income = d.incomeUnknown ? null : (typeof d.income === "number" ? d.income : null); state.sit = {}; if(d.sit){ Object.keys(d.sit).forEach(function(k){ if(d.sit[k]) state.sit[k] = true; }); } state.top_needs = (d.top_needs || []).slice(); /* v6: split the saved map back into engine situations, Q4 chips and flags, then re-derive — keeping any kids/unemployed flags older saves earned */ var keepKids = !!state.sit.kids, keepUnemp = !!state.sit.unemployed; state.flags = {}; FLAG_KEYS.forEach(function(k){ if(state.sit[k]) state.flags[k] = true; }); state.sitRaw = { veteran: !!state.sit.veteran, disabled: !!state.sit.disabled, senior: !!state.sit.senior, youngchild: !!state.sit.youngchild, pregnant: false }; if(state.sit.senior) state.ageRange = "65plus"; state.moneySrc = state.flags.srcnone ? "srcnone" : (state.flags.srcjob ? "srcjob" : (state.flags.srcmix ? "srcmix" : (state.flags.srcbenefits ? "srcbenefits" : ""))); state.tenure = state.flags.renting ? "renting" : (state.flags.ownhome ? "ownhome" : (state.flags.withfamily ? "withfamily" : "")); deriveSit(); if(keepKids) state.sit.kids = true; if(keepUnemp) state.sit.unemployed = true; var res = match(); var resShow = res.filter(function(r){ return !appliedHas(r.p.id); }); var savedIds = d.matched || []; var newOnes = resShow.filter(function(r){ return savedIds.indexOf(r.p.id) === -1; }); var hotOnes = resShow.filter(function(r){ return r.p.hot; }); var when = ""; try{ when = new Date(d.ts).toLocaleDateString("en-US", {month:"long", day:"numeric", year:"numeric"}); }catch(e){} var sub = $("#sabck-wb-sub"); if(sub) sub.textContent = "You last checked on " + when + ". Here are your matches today, based on the answers you gave then."; /* v6.8.6 (Mohamed's add-on order 2026-10-24): the savings figure at the top of Welcome Back — the same estimated-savings sum as the results value box, but only over programs NOT yet marked applied, so every application visibly shrinks what's still on the table. Nothing valued left -> a friendly all-caught-up state, never $0. */ var wbVal = $("#sabck-wb-value"); if(wbVal){ var stillTotal = totalVal(resShow); var wbLabel = $("#sabck-wb-value-label"), wbNum = $("#sabck-wb-value-num"), wbPer = $("#sabck-wb-value-per"), wbFine = $("#sabck-wb-value-fine"); if(stillTotal > 0){ wbVal.style.display = "block"; if(wbLabel) wbLabel.textContent = "Estimated total value you could be missing"; if(wbNum){ wbNum.style.display = ""; wbNum.textContent = "$" + stillTotal.toLocaleString(); } if(wbPer) wbPer.style.display = ""; if(wbFine) wbFine.textContent = "*Estimated from what each program typically saves a household each month. What you actually save depends on the programs you use and your full application."; } else if(res.length && !resShow.length){ wbVal.style.display = "block"; if(wbLabel) wbLabel.textContent = "You're all caught up"; if(wbNum) wbNum.style.display = "none"; if(wbPer) wbPer.style.display = "none"; if(wbFine) wbFine.textContent = "Every program on your list is marked as applied. Redo your check any time your situation changes."; } else { wbVal.style.display = "none"; } } var news = $("#sabck-wb-news"); if(news){ var html = ""; if(!newOnes.length && !hotOnes.length){ html = '

What\'s new for you

' + '

Nothing has changed since your last check — your matches are below.

'; } else { html = '

What\'s new for you

'; if(newOnes.length){ html += '

' + (newOnes.length === 1 ? "We found a new program you qualify for" : "We found " + newOnes.length + " new programs you qualify for") + '

'; newOnes.forEach(function(r){ html += '

' + r.p.name + '

'; }); } if(hotOnes.length){ html += '

Time-sensitive for you — tap one to jump to it

'; hotOnes.forEach(function(r){ html += ''; }); } html += '
'; } news.innerHTML = html; } var list = $("#sabck-wb-list"); if(!list) return; list.innerHTML = ""; if(!resShow.length && res.length){ list.innerHTML = '

You\'ve applied for everything on your list

' + '

Nice work — every program you matched is marked as applied. If your situation changes, redo your check below.

'; } resShow.forEach(function(r){ var card = document.createElement("div"); card.className = "sabck-card"; card.setAttribute("data-cardid", r.p.id); var badge = (savedIds.indexOf(r.p.id) === -1 ? 'NEW' : '') + (r.p.hot ? '' + SVG_CLOCK + 'Time-sensitive' : '✓ May qualify'); var valLine = r.val ? '

Worth up to ~$' + r.val.toLocaleString() + '/mo

' : ''; card.innerHTML = '

' + r.p.name + '

' + badge + '
' + '

' + r.why + '

' + valLine + '

' + r.p.what + '

' + '
' + phoneLinks(r.p.phone) + 'Apply →' + '' + '
'; list.appendChild(card); }); } /* ---------- takeover chrome ---------- */ var tk = $("#sabck-tk"), tkBody = $("#sabck-tk-body"), tkBack = $("#sabck-tk-back"), tkNext = $("#sabck-tk-next"), tkClose = $("#sabck-tk-close"); var current = "needs"; var PREV = {needs:null, zip:"needs", household:"zip", situations:"household", getting:"situations", age:"getting", money:"age", income:"money", home:"income", reveal:"home", personal:"reveal", claimaddr:"personal", job:"claimaddr", citizenship:"job", language:"citizenship", reachtime:"language", hear:"reachtime", results:"hear"}; /* Footer Continue per screen: label + which hidden inline control to activate (reuses the exact validation/handlers — no duplicated logic). */ var NEXT_ACTION = { needs: {label:"Continue →", click:"#sabck-needs-next"}, zip: {label:"Continue →", click:"#sabck-zip-next"}, household: null, /* number taps auto-advance */ situations: {label:"Continue →", click:"#sabck-sit-next"}, getting: {label:"Continue →", click:"#sabck-get-next"}, age: null, /* auto-advances on tap */ money: null, /* auto-advances on tap */ income: null, /* auto-advances on tap */ home: {label:"Continue →", click:"#sabck-home-next"}, reveal: {label:"See my results →", click:"#sabck-gate-next"}, personal: {label:"Continue →", click:"#sabck-personal-next"}, claimaddr: {label:"Continue →", click:"#sabck-claimaddr-next"}, job: null, /* auto-advances on tap */ citizenship: null, /* auto-advances on tap */ language: null, /* auto-advances on tap */ reachtime: null, /* auto-advances on tap */ hear: {label:"Send my guide →", click:"#sabck-final-submit"}, results: {label:"Done", close:true} }; function updateChrome(name){ current = name; if(!tk) return; var nx = NEXT_ACTION[name]; if(!nx){ tkNext.style.display = "none"; } else { tkNext.style.display = ""; tkNext.textContent = nx.label; tkNext.setAttribute("data-tkact", nx.close ? "close" : "click"); tkNext.setAttribute("data-tkclick", nx.click || ""); } /* v6.8.2 (Mohamed's order 2026-10-24): on the email-gate screen ONLY, hide the X, the footer Back, and the footer primary (a duplicate "See my results") — the email field and the in-screen Claim button are the only way forward. Every other screen restores all three. */ if(name === "reveal"){ if(tkBack) tkBack.style.display = "none"; if(tkClose) tkClose.style.display = "none"; tkNext.style.display = "none"; } else { if(tkBack) tkBack.style.display = ""; if(tkClose) tkClose.style.display = ""; } if(tkBody){ try{ tkBody.scrollTop = 0; }catch(e){} } } var QUIZ_SCREENS = {needs:1, zip:1, household:1, situations:1, getting:1, age:1, money:1, income:1, home:1, reveal:1, personal:1, claimaddr:1, job:1, citizenship:1, language:1, reachtime:1, hear:1}; function openTakeover(){ if(!tk) return; var resume; /* welcome-back wins on ANY open once answers are saved — except mid-quiz, where reopening resumes the run already in progress */ var midQuiz = quizLive && QUIZ_SCREENS[current]; /* v6.9.3 tracking: a device coming back on a later day — a saved check or an unfinished run from before today — flags stage 11, once a day. */ try{ var retKey = "sabck_ret_" + new Date().toDateString(); if(!window.localStorage.getItem(retKey)){ var pgR = progLoad(); var oldProg = !!(pgR && pgR.ts && new Date(pgR.ts).toDateString() !== new Date().toDateString()); if(oldProg || wbLoad()){ window.localStorage.setItem(retKey, "1"); sabckStage(11); } } }catch(e){} if(wbLoad() && !midQuiz){ resume = "welcome"; } else if(midQuiz){ resume = current; } else { /* an unfinished run saved on this device resumes where it stopped */ var prog = progLoad(); if(prog){ progApply(prog); resume = prog.screen; } else { resume = "needs"; } } tk.hidden = false; try{ document.body.classList.add("sabck-tk-lock"); }catch(e){} show(resume, "fwd"); if(!REDUCED){ tk.classList.remove("sabck-tk-opening"); void tk.offsetWidth; tk.classList.add("sabck-tk-opening"); } if(tkClose){ try{ tkClose.focus({preventScroll:true}); }catch(e){} } } function closeTakeover(){ if(!tk) return; tk.hidden = true; try{ document.body.classList.remove("sabck-tk-lock"); }catch(e){} } try{ document.addEventListener("keydown", function(e){ if(e.key === "Escape" && tk && !tk.hidden) closeTakeover(); }); }catch(e){} /* ---------- navigation ---------- */ /* v6: progress is "Question X of 10" + the slim bar + the live kinds counter */ var STEPS = {needs:["Question 1 of 10",6], zip:["Question 2 of 10",12], household:["Question 3 of 10",18], situations:["Question 4 of 10",24], getting:["Question 5 of 10",30], age:["Question 6 of 10",36], money:["Question 7 of 10",42], income:["Question 8 of 10",48], home:["Question 9 of 10",54], reveal:["Your matches",62], personal:["Question 10 of 10",68], claimaddr:["Question 10 of 10",74], job:["Question 10 of 10",84], citizenship:["Question 10 of 10",88], language:["Question 10 of 10",92], reachtime:["Question 10 of 10",96], hear:["Question 10 of 10",98], results:["",100]}; var FRONT_SCREENS = {needs:1, zip:1, household:1, situations:1, getting:1, age:1, money:1, income:1, home:1, reveal:1}; function show(name, dir){ dir = (dir === "back") ? "back" : "fwd"; if(name === "welcome"){ wbRender(); } else if(name !== "results"){ quizLive = true; } if(name === "reveal"){ renderReveal(); } if(name === "claimaddr"){ prefillAddress(); } var pw = $("#sabck-pillwrap"); if(pw) pw.classList.toggle("on", !!FRONT_SCREENS[name]); if(QUIZ_SCREENS[name]){ current = name; progSave(); } $$(".sabck-screen").forEach(function(el){ var on = el.dataset.screen === name; el.classList.toggle("sabck-active", on); el.classList.remove("sabck-dir-f","sabck-dir-b"); if(on) el.classList.add(dir === "back" ? "sabck-dir-b" : "sabck-dir-f"); }); var st = STEPS[name] || ["",0]; var lbl = $("#sabck-step-label"); if(lbl) lbl.textContent = st[0]; var pf = $("#sabck-pfill"); if(pf) pf.style.width = st[1] + "%"; updateChrome(name); /* funnel analytics: quiz start + per-step progress (server dedupes per visitor/day). v6 keeps the tracker's stage meanings: household = zip done, situations = household done, home = income done, reveal = front questions done; the completion event (in renderResults) stays the finish line — comparable to the old flow's 44 starts → 19 finishes. */ try{ if(name==="needs") sabckFunnel("quiz_start"); else if(name==="household") sabckFunnel("quiz_step", 2); else if(name==="situations") sabckFunnel("quiz_step", 3); else if(name==="home") sabckFunnel("quiz_step", 4); else if(name==="reveal") sabckFunnel("quiz_step", 5); else if(name==="gate") sabckStage(6); else if(name==="personal") sabckStage(7); else if(name==="claimaddr") sabckStage(8); }catch(e){} } /* Animated count-up (respects prefers-reduced-motion) */ function countUp(el, to, prefix){ if(!el) return; prefix = prefix || ""; if(REDUCED || !window.requestAnimationFrame){ el.textContent = prefix + to.toLocaleString(); return; } var start = null, dur = 900; function frame(t){ if(!start) start = t; var p = Math.min((t - start) / dur, 1); var e = 1 - Math.pow(1 - p, 3); el.textContent = prefix + Math.round(to * e).toLocaleString(); if(p < 1) requestAnimationFrame(frame); } requestAnimationFrame(frame); } /* Gentle inline validation: highlight the fields that need attention */ function markInvalid(id, valid){ var el = root.querySelector("#" + id); if(!el) return; var wrap = (el.closest && (el.closest(".sabck-field") || el)) || el; if(valid) wrap.classList.remove("sabck-invalid"); else wrap.classList.add("sabck-invalid"); } /* Anonymous funnel beacon: start/step events only, no answers attached. */ function sabckFunnel(ev, step){ try{ var d = {event: ev, page: window.location.pathname}; if(step) d.step = step; var x = new XMLHttpRequest(); x.open("POST", "/wp-json/sab/v1/quiz-event", true); x.setRequestHeader("Content-Type", "application/json"); x.send(JSON.stringify(d)); }catch(e){} } /* v6.9.3 (test window Oct 26-28): back-half stage beacons via the capture snippet — 6 gate shown, 7 personal shown (gate passed), 8 address shown (personal done), 11 returning device. Anonymous, same transport shape. */ function sabckStage(n){ try{ var x = new XMLHttpRequest(); x.open("POST", "/wp-json/sab/v1/quiz-stage", true); x.setRequestHeader("Content-Type", "application/json"); x.send(JSON.stringify({stage: n, page: window.location.pathname})); }catch(e){} } /* Lead capture: contact info + quiz answers, stored and emailed to the owner. */ function sendLead(){ try{ var res = match(); var sitKeys = Object.keys(state.sit).filter(function(k){ return state.sit[k]; }); Object.keys(state.flags).forEach(function(k){ if(state.flags[k] && sitKeys.indexOf(k) === -1) sitKeys.push(k); }); var d = { name: state.name, phone: state.phone, email: state.email, pref: state.pref, consent: state.consent ? 1 : 0, dob: state.dob, street: state.street, city: state.city, employment: state.employment, income_amount: state.income_amount, income_period: state.income_period, citizenship: state.citizenship, top_need: state.top_needs.join(","), language: state.language, contact_time: state.contact_time, hear_about: state.hear_about, notes: state.notes, zip: state.zip, hh: state.hh, income: state.income, sit: sitKeys.join(","), matched: res.map(function(r){ return r.p.id; }).join(","), count: res.length, value: totalVal(res), page: window.location.pathname }; var payload = JSON.stringify(d); var leadTried = false; function postLead(){ var x = new XMLHttpRequest(); x.open("POST", "/wp-json/sab/v1/quiz-lead-v2", true); x.setRequestHeader("Content-Type", "application/json"); x.onreadystatechange = function(){ if(x.readyState === 4 && x.status !== 200 && !leadTried){ leadTried = true; setTimeout(postLead, 1500); } }; x.onerror = function(){ if(!leadTried){ leadTried = true; setTimeout(postLead, 1500); } }; x.send(payload); } postLead(); try{ var ab = document.querySelector("#sabck-alerts"); if(ab && ab.checked && d.email){ var ax = new XMLHttpRequest(); ax.open("POST", "/wp-json/sab/v1/alerts-signup", true); ax.setRequestHeader("Content-Type", "application/json"); ax.send(JSON.stringify({ email: d.email, source: "checker", website: "" })); } }catch(e){} }catch(e){} } /* Gate field: Enter submits the gate, same as tapping the button. */ var gateEl = $("#sabck-gate-email"); if(gateEl){ gateEl.addEventListener("keydown", function(e){ if(e.key === "Enter"){ e.preventDefault(); var gb = $("#sabck-gate-next"); if(gb) gb.click(); } }); } /* ZIP auto-advance: 5 valid digits completes the step — no dead tap. */ var zipEl = $("#sabck-zip"); if(zipEl){ zipEl.addEventListener("input", function(){ var v = zipEl.value.replace(/\D/g,"").slice(0,5); if(v !== zipEl.value) zipEl.value = v; var err = $("#sabck-zip-err"); if(err) err.style.display = "none"; if(/^\d{5}$/.test(v)){ var zipActive = root.querySelector('.sabck-screen[data-screen="zip"].sabck-active'); if(zipActive){ state.zip = v; show("household","fwd"); } } }); } /* Delegated clicks: one listener on the checker root survives DOM rewrites by translators/optimizers that replace child nodes. */ root.addEventListener("click", function(e){ var t = e.target; var el = (t && t.nodeType === 1) ? t : (t && t.parentElement ? t.parentElement : null); function closest(sel){ var n = el; while(n && n !== root){ if(n.matches && n.matches(sel)) return n; n = n.parentElement; } return null; } /* takeover chrome first */ var opener = closest("[data-sabck-open]"); if(opener){ if(e.preventDefault) e.preventDefault(); openTakeover(); return; } if(closest("#sabck-tk-close")){ closeTakeover(); return; } if(closest("#sabck-tk-back")){ if(current === "needs" || current === "welcome"){ closeTakeover(); } else { var p = PREV[current]; show(p || "needs", "back"); } return; } var apBtn = closest("[data-applied]"); if(apBtn){ /* v6.8.5: mark applied -> persist on the device, then re-render the list the visitor is looking at (results re-render skips the tracker event; the guide/lead were already sent in full). */ appliedAdd(apBtn.getAttribute("data-applied")); if(apBtn.closest && apBtn.closest("#sabck-res-list")){ renderResults(true); } else if(apBtn.closest && apBtn.closest("#sabck-wb-list")){ wbRender(); } return; } var jumpBtn = closest("[data-jump]"); if(jumpBtn){ /* v6.8.5: a time-sensitive item on Welcome Back jumps straight to that program's card in the saved list below — one tap. */ var jid = jumpBtn.getAttribute("data-jump"); var jcard = document.querySelector('#sabck-wb-list [data-cardid="' + jid + '"]'); if(jcard){ try{ jcard.scrollIntoView({block: "start", behavior: REDUCED ? "auto" : "smooth"}); }catch(e){ jcard.scrollIntoView(); } jcard.classList.add("sabck-flash"); setTimeout(function(){ jcard.classList.remove("sabck-flash"); }, 1600); } return; } if(closest("#sabck-res-restart")){ /* v6.8.5: Start over on results — clears saved answers AND applied marks, then a fresh check. */ wbForget(); appliedClear(); quizLive = true; wbResetQuiz(); current = "needs"; show("needs", "fwd"); return; } if(closest("#sabck-wb-retake")){ wbForget(); appliedClear(); quizLive = true; wbResetQuiz(); current = "needs"; show("needs", "fwd"); return; } if(closest("#sabck-wb-forget")){ wbForget(); appliedClear(); quizLive = false; wbResetQuiz(); current = "needs"; closeTakeover(); return; } if(closest("#sabck-tk-next")){ var act = tkNext.getAttribute("data-tkact"); if(act === "close"){ closeTakeover(); return; } var sel = tkNext.getAttribute("data-tkclick"); if(sel){ var tgt = root.querySelector(sel); if(tgt) tgt.click(); } return; } var nx = closest("[data-next]"); if(nx){ show(nx.getAttribute("data-next"), "fwd"); return; } var bk = closest("[data-back]"); if(bk){ show(bk.getAttribute("data-back"), "back"); return; } if(closest("#sabck-needs-next")){ syncNeeds(); /* the server requires at least one need — "Something else" is the escape */ if(!state.top_needs.length){ $("#sabck-needs-err").style.display = "block"; return; } $("#sabck-needs-err").style.display = "none"; show("zip","fwd"); return; } if(closest("#sabck-zip-next")){ var z = $("#sabck-zip").value.trim(); if(!/^\d{5}$/.test(z)){ $("#sabck-zip-err").style.display="block"; return; } $("#sabck-zip-err").style.display="none"; state.zip = z; show("household","fwd"); return; } var hhBtn = closest("#sabck-hhgrid button"); if(hhBtn){ state.hh = parseInt(hhBtn.getAttribute("data-hh"), 10) || 3; $$("#sabck-hhgrid button").forEach(function(b){ b.classList.toggle("sel", b === hhBtn); }); progSave(); /* big number taps advance on their own — one less button to hunt for */ setTimeout(function(){ if(current === "household") show("situations","fwd"); }, REDUCED ? 0 : 300); return; } if(closest("#sabck-sit-none")){ /* escape: clear every ticked box on THIS screen and continue with zero situations */ $$("#sabck-sits input").forEach(function(c){ c.checked=false; state.sitRaw[c.value]=false; }); deriveSit(); updatePill(); show("getting","fwd"); return; } if(closest("#sabck-sit-next")){ $$("#sabck-sits input").forEach(function(c){ state.sitRaw[c.value]=c.checked; }); deriveSit(); show("getting","fwd"); return; } if(closest("#sabck-get-next")){ $$("#sabck-getting input").forEach(function(c){ if(c.value !== "getnone") state.flags[c.value] = c.checked; }); progSave(); show("age","fwd"); return; } var ageBtn = closest("#sabck-age-opts button"); if(ageBtn){ $$("#sabck-age-opts button").forEach(function(b){ b.classList.toggle("sel", b === ageBtn); }); state.ageRange = ageBtn.getAttribute("data-skip") ? "" : (ageBtn.getAttribute("data-age") || ""); deriveSit(); progSave(); updatePill(); setTimeout(function(){ if(current === "age") show("money","fwd"); }, REDUCED ? 0 : 300); return; } var moneyBtn = closest("#sabck-money-opts button"); if(moneyBtn){ $$("#sabck-money-opts button").forEach(function(b){ b.classList.toggle("sel", b === moneyBtn); }); state.moneySrc = moneyBtn.getAttribute("data-src") || ""; ["srcjob","srcbenefits","srcmix","srcnone"].forEach(function(k){ state.flags[k] = (k === state.moneySrc); }); deriveSit(); progSave(); updatePill(); setTimeout(function(){ if(current === "money") show("income","fwd"); }, REDUCED ? 0 : 300); return; } var incBtn = closest("#sabck-income-opts button"); if(incBtn){ $$("#sabck-income-opts button").forEach(function(b){ b.classList.toggle("sel", b === incBtn); }); if(incBtn.getAttribute("data-skip")){ state.incomeUnknown=true; state.income=null; } else { state.incomeUnknown=false; state.income=parseFloat(incBtn.getAttribute("data-val")); } progSave(); updatePill(); setTimeout(function(){ if(current === "income") show("home","fwd"); }, REDUCED ? 0 : 300); return; } var tenBtn = closest("#sabck-tenure-opts button"); if(tenBtn){ state.tenure = tenBtn.getAttribute("data-tenure") || ""; $$("#sabck-tenure-opts button").forEach(function(b){ b.classList.toggle("sel", b === tenBtn); }); ["renting","ownhome","withfamily"].forEach(function(k){ state.flags[k] = (k === state.tenure); }); progSave(); return; } if(closest("#sabck-home-next")){ show("reveal","fwd"); return; } if(closest("#sabck-gate-next")){ /* EMAIL GATE: a valid email is required to go any further. Giving it is the agreement the gate copy states — guide emailed + newsletter add — so consent is recorded here, not by a later tick box. */ var gem = $("#sabck-gate-email"); var gemail = gem ? gem.value.trim() : ""; var gOk = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(gemail); markInvalid("sabck-gate-email", gOk); var gerr = $("#sabck-gate-err"); if(gerr) gerr.style.display = gOk ? "none" : "block"; if(!gOk) return; state.email = gemail; state.consent = true; progSave(); show("personal","fwd"); return; } if(closest("#sabck-personal-next")){ /* v6.9.2 (Mohamed's order 2026-10-25): ONE personal-info step directly after the email screen — full name + birthday + phone + reach picks — and results never render before it is completed. The email was collected on the gate screen and is never asked twice. */ var nm = $("#sabck-name").value.trim(); var dob = $("#sabck-dob").value; var dobOk = /^\d{4}-\d{2}-\d{2}$/.test(dob) && (function(){ var p = dob.split("-"); var y = +p[0], m = +p[1], d = +p[2]; if(y < 1900 || y > new Date().getFullYear()) return false; var dt = new Date(y, m-1, d); return dt.getFullYear()===y && dt.getMonth()===m-1 && dt.getDate()===d && dt <= new Date(); })(); var nameOk = nm.length >= 2; var ph = $("#sabck-phone").value.trim(); var digits = ph.replace(/[^0-9]/g, ""); var phoneOk = digits.length >= 7 && digits.length <= 15; markInvalid("sabck-name", nameOk); markInvalid("sabck-dob", dobOk); markInvalid("sabck-phone", phoneOk); var pErr = $("#sabck-personal-err"); if(!nameOk || !dobOk || !phoneOk){ pErr.textContent = "Please add your full name, birthday and a valid phone number — the highlighted ones need attention."; pErr.style.display = "block"; return; } if(!state.pref){ pErr.textContent = "Please pick at least one way to reach you — email, text, or call."; pErr.style.display = "block"; return; } pErr.style.display = "none"; state.name = nm; state.dob = dob; state.phone = ph; show("claimaddr","fwd"); return; } if(closest("#sabck-claimaddr-next")){ var street = $("#sabck-street").value.trim(); var city = $("#sabck-city").value.trim(); var st = $("#sabck-state").value.trim(); var z2 = $("#sabck-zip2").value.replace(/\D/g, "").slice(0, 5); var streetOk = street.length >= 3, cityOk = city.length >= 2, stOk = st.length >= 2, zipOk = /^\d{5}$/.test(z2); markInvalid("sabck-street", streetOk); markInvalid("sabck-city", cityOk); markInvalid("sabck-state", stOk); markInvalid("sabck-zip2", zipOk); $("#sabck-claimaddr-err").style.display = (streetOk && cityOk && stOk && zipOk) ? "none" : "block"; if(!(streetOk && cityOk && stOk && zipOk)) return; state.street = street; state.city = city; state.zip = z2; hideSugg(); show("job","fwd"); return; } var resChip = closest("#sabck-res-filters .sabck-chip"); if(resChip){ /* v6.8.4: category filter — display-only show/hide of the section heads and cards for the tapped kind; "all" restores everything. */ var fk = resChip.getAttribute("data-kind") || "all"; $$("#sabck-res-filters .sabck-chip").forEach(function(c){ c.classList.toggle("sel", c === resChip); }); $$("#sabck-res-list [data-kind]").forEach(function(el){ el.style.display = (fk === "all" || el.getAttribute("data-kind") === fk) ? "" : "none"; }); return; } var prefChip = closest("#sabck-prefrow .sabck-prefchip"); if(prefChip){ /* v6.8.1 (Mohamed's order 2026-10-24): reach methods are MULTI-SELECT — any combination of email / text / call. state.pref records ALL chosen methods, comma-joined in on-screen order, so the saved lead and the guide pipeline see every method the visitor picked. */ prefChip.classList.toggle("sel"); var picked = []; $$("#sabck-prefrow .sabck-prefchip").forEach(function(b){ if(b.classList.contains("sel")) picked.push(b.getAttribute("data-pref")); }); state.pref = picked.join(","); return; } var jobBtn = closest("#sabck-job-opts button"); if(jobBtn){ $$("#sabck-job-opts button").forEach(function(b){ b.classList.toggle("sel", b === jobBtn); }); state.employment = jobBtn.getAttribute("data-emp") || ""; deriveSit(); progSave(); setTimeout(function(){ if(current === "job") show("citizenship","fwd"); }, REDUCED ? 0 : 300); return; } var citBtn = closest("#sabck-cit-opts button"); if(citBtn){ $$("#sabck-cit-opts button").forEach(function(b){ b.classList.toggle("sel", b === citBtn); }); state.citizenship = citBtn.getAttribute("data-cit") || ""; progSave(); setTimeout(function(){ if(current === "citizenship") show("language","fwd"); }, REDUCED ? 0 : 300); return; } var langBtn = closest("#sabck-lang-opts button"); if(langBtn){ $$("#sabck-lang-opts button").forEach(function(b){ b.classList.toggle("sel", b === langBtn); }); state.language = langBtn.getAttribute("data-lang") || "en"; progSave(); setTimeout(function(){ if(current === "language") show("reachtime","fwd"); }, REDUCED ? 0 : 300); return; } var timeBtn = closest("#sabck-time-opts button"); if(timeBtn){ $$("#sabck-time-opts button").forEach(function(b){ b.classList.toggle("sel", b === timeBtn); }); state.contact_time = timeBtn.getAttribute("data-time") || ""; progSave(); setTimeout(function(){ if(current === "reachtime") show("hear","fwd"); }, REDUCED ? 0 : 300); return; } var hearBtn = closest("#sabck-hear-opts button"); if(hearBtn){ $$("#sabck-hear-opts button").forEach(function(b){ b.classList.toggle("sel", b === hearBtn); }); state.hear_about = hearBtn.getAttribute("data-hear") || ""; return; } if(closest("#sabck-final-submit")){ var nt = $("#sabck-notes"); state.notes = nt ? nt.value.trim().slice(0, 2000) : ""; /* income as the server needs it: the tapped range's monthly value, or 0/month ("not stated") when they tapped "I'm not sure" — a lead must never bounce over an honestly skipped question */ if(!state.incomeUnknown && typeof state.income === "number"){ state.income_amount = state.income; state.income_period = "month"; } else { state.income_amount = 0; state.income_period = "month"; state.income = 0; } if(!leadSent){ sendLead(); leadSent = true; } progClear(); renderResults(); show("results","fwd"); return; } var sh = closest("[data-share]"); if(sh){ doShare(sh.getAttribute("data-share")); } }); /* Live tap sync: needs + situations update state (and the counter pill) the moment a chip is tapped — tap order is kept for needs, because the first need named is the first one solved (problem-first). */ function syncNeeds(){ var boxes = $$("#sabck-needs input"); var now = boxes.filter(function(c){ return c.checked; }).map(function(c){ return c.value; }); state.top_needs = state.top_needs.filter(function(n){ return now.indexOf(n) !== -1; }); now.forEach(function(n){ if(state.top_needs.indexOf(n) === -1) state.top_needs.push(n); }); } root.addEventListener("change", function(e){ var t = e.target; if(!t || t.type !== "checkbox") return; if(t.closest && t.closest("#sabck-needs")){ syncNeeds(); deriveSit(); updatePill(); progSave(); return; } if(t.closest && t.closest("#sabck-sits")){ state.sitRaw[t.value] = t.checked; deriveSit(); updatePill(); progSave(); return; } if(t.closest && t.closest("#sabck-getting")){ /* "none" is exclusive: ticking it clears the rest, and vice versa */ if(t.value === "getnone" && t.checked){ $$("#sabck-getting input").forEach(function(c){ if(c.value !== "getnone"){ c.checked = false; state.flags[c.value] = false; } }); } else if(t.value !== "getnone" && t.checked){ var nn = root.querySelector('#sabck-getting input[value="getnone"]'); if(nn) nn.checked = false; state.flags[t.value] = true; } else if(t.value !== "getnone"){ state.flags[t.value] = false; } progSave(); return; } if(t.id === "sabck-utilbills"){ state.flags.utilbills = t.checked; var ui = state.top_needs.indexOf("utilities"); if(t.checked && ui === -1) state.top_needs.push("utilities"); if(!t.checked && ui !== -1) state.top_needs.splice(ui, 1); updatePill(); progSave(); return; } }); /* ---------- v5: kinds counter + problem-first order + the reveal ---------- */ /* Which checker programs answer which need (the need named first is solved first). */ var NEED_PROGRAMS = { food:["snap","wic","foodbank"], housing:["trla_rtc","hap","opportunity_home","hud_vash"], utilities:["cps_discount","sa_utility"], health:["medicaid","chip"], childcare:["ccs"], jobs:["wioa","twc_unemp"], transport:["via_link"], phone:["lifeline"], other:[] }; function needRank(pid){ var flat = []; state.top_needs.forEach(function(n){ (NEED_PROGRAMS[n] || []).forEach(function(id){ if(flat.indexOf(id) === -1) flat.push(id); }); }); var i = flat.indexOf(pid); return i === -1 ? 999 : i; } /* Program → the "kind of help" it belongs to (needs use the same kind keys). */ var KIND_OF = {snap:"food", wic:"food", foodbank:"food", hap:"housing", opportunity_home:"housing", trla_rtc:"housing", hud_vash:"housing", cps_discount:"utilities", sa_utility:"utilities", medicaid:"health", chip:"health", ccs:"childcare", wioa:"jobs", twc_unemp:"jobs", va:"veteran", tvc:"veteran", vita:"money", tanf:"money", trla:"legal", via_link:"transport", lifeline:"phone", "211":"other"}; /* Category labels + display order for the JAX-shape results sections (v6.8). */ var KIND_LABELS = {food:"Food", housing:"Rent / housing", utilities:"Utility bills", health:"Health care", childcare:"Child care", jobs:"Jobs / training", veteran:"Veterans", money:"Money & cash help", legal:"Legal help", transport:"Rides / transportation", phone:"Phone / internet", other:"More help"}; var KIND_ORDER = ["food","housing","utilities","health","childcare","jobs","veteran","money","legal","transport","phone","other"]; function kindsCount(res){ var s = {}; state.top_needs.forEach(function(n){ s[n] = 1; }); (res || match()).forEach(function(r){ var k = KIND_OF[r.p.id]; if(k) s[k] = 1; }); return Object.keys(s).length; } /* The chrome counter: one pill, always visible across the front questions, counting the kinds of help found so far from the answers given. */ function updatePill(){ var pill = $("#sabck-pill"); if(!pill) return; var answered = state.top_needs.length > 0 || Object.keys(state.sitRaw).some(function(k){ return state.sitRaw[k]; }) || !!state.ageRange || !!state.moneySrc || typeof state.income === "number"; var k = answered ? kindsCount() : 0; pill.innerHTML = k ? "✓ " + k + " kinds of help found so far" : ""; pill.classList.toggle("on", k > 0); if(k && !REDUCED){ pill.classList.remove("pop"); void pill.offsetWidth; pill.classList.add("pop"); } } /* ---------- claim address: ZIP prefill + street autocomplete ---------- */ var addrPrefilled = false; function prefillAddress(){ var zipEl = $("#sabck-zip2"); if(zipEl && !zipEl.value && state.zip) zipEl.value = state.zip; if(addrPrefilled) return; addrPrefilled = true; var cityEl = $("#sabck-city"), stEl = $("#sabck-state"); if(/^782/.test(state.zip)){ if(cityEl && !cityEl.value) cityEl.value = "San Antonio"; if(stEl && !stEl.value) stEl.value = "TX"; return; } /* other ZIPs: look the city/state up (free ZIP service); typing still works */ try{ fetch("https://api.zippopotam.us/us/" + encodeURIComponent(state.zip)) .then(function(r){ return r.ok ? r.json() : null; }) .then(function(j){ if(!j || !j.places || !j.places.length) return; if(cityEl && !cityEl.value) cityEl.value = j.places[0]["place name"] || ""; if(stEl && !stEl.value) stEl.value = j.places[0]["state abbreviation"] || ""; }).catch(function(){}); }catch(e){} } function hideSugg(){ var box = $("#sabck-addr-sugg"); if(box){ box.hidden = true; box.innerHTML = ""; } } /* Street autocomplete via Photon (OpenStreetMap-based, no key needed). Best-effort: if it is slow or down, the field is just a normal field. */ var suggTimer = null; var streetEl = $("#sabck-street"); if(streetEl){ streetEl.addEventListener("input", function(){ var q = streetEl.value.trim(); if(suggTimer) clearTimeout(suggTimer); if(q.length < 4){ hideSugg(); return; } suggTimer = setTimeout(function(){ try{ fetch("https://photon.komoot.io/api/?limit=5&lang=en&q=" + encodeURIComponent(q)) .then(function(r){ return r.ok ? r.json() : null; }) .then(function(j){ var box = $("#sabck-addr-sugg"); if(!box || !j || !j.features) return; var feats = j.features.filter(function(f){ return f.properties && f.properties.countrycode === "US" && (f.properties.housenumber || f.properties.street); }).slice(0, 5); if(!feats.length){ hideSugg(); return; } box.innerHTML = ""; feats.forEach(function(f){ var p = f.properties; var line1 = ((p.housenumber || "") + " " + (p.street || p.name || "")).trim(); var line2 = ((p.city || "") + ", " + (p.state || "") + " " + (p.postcode || "")).trim(); var b = document.createElement("button"); b.type = "button"; b.innerHTML = ""; b.textContent = line1; var sm = document.createElement("small"); sm.textContent = line2; b.appendChild(sm); b.addEventListener("click", function(){ streetEl.value = line1; var c = $("#sabck-city"); if(c && p.city) c.value = p.city; var s = $("#sabck-state"); if(s && p.state) s.value = p.state; var z = $("#sabck-zip2"); if(z && p.postcode) z.value = String(p.postcode).slice(0, 5); hideSugg(); }); box.appendChild(b); }); box.hidden = false; }).catch(function(){}); }catch(e){} }, 280); }); streetEl.addEventListener("blur", function(){ setTimeout(hideSugg, 200); }); } /* The reveal is now the EMAIL GATE (v6.7, Mohamed's order 2026-10-23): no program names show before an email is given. Count only as a teaser; the gate copy in the markup states the terms plainly. */ function renderReveal(){ var res = match(); countUp($("#sabck-rev-count"), res.length, ""); /* v6.8 (Mohamed's order 2026-10-24): the gate ALSO shows the estimated total value the visitor could be missing — same matched set and same per-program values as the results box, so the two figures always agree. Still no program names before the email. Hidden when the honest total is $0 (income-unknown answers zero the values). */ var gTotal = totalVal(res); var gv = $("#sabck-gate-value"); var gf = $("#sabck-gate-fine"); if(gv){ if(gTotal>0){ gv.style.display="block"; if(gf) gf.style.display="block"; countUp($("#sabck-gate-value-num"), gTotal, "$"); } else { gv.style.display="none"; if(gf) gf.style.display="none"; } } /* v6.8.2 (Mohamed's order 2026-10-24): the gate's one button carries the money — it is labelled with THIS visitor's own estimated total. $0 total falls back to a plain label; never print a $0 figure. */ var gb = $("#sabck-gate-next"); if(gb) gb.textContent = gTotal > 0 ? ("Claim my $" + gTotal.toLocaleString() + " in benefits") : "Claim my benefits"; var ge = $("#sabck-gate-email"); if(ge && state.email && !ge.value) ge.value = state.email; } /* ---------- matching engine (unchanged) ---------- */ /* Standard savings-estimate table (Mohamed's order 2026-10-25, the same on every benefits site): what each kind of help typically saves a household a month - free food $400, free transportation $500, housing/rent help $500, utility help $150, health coverage $400, free clinic/health services $250, childcare $400, phone/internet $50, baby/diapers $75, clothing/school supplies $50, legal help $100. A program WITH a verified amount keeps it (snap, tanf and cps_discount pass theirs in explicitly inside match()); every other matched program contributes its estimate from this table; a program with neither contributes 0 (vita, twc_unemp, wioa, va, tvc, 211 - no clear monthly replacement cost). trla_rtc is a free eviction LAWYER, so it takes the legal value even though its kind is housing. */ var EST_VAL = {wic:400, foodbank:400, opportunity_home:500, hap:500, hud_vash:500, sa_utility:150, medicaid:400, chip:400, ccs:400, via_link:500, lifeline:50, trla:100, trla_rtc:100}; /* v6.9.2 COUNTING RULE (Mohamed, 2026-10-25): a program's VERIFIED amount always counts in full; SAVINGS ESTIMATES count ONCE per kind per household — one food source, one childcare, one ride — however many programs of that kind match. Per-card values still show per program; only the headline totals (gate, results, Welcome Back, lead) use this. */ var VERIFIED_IDS = {snap:1, tanf:1, cps_discount:1}; function totalVal(list){ var vSum = 0, estByKind = {}; (list || []).forEach(function(r){ var v = r.val || 0; if(!v) return; var id = r.p ? r.p.id : r.id; if(VERIFIED_IDS[id]){ vSum += v; return; } var k = KIND_OF[id] || "other"; if(!estByKind[k] || v > estByKind[k]) estByKind[k] = v; }); var t = vSum; for(var k2 in estByKind){ if(Object.prototype.hasOwnProperty.call(estByKind, k2)) t += estByKind[k2]; } return t; } function match(){ var hh=state.hh, inc=state.income, s=state.sit, out=[]; var incK = !state.incomeUnknown && typeof inc === "number"; function incLte(lim){ return !incK || inc <= lim; } function valK(v){ return incK ? v : 0; } function add(id, why, val){ var p=null; PROGRAMS.forEach(function(x){ if(x.id===id) p=x; }); if(!p) return; var v = (arguments.length >= 3) ? (val||0) : (EST_VAL[id]||0); out.push({p:p, why:why, val:v}); } var snapLim = fpl(hh,1.65), medLim = fpl(hh,1.33), chipLim = fpl(hh,2.00); if(incLte(snapLim)) add("snap", incK ? "Your income is under the SNAP limit for a household of "+hh+"." : "SNAP is income-based — households under the limit for their size qualify; the agency confirms when you apply.", valK(SNAP_MAX[Math.min(hh,8)]||1755)); if(s.youngchild) add("wic","You have a young child or are pregnant — WIC has no citizenship requirement."); if(s.kids && incLte(medLim)) add("medicaid", incK ? "Your kids likely qualify for free coverage at your income." : "Kids' Medicaid is income-based — this is the first coverage to check."); else if((s.senior||s.disabled) && incLte(medLim)) add("medicaid", incK ? "Seniors and people with disabilities can qualify at your income." : "Seniors and people with disabilities can qualify for Medicaid — it is income-based, so apply to confirm."); if(s.kids && incK && inc > medLim && inc <= chipLim) add("chip","Your income is above Medicaid but under the CHIP limit — low-cost kids' coverage."); else if(s.kids && incLte(chipLim)) add("chip","Backup option if Medicaid doesn't come through — CHIP covers kids cheaply."); if(s.kids && incLte(1000)) add("tanf", incK ? "Very low income with kids — TANF provides monthly cash help." : "For families with kids and very low income — TANF provides monthly cash help.", valK(382)); if(incLte(snapLim)) add("opportunity_home","Low-income households can apply when waitlists open."); if(s.eviction){ add("trla_rtc","You're facing eviction — a free lawyer can fight it."); add("hap","Behind on rent? HAP can cover up to 3 months."); } else if(incLte(fpl(hh,0.80))) add("hap", incK ? "Your income is under 80% of area median — HAP rental help when enrollment opens." : "HAP rental help is income-based — watch for enrollment openings."); if(incLte(fpl(hh,1.25))) add("cps_discount", incK ? "Your income qualifies for up to 25% off your CPS Energy bill." : "Income-based — qualifying households get up to 25% off their CPS Energy bill.", valK(45)); if(incLte(fpl(hh,1.50))) add("sa_utility", incK ? "Your income is under the city utility-assistance limit." : "City utility assistance is income-based — worth applying."); if(incLte(5417)) add("vita", incK ? "Your income is under $65k/yr — get your taxes done free." : "Free tax prep for households under about $65k/yr."); if(s.unemployed){ add("twc_unemp","You lost work or hours — unemployment can replace part of your income."); add("wioa","Free job training while you get back on your feet."); } if(s.kids && (s.unemployed || true)) add("ccs","Working or training with kids? Child care scholarships help pay for it."); if(s.veteran){ add("va","As a veteran, start with your full VA benefits check."); add("tvc","Free help filing VA claims and finding work."); if(s.eviction) add("hud_vash","Veteran facing housing loss — HUD-VASH combines a voucher with VA support."); } add("foodbank","Open to everyone in the area — free food plus free help applying for benefits."); add("via_link","Rides across San Antonio — on-demand vans from $1.30 a ride, plus regular VIA bus routes."); if(incLte(fpl(hh,1.35))) add("lifeline", incK ? "Your income is under the Lifeline limit — $9.25/month off phone or internet, free plans available." : "Lifeline discounts phone/internet for lower-income households — apply free to confirm."); if(s.eviction || incLte(snapLim)) add("trla","Free legal help if any application is denied or you need legal aid."); add("211","Catch-all: one call finds any local help we didn't list."); /* de-dupe + sort (v5): focus first, then PROBLEM-FIRST — programs that answer the needs the person named, in the order they named them — then time-sensitive, then by value. The matched SET is unchanged from v4. */ var seen={}, ded=[]; out.forEach(function(r){ if(!seen[r.p.id]){ seen[r.p.id]=1; ded.push(r); } }); ded.sort(function(a,b){ if(state.focus){ if(a.p.id===state.focus) return -1; if(b.p.id===state.focus) return 1; } var ra = needRank(a.p.id), rb = needRank(b.p.id); if(ra !== rb) return ra - rb; if(!!a.p.hot!==!!b.p.hot) return a.p.hot?-1:1; return (b.val||0)-(a.val||0); }); return ded; } /* v6.8.3 (Mohamed's order 2026-10-24): a program phone string may list TWO numbers ("2-1-1 or 877-541-7905"). Each number becomes its OWN tap-to-call link whose tel: holds exactly that number - never glue numbers together (the old render produced tel:2118775417905, an uncallable number). Short codes link as tel:; full numbers in the site's +1 form. Each number's display text is unchanged. */ function phoneLinks(phone){ var parts = String(phone || "").split(/\s+or\s+|\s*\/\s*|,\s*/); var out = []; parts.forEach(function(part){ var d = part.replace(/[^0-9]/g, ""); var href = null; if(d.length === 3) href = "tel:" + d; else if(d.length === 10) href = "tel:+1" + d; else if(d.length === 11 && d.charAt(0) === "1") href = "tel:+" + d; if(href) out.push('' + SVG_PHONE + '' + part + ''); }); if(!out.length) return '' + SVG_PHONE + '' + phone + ''; return out.join(""); } function buildResultCard(r, i){ var card=document.createElement("div"); card.className="sabck-card sabck-card-in"; card.setAttribute("data-kind", KIND_OF[r.p.id] || "other"); card.setAttribute("data-cardid", r.p.id); if(!REDUCED) card.style.animationDelay = Math.min(i * 60, 420) + "ms"; var badge = r.p.hot ? ''+SVG_CLOCK+'Time-sensitive' : '✓ May qualify'; var valLine = r.val ? '

Worth up to ~$'+r.val.toLocaleString()+'/mo

' : ''; card.innerHTML = '

'+r.p.name+'

'+badge+'
' + '

'+r.why+'

' + valLine + '

'+r.p.what+'

' + '
' + phoneLinks(r.p.phone) + 'Apply →' + '' + '
'; return card; } function renderResults(fromApplied){ var res = match(); wbSave(res); /* v4.3: remember this visitor's answers on their device */ /* v6.8.5: programs the visitor marked "I applied for this" leave the DISPLAYED list only — res itself stays whole for wbSave above, the emailed guide, the saved lead and the tracker event below. */ var resShow = res.filter(function(r){ return !appliedHas(r.p.id); }); var total = totalVal(resShow); $("#sabck-res-headline").innerHTML = 'You may qualify for 0 programs'; countUp($("#sabck-res-count"), resShow.length, ""); var inSA = /^782\d{2}$/.test(state.zip); if(state.incomeUnknown){ $("#sabck-res-sub").textContent = "Based on a household of "+state.hh+" in "+state.zip+". You skipped the income question, so this list shows everything you may qualify for — each program confirms income when you apply."; } else { $("#sabck-res-sub").textContent = inSA ? "Based on a household of "+state.hh+" with ~$"+state.income.toLocaleString()+"/mo income in "+state.zip+"." : "Based on a household of "+state.hh+" with ~$"+state.income.toLocaleString()+"/mo income. (Note: "+state.zip+" looks outside San Antonio — some local programs may not apply.)"; } if(total>0){ $("#sabck-res-value").style.display="block"; countUp($("#sabck-res-value-num"), total, "$"); } else { $("#sabck-res-value").style.display="none"; } /* #1-need pill (JAX shape): names the first need the visitor tapped, but only when a matched program actually answers it. */ var needEl = $("#sabck-res-need"); var topKind = state.top_needs.length ? state.top_needs[0] : null; if(needEl){ var topHas = !!topKind && resShow.some(function(r){ return (KIND_OF[r.p.id] || "other") === topKind; }); needEl.innerHTML = (topKind && topHas && KIND_LABELS[topKind]) ? 'Starting with your #1 need: ' + KIND_LABELS[topKind] + '' : ""; } /* Sectioned results (JAX shape): the visitor's #1-need category first as "Start here", then their other named needs in tap order, then the rest in fixed order. Cards keep the problem-first sort inside each section. */ var list = $("#sabck-res-list"); list.innerHTML=""; var order = KIND_ORDER.slice(); var named = state.top_needs.filter(function(n){ return order.indexOf(n) !== -1; }); order = named.concat(order.filter(function(k){ return named.indexOf(k) === -1; })); if(topKind && order.indexOf(topKind) > 0){ order.splice(order.indexOf(topKind), 1); order.unshift(topKind); } var byKind = {}; resShow.forEach(function(r){ var k = KIND_OF[r.p.id] || "other"; (byKind[k] = byKind[k] || []).push(r); }); var cardIdx = 0; order.forEach(function(k){ var grp = byKind[k]; if(!grp || !grp.length) return; var head = document.createElement("h3"); head.className = "sabck-cat-title"; head.setAttribute("data-kind", k); head.textContent = (k === topKind ? "Start here — " : "") + (KIND_LABELS[k] || "More help"); list.appendChild(head); grp.forEach(function(r){ list.appendChild(buildResultCard(r, cardIdx++)); }); }); /* v6.8.4 (Mohamed's order 2026-10-24): category filter chips with THIS visitor's real counts — All (total) + one chip per category present, in the same priority order as the sections below. Counts come from the same byKind map, so they always sum to the headline total. Filtering is display-only (see the chip click branch): the emailed guide and the saved lead always keep every matched program. */ var filt = $("#sabck-res-filters"); if(filt){ var chips = ''; order.forEach(function(k){ var n = (byKind[k] || []).length; if(n) chips += ''; }); filt.innerHTML = chips; } if(!resShow.length && res.length){ list.innerHTML = '

You\'ve applied for everything on your list

' + '

Nice work — every program you matched is marked as applied. If your situation changes, start over below and check again.

'; } /* ---------- anonymous usage analytics (no IP / no email stored) ---------- */ /* v6.8.5: an applied-mark re-render (fromApplied) must not fire a second "complete" event — the tracker sees exactly one per check. */ if(!fromApplied){ try { var ev = { event: "complete", zip3: (state.zip||"").substring(0,3), hh: state.hh, income: state.income, sit: Object.keys(state.sit).filter(function(k){ return state.sit[k]; }).join(","), matched: res.map(function(r){ return r.p.id; }).join(","), count: res.length, value: totalVal(res), page: window.location.pathname }; var xhr = new XMLHttpRequest(); xhr.open("POST", "/wp-json/sab/v1/quiz-event", true); xhr.setRequestHeader("Content-Type", "application/json"); xhr.send(JSON.stringify(ev)); } catch(e){} } } /* ---------- share ---------- */ var shareUrl = window.location.href.split('#')[0]; var shareText = "I just checked which San Antonio benefits I qualify for — it takes 60 seconds and it's free:"; function doShare(k){ var u=encodeURIComponent(shareUrl), t=encodeURIComponent(shareText); if(k==="facebook") window.open("https://www.facebook.com/sharer/sharer.php?u="+u,"_blank","width=600,height=500"); else if(k==="whatsapp") window.open("https://wa.me/?text="+t+"%20"+u,"_blank"); else if(k==="x") window.open("https://twitter.com/intent/tweet?text="+t+"&url="+u,"_blank"); else if(k==="copy"){ var done=function(){ var d=$("#sabck-share-done"); d.style.display="block"; setTimeout(function(){d.style.display="none";},2500); }; if(navigator.clipboard) navigator.clipboard.writeText(shareText+" "+shareUrl).then(done,done); else done(); } } } /* end initChecker */ function initAllCheckers(){ var nodes = document.querySelectorAll(".sab-checker"); for(var i=0;i