/* 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";
/* v78 (2026-11-02): ES language layer — active only when the checker root
carries data-lang="es" (the Spanish checker page). EN pages load this
same file and behave exactly as v77, except the two display fixes that
ride along: post-quiz step labels and the income bracket label. */
var SAB_LANG = "en";
try{ var _ckr = document.getElementById("sab-checker"); if(_ckr && _ckr.getAttribute("data-lang") === "es") SAB_LANG = "es"; }catch(e){}
function T(en, es){ return SAB_LANG === "es" ? es : en; }
/* ---------- 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/"}
];
/* v78: Spanish program names + descriptions (facts identical to EN). */
var PROG_ES = {
snap:["SNAP \u2014 Cupones de Comida (HHSC de Texas)","Dinero mensual para compras en una Lone Star Card \u2014 hasta $994/mes para una familia de 4 (m\u00e1ximo del a\u00f1o fiscal 2026)."],
wic:["WIC \u2014 Mujeres, Infantes y Ni\u00f1os","Comida saludable, clases de nutrici\u00f3n y apoyo con la lactancia para mujeres embarazadas y ni\u00f1os menores de 5. Sin requisito de ciudadan\u00eda."],
medicaid:["Medicaid de Texas","Cobertura de salud GRATIS para ni\u00f1os, mujeres embarazadas, adultos mayores y personas con discapacidad."],
chip:["CHIP de Texas \u2014 Seguro de Salud para Ni\u00f1os","Cobertura de salud de bajo costo para ni\u00f1os de 18 a\u00f1os o menos \u2014 inscripci\u00f3n de $50 o menos al a\u00f1o, copagos de $3\u2013$35."],
tanf:["TANF \u2014 Ayuda en Efectivo","Dinero mensual para familias con ni\u00f1os y poco o ning\u00fan ingreso (familia de 3 \u2248 $382/mes)."],
opportunity_home:["Opportunity Home \u2014 Vivienda P\u00fablica y Secci\u00f3n 8","Renta seg\u00fan sus ingresos (alrededor del 30%). Solicite en cuanto se abra una lista de espera \u2014 se llenan r\u00e1pido."],
hap:["Asistencia de Vivienda de la Ciudad de San Antonio (HAP)","Hasta 3 meses de renta (m\u00e1x. ~$3,500) + hasta $1,500 de ayuda con servicios, pagados directamente a su casero. Urgente: los per\u00edodos de inscripci\u00f3n se llenan r\u00e1pido."],
trla_rtc:["Derecho a un Abogado \u2014 Abogado Gratis para Desalojo","Un abogado GRATIS si enfrenta un desalojo. Llame de inmediato si tiene documentos de la corte. Urgente."],
cps_discount:["Descuento de CPS Energy por Ingresos","Hasta 25% de descuento en su factura de luz cada mes (35% si tiene 60 a\u00f1os o m\u00e1s)."],
sa_utility:["Asistencia de Servicios de la Ciudad de San Antonio","Ayuda para pagar facturas de energ\u00eda \u2014 prioridad para hogares con ni\u00f1os, discapacidades o adultos de 60+."],
vita:["VITA \u2014 Preparaci\u00f3n de Impuestos Gratis","Voluntarios certificados por el IRS hacen sus impuestos GRATIS (enero\u2013abril) \u2014 incluyendo EITC y el Cr\u00e9dito Tributario por Hijos."],
twc_unemp:["Beneficios de Desempleo de Texas","Reemplazo parcial de ingresos si perdi\u00f3 su trabajo o le redujeron las horas \u2014 aproximadamente $75\u2013$605/semana."],
wioa:["WIOA \u2014 Capacitaci\u00f3n Laboral Gratis (Workforce Solutions Alamo)","Orientaci\u00f3n laboral, capacitaci\u00f3n y certificaciones GRATIS en 16 centros de carrera del \u00e1rea Alamo."],
ccs:["Becas de Cuidado de Ni\u00f1os (CCS)","Ayuda a los padres que trabajan a pagar el cuidado de ni\u00f1os. Solicite temprano \u2014 puede haber lista de espera."],
va:["Beneficios VA","Salud, compensaci\u00f3n por discapacidad, GI Bill, pr\u00e9stamos de vivienda y m\u00e1s para veteranos."],
tvc:["Comisi\u00f3n de Veteranos de Texas","Ayuda GRATIS para presentar reclamos de discapacidad del VA, empleo y beneficios de educaci\u00f3n."],
hud_vash:["HUD-VASH \u2014 Vivienda para Veteranos sin Hogar","Vale de renta + gesti\u00f3n de casos del VA para veteranos en riesgo de quedarse sin hogar. Urgente."],
foodbank:["San Antonio Food Bank \u2014 Comida Gratis + Ayuda con Solicitudes","Comida GRATIS Y ayuda personal gratuita para solicitar SNAP, Medicaid y TANF. Sin requisito de ciudadan\u00eda para la comida."],
trla:["Texas RioGrande Legal Aid","Ayuda legal civil GRATIS \u2014 vivienda, familia, negaciones de beneficios. Ingl\u00e9s y espa\u00f1ol."],
via_link:["VIA Link \u2014 Viajes a Demanda + Autobuses VIA","Las camionetas a demanda de VIA lo recogen cerca de su casa y lo llevan a cualquier lugar dentro de su zona por $1.30 el viaje (65\u00a2 con tarifa reducida). Reserve hasta 7 d\u00edas antes \u2014 bueno para entrevistas de trabajo y citas. Los autobuses regulares de VIA cubren el resto de la ciudad."],
lifeline:["Lifeline \u2014 Descuento en Tel\u00e9fono e Internet","Un programa federal que quita $9.25 al mes de su factura de tel\u00e9fono o internet \u2014 algunas compa\u00f1\u00edas lo convierten en un plan de tel\u00e9fono completamente gratis. Califica con SNAP, Medicaid o SSI, o con ingresos iguales o menores al 135% de la l\u00ednea federal de pobreza."],
"211":["2-1-1 \u2014 L\u00ednea de Ayuda de Texas","Una llamada gratis lo conecta con miles de servicios locales. 24/7, m\u00e1s de 100 idiomas."]
};
if(SAB_LANG === "es"){ PROGRAMS.forEach(function(p){ var e = PROG_ES[p.id]; if(e){ p.name = e[0]; p.what = e[1]; } }); }
/* 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:"" };
var INC_LABELS = {750:["under $1,500","menos de $1,500"], 2000:["$1,500 \u2013 $2,500","$1,500 \u2013 $2,500"], 3000:["$2,500 \u2013 $3,500","$2,500 \u2013 $3,500"], 4000:["$3,500 \u2013 $4,500","$3,500 \u2013 $4,500"], 5250:["$4,500 \u2013 $6,000","$4,500 \u2013 $6,000"], 7500:["over $6,000","m\u00e1s de $6,000"]};
function incLabel(){ var e = INC_LABELS[state.income]; if(!e) return "~$" + Number(state.income||0).toLocaleString() + "/mo"; return SAB_LANG === "es" ? e[1] : e[0]; }
/* 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 = '
' + T("Welcome back","Bienvenido de nuevo") + '
'
+ ''
+ '
' + T("Estimated total value you could be missing","Valor total estimado que podría estar perdiendo") + '$0' + T("per month*","por mes*") + '' + T("*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.", "*Estimado según lo que cada programa suele ahorrar a un hogar cada mes. Lo que usted ahorre en realidad depende de los programas que use y de su solicitud completa.") + '
'
+ ''
+ ''
+ ''
+ ''
+ '
' + T("We remember your answers on this phone only, so you don't have to answer again. Nothing extra is sent to us.", "Recordamos sus respuestas solo en este teléfono, para que no tenga que responder de nuevo. No se nos envía nada extra.") + '
';
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(SAB_LANG === "es" ? "es-US" : "en-US", {month:"long", day:"numeric", year:"numeric"}); }catch(e){}
var sub = $("#sabck-wb-sub");
if(sub) sub.textContent = T("You last checked on ","Su última revisión fue el ") + when + T(". Here are your matches today, based on the answers you gave then.", ". Estas son sus coincidencias de hoy, según las respuestas que dio entonces.");
/* 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 = T("*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.", "*Estimado según lo que cada programa suele ahorrar a un hogar cada mes. Lo que usted ahorre en realidad depende de los programas que use y de su solicitud completa.");
} 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 = T("Every program on your list is marked as applied. Redo your check any time your situation changes.", "Todos los programas de su lista están marcados como solicitados. Repita su revisión cuando su situación cambie.");
} else {
wbVal.style.display = "none";
}
}
var news = $("#sabck-wb-news");
if(news){
var html = "";
if(!newOnes.length && !hotOnes.length){
html = '
' + T("What's new for you","Novedades para usted") + '
'
+ '
' + T("Nothing has changed since your last check — your matches are below.", "Nada ha cambiado desde su última revisión — sus coincidencias están abajo.") + '
';
} else {
html = '
' + T("What's new for you","Novedades para usted") + '
';
if(newOnes.length){
html += '
' + (newOnes.length === 1 ? T("We found a new program you qualify for","Encontramos un programa nuevo para el que califica") : T("We found ","Encontramos ") + newOnes.length + T(" new programs you qualify for"," programas nuevos para los que califica")) + '
';
newOnes.forEach(function(r){ html += '
' + r.p.name + '
'; });
}
if(hotOnes.length){
html += '
' + T("Time-sensitive for you","Urgente para usted") + '' + T(" — tap one to jump to it"," — toque uno para ir a él") + '
';
hotOnes.forEach(function(r){ html += ''; });
}
html += '
' + T("You've applied for everything on your list","Ya solicitó todo lo de su lista") + '
'
+ '
' + T("Nice work — every program you matched is marked as applied. If your situation changes, redo your check below.", "Buen trabajo — todos los programas de su lista están marcados como solicitados. Si su situación cambia, repita su revisión abajo.") + '
';
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}
};
if(SAB_LANG === "es"){ var _NL = {"Continue \u2192":"Continuar \u2192", "See my results \u2192":"Ver mis resultados \u2192", "Send my guide \u2192":"Enviar mi gu\u00eda \u2192", "Done":"Listo"}; Object.keys(NEXT_ACTION).forEach(function(k){ var a = NEXT_ACTION[k]; if(a && _NL[a.label]) a.label = _NL[a.label]; }); }
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:["Almost done",68], claimaddr:["Almost done",74],
job:["Almost done",84], citizenship:["Almost done",88],
language:["Almost done",92], reachtime:["Almost done",96], hear:["Almost done",98],
results:["",100]};
if(SAB_LANG === "es"){ var _SL = {needs:"Pregunta 1 de 10", zip:"Pregunta 2 de 10", household:"Pregunta 3 de 10", situations:"Pregunta 4 de 10", getting:"Pregunta 5 de 10", age:"Pregunta 6 de 10", money:"Pregunta 7 de 10", income:"Pregunta 8 de 10", home:"Pregunta 9 de 10", reveal:"Sus programas", personal:"Casi listo", claimaddr:"Casi listo", job:"Casi listo", citizenship:"Casi listo", language:"Casi listo", reachtime:"Casi listo", hear:"Casi listo", results:""}; Object.keys(_SL).forEach(function(k){ if(STEPS[k]) STEPS[k][0] = _SL[k]; }); }
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"};
if(SAB_LANG === "es"){ var _KL = {food:"Comida", housing:"Renta / vivienda", utilities:"Facturas de servicios", health:"Salud", childcare:"Cuidado de ni\u00f1os", jobs:"Trabajo / capacitaci\u00f3n", veteran:"Veteranos", money:"Dinero y ayuda en efectivo", legal:"Ayuda legal", transport:"Transporte", phone:"Tel\u00e9fono / internet", other:"M\u00e1s ayuda"}; Object.keys(_KL).forEach(function(k){ KIND_LABELS[k] = _KL[k]; }); }
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 + " " + T("kinds of help found so far","tipos de ayuda encontrados hasta ahora") : "";
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 ? (T("Claim my $","Reclama mis $") + gTotal.toLocaleString() + T(" in benefits"," en beneficios")) : T("Claim my benefits","Reclamar mis beneficios");
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 ? T("Your income is under the SNAP limit for a household of ","Sus ingresos están bajo el límite de SNAP para un hogar de ") + hh + "." : T("SNAP is income-based — households under the limit for their size qualify; the agency confirms when you apply.","SNAP se basa en los ingresos — los hogares bajo el límite para su tamaño califican; la agencia lo confirma cuando usted solicita."), valK(SNAP_MAX[Math.min(hh,8)]||1755));
if(s.youngchild) add("wic",T("You have a young child or are pregnant — WIC has no citizenship requirement.","Tiene un niño pequeño o está embarazada — WIC no tiene requisito de ciudadanía."));
if(s.kids && incLte(medLim)) add("medicaid", incK ? T("Your kids likely qualify for free coverage at your income.","Es probable que sus hijos califiquen para cobertura gratis con sus ingresos.") : T("Kids' Medicaid is income-based — this is the first coverage to check.","Medicaid para niños se basa en los ingresos — es la primera cobertura que debe revisar."));
else if((s.senior||s.disabled) && incLte(medLim)) add("medicaid", incK ? T("Seniors and people with disabilities can qualify at your income.","Los adultos mayores y las personas con discapacidad pueden calificar con sus ingresos.") : T("Seniors and people with disabilities can qualify for Medicaid — it is income-based, so apply to confirm.","Los adultos mayores y las personas con discapacidad pueden calificar para Medicaid — se basa en los ingresos, solicite para confirmarlo."));
if(s.kids && incK && inc > medLim && inc <= chipLim) add("chip",T("Your income is above Medicaid but under the CHIP limit — low-cost kids' coverage.","Sus ingresos están sobre Medicaid pero bajo el límite de CHIP — cobertura de bajo costo para niños."));
else if(s.kids && incLte(chipLim)) add("chip",T("Backup option if Medicaid doesn't come through — CHIP covers kids cheaply.","Opción de respaldo si Medicaid no se aprueba — CHIP cubre a los niños a bajo costo."));
if(s.kids && incLte(1000)) add("tanf", incK ? T("Very low income with kids — TANF provides monthly cash help.","Ingresos muy bajos con niños — TANF da ayuda mensual en efectivo.") : T("For families with kids and very low income — TANF provides monthly cash help.","Para familias con niños e ingresos muy bajos — TANF da ayuda mensual en efectivo."), valK(382));
if(incLte(snapLim)) add("opportunity_home",T("Low-income households can apply when waitlists open.","Los hogares de bajos ingresos pueden solicitar cuando se abren las listas de espera."));
if(s.eviction){ add("trla_rtc",T("You're facing eviction — a free lawyer can fight it.","Usted enfrenta un desalojo — un abogado gratis puede defender su caso.")); add("hap",T("Behind on rent? HAP can cover up to 3 months.","¿Atrasado en la renta? HAP puede cubrir hasta 3 meses.")); }
else if(incLte(fpl(hh,0.80))) add("hap", incK ? T("Your income is under 80% of area median — HAP rental help when enrollment opens.","Sus ingresos están bajo el 80% del ingreso medio del área — ayuda de renta de HAP cuando se abre la inscripción.") : T("HAP rental help is income-based — watch for enrollment openings.","La ayuda de renta de HAP se basa en los ingresos — esté atento a las aperturas de inscripción."));
if(incLte(fpl(hh,1.25))) add("cps_discount", incK ? T("Your income qualifies for up to 25% off your CPS Energy bill.","Sus ingresos califican para hasta 25% de descuento en su factura de CPS Energy.") : T("Income-based — qualifying households get up to 25% off their CPS Energy bill.","Según sus ingresos — los hogares que califican reciben hasta 25% de descuento en su factura de CPS Energy."), valK(45));
if(incLte(fpl(hh,1.50))) add("sa_utility", incK ? T("Your income is under the city utility-assistance limit.","Sus ingresos están bajo el límite de asistencia de servicios de la ciudad.") : T("City utility assistance is income-based — worth applying.","La asistencia de servicios de la ciudad se basa en los ingresos — vale la pena solicitar."));
if(incLte(5417)) add("vita", incK ? T("Your income is under $65k/yr — get your taxes done free.","Sus ingresos son menores de $65k/año — haga sus impuestos gratis.") : T("Free tax prep for households under about $65k/yr.","Preparación de impuestos gratis para hogares con menos de $65k/año aproximadamente."));
if(s.unemployed){ add("twc_unemp",T("You lost work or hours — unemployment can replace part of your income.","Perdió su trabajo u horas — el desempleo puede reemplazar parte de sus ingresos.")); add("wioa",T("Free job training while you get back on your feet.","Capacitación laboral gratis mientras se recupera.")); }
if(s.kids && (s.unemployed || true)) add("ccs",T("Working or training with kids? Child care scholarships help pay for it.","¿Trabaja o se capacita y tiene niños? Las becas de cuidado de niños ayudan a pagarlo."));
if(s.veteran){ add("va",T("As a veteran, start with your full VA benefits check.","Como veterano, empiece con su revisión completa de beneficios VA.")); add("tvc",T("Free help filing VA claims and finding work.","Ayuda gratis para presentar reclamos del VA y encontrar trabajo.")); if(s.eviction) add("hud_vash",T("Veteran facing housing loss — HUD-VASH combines a voucher with VA support.","Veterano en riesgo de perder su vivienda — HUD-VASH combina un vale con apoyo del VA.")); }
add("foodbank",T("Open to everyone in the area — free food plus free help applying for benefits.","Abierto para todos en el área — comida gratis y ayuda gratis para solicitar beneficios."));
add("via_link",T("Rides across San Antonio — on-demand vans from $1.30 a ride, plus regular VIA bus routes.","Viajes por San Antonio — camionetas a demanda desde $1.30 por viaje, además de las rutas regulares de VIA."));
if(incLte(fpl(hh,1.35))) add("lifeline", incK ? T("Your income is under the Lifeline limit — $9.25/month off phone or internet, free plans available.","Sus ingresos están bajo el límite de Lifeline — $9.25/mes de descuento en teléfono o internet, planes gratis disponibles.") : T("Lifeline discounts phone/internet for lower-income households — apply free to confirm.","Lifeline da descuento en teléfono/internet a hogares de menores ingresos — solicite gratis para confirmarlo."));
if(s.eviction || incLte(snapLim)) add("trla",T("Free legal help if any application is denied or you need legal aid.","Ayuda legal gratis si le niegan alguna solicitud o necesita ayuda legal."));
add("211",T("Catch-all: one call finds any local help we didn't list.","Para todo lo demás: una llamada encuentra cualquier ayuda local que no listamos."));
/* 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+T("Time-sensitive","Urgente")
: '' + T("✓ May qualify","✓ Podría calificar") + '';
var valLine = r.val ? '
' + T("Worth up to ~$","Vale hasta ~$") + r.val.toLocaleString() + T("/mo","/mes") + '
';
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 = T("You may qualify for ","Podría calificar para ") + '0' + T(" programs"," programas");
countUp($("#sabck-res-count"), resShow.length, "");
var inSA = /^782\d{2}$/.test(state.zip);
if(state.incomeUnknown){
$("#sabck-res-sub").textContent = T("Based on a household of ","Basado en un hogar de ") + state.hh + T(" in "," en ") + state.zip + T(". You skipped the income question, so this list shows everything you may qualify for — each program confirms income when you apply.", ". Usted saltó la pregunta de ingresos, así que esta lista muestra todo para lo que podría calificar — cada programa confirma los ingresos cuando usted solicita.");
} else {
$("#sabck-res-sub").textContent = inSA
? T("Based on a household of ","Basado en un hogar de ") + state.hh + T(" with income "," con ingresos de ") + incLabel() + T(" in "," en ") + state.zip + "."
: T("Based on a household of ","Basado en un hogar de ") + state.hh + T(" with income "," con ingresos de ") + incLabel() + T(". (Note: ",". (Nota: ") + state.zip + T(" looks outside San Antonio — some local programs may not apply.)"," parece estar fuera de San Antonio — algunos programas locales podrían no aplicar.)");
}
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])
? '' + T("Starting with your #1 need: ","Empezando con su necesidad #1: ") + 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 ? T("Start here — ","Empiece aquí — ") : "") + (KIND_LABELS[k] || T("More help","M\u00e1s ayuda"));
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 = '
' + T("You've applied for everything on your list","Ya solicitó todo lo de su lista") + '
'
+ '
' + T("Nice work — every program you matched is marked as applied. If your situation changes, start over below and check again.", "Buen trabajo — todos los programas de su lista están marcados como solicitados. Si su situación cambia, empiece de nuevo abajo y revise otra vez.") + '
';
}
/* ---------- 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