PageSourceSearch

https://bigmotoringworld.co.uk/static-20260924.4/js/?internal-page…FCars%2FFinance%2Ffinance-eligibility-panel

js bigmotoringworld.co.uk collected 2026-09-25 23:34:45 UTC 342,622 bytes, 9,339 lines download raw bytes

1const FINANCE_ELIGIBILITY_UK_DIAL = Object.freeze({
2    iso: 'gb',
3    dial: '44',
4});
5
6/**
7 * The phone field is a single mobile input with no flag or dial-code prefix, so the UK country and
8 * dial code the eligibility payload expects are held on hidden inputs and set here.
9 */
10function applyFinanceEligibilityUkDial() {
11    const { iso, dial } = FINANCE_ELIGIBILITY_UK_DIAL;
12    const dialHidden = document.getElementById('cars-fe-phone-dial');
13    const countryHidden = document.getElementById('cars-fe-phone-country');
14
15    if (dialHidden) {
16        dialHidden.value = dial;
17    }
18    if (countryHidden) {
19        countryHidden.value = iso;
20    }
21}
22
23/** Sorted list for employment occupation comboboxes (search + pick). */
24const FINANCE_ELIGIBILITY_OCCUPATIONS = [
25    'Abattoir Worker','Abbot or Abbess','Abstractor','Accommodation Officer','Accompanist','Account Director','Account Executive','Account Manager','Accountant',
26    'Accounts Assistant','Accounts Clerk','Accounts Manager','Accounts Staff','Acoustic Engineer','Acrobat','Actor','Actuary','Acupuncturist',
27    'Administration Assistant','Administration Clerk','Administration Manager','Administration Officer','Administration Staff','Administrator',
28    'Advertising Agent','Advertising Assistant','Advertising Buyer','Advertising Clerk','Advertising Contractor','Advertising Controller','Advertising Director',
29    'Advertising Executive',
30    'Advertising Manager',
31    'Advertising Staff',
32    'Advocate', 
33    'Aerial Erector',
34    'Aerobic Instructor',
35    'Aeronautical Engineer',
36    'After Dinner Speaker',
37    'Agister',
38    'Agricultural Consultant',
39    'Agricultural Contractor',
40    'Agricultural Engineer',
41    'Agricultural Merchant',
42    'Agricultural Worker',
43    'Agriculturalist',
44    'Agronomist',
45    'Air Force Crew',
46    'Air Traffic Controller',
47    'Aircraft Buyer',           
48    'Aircraft Cabin Crew',
49    'Aircraft Designer',
50    'Aircraft Engineer',
51    'Aircraft Fitter',
52    'Aircraft Hand',
53    'Aircraft Maintenance Engineer',
54    'Aircraft Surface Finisher',
55    'Airline Broker',
56    'Airline Check-in Staff',
57    'Airport Controller',
58    'Airport Manager',
59    'Alarm Fitter',
60    'Almoner',
61    'Ambulance Controller',
62    'Ambulance Crew',
63    'Ambulance Driver',
64    'Amusement Arcade Worker',
65    'Anaesthetist',
66    'Analytical Chemist',
67    'Animal Breeder',
68    'Animator',
69    'Announcer',
70    'Anthropologist',
71    'Antique Dealer',
72    'Antique Renovator',
73    'Appeal/Tribunal Chairperson',
74    'Applications Engineer',
75    'Applications Programmer',
76    'Aquarist',
77    'Arbitrator',
78    'Arborist',
79    'Archaeologist',
80    'Archbishop',
81    'ArchDeacon',
82    'Architect',
83    'Architects Technician',
84    'Architectural Surveyor',
85    'Archivist',
86    'Area Manager',
87    'Armourer',
88    'Army - NCO/Commissioned Officer',
89    'Army - Other Ranks',
90    'Aromatherapist',
91    'Art Buyer',
92    'Art Critic',
93    'Art Dealer',
94    'Art Director',
95    'Art Historian',
96    'Art Restorer',
97    'Artexer',
98    'Articled Clerk',
99    'Artificial Inseminator',
100    'Artificial Intelligence Engineer',
101    'Artificial Limb Fitter',
102    'Artist - Commercial',
103    'Artist - Freelance',
104    'Artist - Portrait',
105    'Artist - Technical',
106    'Asbestos Remover',
107    'Asphalter',
108    'Assembly Worker',
109    'Assessor',
110    'Assistant Accounts Manager',
111    'Assistant Caretaker',
112    'Assistant Cook',
113    'Assistant Manager',
114    'Assistant Nurse',
115    'Assistant Teacher',    
116    'Astrologer',
117    'Astronomer',
118    'Athlete',
119    'ATM Fitter',
120    'Au Pair',
121    'Auction Worker',
122    'Auctioneer',
123    'Audiologist',
124    'Audit Clerk',
125    'Audit Manager',
126    'Auditor',
127    'Author',
128    'Auto Electrician',
129    'Auxiliary Nurse',
130    'Bacon Curer',
131    'Bacteriologist',
132    'Baggage Handler',
133    'Bailiff',
134    'Baker',
135    'Bakery Assistant',
136    'Bakery Manager',
137    'Bakery Operator',
138    'Ballistics Expert',
139    'Balloonist',
140    'Bank Clerk',
141    'Bank Manager',
142    'Bank Messenger',
143    'Bank Note Checker',
144    'Bank Staff',
145    'Banking Correspondent',    
146    'Baptist Minister',
147    'Bar Manager',
148    'Bar Staff',
149    'Bar Steward',
150    'Barber',
151    'Barge Operator',
152    'Barista',
153    'Barrister',
154    'Basket Worker',
155    'Beautician',
156    'Beauty Therapist',
157    'BeeKeeper',
158    'Betting Shop Clerk',
159    'Bill Poster',
160    'Bingo Caller',
161    'Bingo Hall Staff',
162    'Biochemist',
163    'Biologist',
164    'Biometrician',
165    'Biophysicist',
166    'Bishop',
167    'Blacksmith',
168    'Blind Assembler',
169    'Blind Fitter',
170    'Blinds Installer',
171    'Blogger/Vlogger',
172    'Boat Builder',
173    'Boatswain',
174    'Body Fitter',
175    'Bodyguard',
176    'Bodyshop Manager',
177    'Boiler Maker',
178    'Boiler Technician',
179    'Book Binder',
180    'Book Seller',
181    'Book-Keeper',
182    'Bookfinisher',
183    'Booking Agent',
184    'Booking Clerk',
185    'Booking Office Clerk',
186    'Boom Operator',
187    'Botanist',
188    'Bottler',
189    'Box Maker',
190    'Box Office Clerk',
191    'Boxer',
192    'Branch Manager',
193    'Brewer',
194    'Brewery Manager',
195    'Brewery Worker',
196    'Bricklayer',
197    'Bridge Operator',
198    'Bridgemaster',
199    'Broadcaster',
200    'Broadcasting Engineer',
201    'Brother or Sister (Church)',
202    'Builder',
203    'Builders Labourer',
204    'Builders Merchant',
205    'Building Advisor',
206    'Building Contractor',
207    'Building Control Officer',
208    'Building Engineer',
209    'Building Estimator',
210    'Building Foreperson',
211    'Building Inspector',
212    'Building Maintenance',
213    'Building Manager',
214    'Building Site Inspector',
215    'Building Society Clerk',
216    'Building Society Staff',
217    'Building Surveyor',
218    'Bursar',
219    'Bus Driver',
220    'Bus Mechanic',
221    'Bus Valeter',
222    'Business Analyst',
223    'Business Consultant',
224    'Butcher',
225    'Butchery Manager',
226    'Butler',
227    'Buyer',
228    'Cabinet Maker',
229    'Cable Contractor',
230    'Cable Jointer',
231    'Cable TV Installer',
232    'Cafe Owner',
233    'Cafe Staff',
234    'Calibration Manager',
235    'Call Centre Manager',
236    'Call Centre Staff',
237    'Calligrapher',
238    'Camera Operator/Technician',
239    'Camera Repairer',
240    'Canal Boat Broker',
241    'Canon or Canoness (Church)',
242    'Canvasser',
243    'Car Body Repairer',
244    'Car Builder',
245    'Car Delivery Driver',
246    'Car Park Attendant',
247    'Car Salesperson',
248    'Car Wash Attendant',
249    'Cardinal',
250    'Cardiographer',
251    'Cardiologist',
252    'Care Assistant',
253    'Care Manager',
254    'Careers Advisor',
255    'Careers Officer',
256    'Carer - Non Professional',
257    'Carer - Professional',
258    'Caretaker',
259    'Cargo Handler',
260    'Cargo Operator',
261    'Carpenter',
262    'Carpenters Assistant',
263    'Carpet Cleaner',
264    'Carpet Retailer',
265    'Carpetfitter',
266    'Cartographer',
267    'Cartoonist',
268    'Cashier',
269    'Cataloguer',
270    'Caterer',
271    'Catering Consultant',
272    'Catering Manager',
273    'Catering Staff',
274    'Caulker',
275    'Ceiling Contractor',
276    'Ceiling Fitter',
277    'Ceiling Fixer',
278    'Cellarperson',
279    'Certified Accountant',
280    'Chandler',
281    'Chaplain',
282    'Charge Hand',
283    'Chartered Accountant',
284    'Chartered Engineer',
285    'Chartered Surveyor',
286    'Chartered Valuer',
287    'Charterer',
288    'Chauffeur',
289    'Chef',
290    'Chemical Engineer',
291    'Chemist',
292    'Chicken Chaser',
293    'Chicken Sexer',
294    'Chief Cashier',
295    'Chief Chemist',
296    'Chief Executive',
297    'Chief Financial Officer',
298    'Chief Operating Officer',
299    'Chief Technical Officer',
300    'Child Care Officer',
301    'Child Minder',
302    'Childrens Entertainer',
303    'Chimney Sweep',
304    'China Restorer',
305    'Chiropodist',
306    'Chiropractor',
307    'Choir Leader',
308    'Choreographer',
309    'Chorister',
310    'Church Army Worker',
311    'Church Dean',
312    'Church Officer',
313    'Church Official',
314    'Church Warden',
315    'Cinema Assistant',
316    'Cinema Manager',
317    'Circus Proprietor',
318    'Circus Worker',
319    'Civil Engineer',
320    'Civil Servant',
321    'Claims Adjuster',
322    'Claims Assessor',
323    'Claims Manager',
324    'Clairvoyant',
325    'Clapper',
326    'Classical Musician',
327    'Clay Pigeon Instructor',
328    'Cleaner',
329    'Cleaning Contractor',
330    'Cleaning Supervisor',
331    'Clergy',
332    'Cleric',
333    'Clerical Assistant',
334    'Clerical Officer',
335    'Clerk',
336    'Clerk of Court',
337    'Clerk Of Works',
338    'Clinical Psychologist',
339    'Clock Maker',
340    'Clothing Design Cutter',
341    'Coach Builder',
342    'Coach Driver',
343    'Coach Sprayer',
344    'Coal Merchant',
345    'Coastguard',
346    'Cobbler',
347    'Coffee Merchant',
348    'Coin Dealer',
349    'College Dean',
350    'College Lecturer',
351    'College Principal',
352    'College Scout',
353    'Colourist (Maps/Photographs)',
354    'Comedian',
355    'Commercial Manager',
356    'Commission Agent',
357    'Commissionaire',
358    'Commissioner of Police',
359    'Commissioning Engineer',
360    'Commodity Broker',
361    'Commodity Dealer',
362    'Communications Officer',
363    'Communications Supervisor',
364    'Community Craft Instructor',
365    'Community Nurse',
366    'Community Worker',
367    'Companion',
368    'Companion Nurse',
369    'Company Chairperson',
370    'Company Director',
371    'Company Search Agent',
372    'Company Secretary',
373    'Compiler',
374    'Complementary Therapist',
375    'Compliance Officer',
376    'Composer',
377    'Compositor',
378    'Computer Analyst',
379    'Computer Consultant',
380    'Computer Editor',
381    'Computer Engineer',
382    'Computer Manager',
383    'Computer Network Controller',
384    'Computer Operator',
385    'Computer Programmer',
386    'Computer Technician',
387    'Conductor',
388    'Confectioner',
389    'Conference Manager',
390    'Conference Organiser',
391    'Conservationist',
392    'Conservator',
393    'Construction Engineer',
394    'Construction Worker',
395    'Consultant Engineer',
396    'Consumer Scientist',
397    'Contract Cleaner',
398    'Contract Furnisher',
399    'Contract Manager',
400    'Contracts Supervisor',
401    'Conveyancer',  
402    'Cook',
403    'Cooper',
404    'Copier',
405    'Coppersmith',
406    'Copy Typist',
407    'Copywriter',
408    'Coroner',
409    'Corrosion Consultant',
410    'Costume Designer',
411    'Costume Jeweller',
412    'Costumier',
413    'Counter Staff',
414    'Countryside Ranger',
415    'County Councillor',
416    'Courier',
417    'Court Officer',
418    'Court Reporter',
419    'Coxswain',
420    'Craft Dealer',
421    'Craftsperson',
422    'Crane Driver',
423    'Crane Erector',
424    'Crane Operator',
425    'Crash Assessor',
426    'Creative Director',
427    'Creche Worker',
428    'Credit Broker',
429    'Credit Controller',
430    'Credit Manager',
431    'Credit Researcher',
432    'Crematorium Attendant',
433    'Cricketer',
434    'Crime Examiner',
435    'Criminologist',
436    'Crofter',
437    'Croupier',
438    'Crown Prosecutor',
439    'Curate',
440    'Curator',
441    'Currency Trader',
442    'Curtain Hanger',
443    'Curtain Maker',
444    'Customer Advisor',
445    'Customer Liaison Officer',
446    'Customer Service Controller',
447    'Customer Services Clerk',
448    'Customs & Excise Officer',
449    'Cutler',
450    'Cutter',
451    'Cycle Repairer',
452    'Cyclist',
453    'Dairy Engineer',
454    'Dairy Worker',
455    'Dance Teacher',
456    'Dancer',
457    'Dark Room Technician',
458    'Darts Player',
459    'Data Administrator',
460    'Data Co-Ordinator',
461    'Data Processing Manager',
462    'Data Processor',
463    'Data Scientist',
464    'Day Care Officer',
465    'Day Centre Officer',
466    'Dealer - General',
467    'Debt Collector',
468    'Debt Counsellor',
469    'Decorator',
470    'Delivery Courier',
471    'Delivery Driver',
472    'Delivery Rounds Person',
473    'Demolition Contractor',
474    'Demolition Worker',
475    'Demonstrator',
476    'Dendrochronologist',
477    'Dental Assistant',
478    'Dental Hygienist',
479    'Dental Mechanic',
480    'Dental Nurse',
481    'Dental Surgeon',
482    'Dental Technician',
483    'Dental Therapist',
484    'Dentist',
485    'Deputy Head Teacher',
486    'Deputy Manager',
487    'Deputy Principal',
488    'Dermatologist',
489    'Design Copier',
490    'Design Director',
491    'Design Engineer',
492    'Design Manager',
493    'Designer',
494    'Despatch Driver',
495    'Despatch Rider',
496    'Despatch Worker',
497    'Development Manager',
498    'Diamond Dealer',
499    'Diecaster',
500    'Dietician',
501    'Diplomat',
502    'Director of Environment',
503    'Director of Housing',
504    'Director of Planning',
505    'Disc Jockey',
506    'Distillery Worker',
507    'Distribution Manager',
508    'Distributor (Leaflets/Circ.)',
509    'District Nurse',
510    'District Valuer',
511    'Diver',
512    'Dock Pilot',
513    'Docker',
514    'Dockyard Worker',
515    'Doctor',
516    'Document Controller',
517    'Dog Beautician',
518    'Dog Breeder',
519    'Dog Groomer',
520    'Dog Handler',
521    'Dog Trainer',
522    'Dog Walker',
523    'Dog Warden',
524    'Doll Maker',
525    'Domestic Servant',
526    'Domestic Staff',
527    'Door Fitter',
528    'Door To Door Collector',
529    'Doorperson',
530    'Double Glazing Fitter',
531    'Double Glazing Salesperson',
532    'Draper',
533    'Draughtsperson',
534    'Drayperson',
535    'Dredger Master',
536    'Dredging Operator',
537    'Dresser Theatre/Films',
538    'Dressmaker',
539    'Driller (Civil Engineering)',
540    'Drilling Technician',
541    'Driver\'s Mate',
542    'Driving Examiner',
543    'Driving Instructor',
544    'Driving Instructor (HGV)',
545    'Driving Instructor - Advanced',
546    'Drone Operator',
547    'Drug Addiction Counsellor',
548    'Dry Cleaner',
549    'Dryliner',
550    'Dye Polisher',
551    'Dyer',
552    'Earth Moving Contractor',
553    'Ecologist',
554    'Economist',
555    'Editor',
556    'Editorial Consultant',
557    'Editorial Staff',
558    'Education Advisor',
559    'Education Officer',
560    'Electrical Contractor',
561    'Electrical Contracts Manager',
562    'Electrical Engineer',
563    'Electrical Fitter',
564    'Electrical Process Worker',
565    'Electrician',
566    'Electrologist',
567    'Electronic Engineer',
568    'Electronics Supervisor',
569    'Electronics Technician',
570    'Electroplater',
571    'Electrotyper',
572    'Embalmer',
573    'Embassy Staff',
574    'Embroiderer',
575    'Embryologist',
576    'Endocrinologist',
577    'Energy Analyst',
578    'Engine Operator',
579    'Engine Room Operator',
580    'Engineer',
581    'Engine Stoker',
582    'Engraver',
583    'Enquiry Agent',
584    'Entertainer',
585    'Entomologist',
586    'Environmental Chemist',
587    'Environmental Consultant',
588    'Environmental Health Officer',
589    'Equity Agent',
590    'Erector',
591    'Ergonomist',
592    'Escort',
593    'Estate Agent',
594    'Estate Manager',
595    'Estimator',
596    'Evangelist',
597    'Events Organiser',
598    'Examiner',
599    'Excursion Manager',
600    'Executive Officer',
601    'Exhaust Fitter',
602    'Exhibition Designer',
603    'Exhibition Organiser',
604    'Exotic Dancer',
605    'Expedition Leader',
606    'Explosives Worker',
607    'Export Consultant',
608    'Exporter',
609    'Extra',
610    'Extrusion Operator',
611    'Fabricator',
612    'Factory Canteen Manager',
613    'Factory Inspector',
614    'Factory Manager',
615    'Factory Worker',
616    'Fairground Worker',
617    'Falconer',
618    'Farm Manager',
619    'Farm Worker',
620    'Farrier',
621    'Fashion Designer',
622    'Fashion Photographer',
623    'Fast Food Delivery Driver',
624    'Fast Food Proprietor',
625    'Fence Erector',
626    'Fettler (Foundry)',
627    'Fibre Glass Moulder',
628    'Field Officer',
629    'Figure Painter',
630    'Filing Clerk',
631    'Film Director',
632    'Film Editor',
633    'Film Editor (Cutter)',
634    'Film Gaffer',
635    'Film Location Manager',
636    'Film Producer',
637    'Film Technician',
638    'Finance Director',
639    'Finance Manager',
640    'Finance Officer',
641    'Financial Advisor',
642    'Financial Analyst',
643    'Financial Consultant',
644    'Financier',
645    'Finisher',
646    'Fire Officer',
647    'Fire Prevention Officer',
648    'Fire Protection Consultant',
649    'Fire Service Inspector',
650    'Firefighter',
651    'Fireplace Fitter',
652    'Firewood Merchant',
653    'First Aid Instructor',
654    'First Aid Worker',
655    'Fish Buyer',
656    'Fish Filleter',
657    'Fish Fryer',
658    'Fish Merchant',
659    'Fish Worker',
660    'Fisher',
661    'Fisheries Inspector',
662    'Fishery Manager',
663    'Fishmonger',
664    'Fitness Instructor',
665    'Fitter',
666    'Flagger',
667    'Fleet Manager',
668    'Flight Attendant',
669    'Flight Deck Crew',
670    'Flight Dispatcher',
671    'Floor Layer',
672    'Floor Manager',
673    'Florist',
674    'Flour Miller',
675    'Flower Arranger',
676    'Flying Instructor',
677    'Foam Convertor',
678    'Food Processor',
679    'Footballer',
680    'Forensic Scientist',
681    'Foreperson',
682    'Forest Ranger',
683    'Forester',
684    'Fork Lift Truck Driver',
685    'Fortune Teller',
686    'Forwarding Agent',
687    'Foster Parent',
688    'Foundry Moulder',
689    'Foundry Worker',
690    'Fraud Investigator',
691    'Freelance Photographer',
692    'French Polisher',
693    'Fruiterer',
694    'Fuel Merchant',
695    'Fund Manager',
696    'Fund Raiser',
697    'Funeral Director',
698    'Funeral Furnisher',
699    'Funfair Employee',
700    'Furnace Operator',
701    'Furniture Dealer',
702    'Furniture Remover',
703    'Furniture Restorer',
704    'Furrier',
705    'Gallery Owner',
706    'Gambler',
707    'Gambling Commission Inspector',
708    'Gamekeeper',
709    'Gaming Club Manager',
710    'Gaming Club Proprietor',
711    'Garage Attendant',
712    'Garage Foreperson',
713    'Garage Manager',
714    'Garda',
715    'Garden Designer',
716    'Gardener',
717    'Gas Fitter',
718    'Gas Mechanic',
719    'Gas Technician',
720    'Gate Keeper',
721    'Genealogist',
722    'General Manager',
723    'General Practitioner',
724    'Geneticist',
725    'Geographer',
726    'Geologist',
727    'Geophysicist',
728    'Geriatrician',
729    'Gilder',
730    'Glass Worker',
731    'Glazier',
732    'Goldsmith',
733    'Golf Caddy',
734    'Golf Club Professional',
735    'Golf Coach',
736    'Golfer',
737    'Goods Despatcher',
738    'Goods Handler',
739    'Governor',
740    'Granite Technician',
741    'Graphic Designer',
742    'Graphologist',
743    'Grave Digger',
744    'Gravel Merchant',
745    'Green Keeper',
746    'Greengrocer',
747    'Grocer',
748    'Groom',
749    'Ground Worker',
750    'Grounds Person',
751    'Guard',
752    'Guest House Proprietor',
753    'Guide',
754    'Gun Smith',
755    'Gynaecologist',
756    'Haematologist',
757    'Hair Stylist',
758    'Hairdresser',
759    'Handyperson',
760    'Harbour Master',
761    'Hardware Dealer',
762    'Haulage Contractor',
763    'Hawker',
764    'Head Greenkeeper',
765    'Head Groom',
766    'Head of Arts',
767    'Head of Trade',
768    'Head of Traffic',
769    'Headteacher',
770    'Health Advisor',
771    'Health And Safety Consultant',
772    'Health And Safety Officer',
773    'Health Care Assistant',
774    'Health Planner',
775    'Health Therapist',
776    'Health Visitor',
777    'Hearing Aid Technician',
778    'Hearing Therapist',
779    'Heating & Ventilation Engineer',
780    'Heating Engineer',
781    'Heating/Ventilation Engineer',
782    'Hedger/Ditcher',
783    'Helmsperson',
784    'Herbalist',
785    'Herdsperson',
786    'HGV Driver',
787    'HGV Mechanic',
788    'Highway Inspector',
789    'Hire Car Driver',
790    'Hirer',
791    'Histologist',
792    'Historian',
793    'Hod Carrier',
794    'Home Economist',
795    'Home Help',
796    'Homemaker',
797    'Homecare Manager',
798    'Homemaker (Stay at Home)',
799    'Homeopath',
800    'Hop Merchant',
801    'Horse Breeder',
802    'Horse Dealer (Non Sport)',
803    'Horse Dealer (Sport)',
804    'Horse Riding Instructor',
805    'Horse Trader',
806    'Horse Trainer',
807    'Horticultural Consultant',
808    'Horticulturalist',
809    'Hosiery Mechanic',
810    'Hosiery Worker',
811    'Hospital Consultant',
812    'Hospital Doctor',
813    'Hospital Manager',
814    'Hospital Orderly',
815    'Hospital Photographer',
816    'Hospital Technician',
817    'Hospital Warden',
818    'Hospital Worker',
819    'Host',
820    'Hot Foil Printer',
821    'Hotel Consultant',
822    'Hotel Worker',
823    'Hotelier',
824    'House Parent',
825    'House Sitter',
826    'Housekeeper',
827    'Housing Assistant',
828    'Housing Officer',
829    'Housing Supervisor',
830    'Human Resources Manager',
831    'Human Resources Staff',
832    'Hunt Master',
833    'Huntsperson',
834    'Hydro Geologist',
835    'Hygienist',
836    'Hypnotherapist',
837    'Hypnotist',
838    'Ice Cream Vendor',
839    'Illustrator',
840    'Immigration Officer',
841    'Immunologist',
842    'Import Consultant',
843    'Import/Export Dealer',
844    'Importer',
845    'Independent Means',
846    'Induction Moulder',
847    'Industrial Chemist',
848    'Industrial Consultant',
849    'Industrial Designer',
850    'Injection Moulder',
851    'Inland Revenue Officer',
852    'Insolvency Practitioner',
853    'Inspector',
854    'Inspector - Insurance',
855    'Installation Worker',
856    'Instructor',
857    'Instrument Engineer',
858    'Instrument Maker',
859    'Instrument Supervisor',
860    'Instrument Technician',
861    'Insurance Assessor',
862    'Insurance Collector',
863    'Insurance Consultant',
864    'Insurance Inspector',
865    'Insurance Representative',
866    'Interior Decorator',
867    'Interior Designer',
868    'Interpreter',
869    'Interviewer',
870    'Inventor',
871    'Investigator',
872    'Investment Advisor',
873    'Investment Banker',
874    'Investment Manager',
875    'Invigilator',
876    'Ironer',
877    'Ironer Finisher',
878    'Ironer Presser',
879    'Ironmonger',
880    'IT Consultant',
881    'IT Manager',
882    'IT Trainer',
883    'IT/Systems Architect',
884    'Janitor',
885    'Jazz Composer',
886    'Jeweller',
887    'Jewellery Consultant',
888    'Jockey',
889    'Joiner',
890    'Joinery Consultant',
891    'Jointer',
892    'Journalist',
893    'Journalist - Freelance',
894    'Journalistic Agent',
895    'Judge',
896    'Juggler',
897    'Junk Shop Proprietor',
898    'Justice Of The Peace',
899    'Keep Fit Instructor',
900    'Kennel Hand',
901    'Kiln Setter',
902    'Kilnperson (Glass/Ceramics)',
903    'Kissagram Person',
904    'Kitchen Worker',
905    'Knitter',
906    'Labelling Operator',
907    'Laboratory Analyst',
908    'Laboratory Assistant',
909    'Laboratory Attendant',
910    'Laboratory Manager',
911    'Laboratory Operative',
912    'Laboratory Supervisor',
913    'Laboratory Technician',
914    'Labourer',
915    'Laminator',
916    'Lampshade Maker',
917    'Land Agent',
918    'Land Surveyor',
919    'Landowner',
920    'Landscape Architect',
921    'Landscape Gardener',
922    'Landworker',
923    'Lathe Operator',
924    'Laundry Worker',
925    'Lavatory Attendant',
926    'Law Clerk',
927    'Lawn Mower Repairer',
928    'Lawyer',
929    'Leaflet Distributor',
930    'Leather Worker',
931    'Lecturer',
932    'Ledger Clerk',
933    'Legal Advisor',
934    'Legal Assistant',
935    'Legal Executive',
936    'Legal Secretary',
937    'Leisure Centre Attendant',
938    'Leisure Centre Manager',
939    'Lengthsperson',
940    'Lens Grinder & Polisher',
941    'Letting Agent',
942    'Lexicographer',
943    'Liaison Officer',
944    'Librarian',
945    'Library Manager',
946    'Licensee',
947    'Licensing Consultant',
948    'Life Assurance Salesperson',
949    'Lifeguard',
950    'Lift Attendant',
951    'Lift Engineer',
952    'Lighterperson',
953    'Lighthouse Keeper',
954    'Lighting Designer',
955    'Lighting Director',
956    'Lighting Technician',
957    'Lime Kiln Attendant',
958    'Line Manager',
959    'Line Worker',
960    'Linesperson',
961    'Linguist',
962    'Linotype Operator',
963    'Literary Agent',
964    'Literary Editor',
965    'Lithographer',
966    'Litigation Manager',
967    'Loader',
968    'Loans Manager',
969    'Local Government Officer',
970    'Lock Keeper',
971    'Locksmith',
972    'Locum Doctor',
973    'Locum Pharmacist',
974    'Log Merchant',
975    'Lorry Driver',
976    'Loss Adjustor',
977    'Loss Assessor',
978    'Lumberjack',
979    'Machine Fitters Mate',
980    'Machine Minder',
981    'Machine Operator',
982    'Machine Setter',
983    'Machine Technician',
984    'Machine Tool Engineer',
985    'Machine Tool Fitter',
986    'Machinist',
987    'Magician',
988    'Magistrate',
989    'Magistrates Clerk',
990    'Maid',
991    'Mail Order Worker',
992    'Maintenance Engineer',
993    'Maintenance Fitter',
994    'Maintenance Manager',
995    'Maintenance Staff',
996    'Maitre D Hotel',
997    'Make Up Artist',
998    'Make Up Supervisor',
999    'Management Consultant',
1000    'Manager',
1001    'Manager - Sports',
1002    'Managing Clerk',
1003    'Managing Director',
1004    'Manicurist',
1005    'Manufacturing Agent',
1006    'Manufacturing Engineer',
1007    'Manufacturing Technician',
1008    'Map Maker',
1009    'Map Mounter',
1010    'Marble Finisher',
1011    'Marble Mason',
1012    'Marine Broker',
1013    'Marine Consultant',
1014    'Marine Electrician',
1015    'Marine Engineer',
1016    'Marine Geologist',
1017    'Marine Pilot',
1018    'Marine Surveyor',
1019    'Market Gardener',
1020    'Market Research Assistant',
1021    'Market Researcher',
1022    'Market Trader',
1023    'Marketing Agent',
1024    'Marketing Assistant',
1025    'Marketing Co-ordinator',
1026    'Marketing Consultant',
1027    'Marketing Director',
1028    'Marketing Executive',
1029    'Marketing Manager',
1030    'Marquee Erector',
1031    'Massage Therapist',
1032    'Master Mariner',
1033    'Master of Ceremonies',
1034    'Master Of Foxhounds',
1035    'Mate (Ship\'s)',
1036    'Materials Controller',
1037    'Materials Manager',
1038    'Mathematician',
1039    'Matron',
1040    'Mattress Maker',
1041    'Mature Student - Living At Home',
1042    'Mature Student - Living Away',
1043    'Meat Inspector',
1044    'Meat Wholesaler',
1045    'Mechanic',
1046    'Mechanical Designer',
1047    'Mechanical Engineer',
1048    'Mechanical Technician',
1049    'Medal Dealer',
1050    'Media Critic',
1051    'Media Planner',
1052    'Medical Advisor',
1053    'Medical Assistant',
1054    'Medical Consultant',
1055    'Medical Diagnostician',
1056    'Medical Officer',
1057    'Medical Physicist',
1058    'Medical Practitioner',
1059    'Medical Researcher',
1060    'Medical Secretary',
1061    'Medical Student - Living At Home',
1062    'Medical Student - Living Away',
1063    'Medical Supplier',
1064    'Medical Technician',
1065    'Member Of Parliament',
1066    'Member of Religious Order',
1067    'Merchandiser',
1068    'Merchant Banker',
1069    'Merchant Ship Captain',
1070    'Merchant Ship Crew',
1071    'Messenger',
1072    'Metal Dealer',
1073    'Metal Engineer',
1074    'Metal Polisher',
1075    'Metal Worker',
1076    'Metallurgist',
1077    'Meteorologist',
1078    'Meter Reader',
1079    'Microbiologist',
1080    'Microfilm Operator',
1081    'Microscopist',
1082    'Midwife',
1083    'Milk Delivery Person',
1084    'Mill Operator',
1085    'Mill Worker',
1086    'Miller',
1087    'Milliner',
1088    'Millwright',
1089    'Miner',
1090    'Mineralologist',
1091    'Minibus Driver',
1092    'Minicab Driver',
1093    'Mining Consultant',
1094    'Mining Engineer',
1095    'Minister Of Religion',
1096    'Minister Of The Crown',
1097    'Missionary',
1098    'Mobile Caterer',
1099    'Mobile Disc Jockey',
1100    'Mobile Disco Owner',
1101    'Mobile Hairdresser',
1102    'Mobile Motor Mechanic',
1103    'Mobile Service Engineer',
1104    'Model',
1105    'Model Maker',
1106    'Moderator',
1107    'Money Broker',
1108    'Money Dealer',
1109    'Moneylender',
1110    'Monk or Nun',
1111    'Monumental Sculptor',
1112    'Mooring Contractor',
1113    'Mortgage Broker',
1114    'Mortgage Consultant',
1115    'Mortician',
1116    'Motor Dealer',
1117    'Motor Engineer',
1118    'Motor Fitter',
1119    'Motor Mechanic',
1120    'Motor Racing Driver',
1121    'Motor Racing Organiser',
1122    'Motor Trader',
1123    'Moulding Process Technician',
1124    'Museum Assistant',
1125    'Museum Attendant',
1126    'Museum Consultant',
1127    'Museum Technician',
1128    'Music Producer',
1129    'Music Teacher',
1130    'Music Therapist',
1131    'Music Wholesaler',
1132    'Musical Arranger',
1133    'Musician',
1134    'Musician - Amateur',
1135    'Nanny',
1136    'Naturalist',
1137    'Naturopath',
1138    'Navigator',
1139    'Negotiator',
1140    'Neurologist',
1141    'Newsagent',
1142    'Newsreader',
1143    'Night Porter',
1144    'Night Watchperson',
1145    'Non Commissioned Officer',
1146    'Non Religious Celebrant',
1147    'Not Employed Due to Disability',
1148    'Not In Employment',
1149    'Notary Public',
1150    'Nuclear Scientist',
1151    'Nurse',
1152    'Nursemaid',
1153    'Nursery Nurse',
1154    'Nursery Worker (Childcare)',
1155    'Nursery Worker (Gardening)',
1156    'Nursing Assistant',
1157    'Nursing Auxiliary',
1158    'Nursing Manager',
1159    'Nursing Officer',
1160    'Nursing Sister',
1161    'Nutritionist',
1162    'Obstetrician',
1163    'Occupational Health Consultant',
1164    'Occupational Health Nurse',
1165    'Occupational Therapist',
1166    'Oceanographer',
1167    'Oculist',
1168    'Off Shore Surveyor',
1169    'Office Administrator',
1170    'Office Manager',
1171    'Oil Broker',
1172    'Oil Rig Crew',
1173    'On Benefits',
1174    'Online/eTrade Seller (e.g. Ebay)',
1175    'Opera Singer',
1176    'Operating Department Practitioner',
1177    'Operations Director',
1178    'Operations Engineer',
1179    'Operations Manager',
1180    'Operations Supervisor',
1181    'Ophthalmic Surgeon',
1182    'Ophthalmic Technician',
1183    'Optical Advisor',
1184    'Optical Assistant',
1185    'Optical Technician',
1186    'Optician',
1187    'Optometrist',
1188    'Orchestra Leader',
1189    'Orchestral Violinist',
1190    'Order Clerk',
1191    'Orderly',
1192    'Organist',
1193    'Ornamental Blacksmith',
1194    'Ornithologist',
1195    'Orthopaedic Technician',
1196    'Orthoptist',
1197    'Osteopath',
1198    'Outdoor Pursuits Instructor',
1199    'Outfitter',
1200    'Outreach Worker',
1201    'Oven Cleaner',
1202    'Overhead Line Instructor',
1203    'Overhead Lineperson',
1204    'Overlocker',
1205    'Overseas Mailer',
1206    'Overwriter',
1207    'Packaging Consultant',
1208    'Packer',
1209    'Paediatrician',
1210    'Paint Consultant',
1211    'Paint Sprayer',
1212    'Paint Technologist',
1213    'Painter',
1214    'Painter And Decorator',
1215    'Palaeobotanist',
1216    'Palaeontologist',
1217    'Pallet Maker',
1218    'Panel Beater',
1219    'Paper Mill Worker',
1220    'Parachute Packer',
1221    'Paramedic',
1222    'Park Attendant',
1223    'Park Keeper',
1224    'Park Ranger',
1225    'Partition Erector',
1226    'Parts Manager',
1227    'Parts Person',
1228    'Parts Supervisor',
1229    'Party Planner',
1230    'Pasteuriser',
1231    'Pastry Chef',
1232    'Patent Agent',
1233    'Patent Attorney',
1234    'Pathologist',
1235    'Patrol Person',
1236    'Pattern Cutter',
1237    'Pattern Maker',
1238    'Pattern Weaver',
1239    'Paviour',
1240    'Pawnbroker',
1241    'Payment Officer',
1242    'Payroll Assistant',
1243    'Payroll Clerk',
1244    'Payroll Manager',
1245    'Payroll Supervisor',
1246    'Pearl Stringer',
1247    'Pedicurist',
1248    'Pensions Consultant',
1249    'Pensions Manager',
1250    'Personal Assistant',
1251    'Personnel Administrator',
1252    'Personnel Manager',
1253    'Personnel Officer',
1254    'Pest Control',
1255    'Pest Controller',
1256    'Pet Minder',
1257    'Petrol Station Attendant',
1258    'Petroleum Engineer',
1259    'Petty Officer',
1260    'Pharmaceutical Assistant',
1261    'Pharmacist',
1262    'Pharmacy Manager',
1263    'Pharmacy Technician',
1264    'Philatelist',
1265    'Phlebotomist',
1266    'Photo Engraver',
1267    'Photo Laboratory Processor',
1268    'Photo Technician',
1269    'Photocopy Machine Technician',
1270    'Photographer',
1271    'Photographer - Location',
1272    'Photographer - Press',
1273    'Photographer - Press (Local)',
1274    'Photographer - Shop Owner',
1275    'Photographer - Street',
1276    'Photographer - Studio',
1277    'Photographic Agent',
1278    'Physician',
1279    'Physicist',
1280    'Physiologist',
1281    'Physiotherapist',
1282    'Piano Teacher',
1283    'Piano Tuner',
1284    'Picker',
1285    'Picture Editor',
1286    'Picture Framer',
1287    'Picture Renovator',
1288    'Picture Researcher',
1289    'Pig Manager',
1290    'Pilot',
1291    'Pilot Helicopter',
1292    'Pilot Test',
1293    'Pipe Fitter',
1294    'Pipe Inspector',
1295    'Pipe Insulator',
1296    'Pipe Layer',
1297    'Planning Engineer',
1298    'Planning Manager',
1299    'Planning Officer',
1300    'Planning Technician',
1301    'Plant Attendant',
1302    'Plant Breeder',
1303    'Plant Driver',
1304    'Plant Engineer',
1305    'Plant Fitter',
1306    'Plant Manager',
1307    'Plant Operator',
1308    'Plasterer',
1309    'Plastics Consultant',
1310    'Plastics Engineer',
1311    'Plate Layer',
1312    'Plater',
1313    'Playgroup Assistant',
1314    'Playgroup Leader',
1315    'Playwright',
1316    'Plumber',
1317    'Plumbing & Heating Engineer',
1318    'Podiatrist',
1319    'Points Person',
1320    'Police - Civilian',
1321    'Police Community Support Officer',
1322    'Police Inspector',
1323    'Police Officer',
1324    'Polisher',
1325    'Pool Attendant',
1326    'Port Officer',
1327    'Porter',
1328    'Portfolio Manager',
1329    'Post Card Seller',
1330    'Post Graduate Student Living at Home',
1331    'Post Graduate Student Living Away from Home',
1332    'Post Office Counter Clerk',
1333    'Post Office Sub-Manager',
1334    'Post Sorter',
1335    'Postmaster or Mistress',
1336    'Postperson',
1337    'Potato Merchant',
1338    'Potter',
1339    'Poultry Worker',
1340    'Practice Manager',
1341    'Preacher',
1342    'Precious Metal Merchant',
1343    'Precision Engineer',
1344    'Premises Security Installers',
1345    'Press Correspondent',
1346    'Press Officer',
1347    'Press Operator',
1348    'Press Photographer - Freelance',
1349    'Press Photographer - National',
1350    'Press Producer',
1351    'Press Setter',
1352    'Presser',
1353    'Priest or Reverend',
1354    'Principal',
1355    'Print Finisher',
1356    'Printer',
1357    'Printing Press Operator',
1358    'Prison Chaplain',
1359    'Prison Officer',
1360    'Private Detective',
1361    'Private Investigator',
1362    'Privy Councillor',
1363    'Probation Officer',
1364    'Probation Worker',
1365    'Process Engineer',
1366    'Process Operator',
1367    'Process Worker',
1368    'Procurator Fiscal',
1369    'Produce Supervisor',
1370    'Producer',
1371    'Product Designer',
1372    'Product Installer',
1373    'Product Manager',
1374    'Production Engineer',
1375    'Production Hand',
1376    'Production Manager',
1377    'Production Planner',
1378    'Professional Gamer',
1379    'Professor',
1380    'Progress Chaser',
1381    'Progress Clerk',
1382    'Project Co-ordinator',
1383    'Project Engineer',
1384    'Project Leader',
1385    'Project Manager',
1386    'Project Worker',
1387    'Projectionist',
1388    'Promoter',
1389    'Promoter - Entertainments',
1390    'Promoter - Racing',
1391    'Promoter - Ring Sports',
1392    'Promoter - Sports',
1393    'Proof Reader',
1394    'Property Buyer',
1395    'Property Dealer',
1396    'Property Developer',
1397    'Property Manager',
1398    'Property Owner (Letting)',
1399    'Property Valuer',
1400    'Proprietor',
1401    'Prosthetist',
1402    'Psychiatrist',
1403    'Psychoanalyst',
1404    'Psychodynamic Counsellor',
1405    'Psychologist',
1406    'Psychotherapist',
1407    'Public House Manager',
1408    'Public Relations Officer',
1409    'Publican',
1410    'Publicity Manager',
1411    'Publisher',
1412    'Publishing Manager',
1413    'Purchase Clerk',
1414    'Purchase Ledger Clerk',
1415    'Purchasing Assistant',
1416    'Purchasing Manager',
1417    'Purser',
1418    'Quality Controller',
1419    'Quality Engineer',
1420    'Quality Inspector',
1421    'Quality Manager',
1422    'Quality Technician',
1423    'Quantity Surveyor',
1424    'Quarry Worker',
1425    'Queens Counsel',
1426    'Rabbi',
1427    'Racehorse Groom',
1428    'Racing Motorcyclist',
1429    'Racing Organiser',
1430    'Radar Mechanic',
1431    'Radio Controller',
1432    'Radio Director',
1433    'Radio Engineer',
1434    'Radio Helpline Coordinator',
1435    'Radio Mechanic',
1436    'Radio Operator',
1437    'Radio Presenter',
1438    'Radio Producer',
1439    'Radiographer',
1440    'Radiologist',
1441    'Radiotherapist',
1442    'Rail Person',
1443    'Rally Driver',
1444    'Ramp Agent',
1445    'Re-Settlement Officer',
1446    'Reactor Attendant',
1447    'Reader',
1448    'Reader Compositor',
1449    'Receptionist',
1450    'Records Supervisor',
1451    'Recovery Vehicle Co-ordinator',
1452    'Recruitment Consultant',
1453    'Rector',
1454    'Referee',
1455    'Refit Merchandiser',
1456    'Reflexologist',
1457    'Refractory Engineer',
1458    'Refrigeration Engineer',
1459    'Refuse Collector',
1460    'Registered Disabled',
1461    'Registrar',
1462    'Regulator',
1463    'Religious Dignitary',
1464    'Religious Leader',
1465    'Relocation Agent',
1466    'Remedial Therapist',
1467    'Rent Collector',
1468    'Rent Officer',
1469    'Repairer',
1470    'Reporter',
1471    'Reporter - Freelance',
1472    'Reprographic Assistant',
1473    'Rescue Worker',
1474    'Research Analyst',
1475    'Research Assistant',
1476    'Research Consultant',
1477    'Research Director',
1478    'Research Scientist',
1479    'Research Technician',
1480    'Researcher',
1481    'Resin Caster',
1482    'Restaurant Manager',
1483    'Restaurateur',
1484    'Restorer',
1485    'Retired',
1486    'Revenue Clerk',
1487    'Revenue Officer',
1488    'Rheumatologist',
1489    'Rig Worker',
1490    'Rigger',
1491    'Riverperson',
1492    'Riveter',
1493    'Road Safety Officer',
1494    'Road Sweeper',
1495    'Road Worker',
1496    'Roof Tiler',
1497    'Roofer',
1498    'Rose Grower',
1499    'Royal Air Force - NCO/Commissioned Officer',
1500    'Royal Air Force - Other Ranks',
1501    'Royal Marine',
1502    'Royal Navy - NCO/Commissioned Officer',
1503    'Royal Navy - Other Ranks',
1504    'Rubber Worker',
1505    'Rug Maker',
1506    'Rugby Player',
1507    'Saddler',
1508    'Safety Officer',
1509    'Sail Maker',
1510    'Sales Administrator',
1511    'Sales Assistant',
1512    'Sales Director',
1513    'Sales Engineer',
1514    'Sales Executive',
1515    'Sales Manager',
1516    'Sales Representative',
1517    'Sales Support',
1518    'Salesperson',
1519    'Salvage Dealer',
1520    'Salvage Hand',
1521    'Salvation Army Officer',
1522    'Sample Hand',
1523    'Sand Blaster',
1524    'Sand Merchant',
1525    'Saw Miller',
1526    'Sawyer',
1527    'Scaffolder',
1528    'Scenehand',
1529    'School Catering Assistant',
1530    'School Counsellor',
1531    'School Crossing Warden',
1532    'School Inspector',
1533    'School Student',
1534    'Scientific Officer',
1535    'Scientist',
1536    'Scientist - Atomic Energy',
1537    'Scrap Dealer',
1538    'Screen Printer',
1539    'Screen Writer',
1540    'Script Writer',
1541    'Sculptor',
1542    'Sea Person',
1543    'Search Engine Optimization Analyst/Consultant/Executive',
1544    'Search Engine Optimization Manager',
1545    'Second Hand Dealer',
1546    'Secretary',
1547    'Secretary And PA',
1548    'Security Consultant',
1549    'Security Controller',
1550    'Security Guard',
1551    'Security Officer',
1552    'Seedsperson',
1553    'Servant',
1554    'Service Engineer',
1555    'Service Engineer (Non-Mobile)',
1556    'Service Manager',
1557    'Setter',
1558    'Sewage Worker',
1559    'Share Dealer',
1560    'Sheet Metal Worker',
1561    'Shelf Filler',
1562    'Shelter Warden',
1563    'Shepherd or Shepherdess',
1564    'Sheriff',
1565    'Sheriff Clerk',
1566    'Sheriff Principal',
1567    'Sheriffs Officer',
1568    'Shift Controller',
1569    'Ship Broker',
1570    'Ship Builder',
1571    'Shipping Clerk',
1572    'Shipping Officer',
1573    'Shipwright',
1574    'Shipyard Worker',
1575    'Shoe Maker',
1576    'Shoe Repairer',
1577    'Shooting Instructor',
1578    'Shop Assistant',
1579    'Shop Fitter',
1580    'Shop Keeper',
1581    'Shop Manager',
1582    'Shorthand Writer',
1583    'Shot Blaster',
1584    'Show Jumper',
1585    'Showperson',
1586    'Shunter',
1587    'Sign Maker',
1588    'Signalperson',
1589    'Signwriter',
1590    'Silversmith',
1591    'Singer',
1592    'Site Agent',
1593    'Site Engineer',
1594    'Skipper',
1595    'Slater',
1596    'Slaughterperson',
1597    'Smallholder',
1598    'Smelter',
1599    'Snooker Player',
1600    'Social Media Influencer',
1601    'Social Worker',
1602    'Sociologist',
1603    'Software Consultant',
1604    'Software Engineer',
1605    'Solar Panel Installer And Repairer',
1606    'Soldier',
1607    'Solicitor',
1608    'Song Writer',
1609    'Sorter',
1610    'Sound Editor',
1611    'Sound Engineer',
1612    'Sound Mixer',
1613    'Sound Technician',
1614    'Special Constable',
1615    'Special Needs Assistant',
1616    'Speech Therapist',
1617    'Sports Administrator',
1618    'Sports Agent',
1619    'Sports Centre Attendant',
1620    'Sports Coach',
1621    'Sports Commentator',
1622    'Sports Equipment Repairer',
1623    'Sports Scout',
1624    'Spring Maker',
1625    'Stable Hand',
1626    'Staff Nurse',
1627    'Stage Director',
1628    'Stage Manager',
1629    'Stage Mover',
1630    'Stamp Dealer',
1631    'State Enrolled Nurse',
1632    'State Registered Nurse',
1633    'Station Manager',
1634    'Stationer',
1635    'Statistician',
1636    'Steel Erector',
1637    'Steel Worker',
1638    'Steeplejack',
1639    'Stenographer',
1640    'Stevedore',
1641    'Stock Controller',
1642    'Stock Exchange Dealer',
1643    'Stock Manager',
1644    'Stockbroker',
1645    'Stockperson',
1646    'Stocktaker',
1647    'Stone Cutter',
1648    'Stone Sawyer',
1649    'Stonemason',
1650    'Store Detective',
1651    'Store Keeper',
1652    'Storeperson',
1653    'Stores Controller',
1654    'Street Entertainer',
1655    'Street Trader',
1656    'Stud Hand',
1657    'Student',
1658    'Student - Living at Home',
1659    'Student - Living Away',
1660    'Student Counsellor',
1661    'Student Nurse - Living At Home',
1662    'Student Nurse - Living Away',
1663    'Student Teacher - Living At Home',
1664    'Student Teacher - Living Away',
1665    'Studio Manager',
1666    'Supervisor',
1667    'Supply Teacher',
1668    'Support Worker',
1669    'Surgeon',
1670    'Surgeon - N.H.S.',
1671    'Surgeon - Non N.H.S.',
1672    'Surveyor',
1673    'Surveyor - Chartered',
1674    'Sustainability Specialist',
1675    'Systems Analyst',
1676    'Systems Engineer',
1677    'Systems Manager',
1678    'T-Shirt Printer',
1679    'Tachograph Analyst',
1680    'Tacker',
1681    'Tailor',
1682    'Tank Farm Operative',
1683    'Tanker Driver',
1684    'Tanner',
1685    'Tape Librarian',
1686    'Tape Operator',
1687    'Tarmacer',
1688    'Tarot Reader/Palmistry Expert',
1689    'Taster',
1690    'Tattooist',
1691    'Tax Advisor',
1692    'Tax Analyst',
1693    'Tax Assistant',
1694    'Tax Consultant',
1695    'Tax Inspector',
1696    'Tax Manager',
1697    'Tax Officer',
1698    'Taxi Controller',
1699    'Taxi Driver',
1700    'Taxidermist',
1701    'Tea Blender',
1702    'Tea Taster',
1703    'Teacher',
1704    'Teachers Assistant',
1705    'Technical Advisor',
1706    'Technical Analyst',
1707    'Technical Assistant',
1708    'Technical Author',
1709    'Technical Clerk',
1710    'Technical Co-ordinator',
1711    'Technical Director',
1712    'Technical Editor',
1713    'Technical Engineer',
1714    'Technical Illustrator',
1715    'Technical Instructor',
1716    'Technical Liaison Engineer',
1717    'Technical Manager',
1718    'Technician',
1719    'Technician - Performing Arts',
1720    'Technologist',
1721    'Telecommunication Consultant',
1722    'Telecommunications Engineer',
1723    'Telecommunications Manager',
1724    'Telemarketer',
1725    'Telephone Engineer',
1726    'Telephonist',
1727    'Telesales Person',
1728    'Television Director',
1729    'Television Engineer',
1730    'Television Presenter',
1731    'Television Producer',
1732    'Television Set Builder',
1733    'Temperature Time Recorder',
1734    'Tennis Coach',
1735    'Tennis Player',
1736    'Terrier',
1737    'Territorial Army Officer',
1738    'Test Driver',
1739    'Test Engineer',
1740    'Textile Consultant',
1741    'Textile Engineer',
1742    'Textile Technician',
1743    'Textile Worker',
1744    'Thatcher',
1745    'Theatre Director',
1746    'Theatre Manager',
1747    'Theatrical Agent',
1748    'Theme Park Worker',
1749    'Therapist',
1750    'Thermal Engineer',
1751    'Thermal Insulator',
1752    'Ticket Agent',
1753    'Ticket Collector',
1754    'Ticket Inspector',
1755    'Tiler',
1756    'Timber Inspector',
1757    'Timber Worker',
1758    'Time and Motion Analyst',
1759    'Tobacconist',
1760    'Toll Collector',
1761    'Tool Maker',
1762    'Tool Setter',
1763    'Tour Agent',
1764    'Tour Guide',
1765    'Town Clerk',
1766    'Town Planner',
1767    'Toy Maker',
1768    'Toy Trader',
1769    'Track Worker',
1770    'Tractor Driver',
1771    'Tractor Mechanic',
1772    'Trade Mark Agent',
1773    'Trade Union Official',
1774    'Trading Standards Officer',
1775    'Traffic Clerk',
1776    'Traffic Despatcher',
1777    'Traffic Engineer',
1778    'Traffic Officer',
1779    'Traffic Planner',
1780    'Traffic Supervisor',
1781    'Traffic Warden',
1782    'Train Despatcher',
1783    'Train Driver',
1784    'Train Motor Operative',
1785    'Trainee Manager',
1786    'Trainer',
1787    'Training Advisor',
1788    'Training Assistant',
1789    'Training Co-ordinator',
1790    'Training Consultant',
1791    'Training Instructor',
1792    'Training Manager',
1793    'Training Officer',
1794    'Transcriber',
1795    'Translator',
1796    'Transmission Controller',
1797    'Transport Clerk',
1798    'Transport Consultant',
1799    'Transport Controller',
1800    'Transport Engineer',
1801    'Transport Manager',
1802    'Transport Officer',
1803    'Transport Planner',
1804    'Transport Police Officer',
1805    'Travel Agent',
1806    'Travel Clerk',
1807    'Travel Consultant',
1808    'Travel Courier',
1809    'Travel Guide',
1810    'Travel Guide Writer',
1811    'Travel Representative',
1812    'Travelling Salesperson',
1813    'Trawler Hand',
1814    'Treasurer',
1815    'Tree Feller',
1816    'Tree Surgeon',
1817    'Trichologist',
1818    'Trinity House Pilot',
1819    'Trout Farmer',
1820    'Truck Driver',
1821    'Tug Skipper',
1822    'Tunneller',
1823    'Turf Accountant',
1824    'Turkey Farmer',
1825    'Turner',
1826    'Tutor',
1827    'TV And Video Installer',
1828    'TV And Video Repairer',
1829    'TV Announcer',
1830    'TV Broadcasting Technician',
1831    'TV Editor',
1832    'Typesetter',
1833    'Typewriter Engineer',
1834    'Typist',
1835    'Typographer',
1836    'Tyre Builder',
1837    'Tyre Fitter',
1838    'Tyre Inspector',
1839    'Tyre Technician',
1840    'Tyre/Exhaust Fitter',
1841    'Umpire',
1842    'Undergraduate Student - Living At Home',
1843    'Undergraduate Student - Living Away from Home',
1844    'Undertaker',
1845    'Underwriter',
1846    'Unemployed',
1847    'University Dean',
1848    'University Reader',
1849    'Upholsterer',
1850    'Usher',
1851    'Valet',
1852    'Valuer',
1853    'Valve Technician',
1854    'Van Driver',
1855    'Van Salesperson',
1856    'VDU Operator',
1857    'Vehicle Assessor',
1858    'Vehicle Bodybuilder',
1859    'Vehicle Delivery Agent',
1860    'Vehicle Engineer',
1861    'Vehicle Technician',
1862    'Vehicle Valet',
1863    'Vending Machine Filler',
1864    'Vending Machine Technician',
1865    'Ventriloquist',
1866    'Verger',
1867    'Veterinary Assistant',
1868    'Veterinary Nurse',
1869    'Veterinary Surgeon',
1870    'Vicar',
1871    'Violin Maker',
1872    'Vision Control Manager',
1873    'Vision Mixer',
1874    'Voluntary Worker',
1875    'Vulcanologist',
1876    'Wages Clerk',
1877    'Waiting Staff',
1878    'Warden',
1879    'Wardrobe Manager',
1880    'Warehouse Manager',
1881    'Warehouse Operative',
1882    'Waste Dealer',
1883    'Waste Disposal Worker',
1884    'Waste Paper Merchant',
1885    'Watchmaker',
1886    'Water Diviner',
1887    'Weaver',
1888    'Web Designer',
1889    'Web Developer',
1890    'Web Programmer',
1891    'Weighbridge Clerk',
1892    'Weighbridge Operator',
1893    'Welder',
1894    'Welfare Assistant',
1895    'Welfare Officer',
1896    'Welfare Rights Officer',
1897    'Wheel Clamper',
1898    'Wheelwright',
1899    'Whisky Blender',
1900    'Wholesale Newspaper Delivery Driver',
1901    'Will Writer',
1902    'Window Cleaner',
1903    'Window Dresser',
1904    'Windscreen Fitter',
1905    'Wine Merchant',
1906    'Wood Carver',
1907    'Wood Cutter',
1908    'Wood Machinist',
1909    'Wood Worker',
1910    'Word Processing Operator',
1911    'Work Study Analyst',
1912    'Works Manager',
1913    'Wrestler',
1914    'Writer',
1915    'Yacht Master',
1916    'Yard Manager',
1917    'Yard Person',
1918    'Yoga Teacher',
1919    'Youth Hostel Warden',
1920    'Youth Worker',
1921    'Zoo Keeper',
1922    'Zoo Manager',
1923    'Zoologist',
1924    'Zoology Consultant'
1925].sort((a, b) => a.localeCompare(b, 'en', { sensitivity: 'base' }));
1926
1927function financeEligibilityFilterOccupations(query) {
1928    const q = String(query || '')
1929        .trim()
1930        .toLowerCase();
1931    if (!q) {
1932        return FINANCE_ELIGIBILITY_OCCUPATIONS.slice();
1933    }
1934    return FINANCE_ELIGIBILITY_OCCUPATIONS.filter((o) => o.toLowerCase().includes(q));
1935}
1936
1937/** Scrollable region inside the active finance eligibility journey step. */
1938function financeEligibilityFindScrollArea(fromEl) {
1939    const offcanvas = document.getElementById('financeEligibilityOffcanvas');
1940    const step =
1941        fromEl?.closest?.('.cars-fe-journey-step') ||
1942        offcanvas?.querySelector('.cars-fe-journey-step:not([hidden])');
1943    if (!step || step.hidden) {
1944        return null;
1945    }
1946    return (
1947        step.querySelector('.cars-fe-scroll') ||
1948        step.querySelector('.finance-eligibility-intro__scroll') ||
1949        step.querySelector('.finance-eligibility-agreement__scroll')
1950    );
1951}
1952
1953function financeEligibilitySetOccupationDropdownOpen(open) {
1954    const stack = document.querySelector('.cars-fe-form-stack--employment');
1955    const step =
1956        stack?.closest('.cars-fe-journey-step') || document.getElementById('cars-fe-step-employment');
1957    if (!step) {
1958        return;
1959    }
1960    step.classList.toggle('cars-fe-occupation-dropdown-open', Boolean(open));
1961    if (!open && stack) {
1962        delete stack.dataset.feScrollPadExtra;
1963        stack.style.removeProperty('padding-bottom');
1964    }
1965}
1966
1967/** Scroll a field (and optional dropdown below it) so it stays above the sticky step footer on small screens. */
1968function financeEligibilityScrollFieldIntoView(targetEl, options = {}) {
1969    if (!targetEl) {
1970        return;
1971    }
1972    const scrollEl = options.scrollArea || financeEligibilityFindScrollArea(targetEl);
1973    const anchor =
1974        targetEl.closest?.('.cars-fe-duration-listbox') ||
1975        targetEl.closest?.('.cars-fe-occupation-combobox') ||
1976        targetEl.closest?.('.cars-fe-field-group') ||
1977        targetEl.closest?.('.cars-fe-field-group__shell') ||
1978        targetEl;
1979    const behavior = options.behavior || 'smooth';
1980    const gap = options.gapPx ?? 12;
1981    if (!scrollEl) {
1982        if (typeof anchor.scrollIntoView === 'function') {
1983            anchor.scrollIntoView({ behavior, block: 'nearest' });
1984        }
1985        return;
1986    }
1987
1988    const listPanel =
1989        options.listPanelEl ||
1990        anchor.querySelector?.('.cars-fe-duration-listbox__panel:not([hidden])') ||
1991        anchor.querySelector?.('.cars-fe-occupation-combobox__panel:not([hidden])');
1992    let reserveBelow = options.reserveBelowPx ?? 0;
1993    if (listPanel && listPanel.hidden === false) {
1994        reserveBelow = Math.max(reserveBelow, listPanel.getBoundingClientRect().height + gap);
1995    } else if (anchor.classList?.contains('cars-fe-occupation-combobox')) {
1996        reserveBelow = Math.max(reserveBelow, 220 + gap);
1997    } else if (anchor.classList?.contains('cars-fe-duration-listbox')) {
1998        reserveBelow = Math.max(reserveBelow, 220 + gap);
1999    } else if (targetEl.tagName === 'SELECT') {
2000        reserveBelow = Math.max(reserveBelow, 180 + gap);
2001    }
2002
2003    const areaRect = scrollEl.getBoundingClientRect();
2004    const anchorRect = anchor.getBoundingClientRect();
2005    const dropdownBottom = anchorRect.bottom + reserveBelow;
2006    let nextScrollTop = scrollEl.scrollTop;
2007
2008    if (dropdownBottom > areaRect.bottom - gap) {
2009        nextScrollTop += dropdownBottom - (areaRect.bottom - gap);
2010    }
2011
2012    const topPad = options.topPaddingPx ?? 16;
2013    const topTarget = anchorRect.top - areaRect.top + scrollEl.scrollTop - topPad;
2014    if (topTarget < nextScrollTop) {
2015        nextScrollTop = topTarget;
2016    }
2017
2018    const maxScroll = Math.max(0, scrollEl.scrollHeight - scrollEl.clientHeight);
2019    if (nextScrollTop > maxScroll) {
2020        const stack =
2021            scrollEl.querySelector('.cars-fe-form-stack') ||
2022            scrollEl.querySelector('.cars-fe-form-stack--employment') ||
2023            scrollEl;
2024        const extra = nextScrollTop - maxScroll + gap;
2025        const prev = parseFloat(stack.dataset.feScrollPadExtra || '0') || 0;
2026        const newPad = prev + extra;
2027        stack.dataset.feScrollPadExtra = String(newPad);
2028        stack.style.paddingBottom = `${newPad}px`;
2029        nextScrollTop = scrollEl.scrollHeight - scrollEl.clientHeight;
2030    }
2031
2032    scrollEl.scrollTo({
2033        top: Math.max(0, nextScrollTop),
2034        behavior,
2035    });
2036}
2037
2038function financeEligibilityScrollOccupationDropdownIntoView(queryInput, listPanel) {
2039    if (!queryInput) {
2040        return;
2041    }
2042    financeEligibilityScrollFieldIntoView(queryInput, {
2043        listPanelEl: listPanel,
2044        topPaddingPx: 8,
2045        gapPx: 16,
2046        behavior: 'smooth',
2047    });
2048}
2049
2050function financeEligibilityScheduleFieldScrollIntoView(targetEl, options = {}) {
2051    if (!targetEl) {
2052        return;
2053    }
2054    window.requestAnimationFrame(() => {
2055        financeEligibilityScrollFieldIntoView(targetEl, options);
2056        window.requestAnimationFrame(() => {
2057            financeEligibilityScrollFieldIntoView(targetEl, options);
2058        });
2059    });
2060}
2061
2062function financeEligibilityCloseOccupationPanels(rootEl) {
2063    const root = rootEl || document.getElementById('cars-fe-step-employment');
2064    if (!root) {
2065        return;
2066    }
2067    root.querySelectorAll('.cars-fe-occupation-combobox__panel').forEach((panel) => {
2068        if (panel.hidden) {
2069            return;
2070        }
2071        panel.hidden = true;
2072        const wrap = panel.closest('.cars-fe-occupation-combobox');
2073        const qInp = wrap?.querySelector('.cars-fe-occupation-combobox__input');
2074        if (qInp) {
2075            qInp.setAttribute('aria-expanded', 'false');
2076            qInp.removeAttribute('aria-activedescendant');
2077        }
2078        wrap?.querySelectorAll('.cars-fe-occupation-combobox__option.is-active').forEach((li) => li.classList.remove('is-active'));
2079    });
2080}
2081
2082/**
2083 * @param {{ hiddenId: string; queryId: string; listboxId: string }} config
2084 * @param {() => void} [onChange]
2085 */
2086function financeEligibilityAttachOccupationCombobox(config, onChange) {
2087    const hidden = document.getElementById(config.hiddenId);
2088    const query = document.getElementById(config.queryId);
2089    const listbox = document.getElementById(config.listboxId);
2090    const panel = query?.closest('.cars-fe-occupation-combobox')?.querySelector('.cars-fe-occupation-combobox__panel');
2091    if (!hidden || !query || !listbox || !panel || query.dataset.feOccInit === '1') {
2092        return;
2093    }
2094    query.dataset.feOccInit = '1';
2095
2096    let activeIdx = -1;
2097
2098    const notify = () => {
2099        if (typeof onChange === 'function') {
2100            onChange();
2101        }
2102    };
2103
2104    const setExpanded = (open) => {
2105        query.setAttribute('aria-expanded', open ? 'true' : 'false');
2106        panel.hidden = !open;
2107        financeEligibilitySetOccupationDropdownOpen(open);
2108        if (!open) {
2109            activeIdx = -1;
2110            query.removeAttribute('aria-activedescendant');
2111            listbox.querySelectorAll('.cars-fe-occupation-combobox__option.is-active').forEach((li) => li.classList.remove('is-active'));
2112        }
2113    };
2114
2115    const optionElements = () => [...listbox.querySelectorAll('li.cars-fe-occupation-combobox__option[role="option"]')];
2116
2117    const highlightActive = () => {
2118        const opts = optionElements();
2119        opts.forEach((li, i) => li.classList.toggle('is-active', i === activeIdx));
2120        if (activeIdx >= 0 && opts[activeIdx]) {
2121            query.setAttribute('aria-activedescendant', opts[activeIdx].id);
2122        } else {
2123            query.removeAttribute('aria-activedescendant');
2124        }
2125    };
2126
2127    const selectLabel = (label) => {
2128        hidden.value = label;
2129        query.value = label;
2130        setExpanded(false);
2131        notify();
2132    };
2133
2134    const tryCommitExactMatch = () => {
2135        const t = query.value.trim();
2136        if (!t) {
2137            hidden.value = '';
2138            notify();
2139            return;
2140        }
2141        const exact = FINANCE_ELIGIBILITY_OCCUPATIONS.find((o) => o.toLowerCase() === t.toLowerCase());
2142        if (exact) {
2143            hidden.value = exact;
2144            query.value = exact;
2145        } else {
2146            hidden.value = '';
2147        }
2148        notify();
2149    };
2150
2151    const renderList = (items) => {
2152        listbox.textContent = '';
2153        activeIdx = -1;
2154        if (items.length === 0) {
2155            const li = document.createElement('li');
2156            li.className = 'cars-fe-occupation-combobox__empty';
2157            li.setAttribute('role', 'presentation');
2158            li.textContent = 'No matches';
2159            listbox.appendChild(li);
2160            return;
2161        }
2162        items.forEach((label, i) => {
2163            const li = document.createElement('li');
2164            li.id = `${config.listboxId}-opt-${i}`;
2165            li.className = 'cars-fe-occupation-combobox__option';
2166            li.setAttribute('role', 'option');
2167            li.dataset.value = label;
2168            li.textContent = label;
2169            li.addEventListener('mousedown', (e) => {
2170                e.preventDefault();
2171                selectLabel(label);
2172            });
2173            listbox.appendChild(li);
2174        });
2175    };
2176
2177    const openAndRender = () => {
2178        const items = financeEligibilityFilterOccupations(query.value);
2179        renderList(items);
2180        setExpanded(true);
2181        window.requestAnimationFrame(() => {
2182            window.requestAnimationFrame(() => {
2183                financeEligibilityScrollOccupationDropdownIntoView(query, panel);
2184            });
2185        });
2186    };
2187
2188    query.addEventListener('input', () => {
2189        const locked = hidden.value;
2190        if (locked && query.value !== locked) {
2191            hidden.value = '';
2192        }
2193        openAndRender();
2194        notify();
2195    });
2196
2197    query.addEventListener('focus', () => {
2198        openAndRender();
2199    });
2200
2201    query.addEventListener('mousedown', () => {
2202        if (panel.hidden) {
2203            openAndRender();
2204        }
2205    });
2206
2207    query.addEventListener('blur', () => {
2208        window.setTimeout(() => {
2209            tryCommitExactMatch();
2210            setExpanded(false);
2211        }, 180);
2212    });
2213
2214    query.addEventListener('keydown', (e) => {
2215        const opts = optionElements();
2216        if (e.key === 'Escape') {
2217            e.preventDefault();
2218            setExpanded(false);
2219            return;
2220        }
2221        if (!panel.hidden && opts.length) {
2222            if (e.key === 'ArrowDown') {
2223                e.preventDefault();
2224                activeIdx = activeIdx < opts.length - 1 ? activeIdx + 1 : 0;
2225                highlightActive();
2226                opts[activeIdx]?.scrollIntoView({ block: 'nearest' });
2227            } else if (e.key === 'ArrowUp') {
2228                e.preventDefault();
2229                activeIdx = activeIdx > 0 ? activeIdx - 1 : opts.length - 1;
2230                highlightActive();
2231                opts[activeIdx]?.scrollIntoView({ block: 'nearest' });
2232            } else if (e.key === 'Enter') {
2233                if (activeIdx >= 0 && opts[activeIdx]) {
2234                    e.preventDefault();
2235                    selectLabel(opts[activeIdx].dataset.value || '');
2236                }
2237            }
2238        }
2239    });
2240}
2241
2242function financeEligibilityInitOccupationComboboxes(onChange) {
2243    [
2244        {
2245            hiddenId: 'cars-fe-employment-occupation',
2246            queryId: 'cars-fe-employment-occupation-q',
2247            listboxId: 'cars-fe-employment-occupation-listbox',
2248        },
2249    ].forEach((cfg) => financeEligibilityAttachOccupationCombobox(cfg, onChange));
2250}
2251
2252/** Months preselect 0 so users only change them for a part-year duration; years stays a required choice. */
2253const FINANCE_ELIGIBILITY_FIELD_DEFAULTS = {
2254    'cars-fe-address-months-at': '0',
2255    'cars-fe-employment-months': '0',
2256};
2257
2258function financeEligibilityFieldDefaultValue(fieldId) {
2259    return FINANCE_ELIGIBILITY_FIELD_DEFAULTS[fieldId] || '';
2260}
2261
2262function financeEligibilitySyncDurationListboxShell(shellOrField) {
2263    const shell =
2264        shellOrField?.classList?.contains('cars-fe-field-group__shell--duration')
2265            ? shellOrField
2266            : shellOrField?.closest?.('.cars-fe-field-group__shell--duration');
2267    if (!shell) {
2268        return;
2269    }
2270    const hidden = shell.querySelector('input[type="hidden"]');
2271    const display = shell.querySelector('.cars-fe-duration-listbox__display');
2272    const unitHint = shell.getAttribute('data-fe-duration-unit') || '';
2273    const raw = String(hidden?.value || '').trim();
2274    const hasValue = Boolean(raw);
2275    shell.classList.toggle('cars-fe-field-group__shell--duration-has-value', hasValue);
2276    if (display) {
2277        if (!hasValue) {
2278            display.textContent = unitHint;
2279        } else {
2280            const match = [...shell.querySelectorAll('.cars-fe-duration-listbox__option')].find(
2281                (opt) => opt.getAttribute('data-value') === raw,
2282            );
2283            display.textContent = match?.getAttribute('data-label') || match?.textContent || raw;
2284        }
2285    }
2286}
2287
2288function financeEligibilityCloseDurationListboxes(rootEl, exceptPanel) {
2289    const root = rootEl || document.getElementById('financeEligibilityOffcanvas');
2290    if (!root) {
2291        return;
2292    }
2293    root.querySelectorAll('.cars-fe-duration-listbox__panel').forEach((panel) => {
2294        if (panel.hidden || panel === exceptPanel) {
2295            return;
2296        }
2297        panel.hidden = true;
2298        const wrap = panel.closest('.cars-fe-duration-listbox');
2299        const trigger = wrap?.querySelector('.cars-fe-duration-listbox__trigger');
2300        if (trigger) {
2301            trigger.setAttribute('aria-expanded', 'false');
2302            trigger.removeAttribute('aria-activedescendant');
2303        }
2304        wrap?.querySelectorAll('.cars-fe-duration-listbox__option.is-active').forEach((li) => li.classList.remove('is-active'));
2305    });
2306    const anyOpen = [...root.querySelectorAll('.cars-fe-duration-listbox__panel')].some((p) => !p.hidden);
2307    const employmentHost =
2308        document.querySelector('.cars-fe-form-stack--employment')?.closest('.cars-fe-journey-step') ||
2309        document.getElementById('cars-fe-step-employment');
2310    const steps = [document.getElementById('cars-fe-step-address'), employmentHost].filter(Boolean);
2311    steps.forEach((step) => {
2312        if (rootEl && !root.contains(step)) {
2313            return;
2314        }
2315        if (anyOpen && step.contains(exceptPanel)) {
2316            return;
2317        }
2318        if (!anyOpen || !step.querySelector('.cars-fe-duration-listbox__panel:not([hidden])')) {
2319            step.classList.remove('cars-fe-duration-dropdown-open');
2320            const stack =
2321                step.querySelector('.cars-fe-form-stack--address') ||
2322                step.querySelector('.cars-fe-form-stack--employment') ||
2323                step.querySelector('.cars-fe-form-stack');
2324            if (stack) {
2325                delete stack.dataset.feScrollPadExtra;
2326                stack.style.removeProperty('padding-bottom');
2327            }
2328        }
2329    });
2330}
2331
2332function financeEligibilityResolveDurationStep(stepId) {
2333    if (stepId === 'cars-fe-step-employment') {
2334        return (
2335            document.querySelector('.cars-fe-form-stack--employment')?.closest('.cars-fe-journey-step') ||
2336            document.getElementById('cars-fe-step-employment')
2337        );
2338    }
2339    return document.getElementById(stepId);
2340}
2341
2342function financeEligibilitySetDurationDropdownOpen(stepId, open) {
2343    const step = financeEligibilityResolveDurationStep(stepId);
2344    if (!step) {
2345        return;
2346    }
2347    step.classList.toggle('cars-fe-duration-dropdown-open', Boolean(open));
2348    if (!open) {
2349        const stack =
2350            step.querySelector('.cars-fe-form-stack--address') ||
2351            step.querySelector('.cars-fe-form-stack--employment') ||
2352            step.querySelector('.cars-fe-form-stack');
2353        if (stack) {
2354            delete stack.dataset.feScrollPadExtra;
2355            stack.style.removeProperty('padding-bottom');
2356        }
2357    }
2358}
2359
2360/**
2361 * Custom years/months listbox (occupation-style panel, no search).
2362 * @param {{ hiddenId: string; listboxId: string; stepId: string }} config
2363 * @param {() => void} [onChange]
2364 */
2365function financeEligibilityAttachDurationListbox(config, onChange) {
2366    const hidden = document.getElementById(config.hiddenId);
2367    const listbox = document.getElementById(config.listboxId);
2368    const shell = hidden?.closest?.('.cars-fe-field-group__shell--duration');
2369    const wrap = shell?.querySelector('.cars-fe-duration-listbox');
2370    const trigger = wrap?.querySelector('.cars-fe-duration-listbox__trigger');
2371    const panel = wrap?.querySelector('.cars-fe-duration-listbox__panel');
2372    if (!hidden || !listbox || !trigger || !panel || hidden.dataset.feDurationInit === '1') {
2373        return;
2374    }
2375    hidden.dataset.feDurationInit = '1';
2376
2377    let activeIdx = -1;
2378
2379    const notify = () => {
2380        financeEligibilitySyncDurationListboxShell(shell);
2381        hidden.dispatchEvent(new Event('change', { bubbles: true }));
2382        if (typeof onChange === 'function') {
2383            onChange();
2384        }
2385    };
2386
2387    const optionElements = () => [...listbox.querySelectorAll('li.cars-fe-duration-listbox__option[role="option"]')];
2388
2389    const setExpanded = (open) => {
2390        if (open) {
2391            financeEligibilityCloseDurationListboxes(null, panel);
2392        }
2393        trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
2394        panel.hidden = !open;
2395        financeEligibilitySetDurationDropdownOpen(config.stepId, open);
2396        if (!open) {
2397            activeIdx = -1;
2398            trigger.removeAttribute('aria-activedescendant');
2399            optionElements().forEach((li) => li.classList.remove('is-active'));
2400        }
2401    };
2402
2403    const highlightActive = () => {
2404        const opts = optionElements();
2405        opts.forEach((li, i) => li.classList.toggle('is-active', i === activeIdx));
2406        if (activeIdx >= 0 && opts[activeIdx]) {
2407            trigger.setAttribute('aria-activedescendant', opts[activeIdx].id);
2408        } else {
2409            trigger.removeAttribute('aria-activedescendant');
2410        }
2411    };
2412
2413    const selectValue = (value) => {
2414        hidden.value = value == null ? '' : String(value);
2415        setExpanded(false);
2416        notify();
2417    };
2418
2419    optionElements().forEach((li) => {
2420        li.addEventListener('mousedown', (e) => {
2421            e.preventDefault();
2422            e.stopPropagation();
2423            selectValue(li.getAttribute('data-value') || li.textContent || '');
2424        });
2425    });
2426
2427    const openPanel = () => {
2428        if (trigger.disabled || shell.classList.contains('cars-fe-field-group__shell--duration-disabled')) {
2429            return;
2430        }
2431        setExpanded(true);
2432        // Single auto scroll after layout — nested smooth scrolls caused years/months flicker.
2433        window.requestAnimationFrame(() => {
2434            financeEligibilityScrollFieldIntoView(trigger, {
2435                listPanelEl: panel,
2436                topPaddingPx: 8,
2437                gapPx: 16,
2438                behavior: 'auto',
2439            });
2440        });
2441    };
2442
2443    trigger.addEventListener('mousedown', (e) => {
2444        // Prevent focus-driven scroll/close races that force a second click on desktop.
2445        e.preventDefault();
2446        e.stopPropagation();
2447        if (panel.hidden) {
2448            openPanel();
2449            trigger.focus({ preventScroll: true });
2450        } else {
2451            setExpanded(false);
2452        }
2453    });
2454
2455    trigger.addEventListener('click', (e) => {
2456        e.preventDefault();
2457        e.stopPropagation();
2458    });
2459
2460    trigger.addEventListener('keydown', (e) => {
2461        const opts = optionElements();
2462        if (e.key === 'Escape') {
2463            e.preventDefault();
2464            setExpanded(false);
2465            return;
2466        }
2467        if ((e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ') && panel.hidden) {
2468            e.preventDefault();
2469            openPanel();
2470            return;
2471        }
2472        if (!panel.hidden && opts.length) {
2473            if (e.key === 'ArrowDown') {
2474                e.preventDefault();
2475                activeIdx = activeIdx < opts.length - 1 ? activeIdx + 1 : 0;
2476                highlightActive();
2477                opts[activeIdx]?.scrollIntoView({ block: 'nearest' });
2478            } else if (e.key === 'ArrowUp') {
2479                e.preventDefault();
2480                activeIdx = activeIdx > 0 ? activeIdx - 1 : opts.length - 1;
2481                highlightActive();
2482                opts[activeIdx]?.scrollIntoView({ block: 'nearest' });
2483            } else if (e.key === 'Enter' || e.key === ' ') {
2484                e.preventDefault();
2485                if (activeIdx >= 0 && opts[activeIdx]) {
2486                    selectValue(opts[activeIdx].getAttribute('data-value') || opts[activeIdx].textContent || '');
2487                }
2488            }
2489        }
2490    });
2491
2492    financeEligibilitySyncDurationListboxShell(shell);
2493}
2494
2495function financeEligibilityInitDurationListboxes(onChange) {
2496    [
2497        {
2498            hiddenId: 'cars-fe-address-years-at',
2499            listboxId: 'cars-fe-address-years-at-listbox',
2500            stepId: 'cars-fe-step-address',
2501        },
2502        {
2503            hiddenId: 'cars-fe-address-months-at',
2504            listboxId: 'cars-fe-address-months-at-listbox',
2505            stepId: 'cars-fe-step-address',
2506        },
2507        {
2508            hiddenId: 'cars-fe-employment-years',
2509            listboxId: 'cars-fe-employment-years-listbox',
2510            stepId: 'cars-fe-step-employment',
2511        },
2512        {
2513            hiddenId: 'cars-fe-employment-months',
2514            listboxId: 'cars-fe-employment-months-listbox',
2515            stepId: 'cars-fe-step-employment',
2516        },
2517    ].forEach((cfg) => financeEligibilityAttachDurationListbox(cfg, onChange));
2518}
2519
2520function setupCarsFinanceEligibilityPanel() {
2521    const panel = document.getElementById('financeEligibilityOffcanvas');
2522    if (!panel) {
2523        return;
2524    }
2525
2526    /** Ivendi funder codes → legal entity names for review-step consent copy. */
2527    const FUNDER_CODE_LENDER_NAMES = Object.freeze({
2528        BLA: 'Black Horse Limited',
2529        STL: 'Startline Motor Finance',
2530        MOT: 'MotoNovo Finance',
2531        FIA: 'CA Auto Finance UK Ltd',
2532    });
2533
2534    const resolveFinanceEligibilityLenderDisplayName = (funderCode, productName) => {
2535        const code = String(funderCode || '').trim().toUpperCase();
2536        if (code && FUNDER_CODE_LENDER_NAMES[code]) {
2537            return FUNDER_CODE_LENDER_NAMES[code];
2538        }
2539        const product = String(productName || '').trim();
2540        if (product && /black\s*horse/i.test(product)) {
2541            return 'Black Horse Limited';
2542        }
2543        if (code) {
2544            return code;
2545        }
2546        return 'Black Horse Limited';
2547    };
2548
2549    const getActiveVdpQuoteLenderMetaFromTab = (tab) => {
2550        if (!tab) {
2551            return null;
2552        }
2553        const quoteKey =
2554            String(tab.getAttribute('currentTab') || 'HP').trim().toUpperCase() === 'PCP' ? 'pcp' : 'hp';
2555        return {
2556            funderCode: tab.getAttribute(`data-${quoteKey}-funder-code`) || '',
2557            productName: tab.getAttribute(`data-${quoteKey}-product-name`) || '',
2558        };
2559    };
2560
2561    const readFinancePageQuoteLenderMeta = () => {
2562        const raw = panel.getAttribute('data-fe-finance-page-quote');
2563        if (!raw) {
2564            return null;
2565        }
2566        try {
2567            const snap = JSON.parse(raw);
2568            return {
2569                funderCode: snap.funderCode || '',
2570                productName: snap.productName || '',
2571            };
2572        } catch {
2573            return null;
2574        }
2575    };
2576
2577    const syncReviewConsentLenderName = () => {
2578        const lenderEl = document.getElementById('cars-fe-review-consent-lender-name');
2579        if (!lenderEl) {
2580            return;
2581        }
2582        const financeMeta = readFinancePageQuoteLenderMeta();
2583        const vdpMeta = getActiveVdpQuoteLenderMetaFromTab(document.querySelector('.tab-content-container'));
2584        let meta = vdpMeta?.funderCode || vdpMeta?.productName ? vdpMeta : financeMeta;
2585        if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true' && (financeMeta?.funderCode || financeMeta?.productName)) {
2586            meta = financeMeta;
2587        }
2588        lenderEl.textContent = resolveFinanceEligibilityLenderDisplayName(
2589            meta?.funderCode,
2590            meta?.productName,
2591        );
2592    };
2593
2594    /** Quote UIDs for finance scan: full list from get-finance-quote, else PCP + HP on the tab. */
2595    const getVdpFinanceQuoteIdsFromTabContainer = (tabEl) => {
2596        if (!tabEl) {
2597            return [];
2598        }
2599        try {
2600            const raw = tabEl.getAttribute('data-fe-eligibility-quote-ids');
2601            if (raw) {
2602                const parsed = JSON.parse(raw);
2603                if (Array.isArray(parsed)) {
2604                    const fromApi = parsed
2605                        .filter((x) => typeof x === 'string' && x.trim().length > 0)
2606                        .map((x) => x.trim());
2607                    if (fromApi.length > 0) {
2608                        return fromApi;
2609                    }
2610                }
2611            }
2612        } catch {
2613            /* ignore */
2614        }
2615        const ids = [];
2616        ['pcpQuoteUID', 'hpQuoteUID'].forEach((attr) => {
2617            const v = tabEl.getAttribute(attr);
2618            if (v && String(v).trim().length > 0) {
2619                const t = String(v).trim();
2620                if (!ids.includes(t)) {
2621                    ids.push(t);
2622                }
2623            }
2624        });
2625        return ids;
2626    };
2627
2628    /** When “Check eligibility” opens the offcanvas, copy live quote UIDs, term, and dealer from the VDP finance calculator. */
2629    const syncFinanceEligibilityPanelIvendiFromVdp = () => {
2630        // Do not overwrite quote IDs/term bound from a listing/special-offer CTA for another stock.
2631        if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true') {
2632            syncReviewConsentLenderName();
2633            return;
2634        }
2635        const tab = document.querySelector('.tab-content-container');
2636        if (tab) {
2637            const quoteIds = getVdpFinanceQuoteIdsFromTabContainer(tab);
2638            if (quoteIds.length > 0) {
2639                panel.setAttribute('data-fe-finance-eligibility-quote-ids', JSON.stringify(quoteIds));
2640            } else {
2641                panel.removeAttribute('data-fe-finance-eligibility-quote-ids');
2642            }
2643            const currentTab = tab.getAttribute('currentTab');
2644            const termAttr =
2645                currentTab === 'PCP' ? tab.getAttribute('data-pcp-term') : tab.getAttribute('data-hp-term');
2646            const termN = termAttr != null ? parseInt(termAttr, 10) : NaN;
2647            if (!Number.isNaN(termN) && termN > 0) {
2648                panel.setAttribute('data-fe-finance-term', String(termN));
2649            }
2650        }
2651        syncReviewConsentLenderName();
2652    };
2653
2654    const syncFinanceDurationSelectShell = (fieldEl) => {
2655        financeEligibilitySyncDurationListboxShell(fieldEl);
2656    };
2657
2658    const syncAllFinanceDurationSelectShells = () => {
2659        panel.querySelectorAll('.cars-fe-field-group__shell--duration').forEach(financeEligibilitySyncDurationListboxShell);
2660    };
2661
2662    const intro = document.getElementById('finance-eligibility-step-intro');
2663    const agreementView = document.getElementById('finance-eligibility-step-agreement');
2664    const detailsStep = document.getElementById('cars-fe-step-details');
2665    const contactStep = document.getElementById('cars-fe-step-contact');
2666    const moreStep = document.getElementById('cars-fe-step-more');
2667    const personalGroupedStep = document.getElementById('cars-fe-step-personal-grouped');
2668    const addressStep = document.getElementById('cars-fe-step-address');
2669    const employmentStep = document.getElementById('cars-fe-step-employment');
2670    const incomeOutgoingsStep = document.getElementById('cars-fe-step-income-outgoings');
2671    const outgoingsFinalStep = document.getElementById('cars-fe-step-outgoings-final');
2672    const incomeOutgoingsGroupedStep = document.getElementById('cars-fe-step-income-outgoings-grouped');
2673    const reviewStep = document.getElementById('cars-fe-step-review');
2674    const nextBtn = document.getElementById('cars-fe-details-next-button');
2675    const contactNextBtn = document.getElementById('cars-fe-contact-next-button');
2676    const moreNextBtn = document.getElementById('cars-fe-more-next-button');
2677    const personalGroupedNextBtn = document.getElementById('cars-fe-personal-grouped-next-button');
2678    const addressNextBtn = document.getElementById('cars-fe-address-next-button');
2679    const employmentNextBtn = document.getElementById('cars-fe-employment-next-button');
2680    const incomeNextBtn = document.getElementById('cars-fe-income-next-button');
2681    const outgoingsSummaryBtn = document.getElementById('cars-fe-outgoings-final-summary-button');
2682    const incomeOutgoingsGroupedSummaryBtn = document.getElementById(
2683        'cars-fe-income-outgoings-grouped-summary-button',
2684    );
2685    const reviewViewResultsBtn = document.getElementById('cars-fe-review-view-results-button');
2686    const reviewConsentCb = document.getElementById('cars-fe-review-consent-checkbox');
2687    const resultsModal = document.getElementById('cars-fe-results-modal');
2688    const resultsCardsMount = document.getElementById('cars-fe-results-cards-mount');
2689    const resultsModalStatus = document.getElementById('cars-fe-results-modal-status');
2690    const resultsSortSelect = document.getElementById('cars-fe-results-sort');
2691
2692    let feFormDensityComposed = false;
2693
2694    const isGroupedFormActive = () =>
2695        typeof window.isFinanceEligibilityGroupedFormVariantActive === 'function' &&
2696        window.isFinanceEligibilityGroupedFormVariantActive();
2697
2698    const isControlMicroStepScreen = () =>
2699        state.screen === 'details' ||
2700        state.screen === 'contact' ||
2701        state.screen === 'more' ||
2702        state.screen === 'address' ||
2703        state.screen === 'employment' ||
2704        state.screen === 'incomeOutgoings' ||
2705        state.screen === 'outgoingsFinal' ||
2706        state.screen === 'review';
2707
2708    const isGroupedJourneyScreen = () =>
2709        state.screen === 'personalGrouped' || state.screen === 'incomeOutgoingsGrouped';
2710
2711    const getEmploymentUiHost = () => {
2712        if (feFormDensityComposed && isGroupedFormActive() && incomeOutgoingsGroupedStep) {
2713            return incomeOutgoingsGroupedStep;
2714        }
2715        return employmentStep;
2716    };
2717
2718    const fieldGroupForId = (id) => document.getElementById(id)?.closest('.cars-fe-field-group');
2719
2720    const moveEl = (el, parent) => {
2721        if (el && parent) {
2722            parent.appendChild(el);
2723        }
2724    };
2725
2726    const syncIntroStartButtonLabel = () => {
2727        if (!panel) {
2728            return;
2729        }
2730        const grouped = panel.dataset.feFormDensity === 'grouped';
2731        const applyLabel = (el, groupedAttr, controlAttr) => {
2732            if (!el) {
2733                return;
2734            }
2735            const label = el.getAttribute(grouped ? groupedAttr : controlAttr);
2736            if (label) {
2737                el.textContent = label;
2738            }
2739        };
2740
2741        applyLabel(
2742            document.getElementById('finance-eligibility-start-button'),
2743            'data-fe-cta-grouped',
2744            'data-fe-cta-control',
2745        );
2746        // Results screen copy (Figma 40003936:37009 for grouped). Only the success line varies by
2747        // variant: while the results screen shows its empty or error state, syncResultsModalEmptyState
2748        // owns this element, and rewriting it here would replace "No finance products matched" or the
2749        // error copy with the success copy.
2750        if (!document.getElementById('cars-fe-results-modal-empty')) {
2751            applyLabel(
2752                panel.querySelector('.cars-fe-results-modal__intro-sub'),
2753                'data-fe-results-sub-grouped',
2754                'data-fe-results-sub-control',
2755            );
2756        }
2757        applyLabel(
2758            document.getElementById('cars-fe-results-book-appointment'),
2759            'data-fe-results-cta-grouped',
2760            'data-fe-results-cta-control',
2761        );
2762    };
2763
2764    /**
2765     * Every side-panel variant names the destination on the CTA that ends a section, so those
2766     * footers spell the next section out instead of reading "Next". The steps inside a section
2767     * (employment, income and outgoings) keep the plain "Next" from the markup.
2768     */
2769    const FE_SECTION_END_CTA_LABELS = Object.freeze({
2770        'cars-fe-more-next-button': 'Continue to Address Details',
2771        'cars-fe-personal-grouped-next-button': 'Continue to Address Details',
2772        'cars-fe-address-next-button': 'Continue to Income and Outgoings',
2773    });
2774
2775    const FE_SECTION_END_CTA_VARIANTS = Object.freeze(['test-a', 'test-b', 'test-c']);
2776
2777    const syncSectionEndCtaLabels = () => {
2778        const variant = String(window.__checkEligibilityJourneyVariant || '').trim().toLowerCase();
2779        const useSectionCopy = FE_SECTION_END_CTA_VARIANTS.includes(variant);
2780        Object.keys(FE_SECTION_END_CTA_LABELS).forEach((id) => {
2781            const btn = document.getElementById(id);
2782            if (!btn) {
2783                return;
2784            }
2785            btn.textContent = useSectionCopy ? FE_SECTION_END_CTA_LABELS[id] : 'Next';
2786        });
2787    };
2788
2789    const restoreControlFormDomIfComposed = () => {
2790        if (!feFormDensityComposed) {
2791            return false;
2792        }
2793
2794        const detailsStack = detailsStep?.querySelector('.cars-fe-scroll > .cars-fe-form-stack');
2795        const contactStack = contactStep?.querySelector('.cars-fe-form-stack--contact');
2796        const moreStack = moreStep?.querySelector('.cars-fe-form-stack--more');
2797        const employmentScroll = employmentStep?.querySelector('.cars-fe-scroll');
2798        const incomeScroll = incomeOutgoingsStep?.querySelector('.cars-fe-scroll');
2799        const outgoingsScroll = outgoingsFinalStep?.querySelector('.cars-fe-scroll');
2800
2801        moveEl(fieldGroupForId('cars-fe-title'), detailsStack);
2802        moveEl(fieldGroupForId('cars-fe-first-name'), detailsStack);
2803        moveEl(fieldGroupForId('cars-fe-last-name'), detailsStack);
2804        moveEl(fieldGroupForId('cars-fe-dob'), detailsStack);
2805        moveEl(fieldGroupForId('cars-fe-marital'), detailsStack);
2806        moveEl(fieldGroupForId('cars-fe-email'), contactStack);
2807        moveEl(fieldGroupForId('cars-fe-phone-national'), contactStack);
2808        moveEl(fieldGroupForId('cars-fe-license-type'), moreStack);
2809        moveEl(fieldGroupForId('cars-fe-dependants'), moreStack);
2810
2811        const employmentStack = document.querySelector('.cars-fe-form-stack--employment');
2812        const incomeStack = document.querySelector('.cars-fe-form-stack--income-outgoings');
2813        const outgoingsStack = document.querySelector('.cars-fe-form-stack--outgoings-final');
2814        moveEl(employmentStack, employmentScroll);
2815        moveEl(incomeStack, incomeScroll);
2816        moveEl(outgoingsStack, outgoingsScroll);
2817
2818        const emailInput = document.getElementById('cars-fe-email');
2819        if (emailInput) {
2820            emailInput.placeholder = '[email protected]';
2821        }
2822        const licenseLabel = fieldGroupForId('cars-fe-license-type')?.querySelector(
2823            '.cars-fe-field-group__label-row .cars-fe-field-group__label, .cars-fe-field-group__label',
2824        );
2825        if (licenseLabel) {
2826            licenseLabel.textContent = 'Driving License Type';
2827        }
2828
2829        incomeOutgoingsGroupedStep?.classList.remove(
2830            'cars-fe-employment--has-saved-summary',
2831            'cars-fe-employment--entry-flow-visible',
2832            'cars-fe-occupation-dropdown-open',
2833            'cars-fe-duration-dropdown-open',
2834        );
2835        personalGroupedStep?.classList.remove(
2836            'cars-fe-occupation-dropdown-open',
2837            'cars-fe-duration-dropdown-open',
2838        );
2839
2840        feFormDensityComposed = false;
2841        return true;
2842    };
2843
2844    const ensureGroupedFormDomComposed = () => {
2845        if (feFormDensityComposed || !isGroupedFormActive()) {
2846            return feFormDensityComposed;
2847        }
2848        const personalMount = document.getElementById('cars-fe-personal-grouped-mount');
2849        const employmentMount = document.getElementById('cars-fe-income-outgoings-grouped-employment-mount');
2850        const incomeMount = document.getElementById('cars-fe-income-outgoings-grouped-income-mount');
2851        const outgoingsMount = document.getElementById('cars-fe-income-outgoings-grouped-outgoings-mount');
2852        if (!personalMount || !employmentMount || !incomeMount || !outgoingsMount) {
2853            return false;
2854        }
2855
2856        // Figma About you order: title, names, DOB, email, phone, marital, licence, dependants.
2857        moveEl(fieldGroupForId('cars-fe-title'), personalMount);
2858        moveEl(fieldGroupForId('cars-fe-first-name'), personalMount);
2859        moveEl(fieldGroupForId('cars-fe-last-name'), personalMount);
2860        moveEl(fieldGroupForId('cars-fe-dob'), personalMount);
2861        moveEl(fieldGroupForId('cars-fe-email'), personalMount);
2862        moveEl(fieldGroupForId('cars-fe-phone-national'), personalMount);
2863        moveEl(fieldGroupForId('cars-fe-marital'), personalMount);
2864        moveEl(fieldGroupForId('cars-fe-license-type'), personalMount);
2865        moveEl(fieldGroupForId('cars-fe-dependants'), personalMount);
2866
2867        const emailInput = document.getElementById('cars-fe-email');
2868        if (emailInput) {
2869            emailInput.placeholder = 'Email';
2870        }
2871        const licenseLabel = fieldGroupForId('cars-fe-license-type')?.querySelector(
2872            '.cars-fe-field-group__label-row .cars-fe-field-group__label, .cars-fe-field-group__label',
2873        );
2874        if (licenseLabel) {
2875            licenseLabel.textContent = 'Driving Licence Type';
2876        }
2877
2878        // Figma combined screen: Employment + About your income + Outgoings.
2879        const employmentStack = employmentStep?.querySelector('.cars-fe-form-stack--employment');
2880        const incomeStack = incomeOutgoingsStep?.querySelector('.cars-fe-form-stack--income-outgoings');
2881        const outgoingsStack = outgoingsFinalStep?.querySelector('.cars-fe-form-stack--outgoings-final');
2882        moveEl(employmentStack, employmentMount);
2883        moveEl(incomeStack, incomeMount);
2884        moveEl(outgoingsStack, outgoingsMount);
2885
2886        if (panel) {
2887            panel.dataset.feFormDensity = 'grouped';
2888        }
2889        feFormDensityComposed = true;
2890        return true;
2891    };
2892
2893    /** Journey variant on the panel so intro markup can be selected in CSS (test-b intro is its own layout). */
2894    const syncJourneyVariantAttribute = () => {
2895        if (!panel) {
2896            return;
2897        }
2898        const variant = String(window.__checkEligibilityJourneyVariant || 'control').trim().toLowerCase();
2899        panel.dataset.feJourneyVariant = variant === '' ? 'control' : variant;
2900    };
2901
2902    const applyFormDensityVariant = () => {
2903        if (!panel) {
2904            return;
2905        }
2906
2907        syncJourneyVariantAttribute();
2908        syncSectionEndCtaLabels();
2909
2910        // Never reparent fields while the user is mid-control micro-steps (late PostHog resolve).
2911        if (isControlMicroStepScreen()) {
2912            return;
2913        }
2914
2915        // Stay on grouped DOM for the rest of this open session if already in a grouped step.
2916        if (isGroupedJourneyScreen()) {
2917            panel.dataset.feFormDensity = 'grouped';
2918            syncIntroStartButtonLabel();
2919            return;
2920        }
2921
2922        if (isGroupedFormActive()) {
2923            ensureGroupedFormDomComposed();
2924            panel.dataset.feFormDensity = 'grouped';
2925        } else {
2926            restoreControlFormDomIfComposed();
2927            panel.dataset.feFormDensity = 'control';
2928        }
2929
2930        syncIntroStartButtonLabel();
2931    };
2932
2933    /**
2934     * Routing has to follow the DOM, not the flag alone. When PostHog resolves to test-c after the
2935     * user has already started the control micro-steps, applyFormDensityVariant deliberately
2936     * refuses to reparent the fields, so the grouped steps hold nothing. Routing on the flag then
2937     * hid every step and showed an empty panel, or made Next a no-op. Ask for c
2937omposition first and
2938     * only take the grouped route once it has actually happened; otherwise stay on the control
2939     * steps, which is the DOM the user is already looking at.
2940     */
2941    const canRouteToGroupedSteps = (stepEl) => {
2942        if (!isGroupedFormActive()) {
2943            return false;
2944        }
2945        applyFormDensityVariant();
2946        return feFormDensityComposed && Boolean(stepEl);
2947    };
2948
2949    let feScanPollGeneration = 0;
2950    let feScanPollRetryContext = null;
2951
2952    const setFinanceScanLoadingRetryVisible = (visible) => {
2953        const btn = document.getElementById('cars-fe-results-loading-retry');
2954        if (!btn) {
2955            return;
2956        }
2957        btn.hidden = !visible;
2958        btn.setAttribute('aria-hidden', visible ? 'false' : 'true');
2959    };
2960
2961    const FE_ELIGIBILITY_CAT_CLASSNAMES = [
2962        'cars-fe-eligibility-cat--pre-approved',
2963        'cars-fe-eligibility-cat--high',
2964        'cars-fe-eligibility-cat--amber',
2965        'cars-fe-eligibility-cat--low',
2966    ];
2967
2968    const FE_ELIGIBILITY_UNKNOWN_LABEL = 'Eligibility unknown';
2969
2970    /** CSS class keys only — badge text always comes from EligibilityOffered (or unknown). */
2971    const FE_ELIGIBILITY_CATEGORY_META = {
2972        'pre-approved': { className: 'cars-fe-eligibility-cat--pre-approved' },
2973        high: { className: 'cars-fe-eligibility-cat--high' },
2974        'alternative-offer': { className: 'cars-fe-eligibility-cat--amber' },
2975        medium: { className: 'cars-fe-eligibility-cat--amber' },
2976        low: { className: 'cars-fe-eligibility-cat--low' },
2977        unable: { className: 'cars-fe-eligibility-cat--low' },
2978        unknown: { className: 'cars-fe-eligibility-cat--low' },
2979    };
2980
2981    const getFeEligibilityOfferedRaw = (quote) => {
2982        if (!quote || typeof quote !== 'object') {
2983            return '';
2984        }
2985        const raw =
2986            quote.EligibilityOffered ??
2987            quote.eligibilityOffered ??
2988            quote?.LenderData?.Eligibility ??
2989            quote?.lenderData?.eligibility;
2990        if (raw == null) {
2991            return '';
2992        }
2993        return String(raw).trim();
2994    };
2995
2996    const FE_ELIGIBILITY_BADGE_LABELS = {
2997        unknown: FE_ELIGIBILITY_UNKNOWN_LABEL,
2998        eligibilityunknown: FE_ELIGIBILITY_UNKNOWN_LABEL,
2999        preapproved: 'Pre-Approved',
3000        preapprovedeligibility: 'Pre-Approved',
3001        alternativeoffer: 'Alternative Offer',
3002        alternative: 'Alternative Offer',
3003        alternativeoffers: 'Alternative Offer',
3004        unabletoassist: 'Unable to assist',
3005        unable: 'Unable to assist',
3006        high: 'High likelihood of approval',
3007        higheligibility: 'High likelihood of approval',
3008        highlikelihoodofapproval: 'High likelihood of approval',
3009        medium: 'Medium likelihood of approval',
3010        mediumeligibility: 'Medium likelihood of approval',
3011        low: 'Low likelihood of approval',
3012        loweligibility: 'Low likelihood of approval',
3013    };
3014
3015    const compactEligibilityOfferedValue = (raw) =>
3016        String(raw)
3017            .trim()
3018            .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
3019            .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
3020            .replace(/[\s_-]+/g, '')
3021            .toLowerCase();
3022
3023    /** Badge text from EligibilityOffered; null/Unknown → "Eligibility unknown". */
3024    const getFeEligibilityBadgeLabel = (quote) => {
3025        const raw = getFeEligibilityOfferedRaw(quote);
3026        if (!raw) {
3027            return FE_ELIGIBILITY_UNKNOWN_LABEL;
3028        }
3029        const compact = compactEligibilityOfferedValue(raw);
3030        if (Object.prototype.hasOwnProperty.call(FE_ELIGIBILITY_BADGE_LABELS, compact)) {
3031            return FE_ELIGIBILITY_BADGE_LABELS[compact];
3032        }
3033        return raw;
3034    };
3035
3036    const resolveFeEligibilityCategoryKey = (quote) => {
3037        const original = getFeEligibilityOfferedRaw(quote);
3038        if (!original) {
3039            return 'unknown';
3040        }
3041        // Style hint only — maps API values like UnableToAssist onto colour classes.
3042        const slug = original
3043            .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
3044            .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
3045            .replace(/[\s_]+/g, '-')
3046            .toLowerCase();
3047        const compact = slug.replace(/-/g, '');
3048        const aliases = {
3049            'pre-approved': 'pre-approved',
3050            preapproved: 'pre-approved',
3051            '1': 'pre-approved',
3052            'high-likelihood-of-approval': 'high',
3053            'high-likelihood': 'high',
3054            'high-eligibility': 'high',
3055            high: 'high',
3056            '2': 'high',
3057            'alternative-offer': 'alternative-offer',
3058            alternativeoffer: 'alternative-offer',
3059            alternative: 'alternative-offer',
3060            '3': 'alternative-offer',
3061            'medium-eligibility': 'medium',
3062            medium: 'medium',
3063            '4': 'medium',
3064            'low-eligibility': 'low',
3065            low: 'low',
3066            '5': 'low',
3067            'unable-to-assist': 'unable',
3068            unabletoassist: 'unable',
3069            unable: 'unable',
3070            'not-eligible': 'unable',
3071            noteligible: 'unable',
3072            '6': 'unable',
3073        };
3074        if (aliases[slug]) {
3075            return aliases[slug];
3076        }
3077        if (aliases[compact]) {
3078            return aliases[compact];
3079        }
3080        if (/^pre[-\s]?approved$/i.test(slug) || compact === 'preapproved') {
3081            return 'pre-approved';
3082        }
3083        if (/alternative/i.test(slug) && /offer/i.test(slug)) {
3084            return 'alternative-offer';
3085        }
3086        if (/unable/i.test(slug) || /not-?eligible/i.test(slug)) {
3087            return 'unable';
3088        }
3089        if (/medium/i.test(slug)) {
3090            return 'medium';
3091        }
3092        if (slug === 'low' || /^low(\s|-|$)/i.test(slug)) {
3093            return 'low';
3094        }
3095        if (/high/i.test(slug) && /(likelihood|approval|eligibility)/i.test(slug)) {
3096            return 'high';
3097        }
3098        return 'unknown';
3099    };
3100
3101    const syncFeEligibilityCategoryPillClass = (el, categoryKey) => {
3102        if (!el) {
3103            return;
3104        }
3105        FE_ELIGIBILITY_CAT_CLASSNAMES.forEach((c) => el.classList.remove(c));
3106        const cn = FE_ELIGIBILITY_CATEGORY_META[categoryKey]?.className;
3107        if (cn) {
3108            el.classList.add(cn);
3109        }
3110    };
3111
3112    const state = { screen: 'intro', dirty: false };
3113
3114    /**
3115     * URL segments aligned to iVendi Online Finance Navigator funnel labels
3116     * (Landing → Personal details → Address → Income/outgoings → Summary → Consent → Submit → Form submitted → Results).
3117     */
3118    const getFinanceEligibilityJourneyUrlSegment = () => {
3119        if (resultsModal && resultsModal.hidden === false) {
3120            const loadingLayer = document.getElementById('cars-fe-results-modal-loading');
3121            if (loadingLayer && loadingLayer.hidden === false) {
3122                return 'form-submitted';
3123            }
3124            return 'results-page';
3125        }
3126        if (state.screen === 'intro' || state.screen === 'agreement') {
3127            return 'landing';
3128        }
3129        if (state.screen === 'details' || state.screen === 'contact' || state.screen === 'more' || state.screen === 'personalGrouped') {
3130            return 'personal-details';
3131        }
3132        if (state.screen === 'address') {
3133            return 'address-details';
3134        }
3135        if (
3136            state.screen === 'employment' ||
3137            state.screen === 'incomeOutgoings' ||
3138            state.screen === 'outgoingsFinal' ||
3139            state.screen === 'incomeOutgoingsGrouped'
3140        ) {
3141            return 'income-outgoings';
3142        }
3143        if (state.screen === 'review') {
3144            const consentOn = Boolean(reviewConsentCb && reviewConsentCb.checked);
3145            return consentOn ? 'consent' : 'summary';
3146        }
3147        return 'landing';
3148    };
3149
3150    const clearFinanceEligibilityJourneyUrl = () => {
3151        if (typeof history === 'undefined' || !history.replaceState) {
3152            return;
3153        }
3154        const h = window.location.hash || '';
3155        if (!h.startsWith('#finance-check=')) {
3156            return;
3157        }
3158        history.replaceState(null, '', `${window.location.pathname}${window.location.search}`);
3159    };
3160
3161    const syncFinanceEligibilityJourneyUrl = () => {
3162        if (typeof history === 'undefined' || !history.replaceState || !panel) {
3163            return;
3164        }
3165        if (!panel.classList.contains('show')) {
3166            return;
3167        }
3168        const segment = getFinanceEligibilityJourneyUrlSegment();
3169        const base = `${window.location.pathname}${window.location.search}`;
3170        const nextUrl = `${base}#finance-check=${encodeURIComponent(segment)}`;
3171        const current = `${base}${window.location.hash || ''}`;
3172        if (current !== nextUrl) {
3173            history.replaceState(null, '', nextUrl);
3174        }
3175    };
3176
3177    let addressLookupTimer = null;
3178    let savedAddressEntries = [];
3179    let editingEntryId = null;
3180    let editingInsertIndex = null;
3181    let savedEmploymentEntries = [];
3182    let employmentFormMode = 'editing';
3183    let editingEmploymentId = null;
3184    let editingEmploymentIndex = null;
3185    let eligibilityQuotesCache = [];
3186    /** When true, completing the current section (Next/Save) returns to Review instead of advancing the journey. */
3187    let reviewEditReturnPending = false;
3188    /** Removes visualViewport listeners + background scroll lock from show.bs.offcanvas. */
3189    let financeEligibilityViewportLockCleanup = null;
3190    let financeEligibilityLockedScrollY = 0;
3191
3192    const clearReviewEditReturnPending = () => {
3193        reviewEditReturnPending = false;
3194    };
3195
3196    const val = (id) => (document.getElementById(id)?.value || '').trim();
3197
3198    const scrollToTop = (rootEl) => {
3199        if (!rootEl) {
3200            return;
3201        }
3202        const scrollArea = rootEl.querySelector(
3203            '.finance-eligibility-intro__scroll, .finance-eligibility-agreement__scroll, .cars-fe-scroll',
3204        );
3205        if (scrollArea) {
3206            scrollArea.scrollTop = 0;
3207        }
3208    };
3209
3210    const setBtnNextDisabled = (btn, disabled) => {
3211        if (!btn) {
3212            return;
3213        }
3214        if (disabled) {
3215            btn.classList.add('cars-fe-next--disabled');
3216            btn.setAttribute('aria-disabled', 'true');
3217        } else {
3218            btn.classList.remove('cars-fe-next--disabled');
3219            btn.removeAttribute('aria-disabled');
3220        }
3221    };
3222
3223    const feStepHighlightRequested = {
3224        details: false,
3225        contact: false,
3226        more: false,
3227        address: false,
3228        employment: false,
3229        income: false,
3230        outgoings: false,
3231    };
3232
3233    /**
3234     * Fields the user has finished with (focusout). A field surfaces its own error once it is
3235     * touched, so problems are visible before Next is pressed — but a field the user is still
3236     * part-way through typing stays unflagged.
3237     */
3238    const feTouchedFields = new Set();
3239
3240    /**
3241     * The id of the field the user is typing in right now, for the fields whose errors would
3242     * otherwise fire mid-entry: a part-typed email, and a date of birth whose year is still being
3243     * filled in. Tracked explicitly rather than read from document.activeElement, which during
3244     * focusout can still report the field being left and so holds its error back a beat too long.
3245     */
3246    let feFieldBeingEdited = null;
3247
3248    const feMarkFieldTouched = (fieldId) => {
3249        if (!fieldId || feTouchedFields.has(fieldId)) {
3250            return false;
3251        }
3252        feTouchedFields.add(fieldId);
3253        return true;
3254    };
3255
3256    const feForgetTouchedFields = (stepEl) => {
3257        feFieldBeingEdited = null;
3258        if (!stepEl) {
3259            feTouchedFields.clear();
3260            return;
3261        }
3262        stepEl.querySelectorAll('[id]').forEach((el) => feTouchedFields.delete(el.id));
3263    };
3264
3265    const feFieldHighlightAllowed = (stepKey, fieldId) =>
3266        feStepHighlightRequested[stepKey] === true || feTouchedFields.has(fieldId);
3267
3268    /** Marks a field invalid only once Next was pressed for its step, or the user has left that field. */
3269    const feStepFieldMarker = (stepKey) => (fieldId, invalid) =>
3270        financeEligibilityMarkFieldInvalid(fieldId, Boolean(invalid) && feFieldHighlightAllowed(stepKey, fieldId));
3271
3272    const financeEligibilityClearStepInvalidMarks = (stepEl) => {
3273        if (!stepEl) {
3274            return;
3275        }
3276        stepEl.querySelectorAll('.cars-fe-field-group--invalid').forEach((group) => {
3277            group.classList.remove('cars-fe-field-group--invalid');
3278        });
3279        const phoneErr = stepEl.querySelector('#cars-fe-phone-format-error');
3280        if (phoneErr) {
3281            phoneErr.hidden = true;
3282            phoneErr.setAttribute('aria-hidden', 'true');
3283        }
3284        const manualPcErr = stepEl.querySelector('#cars-fe-address-postcode-manual-error');
3285        if (manualPcErr) {
3286            manualPcErr.hidden = true;
3287            manualPcErr.setAttribute('aria-hidden', 'true');
3288        }
3289    };
3290
3291    const resetFeStepFieldHighlights = (stepEl, stepKey) => {
3292        if (stepKey && Object.prototype.hasOwnProperty.call(feStepHighlightRequested, stepKey)) {
3293            feStepHighlightRequested[stepKey] = false;
3294        }
3295        feForgetTouchedFields(stepEl);
3296        financeEligibilityClearStepInvalidMarks(stepEl);
3297    };
3298
3299    const financeEligibilityMarkFieldInvalid = (fieldId, invalid) => {
3300        const el = document.getElementById(fieldId);
3301        if (!el) {
3302            return;
3303        }
3304        let group = el.closest('.cars-fe-field-group');
3305        if (!group && el.closest('.cars-fe-pills')) {
3306            group = el.closest('.cars-fe-pills')?.closest('.cars-fe-field-group') ?? null;
3307        }
3308        if (group) {
3309            group.classList.toggle('cars-fe-field-group--invalid', Boolean(invalid));
3310        }
3311    };
3312
3313    const financeEligibilityScrollToFirstInvalid = (stepEl) => {
3314        if (!stepEl) {
3315            return;
3316        }
3317        const first = stepEl.querySelector('.cars-fe-field-group--invalid');
3318        if (!first) {
3319            return;
3320        }
3321        const focusable = first.querySelector('select, input:not([type="hidden"]), textarea, .cars-fe-occupation-combobox__input');
3322        financeEligibilityScrollFieldIntoView(focusable || first, {
3323            scrollArea: stepEl.querySelector('.cars-fe-scroll'),
3324            topPaddingPx: 24,
3325        });
3326    };
3327
3328    const requestFeStepFieldHighlights = (stepKey) => {
3329        if (Object.prototype.hasOwnProperty.call(feStepHighlightRequested, stepKey)) {
3330            feStepHighlightRequested[stepKey] = true;
3331        }
3332        feFieldBeingEdited = null;
3333    };
3334
3335    const attemptFeJourneyNext = (stepKey, stepEl, validateFn, onSuccess) => {
3336        requestFeStepFieldHighlights(stepKey);
3337        const ok = validateFn();
3338        if (!ok) {
3339            financeEligibilityScrollToFirstInvalid(stepEl);
3340            return;
3341        }
3342        resetFeStepFieldHighlights(stepEl, stepKey);
3343        onSuccess();
3344    };
3345
3346    const attemptPersonalGroupedNext = (onSuccess) => {
3347        requestFeStepFieldHighlights('details');
3348        requestFeStepFieldHighlights('contact');
3349        requestFeStepFieldHighlights('more');
3350        const ok = validatePersonalGrouped();
3351        if (!ok) {
3352            financeEligibilityScrollToFirstInvalid(personalGroupedStep);
3353            return;
3354        }
3355        resetFeStepFieldHighlights(personalGroupedStep, 'details');
3356        feStepHighlightRequested.contact = false;
3357        feStepHighlightRequested.more = false;
3358        onSuccess();
3359    };
3360
3361    const attemptIncomeOutgoingsGroupedNext = (onSuccess) => {
3362        requestFeStepFieldHighlights('employment');
3363        requestFeStepFieldHighlights('income');
3364        requestFeStepFieldHighlights('outgoings');
3365        const ok = validateIncomeOutgoingsGrouped();
3366        if (!ok) {
3367            financeEligibilityScrollToFirstInvalid(incomeOutgoingsGroupedStep);
3368            return;
3369        }
3370        resetFeStepFieldHighlights(incomeOutgoingsGroupedStep, 'employment');
3371        feStepHighlightRequested.income = false;
3372        feStepHighlightRequested.outgoings = false;
3373        onSuccess();
3374    };
3375
3376    const dobIsoOk = (v) => {
3377        const s = String(v || '').trim();
3378        if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
3379            return false;
3380        }
3381        const parts = s.split('-').map((x) => parseInt(x, 10));
3382        if (parts.length !== 3 || parts.some((n) => Number.isNaN(n))) {
3383            return false;
3384        }
3385        const [y, mo, da] = parts;
3386        const d = new Date(y, mo - 1, da);
3387        if (d.getFullYear() !== y || d.getMonth() !== mo - 1 || d.getDate() !== da) {
3388            return false;
3389        }
3390        const today = new Date();
3391        today.setHours(0, 0, 0, 0);
3392        d.setHours(0, 0, 0, 0);
3393        let age = today.getFullYear() - d.getFullYear();
3394        const dm = today.getMonth() - d.getMonth();
3395        if (dm < 0 || (dm === 0 && today.getDate() < d.getDate())) {
3396            age -= 1;
3397        }
3398        return age >= 18 && age <= 98;
3399    };
3400
3401    const formatIsoDobToUk = (iso) => {
3402        const s = String(iso || '').trim();
3403        if (!/^\d{4}-\d{2}-\d{2}$/.test(s)) {
3404            return '';
3405        }
3406        const [y, m, d] = s.split('-');
3407        return `${d}/${m}/${y}`;
3408    };
3409
3410    const EMAIL_LOCAL_PART = /^[A-Za-z0-9_+-]+(?:\.[A-Za-z0-9_+-]+)*$/;
3411    const EMAIL_LOCAL_CHARS = /^[A-Za-z0-9._+-]*$/;
3412
3413    const emailLocalCanStillGrow = (local) =>
3414        EMAIL_LOCAL_CHARS.test(local) && local.startsWith('.') === false && local.includes('..') === false;
3415    const EMAIL_DOMAIN_NAME =
3416        /^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$/;
3417    // Four octets, each 0-255. A partial or out-of-range one (192.168.1, 999.999.999.999) fails.
3418    const EMAIL_DOMAIN_IPV4 =
3419        /^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
3420
3421    const emailOk = (v) => {
3422        const s = String(v || '').trim();
3423        const at = s.lastIndexOf('@');
3424        if (at <= 0 || at === s.length - 1) {
3425            return false;
3426        }
3427        if (!EMAIL_LOCAL_PART.test(s.slice(0, at))) {
3428            return false;
3429        }
3430        const domain = s.slice(at + 1);
3431        return EMAIL_DOMAIN_NAME.test(domain) || EMAIL_DOMAIN_IPV4.test(domain);
3432    };
3433
3434    const financeEligibilityIsUkPhoneContext = () => {
3435        const iso = String(val('cars-fe-phone-country') || '').toLowerCase();
3436        const dial = String(val('cars-fe-phone-dial') || '').replace(/\D/g, '');
3437        return iso === 'gb' || dial === '44';
3438    };
3439
3440    const nationalPhoneOk = (v) => {
3441        const raw = String(v || '').trim();
3442        const compact = raw.replace(/[\s()-]/g, '');
3443        // Full international: +44 then 10 subscriber digits (12 chars after + including country code digits).
3444        if (/^\+44\d{10}$/.test(compact)) {
3445            return true;
3446        }
3447        const digits = compact.replace(/\D/g, '');
3448        if (financeEligibilityIsUkPhoneContext()) {
3449            // National: 07xxxxxxxxx (11) or 7xxxxxxxxx (10, dial already +44).
3450            return /^07\d{9}$/.test(digits) || /^7\d{9}$/.test(digits);
3451        }
3452        return /^07\d{9}$/.test(digits) || /^\+44\d{10}$/.test(compact);
3453    };
3454
3455    /** True once the user has typed more digits than a valid UK mobile allows. */
3456    const nationalPhoneExceedsLimit = (v) => {
3457        const raw = String(v || '').trim();
3458        if (!raw) {
3459            return false;
3460        }
3461        const compact = raw.replace(/[\s()-]/g, '');
3462        if (compact.startsWith('+44')) {
3463            const afterPlus = compact.slice(1).replace(/\D/g, '');
3464            // +44 + 10 subscriber digits => 12 digits after the +
3465            return afterPlus.length > 12;
3466        }
3467        const digits = compact.replace(/\D/g, '');
3468        if (financeEligibilityIsUkPhoneContext()) {
3469            // Allow typing toward 07… (11) or 7… (10); over either max is too long.
3470            if (digits.startsWith('0')) {
3471                return digits.length > 11;
3472            }
3473            return digits.length > 10;
3474        }
3475        return digits.length > 11;
3476    };
3477
3478    /** True while the digits typed so far are still a prefix of a valid UK number (07… / 7…). */
3479    const ukNationalDigitsPrefixOk = (digits) => {
3480        if (digits === '') {
3481            return true;
3482        }
3483        if (digits.startsWith('0')) {
3484            if (digits.length === 1) {
3485                return true;
3486            }
3487            return digits[1] === '7' && digits.length <= 11;
3488        }
3489        return digits[0] === '7' && digits.length <= 10;
3490    };
3491
3492    /**
3493     * False once the value can no longer grow into a valid number (e.g. "921" — wrong leading digit).
3494     * Lets a wrong format show its error while typing, without flagging a half-typed valid one.
3495     */
3496    const nationalPhoneCanStillBecomeValid = (v) => {
3497        const compact = String(v || '').trim().replace(/[\s()-]/g, '');
3498        if (compact === '') {
3499            return true;
3500        }
3501        if (compact.startsWith('+')) {
3502            // Typing toward +44 then 10 subscriber digits.
3503            return /^\+(4(4\d{0,10})?)?$/.test(compact);
3504        }
3505        const digits = compact.replace(/\D/g, '');
3506        if (digits !== compact) {
3507            return false;
3508        }
3509        if (financeEligibilityIsUkPhoneContext()) {
3510            return ukNationalDigitsPrefixOk(digits);
3511        }
3512        // Non-UK dial context still expects 07… locally.
3513        return digits.startsWith('0') ? ukNationalDigitsPrefixOk(digits) : digits[0] === '0';
3514    };
3515
3516    const emailLooksComplete = (v) => {
3517        const s = String(v || '').trim();
3518        const at = s.indexOf('@');
3519        if (at <= 0 || (s.match(/@/g) || []).length !== 1) {
3520            return false;
3521        }
3522        const labels = s.slice(at + 1).split('.');
3523        if (labels.length < 2) {
3524            return false;
3525        }
3526
3527        if (/^[A-Za-z]{2,}$/.test(labels[labels.length - 1])) {
3528            return true;
3529        }
3530
3531        // An all-numeric domain can only be an IPv4 address, so it is judged as soon as it has a dot
3532        // and a non-empty last label. That flags "[email protected]" and "[email protected]" as they are typed; a
3533        // full address passes on its fourth octet. A numeral inside a name is unaffected, since
3534        // "[email protected]" is judged by the branch above and passes.
3535        return labels.every((label) => /^\d+$/.test(label));
3536    };
3537
3538    /** Live check: only rejects what no further typing could rescue, so a part-typed address waits. */
3539    const emailCanStillBecomeValid = (v) => {
3540        const s = String(v || '');
3541        if (s === '') {
3542            return true;
3543        }
3544        if (/\s/.test(s) || (s.match(/@/g) || []).length > 1) {
3545            return false;
3546        }
3547        const at = s.indexOf('@');
3548        const local = at === -1 ? s : s.slice(0, at);
3549        // Once the @ is there the local part can no longer grow, so it is held to the full rule.
3550        const localIsFinished = at !== -1;
3551        if (local !== '' && !(localIsFinished ? EMAIL_LOCAL_PART.test(local) : emailLocalCanStillGrow(local))) {
3552            return false;
3553        }
3554        if (at === -1) {
3555            return true;
3556        }
3557        // The domain is still growing, so only what it can never contain is rejected here.
3558        const domain = s.slice(at + 1);
3559        if (domain === '') {
3560            return true;
3561        }
3562        return /^[A-Za-z0-9.-]+$/.test(domain) && domain.startsWith('.') === false && domain.includes('..') === false;
3563    };
3564
3565    const syncFieldInlineError = (errorId, invalid) => {
3566        const err = document.getElementById(errorId);
3567        if (!err) {
3568            return;
3569        }
3570        const show = Boolean(invalid);
3571        err.hidden = !show;
3572        err.setAttribute('aria-hidden', show ? 'false' : 'true');
3573    };
3574
3575    const syncPhoneFormatError = (invalid) => syncFieldInlineError('cars-fe-phone-format-error', invalid);
3576
3577    const syncEmailFormatError = (invalid) => syncFieldInlineError('cars-fe-email-format-error', invalid);
3578
3579    /**
3580     * A date input reports a complete value as soon as every segment holds something, so a year
3581     * typed as 1, 19, 199 reads as 0001, 0019, 0199 and each of those is a real date far outside
3582     * the age range. Waiting for all four digits keeps the error from flashing mid-year.
3583     */
3584    const dobYearIsComplete = (iso) => {
3585        const parsed = /^(\d{4})-\d{2}-\d{2}$/.exec(String(iso || '').trim());
3586        return parsed ? parseInt(parsed[1], 10) >= 1000 : false;
3587    };
3588
3589    /** Part-way through the year is not a wrong date, so neither the error nor the red shows yet. */
3590    /**
3591     * A wrong date is called out as soon as the year reads as finished, so picking one from the
3592     * calendar flags straight away. A year still being typed (1, then 19, then 199 on the way to
3593     * 1990) is left alone until the user moves off the field, at which point whatever is there is
3594     * final and is judged in full — including a year like 0001 that was abandoned part-typed.
3595     */
3596    const dobJudgeable = (iso) => dobYearIsComplete(iso) || feFieldBeingEdited !== 'cars-fe-dob';
3597
3598    const syncDobAgeError = () => {
3599        const dob = val('cars-fe-dob');
3600        const hasValue = String(dob || '').trim() !== '';
3601        syncFieldInlineError('cars-fe-dob-age-error', hasValue && dobJudgeable(dob) && !dobIsoOk(dob));
3602    };
3603
3604    /** Single source of truth for the phone field, shared by the live-typing and Next paths. */
3605    const contactPhoneFeedbackState = () => {
3606        const phoneRaw = val('cars-fe-phone-national');
3607        const trimmed = String(phoneRaw || '').trim();
3608        const overLimit = nationalPhoneExceedsLimit(phoneRaw);
3609        const invalid = !nationalPhoneOk(phoneRaw);
3610        // A value that can never become valid is shown while typing; a half-typed valid one
3611        // waits until the field is touched or Next is pressed.
3612        const wrongFormat = trimmed !== '' && !nationalPhoneCanStillBecomeValid(phoneRaw);
3613        const live = overLimit || wrongFormat;
3614        const gated = feFieldHighlightAllowed('contact', 'cars-fe-phone-national');
3615        return {
3616            markInvalid: live || (invalid && gated),
3617            showFormatError: live || (invalid && trimmed !== '' && gated),
3618        };
3619    };
3620
3621    const syncLivePhoneFormatFeedback = () => {
3622        const phone = contactPhoneFeedbackState();
3623        financeEligibilityMarkFieldInvalid('cars-fe-phone-national', phone.markInvalid);
3624        syncPhoneFormatError(phone.showFormatError);
3625    };
3626
3627    const syncManualUkPostcodeError = (invalid) => {
3628        const err = document.getElementById('cars-fe-address-postcode-manual-error');
3629        if (!err) {
3630            return;
3631        }
3632        const show = Boolean(invalid);
3633        err.hidden = !show;
3634        err.setAttribute('aria-hidden', show ? 'false' : 'true');
3635    };
3636
3637    const syncLiveManualUkPostcodeFeedback = () => {
3638        const pc = String(val('cars-fe-address-postcode-manual') || '').trim();
3639        // A part-typed postcode is always flagged live; an empty one waits for touched/Next.
3640        const show = pc === ''
3641            ? feFieldHighlightAllowed('address', 'cars-fe-address-postcode-manual')
3642            : !ukPostcodeLooksComplete(pc);
3643        financeEligibilityMarkFieldInvalid('cars-fe-address-postcode-manual', show);
3644        syncManualUkPostcodeError(pc !== '' && !ukPostcodeLooksComplete(pc));
3645    };
3646
3647    /** UK national: accept 7xxxxxxxxx (no leading 0); payload uses 07… for +44 dial context. */
3648    const normalizeUkNationalForPayload = (iso, dial, nationalRaw) => {
3649        const trimmed = String(nationalRaw || '').trim();
3650        const compact = trimmed.replace(/[\s()-]/g, '');
3651        if (/^\+44\d{10}$/.test(compact)) {
3652            return `0${compact.slice(3)}`;
3653        }
3654        const digits = trimmed.replace(/\D/g, '');
3655        const dIso = String(iso || '').toLowerCase();
3656        const dDial = String(dial || '').replace(/\D/g, '');
3657        const uk = dIso === 'gb' || dDial === '44';
3658        if (uk && /^7\d{9}$/.test(digits)) {
3659            return `0${digits}`;
3660        }
3661        return trimmed;
3662    };
3663
3664    const personNameOk = (v) => {
3665        const s = String(v || '').trim();
3666        return s.length > 0 && !/\d/.test(s);
3667    };
3668
3669    const stripDigitsFromPersonNameInput = (input) => {
3670        if (!input || input.tagName !== 'INPUT') {
3671            return;
3672        }
3673        const prev = String(input.value || '');
3674        const next = prev.replace(/\d+/g, '');
3675        if (next === prev) {
3676            return;
3677        }
3678        const start = input.selectionStart;
3679        const end = input.selectionEnd;
3680        input.value = next;
3681        if (typeof start === 'number' && typeof end === 'number') {
3682            const removedBefore = (prev.slice(0, start).match(/\d/g) || []).length;
3683            const removedInSel = (prev.slice(start, end).match(/\d/g) || []).length;
3684            const nextStart = Math.max(0, start - removedBefore);
3685            const nextEnd = Math.max(nextStart, end - removedBefore - removedInSel);
3686            try {
3687                input.setSelectionRange(nextStart, nextEnd);
3688            } catch {
3689                /* ignore */
3690            }
3691        }
3692    };
3693
3694    const applyDetailsFieldHighlights = () => {
3695        const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : detailsStep;
3696        if (!host) {
3697            return;
3698        }
3699        const mark = feStepFieldMarker('details');
3700        mark('cars-fe-title', !val('cars-fe-title'));
3701        mark('cars-fe-first-name', !personNameOk(val('cars-fe-first-name')));
3702        mark('cars-fe-last-name', !personNameOk(val('cars-fe-last-name')));
3703        const dobRaw = val('cars-fe-dob');
3704        mark('cars-fe-dob', dobJudgeable(dobRaw) && !dobIsoOk(dobRaw));
3705        mark('cars-fe-marital', !val('cars-fe-marital'));
3706        syncDobAgeError();
3707    };
3708
3709    const isDetailsFieldsValid = () =>
3710        Boolean(
3711            val('cars-fe-title') &&
3712                personNameOk(val('cars-fe-first-name')) &&
3713                personNameOk(val('cars-fe-last-name')) &&
3714                dobIsoOk(val('cars-fe-dob')) &&
3715                val('cars-fe-marital'),
3716        );
3717
3718    const validateDetails = () => {
3719        syncDobPlaceholderState();
3720        const ok = isDetailsFieldsValid();
3721        applyDetailsFieldHighlights();
3722        setBtnNextDisabled(nextBtn, !ok);
3723        return ok;
3724    };
3725
3726    const syncDobPlaceholderState = () => {
3727        const dob = document.getElementById('cars-fe-dob');
3728        const shell = dob?.closest('.cars-fe-field-group__shell--dob');
3729        if (!shell) {
3730            return;
3731        }
3732        shell.classList.toggle('cars-fe-field-group__shell--dob-has-value', String(dob.value || '').trim() !== '');
3733    };
3734
3735    const applyContactFieldHighlights = () => {
3736        const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : contactStep;
3737        if (!host) {
3738            return;
3739        }
3740        const emailRaw = val('cars-fe-email');
3741        const emailWrongFormat = !emailCanStillBecomeValid(emailRaw);
3742        const emailInvalid = !emailOk(emailRaw);
3743        // Shown while the user is still in the field once the address can never be valid, or once it
3744        // is complete enough to judge — otherwise Next sits disabled with nothing saying why.
3745        const emailLive = emailWrongFormat || (emailInvalid && emailLooksComplete(emailRaw));
3746        // Touched stays set for the rest of the journey, so without this an address the user came
3747        // back to would be flagged again from its first keystroke. A half-typed address is only
3748        // flagged once they leave the field, or Next blurs it for them.
3749        const emailGated = feFieldBeingEdited !== 'cars-fe-email' && feFieldHighlightAllowed('contact', 'cars-fe-email');
3750        financeEligibilityMarkFieldInvalid('cars-fe-email', emailLive || (emailInvalid && emailGated));
3751        syncEmailFormatError(
3752            emailLive || (emailInvalid && String(emailRaw || '').trim() !== '' && emailGated),
3753        );
3754        syncLivePhoneFormatFeedback();
3755    };
3756
3757    const isContactFieldsValid = () => emailOk(val('cars-fe-email')) && nationalPhoneOk(val('cars-fe-phone-national'));
3758
3759    const validateContact = () => {
3760        const ok = isContactFieldsValid();
3761        applyContactFieldHighlights();
3762        setBtnNextDisabled(contactNextBtn, !ok);
3763        return ok;
3764    };
3765
3766    const applyMoreFieldHighlights = () => {
3767        const host = feFormDensityComposed && personalGroupedStep ? personalGroupedStep : moreStep;
3768        if (!host) {
3769            return;
3770        }
3771        const mark = feStepFieldMarker('more');
3772        mark('cars-fe-license-type', !val('cars-fe-license-type'));
3773        mark('cars-fe-dependants', !val('cars-fe-dependants'));
3774    };
3775
3776    const isMoreFieldsValid = () => Boolean(val('cars-fe-license-type') && val('cars-fe-dependants'));
3777
3778    const validateMore = () => {
3779        const ok = isMoreFieldsValid();
3780        applyMoreFieldHighlights();
3781        setBtnNextDisabled(moreNextBtn, !ok);
3782        return ok;
3783    };
3784
3785    const validatePersonalGrouped = () => {
3786        if (!personalGroupedNextBtn) {
3787            return false;
3788        }
3789        syncDobPlaceholderState();
3790        const ok = isDetailsFieldsValid() && isContactFieldsValid() && isMoreFieldsValid();
3791        applyDetailsFieldHighlights();
3792        applyContactFieldHighlights();
3793        applyMoreFieldHighlights();
3794        setBtnNextDisabled(personalGroupedNextBtn, !ok);
3795        return ok;
3796    };
3797
3798    const employmentTenureTotalMonthsFromStrings = (yStr, mStr) => {
3799        if (yStr === '' || mStr === '') {
3800            return null;
3801        }
3802        const y = parseInt(String(yStr), 10) || 0;
3803        const m = parseInt(String(mStr), 10) || 0;
3804        return y * 12 + m;
3805    };
3806
3807    const employmentEntryTenureMonths = (entry) => {
3808        if (!entry) {
3809            return null;
3810        }
3811        return employmentTenureTotalMonthsFromStrings(
3812            entry.years != null ? String(entry.years) : '',
3813            entry.months != null ? String(entry.months) : '',
3814        );
3815    };
3816
3817    const getTotalEmploymentMonthsFromSavedEntries = () =>
3818        savedEmploymentEntries.reduce((sum, e) => sum + (employmentEntryTenureMonths(e) || 0), 0);
3819
3820    /** Months counted toward the 3-year rule: saved rows, with the row under edit replaced by live form values. */
3821    const getEmploymentHistoryMonthsForValidation = () => {
3822        if (editingEmploymentIndex != null && typeof editingEmploymentIndex === 'number') {
3823            const sumOthers = savedEmploymentEntries.reduce((sum, e, i) => {
3824                if (i === editingEmploymentIndex) {
3825                    return sum;
3826                }
3827                return sum + (employmentEntryTenureMonths(e) || 0);
3828            }, 0);
3829            const live = employmentTenureTotalMonthsFromStrings(
3830                val('cars-fe-employment-years'),
3831                val('cars-fe-employment-months'),
3832            );
3833            return sumOthers + (live != null ? live : 0);
3834        }
3835        return getTotalEmploymentMonthsFromSavedEntries();
3836    };
3837
3838    const syncEmploymentHistoryAlert = () => {
3839        const alertEl = document.getElementById('cars-fe-employment-history-alert');
3840        if (!alertEl) {
3841            return;
3842        }
3843        const host = getEmploymentUiHost();
3844        if (!host || host.hidden) {
3845            alertEl.hidden = true;
3846            return;
3847        }
3848        const savedPanel = document.getElementById('cars-fe-employment-saved-panel');
3849        const panelVisible = savedPanel && savedPanel.hidden === false;
3850        const total = getTotalEmploymentMonthsFromSavedEntries();
3851        const show = panelVisible && savedEmploymentEntries.length > 0 && total < 36;
3852        alertEl.hidden = !show;
3853    };
3854
3855    const isPastEmploymentFormContext = () => {
3856        if (editingEmploymentIndex != null && editingEmploymentIndex > 0) {
3857            return true;
3858        }
3859        if (editingEmploymentIndex != null && editingEmploymentIndex === 0) {
3860            return false;
3861        }
3862        return savedEmploymentEntries.length >= 1;
3863    };
3864
3865    const EMPLOYMENT_STATUS_AUTOFILL_ALL_FIELDS = new Set([
3866        'Retired',
3867        'Student',
3868        'Homemaker',
3869        'Unemployed',
3870        'On Benefits',
3871    ]);
3872
3873    const getEmploymentDurationQuestionLabel = (status) => {
3874        const s = String(status || '').trim();
3875        switch (s) {
3876            case 'Retired':
3877                return 'How long have you been retired?';
3878            case 'Student':
3879                return 'How long have you been a student?';
3880            case 'Homemaker':
3881                return 'How long have you been a homemaker?';
3882            case 'Unemployed':
3883                return 'How long have you been unemployed?';
3884            case 'On Benefits':
3885                return 'How long have you been on benefits?';
3886            default:
3887                return 'Employment duration';
3888        }
3889    };
3890
3891    const syncEmploymentDurationLabelForStatus = (status) => {
3892        const el = document.getElementById('cars-fe-employment-duration-label');
3893        if (!el) {
3894            return;
3895        }
3896        el.textContent = getEmploymentDurationQuestionLabel(status);
3897    };
3898
3899    const clearEmploymentAutofillFields = () => {
3900        const employer = document.getElementById('cars-fe-employment-employer-name');
3901        const occHidden = document.getElementById('cars-fe-employment-occupation');
3902        const occQ = document.getElementById('cars-fe-employment-occupation-q');
3903        const basis = document.getElementById('cars-fe-employment-occupational-basis');
3904        const occType = document.getElementById('cars-fe-employment-occupational-type');
3905        const years = document.getElementById('cars-fe-employment-years');
3906        const months = document.getElementById('cars-fe-employment-months');
3907        if (employer) {
3908            employer.value = '';
3909        }
3910        if (occHidden) {
3911            occHidden.value = '';
3912        }
3913        if (occQ) {
3914            occQ.value = '';
3915        }
3916        if (basis) {
3917            basis.value = '';
3918        }
3919        if (occType) {
3920            occType.value = '';
3921        }
3922        if (years) {
3923            years.value = '';
3924        }
3925        if (months) {
3926            months.value = financeEligibilityFieldDefaultValue('cars-fe-employment-months');
3927        }
3928        financeEligibilityCloseOccupationPanels(getEmploymentUiHost());
3929        syncAllFinanceDurationSelectShells();
3930    };
3931
3932    /**
3933     * Autofill statuses: set employer/occupation/basis/type. Any other status: clear those fields and years
3934     * (leaving autofill); months returns to its preselected 0 rather than blank.
3935     */
3936    const applyEmploymentStatusAutofill = (status) => {
3937        const employer = document.getElementById('cars-fe-employment-employer-name');
3938        const occHidden = document.getElementById('cars-fe-employment-occupation');
3939        const occQ = document.getElementById('cars-fe-employment-occupation-q');
3940        const basis = document.getElementById('cars-fe-employment-occupational-basis');
3941        const occType = document.getElementById('cars-fe-employment-occupational-type');
3942
3943        if (!EMPLOYMENT_STATUS_AUTOFILL_ALL_FIELDS.has(status)) {
3944            clearEmploymentAutofillFields();
3945            return;
3946        }
3947
3948        if (!employer || !occHidden || !basis || !occType) {
3949            return;
3950        }
3951        const label = status;
3952        employer.value = label;
3953        occHidden.value = label;
3954        if (occQ) {
3955            occQ.value = label;
3956        }
3957        basis.value = label;
3958        occType.value = label;
3959        syncAllFinanceDurationSelectShells();
3960    };
3961
3962    const syncEmploymentFormFieldLabelsForContext = () => {
3963        const past = isPastEmploymentFormContext();
3964        const empNameInput = document.getElementById('cars-fe-employment-employer-name');
3965        const occHidden = document.getElementById('cars-fe-employment-occupation');
3966        const empWrap = empNameInput ? empNameInput.closest('.cars-fe-field-group') : null;
3967        const occWrap = occHidden ? occHidden.closest('.cars-fe-field-group') : null;
3968        const empLab = empWrap ? empWrap.querySelector('.cars-fe-field-group__label') : null;
3969        const occLab = occWrap ? occWrap.querySelector('.cars-fe-field-group__label') : null;
3970        if (empLab) {
3971            empLab.textContent = past ? 'Past employer' : 'Employers name';
3972        }
3973        if (occLab) {
3974            occLab.textContent = past ? 'Past occupation' : 'Occupation';
3975        }
3976    };
3977
3978    const employmentFormValidForSave = () => {
3979        const hasStatus = Boolean(val('cars-fe-employment-status'));
3980        const hasDetails =
3981            Boolean(val('cars-fe-employment-employer-name')) &&
3982            Boolean(val('cars-fe-employment-occupation')) &&
3983            Boolean(val('cars-fe-employment-occupational-basis')) &&
3984            Boolean(val('cars-fe-employment-occupational-type')) &&
3985            durationYearsMonthsOk('cars-fe-employment-years', 'cars-fe-employment-months');
3986        return hasStatus && hasDetails;
3987    };
3988
3989    const applyEmploymentFormFieldHighlights = () => {
3990        const host = getEmploymentUiHost();
3991        if (!host) {
3992            return;
3993        }
3994        const entryBlock = document.getElementById('cars-fe-employment-entry-block');
3995        if (!entryBlock || entryBlock.hidden) {
3996            return;
3997        }
3998        const durationInvalid = !durationYearsMonthsOk(
3999            'cars-fe-employment-years',
4000            'cars-fe-employment-months',
4001        );
4002        const mark = feStepFieldMarker('employment');
4003        mark('cars-fe-employment-status', !val('cars-fe-employment-status'));
4004        mark('cars-fe-employment-employer-name', !val('cars-fe-employment-employer-name'));
4005        mark('cars-fe-employment-occupation', !val('cars-fe-employment-occupation'));
4006        mark('cars-fe-employment-occupational-basis', !val('cars-fe-employment-occupational-basis'));
4007        mark('cars-fe-employment-occupational-type', !val('cars-fe-employment-occupational-type'));
4008        mark('cars-fe-employment-years', durationInvalid);
4009        mark('cars-fe-employment-months', durationInvalid);
4010    };
4011
4012    const isEmploymentReadyForNext = () => {
4013        syncEmploymentSummaryUiState();
4014        const totalMonths = getEmploymentHistoryMonthsForValidation();
4015        const employmentHistoryInsufficient =
4016            savedEmploymentEntries.length > 0 && totalMonths < 36;
4017        const midEmploymentEdit = editingEmploymentId != null && editingEmploymentIndex != null;
4018        return savedEmploymentEntries.length > 0 && !employmentHistoryInsufficient && !midEmploymentEdit;
4019    };
4020
4021    const validateEmployment = () => {
4022        const groupedActive =
4023            feFormDensityComposed && incomeOutgoingsGroupedStep && !incomeOutgoingsGroupedStep.hidden;
4024        const controlActive = employmentStep && !employmentStep.hidden;
4025        if (!groupedActive && !controlActive) {
4026            if (employmentNextBtn) {
4027                setBtnNextDisabled(employmentNextBtn, true);
4028            }
4029            return false;
4030        }
4031        const ok = isEmploymentReadyForNext();
4032        applyEmploymentFormFieldHighlights();
4033        if (controlActive && employmentNextBtn) {
4034            setBtnNextDisabled(employmentNextBtn, !ok);
4035        }
4036        syncEmploymentHistoryAlert();
4037        return ok;
4038    };
4039
4040    financeEligibilityInitOccupationComboboxes(() => {
4041        syncEmploymentFormFieldLabelsForContext();
4042        syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status'));
4043        if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') {
4044            validateIncomeOutgoingsGrouped();
4045        } else {
4046            validateEmployment();
4047        }
4048    });
4049
4050    financeEligibilityInitDurationListboxes(() => {
4051        const addressStepOpen = addressStep && addressStep.hidden === false;
4052        const employmentHost = getEmploymentUiHost();
4053        const employmentStepOpen = employmentHost && employmentHost.hidden === false;
4054        if (addressStepOpen) {
4055            syncAddressMonthsDisabledForFifteenPlus();
4056            validateAddress();
4057        }
4058        if (employmentStepOpen) {
4059            if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') {
4060                validateIncomeOutgoingsGrouped();
4061            } else {
4062                validateEmployment();
4063            }
4064        }
4065    });
4066
4067    const parseSterlingInputToNumber = (raw) => {
4068        const s = String(raw || '')
4069            .replace(/£/g, '')
4070            .replace(/,/g, '')
4071            .replace(/\s+/g, '')
4072            .trim();
4073        if (s === '') {
4074            return NaN;
4075        }
4076        const n = parseFloat(s);
4077        return n;
4078    };
4079
4080    const stripSterlingCommas = (raw) =>
4081        String(raw || '')
4082            .replace(/£/g, '')
4083            .replace(/,/g, '')
4084            .replace(/\s+/g, '')
4085            .trim();
4086
4087    /** Digits only → en-GB thousands separators for display (e.g. 25000 → 25,000). */
4088    const formatSterlingThousandsDisplay = (raw) => {
4089        const digits = String(raw || '').replace(/[^\d]/g, '');
4090        if (digits === '') {
4091            return '';
4092        }
4093        const normalized = digits.replace(/^0+(?=\d)/, '');
4094        return Number(normalized).toLocaleString('en-GB');
4095    };
4096
4097    /** Review summary money: £25,000 (empty → blank so callers can fall back). */
4098    const formatSterlingForReview = (raw) => {
4099        const formatted = formatSterlingThousandsDisplay(raw);
4100        return formatted ? `£${formatted}` : '';
4101    };
4102
4103    const applySterlingThousandsFormatting = (inputEl) => {
4104        if (!inputEl || inputEl.tagName !== 'INPUT') {
4105            return;
4106        }
4107        const prev = String(inputEl.value || '');
4108        const digitsBeforeCaret = prev.slice(0, inputEl.selectionStart ?? prev.length).replace(/[^\d]/g, '').length;
4109        const next = formatSterlingThousandsDisplay(prev);
4110        if (next === prev) {
4111            return;
4112        }
4113        inputEl.value = next;
4114        let caret = 0;
4115        let seen = 0;
4116        while (caret < next.length && seen < digitsBeforeCaret) {
4117            if (/\d/.test(next[caret])) {
4118                seen += 1;
4119            }
4120            caret += 1;
4121        }
4122        try {
4123            inputEl.setSelectionRange(caret, caret);
4124        } catch {
4125            /* ignore */
4126        }
4127    };
4128
4129    const applyIncomeOutgoingsFieldHighlights = () => {
4130        const host =
4131            feFormDensityComposed && incomeOutgoingsGroupedStep ? incomeOutgoingsGroupedStep : incomeOutgoingsStep;
4132        if (!host) {
4133            return;
4134        }
4135        const annualBeforeTax = parseSterlingInputToNumber(val('cars-fe-income-annual-before-tax'));
4136        const monthlyTakeHome = parseSterlingInputToNumber(val('cars-fe-income-monthly-take-home'));
4137        const otherAnnual = parseSterlingInputToNumber(val('cars-fe-income-annual-household-other'));
4138        const mark = feStepFieldMarker('income');
4139        mark('cars-fe-income-annual-before-tax', Number.isNaN(annualBeforeTax) || annualBeforeTax <= 0);
4140        mark('cars-fe-income-monthly-take-home', Number.isNaN(monthlyTakeHome) || monthlyTakeHome <= 0);
4141        mark('cars-fe-income-annual-household-other', Number.isNaN(otherAnnual) || otherAnnual < 0);
4142    };
4143
4144    const isIncomeOutgoingsFieldsValid = () => {
4145        const annualBeforeTax = parseSterlingInputToNumber(val('cars-fe-income-annual-before-tax'));
4146        const monthlyTakeHome = parseSterlingInputToNumber(val('cars-fe-income-monthly-take-home'));
4147        const otherAnnual = parseSterlingInputToNumber(val('cars-fe-income-annual-household-other'));
4148        return (
4149            !Number.isNaN(annualBeforeTax) &&
4150            annualBeforeTax > 0 &&
4151            !Number.isNaN(monthlyTakeHome) &&
4152            monthlyTakeHome > 0 &&
4153            !Number.isNaN(otherAnnual) &&
4154            otherAnnual >= 0
4155        );
4156    };
4157
4158    const validateIncomeOutgoings = () => {
4159        if (!incomeNextBtn) {
4160            return false;
4161        }
4162        const controlActive = incomeOutgoingsStep && !incomeOutgoingsStep.hidden;
4163        if (!controlActive) {
4164            setBtnNextDisabled(incomeNextBtn, true);
4165            return false;
4166        }
4167        const ok = isIncomeOutgoingsFieldsValid();
4168        applyIncomeOutgoingsFieldHighlights();
4169        setBtnNextDisabled(incomeNextBtn, !ok);
4170        return ok;
4171    };
4172
4173    const syncOutgoingsConditionalVisibility = () => {
4174        const repayFollow = document.getElementById('cars-fe-outgoings-repay-followup');
4175        const downturnFollow = document.getElementById('cars-fe-outgoings-downturn-followup');
4176        const repay = val('cars-fe-outgoings-repay-existing-finance');
4177        const downturn = val('cars-fe-outgoings-downturn-expected');
4178        if (repayFollow) {
4179            repayFollow.hidden = repay !== 'Yes';
4180        }
4181        if (downturnFollow) {
4182            downturnFollow.hidden = downturn !== 'Yes';
4183        }
4184    };
4185
4186    const clearOutgoingsRepayFollowupFields = () => {
4187        const el = document.getElementById('cars-fe-outgoings-existing-loan-monthly-payment');
4188        if (el) {
4189            el.value = '';
4190        }
4191    };
4192
4193    const clearOutgoingsDownturnFollowupFields = () => {
4194        const reason = document.getElementById('cars-fe-outgoings-downturn-reason');
4195        if (reason) {
4196            reason.value = '';
4197        }
4198        const reduced = document.getElementById('cars-fe-outgoings-reduced-annual-income-before-tax');
4199        if (reduced) {
4200            reduced.value = '';
4201        }
4202    };
4203
4204    const applyOutgoingsFinalFieldHighlights = () => {
4205        const host =
4206            feFormDensityComposed && incomeOutgoingsGroupedStep ? incomeOutgoingsGroupedStep : outgoingsFinalStep;
4207        if (!host) {
4208            return;
4209        }
4210        const mortgageShare = parseSterlingInputToNumber(val('cars-fe-outgoings-monthly-mortgage-rent-share'));
4211        const otherBills = parseSterlingInputToNumber(val('cars-fe-outgoings-other-monthly-bills'));
4212        const repay = val('cars-fe-outgoings-repay-existing-finance');
4213        const downturn = val('cars-fe-outgoings-downturn-expected');
4214        const yesNoOk = (v) => v === 'Yes' || v === 'No';
4215        const mark = feStepFieldMarker('outgoings');
4216
4217        mark('cars-fe-outgoings-monthly-mortgage-rent-share', Number.isNaN(mortgageShare) || mortgageShare < 0);
4218        mark('cars-fe-outgoings-other-monthly-bills', Number.isNaN(otherBills) || otherBills < 0);
4219        mark('cars-fe-outgoings-repay-existing-finance', !yesNoOk(repay));
4220        mark('cars-fe-outgoings-downturn-expected', !yesNoOk(downturn));
4221
4222        if (repay === 'Yes') {
4223            const existingLoanMonthly = parseSterlingInputToNumber(val('cars-fe-outgoings-existing-loan-monthly-payment'));
4224            mark(
4225                'cars-fe-outgoings-existing-loan-monthly-payment',
4226                Number.isNaN(existingLoanMonthly) || existingLoanMonthly < 0,
4227            );
4228        } else {
4229            financeEligibilityMarkFieldInvalid('cars-fe-outgoings-existing-loan-monthly-payment', false);
4230        }
4231
4232        if (downturn === 'Yes') {
4233            const reducedAnnual = parseSterlingInputToNumber(val('cars-fe-outgoings-reduced-annual-income-before-tax'));
4234            mark('cars-fe-outgoings-downturn-reason', !val('cars-fe-outgoings-downturn-reason'));
4235            mark('cars-fe-outgoings-reduced-annual-income-before-tax', Number.isNaN(reducedAnnual) || reducedAnnual < 0);
4236        } else {
4237            financeEligibilityMarkFieldInvalid('cars-fe-outgoings-downturn-reason', false);
4238            financeEligibilityMarkFieldInvalid('cars-fe-outgoings-reduced-annual-income-before-tax', false);
4239        }
4240    };
4241
4242    const isOutgoingsFinalFieldsValid = () => {
4243        syncOutgoingsConditionalVisibility();
4244
4245        const mortgageShare = parseSterlingInputToNumber(val('cars-fe-outgoings-monthly-mortgage-rent-share'));
4246        const otherBills = parseSterlingInputToNumber(val('cars-fe-outgoings-other-monthly-bills'));
4247        const repay = val('cars-fe-outgoings-repay-existing-finance');
4248        const downturn = val('cars-fe-outgoings-downturn-expected');
4249        const yesNoOk = (v) => v === 'Yes' || v === 'No';
4250        let ok =
4251            !Number.isNaN(mortgageShare) &&
4252            mortgageShare >= 0 &&
4253            !Number.isNaN(otherBills) &&
4254            otherBills >= 0 &&
4255            yesNoOk(repay) &&
4256            yesNoOk(downturn);
4257
4258        if (ok && repay === 'Yes') {
4259            const existingLoanMonthly = parseSterlingInputToNumber(val('cars-fe-outgoings-existing-loan-monthly-payment'));
4260            ok = ok && !Number.isNaN(existingLoanMonthly) && existingLoanMonthly >= 0;
4261        }
4262
4263        if (ok && downturn === 'Yes') {
4264            const downturnReason = val('cars-fe-outgoings-downturn-reason');
4265            const reducedAnnual = parseSterlingInputToNumber(val('cars-fe-outgoings-reduced-annual-income-before-tax'));
4266            ok = ok && Boolean(downturnReason) && !Number.isNaN(reducedAnnual) && reducedAnnual >= 0;
4267        }
4268        return ok;
4269    };
4270
4271    const validateOutgoingsFinal = () => {
4272        if (!outgoingsSummaryBtn) {
4273            return false;
4274        }
4275        if (!outgoingsFinalStep || outgoingsFinalStep.hidden) {
4276            setBtnNextDisabled(outgoingsSummaryBtn, true);
4277            return false;
4278        }
4279        const ok = isOutgoingsFinalFieldsValid();
4280        applyOutgoingsFinalFieldHighlights();
4281        setBtnNextDisabled(outgoingsSummaryBtn, !ok);
4282        return ok;
4283    };
4284
4285    const validateIncomeOutgoingsGrouped = () => {
4286        if (!incomeOutgoingsGroupedSummaryBtn) {
4287            return false;
4288        }
4289        if (!incomeOutgoingsGroupedStep || incomeOutgoingsGroupedStep.hidden) {
4290            setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true);
4291            return false;
4292        }
4293        const employmentOk = validateEmployment();
4294        const incomeOk = isIncomeOutgoingsFieldsValid();
4295        applyIncomeOutgoingsFieldHighlights();
4296        const outgoingsOk = isOutgoingsFinalFieldsValid();
4297        applyOutgoingsFinalFieldHighlights();
4298        const ok = employmentOk && incomeOk && outgoingsOk;
4299        setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, !ok);
4300        return ok;
4301    };
4302
4303    /** Strict UK postcode format — use only for the manual postcode field, not Postcoder search. */
4304    const ukPostcodeLooksComplete = (raw) => {
4305        const x = String(raw || '')
4306            .trim()
4307            .toUpperCase()
4308            .replace(/\s+/g, ' ');
4309        if (!x) {
4310            return false;
4311        }
4312        // Full UK postcode (outward + inward), optional single space. Rejects non-UK formats.
4313        return /^(GIR 0AA|[A-Z]{1,2}\d[A-Z\d]? \d[A-Z]{2})$/.test(x) ||
4314            /^(GIR0AA|[A-Z]{1,2}\d[A-Z\d]?\d[A-Z]{2})$/.test(x.replace(/\s/g, ''));
4315    };
4316
4317    const parseDurationYearsMonths = (yearsId, monthsId) => {
4318        const yRaw = val(yearsId);
4319        const mRaw = val(monthsId);
4320        if (yRaw === '' || mRaw === '') {
4321            return null;
4322        }
4323        const years = Number(yRaw);
4324        const months = Number(mRaw);
4325        if (!Number.isFinite(years) || !Number.isFinite(months) || years < 0 || months < 0) {
4326            return null;
4327        }
4328        return { years, months };
4329    };
4330
4331    /** At least one month of history; 15+ years (address) counts as valid without months. */
4332    const durationYearsMonthsOk = (yearsId, monthsId, opts = {}) => {
4333        const parsed = parseDurationYearsMonths(yearsId, monthsId);
4334        if (!parsed) {
4335            return false;
4336        }
4337        const plusYears = opts.plusYearsValue;
4338        if (plusYears != null && parsed.years >= plusYears) {
4339            return true;
4340        }
4341        return parsed.years * 12 + parsed.months > 0;
4342    };
4343
4344    const syncAddressMonthsDisabledForFifteenPlus = () => {
4345        const yearsEl = document.getElementById('cars-fe-address-years-at');
4346        const monthsEl = document.getElementById('cars-fe-address-months-at');
4347        const monthsShell = monthsEl?.closest?.('.cars-fe-field-group__shell--duration');
4348        const monthsTrigger = monthsShell?.querySelector?.('.cars-fe-duration-listbox__trigger');
4349        if (!yearsEl || !monthsEl || !monthsShell || !monthsTrigger) {
4350            return;
4351        }
4352        const isFifteenPlus = String(yearsEl.value || '').trim() === '15';
4353        monthsShell.classList.toggle('cars-fe-field-group__shell--duration-disabled', isFifteenPlus);
4354        monthsTrigger.disabled = isFifteenPlus;
4355        monthsTrigger.setAttribute('aria-disabled', isFifteenPlus ? 'true' : 'false');
4356        if (isFifteenPlus) {
4357            const monthsPanel = monthsShell.querySelector('.cars-fe-duration-listbox__panel');
4358            if (monthsPanel && !monthsPanel.hidden) {
4359                financeEligibilityCloseDurationListboxes(addressStep || document.getElementById('cars-fe-step-address'));
4360            }
4361            if (String(monthsEl.value || '') !== '0') {
4362                monthsEl.value = '0';
4363                financeEligibilitySyncDurationListboxShell(monthsShell);
4364            }
4365        }
4366    };
4367
4368    const syncAddressSelectBlockVisibility = () => {
4369        const block = document.getElementById('cars-fe-address-select-block');
4370        if (!block || !addressStep) {
4371            return;
4372        }
4373        const manual = addressStep.classList.contains('cars-fe-address--manual');
4374        const show = !manual && ukPostcodeLooksComplete(val('cars-fe-address-postcode'));
4375        block.hidden = !show;
4376    };
4377
4378    const newAddressEntryId = () => `cars-fe-addr-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
4379
4380    const newEmploymentEntryId = () => `cars-fe-emp-${Date.now()}-${Math.floor(Math.random() * 1e6)}`;
4381
4382    const clearEmploymentDetailFormFields = () => {
4383        [
4384            'cars-fe-employment-employer-name',
4385            'cars-fe-employment-occupation',
4386            'cars-fe-employment-occupation-q',
4387            'cars-fe-employment-occupational-basis',
4388            'cars-fe-employment-occupational-type',
4389            'cars-fe-employment-years',
4390            'cars-fe-employment-months',
4391        ].forEach((id) => {
4392            const el = document.getElementById(id);
4393            if (el) {
4394                el.value = financeEligibilityFieldDefaultValue(id);
4395            }
4396        });
4397        financeEligibilityCloseOccupationPanels(getEmploymentUiHost());
4398        syncAllFinanceDurationSelectShells();
4399        syncEmploymentFormFieldLabelsForContext();
4400        syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status'));
4401    };
4402
4403    const buildEmploymentEntryFromForm = (idOverride) => ({
4404        id: typeof idOverride === 'string' && idOverride.length > 0 ? idOverride : newEmploymentEntryId(),
4405        status: val('cars-fe-employment-status'),
4406        employerName: val('cars-fe-employment-employer-name'),
4407        occupation: val('cars-fe-employment-occupation'),
4408        occupationalBasis: val('cars-fe-employment-occupational-basis'),
4409        occupationalType: val('cars-fe-employment-occupational-type'),
4410        phoneDial: '',
4411        phoneCountry: '',
4412        phoneNational: '',
4413        years: val('cars-fe-employment-years'),
4414        months: val('cars-fe-employment-months'),
4415        pastEmployerName: '',
4416        pastOccupation: '',
4417    });
4418
4419    const applyEmploymentEntryToForm = (entry) => {
4420        const setVal = (id, v) => {
4421            const el = document.getElementById(id);
4422            if (el) {
4423                el.value = v != null ? String(v) : '';
4424            }
4425        };
4426        setVal('cars-fe-employment-employer-name', entry.employerName);
4427        setVal('cars-fe-employment-occupation', entry.occupation);
4428        setVal('cars-fe-employment-occupation-q', entry.occupation);
4429        setVal('cars-fe-employment-occupational-basis', entry.occupationalBasis);
4430        setVal('cars-fe-employment-occupational-type', entry.occupationalType);
4431        setVal('cars-fe-employment-years', entry.years);
4432        setVal('cars-fe-employment-months', entry.months);
4433        const hiddenStatus = document.getElementById('cars-fe-employment-status');
4434        if (hiddenStatus) {
4435            hiddenStatus.value = entry.status || '';
4436        }
4437        const pillGroup = getEmploymentUiHost()?.querySelector('.cars-fe-pills--employment');
4438        if (pillGroup) {
4439            pillGroup.querySelectorAll('.cars-fe-pill').forEach((p) => {
4440                p.classList.toggle('is-selected', (p.dataset.value || '') === (entry.status || ''));
4441            });
4442        }
4443        syncAllFinanceDurationSelectShells();
4444        syncEmploymentFormFieldLabelsForContext();
4445    };
4446
4447    const formatEmploymentDurationSummary = (yearsStr, monthsStr) => {
4448        const y = parseInt(String(yearsStr || ''), 10) || 0;
4449        const m = parseInt(String(monthsStr || ''), 10) || 0;
4450        const yPart = y === 1 ? '1 year' : `${y} years`;
4451        const mPart = m === 1 ? '1 month' : `${m} months`;
4452        if (y > 0 && m > 0) {
4453            return `${yPart}, ${mPart}`;
4454        }
4455        if (y > 0) {
4456            return yPart;
4457        }
4458        return mPart;
4459    };
4460
4461    const createEmploymentSavedFieldBlock = (labelText, valueText) => {
4462        const block = document.createElement('div');
4463        block.className = 'cars-fe-employment-saved-field';
4464        const labelRow = document.createElement('div');
4465        labelRow.className = 'cars-fe-employment-saved-field__label-row';
4466        const lab = document.createElement('p');
4467        lab.className = 'cars-fe-employment-saved-field__label';
4468        lab.textContent = labelText;
4469        labelRow.appendChild(lab);
4470        block.appendChild(labelRow);
4471        const valueEl = document.createElement('p');
4472        valueEl.className = 'cars-fe-employment-saved-field__value';
4473        valueEl.textContent = valueText || '';
4474        block.appendChild(valueEl);
4475        return block;
4476    };
4477
4478    const renderEmploymentSavedCards = () => {
4479        const mount = document.getElementById('cars-fe-employment-saved-mount');
4480        if (!mount) {
4481            return;
4482        }
4483        mount.textContent = '';
4484        savedEmploymentEntries.forEach((entry, index) => {
4485            const card = document.createElement('article');
4486            card.className = 'cars-fe-employment-saved-card';
4487
4488            const header = document.createElement('div');
4489            header.className = 'cars-fe-employment-saved-card__header';
4490            const title = document.createElement('p');
4491            title.className = 'cars-fe-employment-saved-card__title';
4492            title.textContent = index === 0 ? 'Current employment' : 'Previous employment';
4493            const editBtn = document.createElement('button');
4494            editBtn.type = 'button';
4495            editBtn.className = 'cars-fe-employment-saved-edit-button';
4496            editBtn.setAttribute('aria-label', 'Edit employment');
4497            editBtn.setAttribute('data-cars-fe-employment-edit', entry.id);
4498            const editIcon = document.createElement('span');
4499            editIcon.className = 'cars-fe-employment-saved-edit-button__icon';
4500            editBtn.appendChild(editIcon);
4501            header.appendChild(title);
4502            header.appendChild(editBtn);
4503            card.appendChild(header);
4504
4505            const rule = document.createElement('div');
4506            rule.className = 'cars-fe-employment-saved-card__rule';
4507            card.appendChild(rule);
4508
4509            const columns = document.createElement('div');
4510            columns.className = 'cars-fe-employment-saved-card__columns';
4511
4512            const colLeft = document.createElement('div');
4513            colLeft.className = 'cars-fe-employment-saved-card__col';
4514            colLeft.appendChild(createEmploymentSavedFieldBlock('Employers n
4514ame', entry.employerName));
4515            colLeft.appendChild(createEmploymentSavedFieldBlock('Occupation', entry.occupation));
4516            colLeft.appendChild(createEmploymentSavedFieldBlock('Occupational basis', entry.occupationalBasis));
4517
4518            const colRight = document.createElement('div');
4519            colRight.className = 'cars-fe-employment-saved-card__col';
4520            colRight.appendChild(createEmploymentSavedFieldBlock('Occupational type', entry.occupationalType));
4521            colRight.appendChild(
4522                createEmploymentSavedFieldBlock(
4523                    getEmploymentDurationQuestionLabel(entry.status),
4524                    formatEmploymentDurationSummary(entry.years, entry.months),
4525                ),
4526            );
4527
4528            columns.appendChild(colLeft);
4529            columns.appendChild(colRight);
4530            card.appendChild(columns);
4531            mount.appendChild(card);
4532        });
4533    };
4534
4535    const syncEmploymentSummaryUiState = () => {
4536        const host = getEmploymentUiHost();
4537        if (!host) {
4538            return;
4539        }
4540        const savedPanel = document.getElementById('cars-fe-employment-saved-panel');
4541        const addAnotherCard = document.getElementById('cars-fe-employment-add-another-card');
4542        const entryBlock = document.getElementById('cars-fe-employment-entry-block');
4543        const hasSaved = savedEmploymentEntries.length > 0;
4544
4545        renderEmploymentSavedCards();
4546
4547        const entryFlowVisible = savedEmploymentEntries.length === 0 || employmentFormMode === 'editing';
4548
4549        if (savedPanel) {
4550            savedPanel.hidden = !hasSaved;
4551        }
4552        if (addAnotherCard) {
4553            const historyIncomplete = getTotalEmploymentMonthsFromSavedEntries() < 36;
4554            const hideWhileAddingAnother = hasSaved && entryFlowVisible;
4555            addAnotherCard.hidden = !hasSaved || !historyIncomplete || hideWhileAddingAnother;
4556        }
4557        if (entryBlock) {
4558            entryBlock.hidden = !entryFlowVisible;
4559        }
4560        host.classList.toggle('cars-fe-employment--has-saved-summary', hasSaved);
4561        host.classList.toggle('cars-fe-employment--entry-flow-visible', entryFlowVisible);
4562        syncEmploymentFormFieldLabelsForContext();
4563        syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status'));
4564        syncEmploymentHistoryAlert();
4565    };
4566
4567    const getTotalAddressHistoryMonthsFromEntries = () =>
4568        savedAddressEntries.reduce((sum, e) => sum + (e.monthsAt || 0), 0);
4569
4570    const syncAddressHistoryAlert = () => {
4571        const alertEl = document.getElementById('cars-fe-address-history-alert');
4572        if (!alertEl) {
4573            return;
4574        }
4575        if (!addressStep || addressStep.hidden) {
4576            alertEl.hidden = true;
4577            return;
4578        }
4579        const savedSummaryPanel = document.getElementById('cars-fe-address-saved-summary-panel');
4580        const panelVisible = savedSummaryPanel && savedSummaryPanel.hidden === false;
4581        // Every saved address counts, whichever route added it, so the 3-year rule cannot be
4582        // sidestepped by an entry that carries no duration.
4583        const show =
4584            panelVisible &&
4585            savedAddressEntries.length > 0 &&
4586            getTotalAddressHistoryMonthsFromEntries() < 36;
4587        alertEl.hidden = !show;
4588    };
4589
4590    const formatYearsMonthsAtSummary = (yearsStr, monthsStr) => {
4591        const y = parseInt(String(yearsStr || ''), 10);
4592        const m = parseInt(String(monthsStr || ''), 10);
4593        if (Number.isNaN(y) || Number.isNaN(m)) {
4594            return '';
4595        }
4596        if (y === 15) {
4597            return '15+ years';
4598        }
4599        const yearLabel = String(y);
4600        if (m === 0) {
4601            return `${yearLabel} ${y === 1 ? 'year' : 'years'}`;
4602        }
4603        const yPart = `${yearLabel} ${y === 1 ? 'year' : 'years'}`;
4604        const mPart = `${m} ${m === 1 ? 'month' : 'months'}`;
4605        return `${yPart}, ${mPart}`;
4606    };
4607
4608    const parseLookupSelectedLineToEntryFields = (full, lookupPc) => {
4609        const parts = String(full || '')
4610            .split(',')
4611            .map((s) => s.trim())
4612            .filter(Boolean);
4613        let line1 = '';
4614        let townCounty = '';
4615        let postcode = lookupPc;
4616        if (parts.length === 0) {
4617            return { line1: '', line2: '', townCounty: '', postcode };
4618        }
4619        const last = parts[parts.length - 1];
4620        const lastCompact = last.replace(/\s+/g, '').toUpperCase();
4621        const lastLooksPc =
4622            lastCompact.length >= 5 &&
4623            lastCompact.length <= 8 &&
4624            /^[A-Z0-9]+$/.test(lastCompact);
4625        if (lastLooksPc && parts.length >= 2) {
4626            line1 = parts[0];
4627            townCounty = parts.slice(1, -1).join(', ').trim();
4628            postcode = last;
4629        } else {
4630            line1 = full;
4631            townCounty = '';
4632            postcode = lookupPc;
4633        }
4634        return { line1, line2: '', townCounty, postcode };
4635    };
4636
4637    /**
4638     * Legacy select fallback, used only when Postcoder is unavailable. Picking an address there
4639     * used to save a lookup entry with no residency or time at address, so the 3-year history rule
4640     * could never be satisfied and Next stayed disabled with nothing flagged. Mirror what Postcoder
4641     * does on selection instead: prefill the manual card from the chosen line and ask for the
4642     * duration.
4643     */
4644    const openManualCardForLookupSelection = (selectedLine) => {
4645        if (!addressStep || !String(selectedLine || '').trim()) {
4646            return;
4647        }
4648        const parsed = parseLookupSelectedLineToEntryFields(selectedLine, val('cars-fe-address-postcode'));
4649        const setFieldValue = (id, v) => {
4650            const el = document.getElementById(id);
4651            if (el) {
4652                el.value = v != null ? String(v) : '';
4653            }
4654        };
4655        setFieldValue('cars-fe-address-line1', parsed.line1);
4656        setFieldValue('cars-fe-address-line2', parsed.line2);
4657        setFieldValue('cars-fe-address-town', parsed.townCounty);
4658        setFieldValue('cars-fe-address-postcode-manual', parsed.postcode);
4659        if (!addressStep.dataset.carsFeManualWrapIntent) {
4660            addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUAL_TOGGLE;
4661        }
4662        setManualAddressMode(true);
4663        const residency = document.getElementById('cars-fe-address-residency');
4664        if (residency && typeof residency.focus === 'function') {
4665            try {
4666                residency.focus({ preventScroll: true });
4667            } catch (err) {
4668                residency.focus();
4669            }
4670        }
4671    };
4672
4673    const buildLookupEntryFromDom = () => {
4674        const full = val('cars-fe-address-select');
4675        const lookupPc = val('cars-fe-address-postcode').trim();
4676        const parsed = parseLookupSelectedLineToEntryFields(full, lookupPc);
4677        return {
4678            id: newAddressEntryId(),
4679            source: 'lookup',
4680            selectedLine: full,
4681            lookupPostcode: lookupPc,
4682            line1: parsed.line1,
4683            line2: parsed.line2,
4684            townCounty: parsed.townCounty,
4685            postcode: parsed.postcode,
4686            residency: '',
4687            years: 0,
4688            months: 0,
4689            monthsAt: 0,
4690            timeSummary: '',
4691        };
4692    };
4693
4694    const buildManualEntryFromForm = (idOverride) => {
4695        const yStr = val('cars-fe-address-years-at');
4696        const mStr = val('cars-fe-address-months-at');
4697        const y = yStr === '' ? 0 : parseInt(yStr, 10);
4698        const m = mStr === '' ? 0 : parseInt(mStr, 10);
4699        const years = Number.isNaN(y) ? 0 : y;
4700        // 15+ years: months are not applicable — store 0.
4701        const months = years >= 15 ? 0 : Number.isNaN(m) ? 0 : m;
4702        const town = val('cars-fe-address-town');
4703        const county = val('cars-fe-address-county');
4704        const townCounty = county ? `${town}, ${county}` : town;
4705        return {
4706            id: typeof idOverride === 'string' && idOverride.length > 0 ? idOverride : newAddressEntryId(),
4707            source: 'manual',
4708            line1: val('cars-fe-address-line1'),
4709            line2: val('cars-fe-address-line2'),
4710            townCounty,
4711            postcode: val('cars-fe-address-postcode-manual'),
4712            residency: val('cars-fe-address-residency'),
4713            years,
4714            months,
4715            monthsAt: years >= 15 ? 15 * 12 : years * 12 + months,
4716            timeSummary: formatYearsMonthsAtSummary(String(years), String(months)),
4717        };
4718    };
4719
4720    const clearManualAddressFormFields = () => {
4721        [
4722            'cars-fe-address-line1',
4723            'cars-fe-address-line2',
4724            'cars-fe-address-town',
4725            'cars-fe-address-postcode-manual',
4726            'cars-fe-address-county',
4727            'cars-fe-address-residency',
4728            'cars-fe-address-years-at',
4729            'cars-fe-address-months-at',
4730        ].forEach((id) => {
4731            const el = document.getElementById(id);
4732            if (el) {
4733                el.value = financeEligibilityFieldDefaultValue(id);
4734            }
4735        });
4736        syncAllFinanceDurationSelectShells();
4737        syncAddressMonthsDisabledForFifteenPlus();
4738    };
4739
4740    const applyManualEntryToForm = (entry) => {
4741        const setId = (id, v) => {
4742            const el = document.getElementById(id);
4743            if (el) {
4744                el.value = v != null ? String(v) : '';
4745            }
4746        };
4747        setId('cars-fe-address-line1', entry.line1);
4748        setId('cars-fe-address-line2', entry.line2);
4749        setId('cars-fe-address-postcode-manual', entry.postcode);
4750        setId('cars-fe-address-residency', entry.residency);
4751        setId('cars-fe-address-years-at', String(entry.years));
4752        setId('cars-fe-address-months-at', entry.years >= 15 ? '0' : String(entry.months));
4753        const townCounty = entry.townCounty || '';
4754        const commaIdx = townCounty.indexOf(',');
4755        if (commaIdx > 0) {
4756            setId('cars-fe-address-town', townCounty.slice(0, commaIdx).trim());
4757            setId('cars-fe-address-county', townCounty.slice(commaIdx + 1).trim());
4758        } else {
4759            setId('cars-fe-address-town', townCounty);
4760            setId('cars-fe-address-county', '');
4761        }
4762        syncAllFinanceDurationSelectShells();
4763        syncAddressMonthsDisabledForFifteenPlus();
4764    };
4765
4766    const placeAddressManualWrapHome = () => {
4767        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
4768        const home = document.querySelector('#cars-fe-step-address .cars-fe-form-stack--address');
4769        if (!manualWrap || !home) {
4770            return;
4771        }
4772        const entryBlock = document.getElementById('cars-fe-address-entry-block');
4773        if (entryBlock && entryBlock.parentElement === home) {
4774            // Only move when not already immediately after the entry block — reparenting steals input focus.
4775            if (manualWrap.previousElementSibling !== entryBlock || manualWrap.parentElement !== home) {
4776                entryBlock.after(manualWrap);
4777            }
4778            return;
4779        }
4780        if (manualWrap.parentElement !== home) {
4781            home.appendChild(manualWrap);
4782        }
4783    };
4784
4785    const placeAddressManualWrapForEdit = () => {
4786        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
4787        const slot = document.getElementById('cars-fe-address-edit-slot');
4788        if (!manualWrap) {
4789            return;
4790        }
4791        if (editingEntryId != null && slot) {
4792            if (manualWrap.parentElement !== slot) {
4793                slot.appendChild(manualWrap);
4794            }
4795            return;
4796        }
4797        placeAddressManualWrapHome();
4798    };
4799
4800    const getSavedAddressCardsRenderKey = () =>
4801        `${editingEntryId || ''}|${savedAddressEntries.map((e) => `${e.id}:${e.source}:${e.timeSummary || ''}:${e.line1 || ''}:${e.postcode || ''}`).join(';')}`;
4802
4803    let lastSavedAddressCardsRenderKey = null;
4804
4805    const renderSavedAddressCards = (force = false) => {
4806        const mount = document.getElementById('cars-fe-address-saved-cards');
4807        if (!mount) {
4808            return;
4809        }
4810        const nextKey = getSavedAddressCardsRenderKey();
4811        if (!force && nextKey === lastSavedAddressCardsRenderKey) {
4812            placeAddressManualWrapForEdit();
4813            return;
4814        }
4815        lastSavedAddressCardsRenderKey = nextKey;
4816        // Moving the edit form into a card slot — pull it out before clearing so we don't destroy it.
4817        if (mount.contains(document.getElementById('cars-fe-address-manual-wrap'))) {
4818            placeAddressManualWrapHome();
4819        }
4820        mount.textContent = '';
4821        savedAddressEntries.forEach((entry, index) => {
4822            if (editingEntryId && entry.id === editingEntryId) {
4823                const slot = document.createElement('div');
4824                slot.id = 'cars-fe-address-edit-slot';
4825                slot.className = 'cars-fe-address-edit-slot';
4826                slot.setAttribute(
4827                    'aria-label',
4828                    index === 0 ? 'Editing current address' : 'Editing previous address',
4829                );
4830                mount.appendChild(slot);
4831                return;
4832            }
4833            const card = document.createElement('article');
4834            card.className = `cars-fe-address-current-summary-card${entry.source === 'lookup' ? ' cars-fe-address-current-summary-card--lookup' : ''}`;
4835            const header = document.createElement('div');
4836            header.className = 'cars-fe-address-current-summary-card__header';
4837            const title = document.createElement('p');
4838            title.className = 'cars-fe-address-current-summary-card__title';
4839            title.textContent = index === 0 ? 'Current address' : 'Previous address';
4840            const editBtn = document.createElement('button');
4841            editBtn.type = 'button';
4842            editBtn.className = 'cars-fe-address-saved-edit-button';
4843            editBtn.setAttribute('aria-label', 'Edit address');
4844            editBtn.setAttribute('data-cars-fe-entry-edit', entry.id);
4845            const icon = document.createElement('span');
4846            icon.className = 'cars-fe-address-saved-edit-button__icon';
4847            editBtn.appendChild(icon);
4848            header.appendChild(title);
4849            header.appendChild(editBtn);
4850            card.appendChild(header);
4851            const rule = document.createElement('div');
4852            rule.className = 'cars-fe-address-current-summary-card__rule';
4853            card.appendChild(rule);
4854            const columns = document.createElement('div');
4855            columns.className = 'cars-fe-address-current-summary-card__columns';
4856            const colAddr = document.createElement('div');
4857            colAddr.className =
4858                'cars-fe-address-current-summary-card__col cars-fe-address-current-summary-card__col--address';
4859            const addrLabelRow = document.createElement('div');
4860            addrLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row';
4861            const addrLabel = document.createElement('p');
4862            addrLabel.className = 'cars-fe-address-current-summary-card__field-label';
4863            addrLabel.textContent = 'Address';
4864            addrLabelRow.appendChild(addrLabel);
4865            colAddr.appendChild(addrLabelRow);
4866            const lines = document.createElement('div');
4867            lines.className = 'cars-fe-saved-address-lines';
4868            const p1 = document.createElement('p');
4869            p1.className = 'cars-fe-saved-address-line';
4870            p1.textContent = entry.line1 || '';
4871            lines.appendChild(p1);
4872            if (entry.line2) {
4873                const p2 = document.createElement('p');
4874                p2.className = 'cars-fe-saved-address-line';
4875                p2.textContent = entry.line2;
4876                lines.appendChild(p2);
4877            }
4878            if (entry.townCounty) {
4879                const pt = document.createElement('p');
4880                pt.className = 'cars-fe-saved-address-line';
4881                pt.textContent = entry.townCounty;
4882                lines.appendChild(pt);
4883            }
4884            const ppc = document.createElement('p');
4885            ppc.className = 'cars-fe-saved-address-line';
4886            ppc.textContent = entry.postcode || '';
4887            lines.appendChild(ppc);
4888            colAddr.appendChild(lines);
4889            columns.appendChild(colAddr);
4890            const colMeta = document.createElement('div');
4891            colMeta.className =
4892                'cars-fe-address-current-summary-card__col cars-fe-address-current-summary-card__col--meta';
4893            const resBlock = document.createElement('div');
4894            resBlock.className = 'cars-fe-address-current-summary-card__meta-block';
4895            const resLabelRow = document.createElement('div');
4896            resLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row';
4897            const resLab = document.createElement('p');
4898            resLab.className = 'cars-fe-address-current-summary-card__field-label';
4899            resLab.textContent = 'Residency type';
4900            resLabelRow.appendChild(resLab);
4901            resBlock.appendChild(resLabelRow);
4902            const resVal = document.createElement('p');
4903            resVal.className = 'cars-fe-saved-meta-value';
4904            resVal.textContent = entry.residency || '';
4905            resBlock.appendChild(resVal);
4906            colMeta.appendChild(resBlock);
4907            const timeBlock = document.createElement('div');
4908            timeBlock.className = 'cars-fe-address-current-summary-card__meta-block';
4909            const timeLabelRow = document.createElement('div');
4910            timeLabelRow.className = 'cars-fe-address-current-summary-card__field-label-row';
4911            const timeLab = document.createElement('p');
4912            timeLab.className = 'cars-fe-address-current-summary-card__field-label';
4913            timeLab.textContent = 'Time at this address';
4914            timeLabelRow.appendChild(timeLab);
4915            timeBlock.appendChild(timeLabelRow);
4916            const timeVal = document.createElement('p');
4917            timeVal.className = 'cars-fe-saved-meta-value';
4918            timeVal.textContent = entry.timeSummary || '';
4919            timeBlock.appendChild(timeVal);
4920            colMeta.appendChild(timeBlock);
4921            columns.appendChild(colMeta);
4922            card.appendChild(columns);
4923            mount.appendChild(card);
4924        });
4925        placeAddressManualWrapForEdit();
4926    };
4927
4928    const populateAddressSelect = (lines) => {
4929        const selectEl = document.getElementById('cars-fe-address-select');
4930        if (!selectEl) {
4931            return;
4932        }
4933        selectEl.innerHTML = '';
4934        const ph = document.createElement('option');
4935        ph.value = '';
4936        ph.textContent = 'Select an address';
4937        selectEl.appendChild(ph);
4938        (lines || []).forEach((line) => {
4939            const o = document.createElement('option');
4940            o.value = line;
4941            o.textContent = line;
4942            selectEl.appendChild(o);
4943        });
4944    };
4945
4946    const MANUAL_WRAP_INTENT_MANUAL_TOGGLE = 'manual-toggle';
4947    const MANUAL_WRAP_INTENT_ADD_ANOTHER = 'add-another';
4948
4949    const addressEntryFlowShouldShow = () => {
4950        if (!addressStep) {
4951            return true;
4952        }
4953        if (savedAddressEntries.length === 0) {
4954            return true;
4955        }
4956        return String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true';
4957    };
4958
4959    const normalizeUkPostcodeKey = (pc) => String(pc || '').toUpperCase().replace(/\s+/g, '');
4960
4961    const setManualAddressMode = (manual) => {
4962        if (!addressStep) {
4963            return;
4964        }
4965        if (manual) {
4966            addressStep.classList.add('cars-fe-address--manual');
4967        } else {
4968            addressStep.classList.remove('cars-fe-address--manual');
4969            delete addressStep.dataset.carsFeManualWrapIntent;
4970        }
4971        validateAddress();
4972    };
4973
4974    const validateManualFormForSave = () =>
4975        Boolean(val('cars-fe-address-line1')) &&
4976        Boolean(val('cars-fe-address-town')) &&
4977        ukPostcodeLooksComplete(val('cars-fe-address-postcode-manual')) &&
4978        Boolean(val('cars-fe-address-residency')) &&
4979        durationYearsMonthsOk('cars-fe-address-years-at', 'cars-fe-address-months-at', {
4980            plusYearsValue: 15,
4981        });
4982
4983    const syncLookupEntryFromSelectIfApplicable = () => {
4984        if (!addressStep) {
4985            return;
4986        }
4987        const manualExpanded = addressStep.classList.contains('cars-fe-address--manual');
4988        const selectEl = document.getElementById('cars-fe-address-select');
4989        const selectedLine = selectEl ? String(selectEl.value || '').trim() : '';
4990        const selectComplete =
4991            ukPostcodeLooksComplete(val('cars-fe-address-postcode')) && Boolean(selectedLine);
4992        const entryFlowActive = String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true';
4993
4994        if (!selectComplete || manualExpanded) {
4995            if (
4996                savedAddressEntries.length === 1 &&
4997                savedAddressEntries[0].source === 'lookup' &&
4998                !selectComplete
4999            ) {
5000                savedAddressEntries = [];
5001            }
5002            return;
5003        }
5004
5005        const next = buildLookupEntryFromDom();
5006
5007        if (savedAddressEntries.length === 0) {
5008            savedAddressEntries = [next];
5009            return;
5010        }
5011
5012        if (entryFlowActive && savedAddressEntries.length >= 1) {
5013            const last = savedAddressEntries[savedAddressEntries.length - 1];
5014            if (
5015                last.source === 'lookup' &&
5016                last.selectedLine === next.selectedLine &&
5017                normalizeUkPostcodeKey(last.lookupPostcode) === normalizeUkPostcodeKey(next.lookupPostcode)
5018            ) {
5019                return;
5020            }
5021            savedAddressEntries.push(next);
5022            delete addressStep.dataset.carsFeAddressEntryActive;
5023            const pc = document.getElementById('cars-fe-address-postcode');
5024            if (pc) {
5025                pc.value = '';
5026            }
5027            populateAddressSelect([]);
5028            addressStep.classList.remove('cars-fe-address--manual');
5029            delete addressStep.dataset.carsFeManualWrapIntent;
5030            return;
5031        }
5032
5033        const existingId =
5034            savedAddressEntries.length > 0 && savedAddressEntries[0].source === 'lookup'
5035                ? savedAddressEntries[0].id
5036                : null;
5037        if (existingId) {
5038            next.id = existingId;
5039        }
5040        if (savedAddressEntries[0].source === 'lookup') {
5041            savedAddressEntries[0] = next;
5042        }
5043    };
5044
5045    const syncAddressSummaryUiState = () => {
5046        const savedSummaryPanel = document.getElementById('cars-fe-address-saved-summary-panel');
5047        if (!addressStep || addressStep.hidden) {
5048            if (savedSummaryPanel) {
5049                savedSummaryPanel.hidden = true;
5050            }
5051            renderSavedAddressCards();
5052            return;
5053        }
5054        syncLookupEntryFromSelectIfApplicable();
5055
5056        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
5057        const manualExpanded = addressStep.classList.contains('cars-fe-address--manual');
5058        const inEdit = editingEntryId != null && editingInsertIndex != null;
5059        const hasSaved =
5060            savedAddressEntries.length > 0 ||
5061            inEdit;
5062        const intent = addressStep.dataset.carsFeManualWrapIntent || '';
5063
5064        if (manualWrap) {
5065            let showManualWrap = false;
5066            if (inEdit) {
5067                // Editing a saved address: always show the form in that address's slot.
5068                showManualWrap = true;
5069            } else if (!hasSaved) {
5070                showManualWrap =
5071                    manualExpanded &&
5072                    (intent === MANUAL_WRAP_INTENT_MANUAL_TOGGLE || intent === MANUAL_WRAP_INTENT_ADD_ANOTHER);
5073            } else if (intent === MANUAL_WRAP_INTENT_MANUAL_TOGGLE) {
5074                showManualWrap = manualExpanded;
5075            } else if (intent === MANUAL_WRAP_INTENT_ADD_ANOTHER) {
5076                showManualWrap = manualExpanded && savedAddressEntries.length >= 1;
5077            }
5078            manualWrap.hidden = !showManualWrap;
5079        }
5080
5081        // While editing a saved address, hide lookup/add flow so only the in-place form is shown.
5082        const entryFlowVisible = inEdit ? false : addressEntryFlowShouldShow();
5083        const entryBlock = document.getElementById('cars-fe-address-entry-block');
5084        if (entryBlock) {
5085            entryBlock.hidden = !entryFlowVisible;
5086        }
5087        addressStep.classList.toggle('cars-fe-address--entry-flow-visible', entryFlowVisible);
5088        addressStep.classList.toggle('cars-fe-address--editing-saved', inEdit);
5089
5090        const deleteBtn = document.getElementById('cars-fe-address-manual-delete-button');
5091        if (deleteBtn) {
5092            deleteBtn.hidden = !inEdit;
5093        }
5094
5095        const addAnotherCardEl = document.querySelector(
5096            '#cars-fe-address-saved-summary-panel .cars-fe-address-add-another-card'
5097        );
5098        if (addAnotherCardEl) {
5099            const historyComplete = getTotalAddressHistoryMonthsFromEntries() >= 36;
5100            const hideWhileAddingAnother =
5101                savedAddressEntries.length > 0 && entryFlowVisible;
5102            addAnotherCardEl.hidden = !hasSaved || historyComplete || hideWhileAddingAnother || inEdit;
5103        }
5104
5105        addressStep.classList.toggle('cars-fe-address--has-saved-summary', hasSaved);
5106        const anyManual = savedAddressEntries.some((e) => e.source === 'manual');
5107        const anyLookup = savedAddressEntries.some((e) => e.source === 'lookup');
5108        addressStep.classList.toggle('cars-fe-address--manual-saved', anyManual);
5109        addressStep.classList.toggle('cars-fe-address--lookup-saved', anyLookup && !anyManual);
5110
5111        renderSavedAddressCards();
5112
5113        if (savedSummaryPanel) {
5114            savedSummaryPanel.hidden = !hasSaved;
5115        }
5116
5117        syncAddressSelectBlockVisibility();
5118    };
5119
5120    const runAddressLookup = () => {
5121        if (!addressStep || addressStep.hidden) {
5122            return;
5123        }
5124        const pc = val('cars-fe-address-postcode');
5125        if (!ukPostcodeLooksComplete(pc)) {
5126            populateAddressSelect([]);
5127            validateAddress();
5128            return;
5129        }
5130        if (typeof API === 'undefined' || !API.Stock || typeof API.Stock.LookupUkPostcodeAddresses !== 'function') {
5131            validateAddress();
5132            return;
5133        }
5134        API.Stock.LookupUkPostcodeAddresses(pc)
5135            .then((res) => {
5136                const lines = res.Addresses || res.addresses || [];
5137                populateAddressSelect(Array.isArray(lines) ? lines : []);
5138                validateAddress();
5139            })
5140            .catch(() => {
5141                populateAddressSelect([]);
5142                validateAddress();
5143            });
5144    };
5145
5146    const applyAddressFieldHighlights = () => {
5147        if (!addressStep) {
5148            return;
5149        }
5150        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
5151        const midEdit = editingEntryId != null && editingInsertIndex != null;
5152        const manualVisible = (manualWrap && manualWrap.hidden === false) || midEdit;
5153        const addressFieldIds = [
5154            'cars-fe-address-line1',
5155            'cars-fe-address-town',
5156            'cars-fe-address-postcode-manual',
5157            'cars-fe-address-residency',
5158            'cars-fe-address-years-at',
5159            'cars-fe-address-months-at',
5160            'cars-fe-address-postcode',
5161            'cars-fe-address-select',
5162        ];
5163        const mark = feStepFieldMarker('address');
5164        if (manualVisible) {
5165            addressFieldIds.forEach((id) =>
5165 financeEligibilityMarkFieldInvalid(id, false));
5166            mark('cars-fe-address-line1', !val('cars-fe-address-line1'));
5167            mark('cars-fe-address-town', !val('cars-fe-address-town'));
5168            syncLiveManualUkPostcodeFeedback();
5169            mark('cars-fe-address-residency', !val('cars-fe-address-residency'));
5170            const durationInvalid = !durationYearsMonthsOk(
5171                'cars-fe-address-years-at',
5172                'cars-fe-address-months-at',
5173                { plusYearsValue: 15 },
5174            );
5175            mark('cars-fe-address-years-at', durationInvalid);
5176            mark('cars-fe-address-months-at', durationInvalid);
5177            return;
5178        }
5179        addressFieldIds.forEach((id) => financeEligibilityMarkFieldInvalid(id, false));
5180        const entryBlock = document.getElementById('cars-fe-address-entry-block');
5181        if (!entryBlock || entryBlock.hidden) {
5182            return;
5183        }
5184        const selectElAddr = document.getElementById('cars-fe-address-select');
5185        const selectedLineAddr = selectElAddr ? String(selectElAddr.value || '').trim() : '';
5186        const postcoderActive = typeof PostcoderAutocomplete === 'function';
5187        // Legacy select path still needs a UK postcode; Postcoder search accepts address text.
5188        const selectComplete = postcoderActive
5189            ? false
5190            : ukPostcodeLooksComplete(val('cars-fe-address-postcode')) && Boolean(selectedLineAddr);
5191        mark(
5192            'cars-fe-address-postcode',
5193            postcoderActive
5194                ? false
5195                : !ukPostcodeLooksComplete(val('cars-fe-address-postcode')),
5196        );
5197        mark('cars-fe-address-select', !selectComplete);
5198    };
5199
5200    const validateAddress = () => {
5201        if (!addressNextBtn) {
5202            return false;
5203        }
5204        if (!addressStep || addressStep.hidden) {
5205            setBtnNextDisabled(addressNextBtn, true);
5206            return false;
5207        }
5208        syncAddressSummaryUiState();
5209
5210        const hasSavedForNext = savedAddressEntries.length > 0;
5211        const midEdit = editingEntryId != null && editingInsertIndex != null;
5212
5213        // 3 years of address history is required whatever route added the entries. A Postcoder
5214        // selection opens the manual card for residency and time at address, so it carries a
5215        // duration; the legacy select fallback does not, and must not skip the rule.
5216        const historyInsufficient = getTotalAddressHistoryMonthsFromEntries() < 36;
5217
5218        const ok = hasSavedForNext && !historyInsufficient && !midEdit;
5219
5220        applyAddressFieldHighlights();
5221        setBtnNextDisabled(addressNextBtn, !ok);
5222        syncAddressHistoryAlert();
5223        return ok;
5224    };
5225
5226    const hideContactStep = () => {
5227        if (contactStep) {
5228            contactStep.hidden = true;
5229        }
5230    };
5231
5232    const hideMoreStep = () => {
5233        if (moreStep) {
5234            moreStep.hidden = true;
5235        }
5236    };
5237
5238    const hidePersonalGroupedStep = () => {
5239        if (personalGroupedStep) {
5240            personalGroupedStep.hidden = true;
5241        }
5242    };
5243
5244    const hideIncomeOutgoingsGroupedStep = () => {
5245        if (incomeOutgoingsGroupedStep) {
5246            incomeOutgoingsGroupedStep.hidden = true;
5247        }
5248    };
5249
5250    const cancelUnsavedAddressEdit = () => {
5251        const wasEditing = editingEntryId != null || editingInsertIndex != null;
5252        const wasAdding =
5253            addressStep &&
5254            String(addressStep.dataset.carsFeAddressEntryActive || '') === 'true' &&
5255            !wasEditing;
5256        if (!wasEditing && !wasAdding) {
5257            return;
5258        }
5259        editingEntryId = null;
5260        editingInsertIndex = null;
5261        clearManualAddressFormFields();
5262        placeAddressManualWrapHome();
5263        addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved');
5264        if (addressStep) {
5265            delete addressStep.dataset.carsFeManualWrapIntent;
5266            delete addressStep.dataset.carsFeAddressEntryActive;
5267        }
5268        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
5269        if (manualWrap) {
5270            manualWrap.hidden = true;
5271        }
5272    };
5273
5274    const hideAddressStep = () => {
5275        if (addressStep) {
5276            cancelUnsavedAddressEdit();
5277            addressStep.classList.remove('cars-fe-address-suggestions-open', 'cars-fe-duration-dropdown-open');
5278            addressStep.hidden = true;
5279        }
5280    };
5281
5282    const hideEmploymentStep = () => {
5283        if (employmentStep) {
5284            employmentStep.hidden = true;
5285        }
5286    };
5287
5288    const hideIncomeOutgoingsStep = () => {
5289        if (incomeOutgoingsStep) {
5290            incomeOutgoingsStep.hidden = true;
5291        }
5292    };
5293
5294    const hideOutgoingsFinalStep = () => {
5295        if (outgoingsFinalStep) {
5296            outgoingsFinalStep.hidden = true;
5297        }
5298    };
5299
5300    const hideReviewStep = () => {
5301        if (reviewStep) {
5302            reviewStep.hidden = true;
5303        }
5304    };
5305
5306    const setReviewText = (id, text) => {
5307        const el = document.getElementById(id);
5308        if (!el) {
5309            return;
5310        }
5311        const s = text != null ? String(text).trim() : '';
5312        el.textContent = s === '' ? '—' : s;
5313    };
5314
5315    const setReviewRowHidden = (rowId, visible) => {
5316        const row = document.getElementById(rowId);
5317        if (row) {
5318            row.hidden = !visible;
5319        }
5320    };
5321
5322    const formatPhoneForReview = (dial, national) => {
5323        const d = String(dial || '').trim();
5324        const n = String(national || '').trim();
5325        if (!d && !n) {
5326            return '';
5327        }
5328        if (d && n) {
5329            return `+${d.replace(/^\+/, '')} ${n}`;
5330        }
5331        return d || n;
5332    };
5333
5334    const formatAddressEntryLines = (entry) => {
5335        if (!entry) {
5336            return '';
5337        }
5338        if (entry.source === 'lookup') {
5339            const line = (entry.selectedLine || '').trim();
5340            if (line) {
5341                return line;
5342            }
5343        }
5344        const bits = [entry.line1, entry.line2, entry.townCounty, entry.postcode]
5345            .map((x) => (x != null ? String(x).trim() : ''))
5346            .filter(Boolean);
5347        return bits.join('\n');
5348    };
5349
5350    const appendReviewSummaryKvRow = (parent, labelText, valueText) => {
5351        const row = document.createElement('div');
5352        row.className = 'cars-fe-review-kv';
5353        const lab = document.createElement('div');
5354        lab.className = 'cars-fe-review-kv__label';
5355        lab.textContent = labelText;
5356        const valCell = document.createElement('div');
5357        valCell.className = 'cars-fe-review-kv__value';
5358        const span = document.createElement('span');
5359        const v = valueText != null ? String(valueText).trim() : '';
5360        span.textContent = v === '' ? '—' : v;
5361        valCell.appendChild(span);
5362        const rule = document.createElement('div');
5363        rule.className = 'cars-fe-review-kv__rule';
5364        row.appendChild(lab);
5365        row.appendChild(valCell);
5366        row.appendChild(rule);
5367        parent.appendChild(row);
5368    };
5369
5370    const populateFinanceEligibilityReview = () => {
5371        const p = collectJourneyDetailPayload();
5372
5373        setReviewText('cars-fe-review-personal-title', p.title);
5374        setReviewText('cars-fe-review-personal-first', p.firstName);
5375        setReviewText('cars-fe-review-personal-last', p.lastName);
5376        setReviewText('cars-fe-review-personal-email', p.email);
5377        setReviewText('cars-fe-review-personal-phone', formatPhoneForReview(p.phoneDial, p.phoneNational));
5378        setReviewText('cars-fe-review-personal-dob', p.dateOfBirth);
5379        setReviewText('cars-fe-review-personal-license', p.licenseType);
5380
5381        setReviewText('cars-fe-review-other-marital', p.maritalStatus);
5382        setReviewText('cars-fe-review-other-dependants', p.numberOfDependants);
5383
5384        const addressMount = document.getElementById('cars-fe-review-address-mount');
5385        if (addressMount) {
5386            addressMount.textContent = '';
5387            savedAddressEntries.forEach((entry, index) => {
5388                const lines = formatAddressEntryLines(entry);
5389                if (!lines) {
5390                    return;
5391                }
5392                const card = document.createElement('div');
5393                card.className = 'cars-fe-review-summary-card';
5394                if (savedAddressEntries.length > 1) {
5395                    appendReviewSummaryKvRow(
5396                        card,
5397                        'Address period:',
5398                        index === 0 ? 'Current' : 'Previous',
5399                    );
5400                }
5401                appendReviewSummaryKvRow(card, 'Address:', lines);
5402                appendReviewSummaryKvRow(
5403                    card,
5404                    'Residency:',
5405                    (entry.residency && String(entry.residency).trim()) || '—',
5406                );
5407                const duration =
5408                    entry.timeSummary ||
5409                    formatYearsMonthsAtSummary(String(entry.years), String(entry.months)) ||
5410                    '—';
5411                appendReviewSummaryKvRow(card, 'Time at Address:', duration);
5412                addressMount.appendChild(card);
5413            });
5414        }
5415
5416        const empMount = document.getElementById('cars-fe-review-employment-mount');
5417        if (empMount) {
5418            empMount.textContent = '';
5419            savedEmploymentEntries.forEach((entry, empIndex) => {
5420                const card = document.createElement('div');
5421                card.className = 'cars-fe-review-summary-card';
5422                const categoryValue =
5423                    (entry.occupationalType && String(entry.occupationalType).trim()) ||
5424                    (entry.occupationalBasis && String(entry.occupationalBasis).trim()) ||
5425                    '—';
5426                if (savedEmploymentEntries.length > 1) {
5427                    appendReviewSummaryKvRow(
5428                        card,
5429                        'Employment period:',
5430                        empIndex === 0 ? 'Current' : 'Previous',
5431                    );
5432                }
5433                appendReviewSummaryKvRow(card, 'Employment Status:', entry.status || '—');
5434                appendReviewSummaryKvRow(card, 'Employers name:', entry.employerName || '—');
5435                appendReviewSummaryKvRow(card, 'Occupation:', entry.occupation || '—');
5436                appendReviewSummaryKvRow(card, 'Employment category:', categoryValue);
5437                appendReviewSummaryKvRow(
5438                    card,
5439                    'Time with employer:',
5440                    formatEmploymentDurationSummary(entry.years, entry.months),
5441                );
5442                empMount.appendChild(card);
5443            });
5444        }
5445
5446        setReviewText(
5447            'cars-fe-review-income-annual',
5448            formatSterlingForReview(p.totalAnnualIncomeBeforeTax) || p.totalAnnualIncomeBeforeTax,
5449        );
5450        setReviewText(
5451            'cars-fe-review-income-takehome',
5452            formatSterlingForReview(p.typicalMonthlyTakeHomePay) || p.typicalMonthlyTakeHomePay,
5453        );
5454        setReviewText(
5455            'cars-fe-review-income-household',
5456            formatSterlingForReview(p.otherAnnualHouseholdIncome) || p.otherAnnualHouseholdIncome,
5457        );
5458        setReviewText('cars-fe-review-income-repay', p.repayExistingVehicleFinanceAgreement);
5459        setReviewText(
5460            'cars-fe-review-income-mortgage',
5461            formatSterlingForReview(p.monthlyMortgageRentShare) || p.monthlyMortgageRentShare,
5462        );
5463        setReviewText(
5464            'cars-fe-review-income-bills',
5465            formatSterlingForReview(p.otherTotalMonthlyBills) || p.otherTotalMonthlyBills,
5466        );
5467
5468        const repayYes = p.repayExistingVehicleFinanceAgreement === 'Yes';
5469        setReviewRowHidden('cars-fe-review-row-existing-loan', repayYes);
5470        if (repayYes) {
5471            setReviewText(
5472                'cars-fe-review-income-existing-loan',
5473                formatSterlingForReview(p.existingVehicleLoanMonthlyPayment) ||
5474                    p.existingVehicleLoanMonthlyPayment,
5475            );
5476        }
5477
5478        const downturnYes = p.foreseeFinancialDownturn === 'Yes';
5479        setReviewRowHidden('cars-fe-review-row-downturn-reason', downturnYes);
5480        setReviewRowHidden('cars-fe-review-row-downturn-income', downturnYes);
5481        setReviewText('cars-fe-review-income-downturn-yn', p.foreseeFinancialDownturn);
5482        if (downturnYes) {
5483            setReviewText('cars-fe-review-income-downturn-reason', p.expectedDownturnReason);
5484            setReviewText(
5485                'cars-fe-review-income-downturn-reduced',
5486                formatSterlingForReview(p.reducedAnnualIncomeBeforeTax) || p.reducedAnnualIncomeBeforeTax,
5487            );
5488        }
5489    };
5490
5491    const syncReviewConsentError = (invalid) => {
5492        const row = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row');
5493        const card = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__card');
5494        const err = document.getElementById('cars-fe-review-consent-error');
5495        const show = Boolean(invalid);
5496        if (row) {
5497            row.classList.toggle('cars-fe-review-consent__row--invalid', show);
5498        }
5499        if (card) {
5500            card.classList.toggle('cars-fe-review-consent__card--invalid', show);
5501        }
5502        if (err) {
5503            err.hidden = !show;
5504            err.setAttribute('aria-hidden', show ? 'false' : 'true');
5505        }
5506    };
5507
5508    const validateReviewConsent = () => {
5509        if (!reviewViewResultsBtn || !reviewStep || reviewStep.hidden) {
5510            return false;
5511        }
5512        const ok = Boolean(reviewConsentCb && reviewConsentCb.checked);
5513        setBtnNextDisabled(reviewViewResultsBtn, !ok);
5514        if (ok) {
5515            syncReviewConsentError(false);
5516        }
5517        return ok;
5518    };
5519
5520    const prepareEditFromReview = () => {
5521        hideReviewStep();
5522        if (intro) {
5523            intro.hidden = true;
5524        }
5525        hideAgreementView();
5526        hideContactStep();
5527        hideMoreStep();
5528        hidePersonalGroupedStep();
5529        hideAddressStep();
5530        hideEmploymentStep();
5531        hideIncomeOutgoingsStep();
5532        hideOutgoingsFinalStep();
5533        hideIncomeOutgoingsGroupedStep();
5534        if (detailsStep) {
5535            detailsStep.hidden = true;
5536        }
5537    };
5538
5539    const jumpToReviewEdit = (target) => {
5540        const key = String(target || '').trim();
5541        if (!key) {
5542            return;
5543        }
5544        prepareEditFromReview();
5545        reviewEditReturnPending = true;
5546        state.dirty = true;
5547        if ((key === 'details' || key === 'more') && canRouteToGroupedSteps(personalGroupedStep)) {
5548            showPersonalGroupedStep();
5549            return;
5550        }
5551        if (key === 'details' && detailsStep) {
5552            detailsStep.hidden = false;
5553            state.screen = 'details';
5554            scrollToTop(detailsStep);
5555            validateDetails();
5556            syncFinanceEligibilityJourneyUrl();
5557            return;
5558        }
5559        if (key === 'more' && moreStep && contactStep) {
5560            contactStep.hidden = true;
5561            moreStep.hidden = false;
5562            state.screen = 'more';
5563            scrollToTop(moreStep);
5564            validateMore();
5565            syncFinanceEligibilityJourneyUrl();
5566            return;
5567        }
5568        if (key === 'address') {
5569            showAddressStep();
5570            return;
5571        }
5572        if ((key === 'employment' || key === 'income') && canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) {
5573            showIncomeOutgoingsGroupedStep();
5574            return;
5575        }
5576        if (key === 'employment') {
5577            showEmploymentStep();
5578            return;
5579        }
5580        if (key === 'income') {
5581            showIncomeOutgoingsStep();
5582            return;
5583        }
5584    };
5585
5586    const showReviewStep = () => {
5587        if (!reviewStep) {
5588            return;
5589        }
5590        clearReviewEditReturnPending();
5591        hideAgreementView();
5592        if (intro) {
5593            intro.hidden = true;
5594        }
5595        if (detailsStep) {
5596            detailsStep.hidden = true;
5597        }
5598        hideContactStep();
5599        hideMoreStep();
5600        hidePersonalGroupedStep();
5601        hideAddressStep();
5602        hideEmploymentStep();
5603        hideIncomeOutgoingsStep();
5604        hideOutgoingsFinalStep();
5605        hideIncomeOutgoingsGroupedStep();
5606        reviewStep.hidden = false;
5607        if (reviewConsentCb) {
5608            reviewConsentCb.checked = false;
5609        }
5610        syncReviewConsentError(false);
5611        populateFinanceEligibilityReview();
5612        syncReviewConsentLenderName();
5613        validateReviewConsent();
5614        state.screen = 'review';
5615        state.dirty = true;
5616        scrollToTop(reviewStep);
5617        syncFinanceEligibilityJourneyUrl();
5618    };
5619
5620    const collectJourneyDetailPayload = () => {
5621        const lookupEntry = savedAddressEntries.find((x) => x.source === 'lookup');
5622        const flatManual = [...savedAddressEntries].reverse().find((x) => x.source === 'manual');
5623        const useManual = Boolean(flatManual);
5624        const phoneCountry = val('cars-fe-phone-country');
5625        const phoneDial = val('cars-fe-phone-dial');
5626        const phoneNationalRaw = val('cars-fe-phone-national');
5627        return {
5628            title: val('cars-fe-title'),
5629            firstName: val('cars-fe-first-name'),
5630            lastName: val('cars-fe-last-name'),
5631            dateOfBirth: formatIsoDobToUk(val('cars-fe-dob')),
5632            maritalStatus: val('cars-fe-marital'),
5633            email: val('cars-fe-email'),
5634            phoneDial,
5635            phoneCountry,
5636            phoneNational: normalizeUkNationalForPayload(phoneCountry, phoneDial, phoneNationalRaw),
5637            licenseType: val('cars-fe-license-type'),
5638            numberOfDependants: val('cars-fe-dependants'),
5639            addressLookupPostcode: val('cars-fe-address-postcode'),
5640            addressSelectedLine: lookupEntry ? lookupEntry.selectedLine : val('cars-fe-address-select'),
5641            addressManual: useManual,
5642            addressLine1: flatManual ? flatManual.line1 : '',
5643            addressLine2: flatManual ? flatManual.line2 : '',
5644            addressTown: flatManual
5645                ? flatManual.townCounty.includes(',')
5646                    ? flatManual.townCounty.split(',')[0].trim()
5647                    : flatManual.townCounty.trim()
5648                : '',
5649            addressCounty:
5650                flatManual && flatManual.townCounty.includes(',')
5651                    ? flatManual.townCounty.split(',').slice(1).join(',').trim()
5652                    : '',
5653            addressPostcode: flatManual ? flatManual.postcode : '',
5654            addressResidencyType: flatManual ? flatManual.residency : '',
5655            addressYearsAt: flatManual ? String(flatManual.years) : '',
5656            addressMonthsAt: flatManual ? String(flatManual.months) : '',
5657            employmentStatus: savedEmploymentEntries[0] ? savedEmploymentEntries[0].status : '',
5658            employmentEmployerName: savedEmploymentEntries[0] ? savedEmploymentEntries[0].employerName : '',
5659            employmentOccupation: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupation : '',
5660            employmentOccupationalBasis: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupationalBasis : '',
5661            employmentOccupationalType: savedEmploymentEntries[0] ? savedEmploymentEntries[0].occupationalType : '',
5662            employmentPhoneDial: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneDial : '',
5663            employmentPhoneCountry: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneCountry : '',
5664            employmentPhoneNational: savedEmploymentEntries[0] ? savedEmploymentEntries[0].phoneNational : '',
5665            employmentYears: savedEmploymentEntries[0] ? savedEmploymentEntries[0].years : '',
5666            employmentMonths: savedEmploymentEntries[0] ? savedEmploymentEntries[0].months : '',
5667            employmentPastEmployerName: savedEmploymentEntries[0] ? savedEmploymentEntries[0].pastEmployerName : '',
5668            employmentPastOccupation: savedEmploymentEntries[0] ? savedEmploymentEntries[0].pastOccupation : '',
5669            savedEmployment: savedEmploymentEntries.map((emp) => ({
5670                status: emp.status,
5671                employerName: emp.employerName,
5672                occupation: emp.occupation,
5673                occupationalBasis: emp.occupationalBasis,
5674                occupationalType: emp.occupationalType,
5675                phoneDial: emp.phoneDial,
5676                phoneCountry: emp.phoneCountry,
5677                phoneNational: emp.phoneNational,
5678                years: emp.years,
5679                months: emp.months,
5680                pastEmployerName: emp.pastEmployerName || '',
5681                pastOccupation: emp.pastOccupation || '',
5682            })),
5683            savedAddresses: savedAddressEntries.map((a) => ({
5684                source: a.source,
5685                line1: a.line1,
5686                line2: a.line2,
5687                townCounty: a.townCounty,
5688                postcode: a.postcode,
5689                residency: a.residency,
5690                years: a.years,
5691                months: a.months,
5692                monthsAt: a.monthsAt,
5693                selectedLine: a.selectedLine || '',
5694                lookupPostcode: a.lookupPostcode || '',
5695            })),
5696            totalAnnualIncomeBeforeTax: stripSterlingCommas(val('cars-fe-income-annual-before-tax')),
5697            typicalMonthlyTakeHomePay: stripSterlingCommas(val('cars-fe-income-monthly-take-home')),
5698            otherAnnualHouseholdIncome: stripSterlingCommas(val('cars-fe-income-annual-household-other')),
5699            monthlyMortgageRentShare: stripSterlingCommas(val('cars-fe-outgoings-monthly-mortgage-rent-share')),
5700            otherTotalMonthlyBills: stripSterlingCommas(val('cars-fe-outgoings-other-monthly-bills')),
5701            repayExistingVehicleFinanceAgreement: val('cars-fe-outgoings-repay-existing-finance'),
5702            existingVehicleLoanMonthlyPayment: stripSterlingCommas(val('cars-fe-outgoings-existing-loan-monthly-payment')),
5703            foreseeFinancialDownturn: val('cars-fe-outgoings-downturn-expected'),
5704            expectedDownturnReason: val('cars-fe-outgoings-downturn-reason'),
5705            reducedAnnualIncomeBeforeTax: stripSterlingCommas(val('cars-fe-outgoings-reduced-annual-income-before-tax')),
5706        };
5707    };
5708
5709    /** Mirrors server IvendiFinanceScanPayloadBuilder.ValidateJourneyForIvendiFinanceScan — keeps iVendi-bound fields complete before POST. */
5710    const getFinanceEligibilityIvendiFieldErrors = (p) => {
5711        const errors = [];
5712        const need = (label, v) => {
5713            if (!String(v ?? '').trim()) {
5714                errors.push(`${label} is required.`);
5715            }
5716        };
5717
5718        need('Title', p.title);
5719        need('First name', p.firstName);
5720        need('Last name', p.lastName);
5721        need('Date of birth', p.dateOfBirth);
5722        need('Email', p.email);
5723        need('Driving licence type', p.licenseType);
5724        need('Mobile number', p.phoneNational);
5725        need('Marital status', p.maritalStatus);
5726        need('Number of dependants', p.numberOfDependants);
5727
5728        const addrs = Array.isArray(p.savedAddresses) ? p.savedAddresses : [];
5729        if (addrs.length > 0) {
5730            addrs.forEach((a, i) => {
5731                const n = i + 1;
5732                need(`Address ${n} line 1`, a.line1);
5733                need(`Address ${n} postcode`, a.postcode);
5734                need(`Address ${n} town`, a.townCounty);
5735                let mo = Number(a.monthsAt);
5736                if (!mo || Number.isNaN(mo)) {
5737                    const y = parseInt(String(a.years ?? ''), 10) || 0;
5738                    const mm = parseInt(String(a.months ?? ''), 10) || 0;
5739                    mo = y >= 15 ? 15 * 12 : y * 12 + mm;
5740                }
5741                const yearsN = parseInt(String(a.years ?? ''), 10) || 0;
5742                if (yearsN < 15 && (!mo || mo <= 0)) {
5743                    errors.push(`Address ${n}: time at address is required.`);
5744                }
5745                if (String(a.source || '').toLowerCase() === 'manual' && !String(a.residency || '').trim()) {
5746                    errors.push(`Address ${n} residency type is required.`);
5747                }
5748            });
5749        } else {
5750            need('Address line 1', p.addressLine1);
5751            need('Address town', p.addressTown);
5752            need('Address postcode', p.addressPostcode);
5753            need('Address residency type', p.addressResidencyType);
5754            const y = parseInt(String(p.addressYearsAt ?? ''), 10) || 0;
5755            const mm = parseInt(String(p.addressMonthsAt ?? ''), 10) || 0;
5756            if (y < 15 && y * 12 + mm <= 0) {
5757                errors.push('Time at current address is required.');
5758            }
5759        }
5760
5761        const empArr = Array.isArray(p.savedEmployment) ? p.savedEmployment : [];
5762        if (empArr.length > 0) {
5763            const e0 = empArr[0];
5764            need('Employment status', e0.status);
5765            need('Employer name', e0.employerName);
5766            need('Occupation', e0.occupation);
5767            need('Occupational basis', e0.occupationalBasis);
5768            need('Occupational type', e0.occupationalType);
5769            const y = parseInt(String(e0.years ?? ''), 10) || 0;
5770            const m = parseInt(String(e0.months ?? ''), 10) || 0;
5771            if (y * 12 + m <= 0) {
5772                errors.push('Time in current employment is required.');
5773            } else if (y === 0 && m === 0) {
5774                errors.push('Time in current employment is required.');
5775            }
5776        } else {
5777            need('Employment status', p.employmentStatus);
5778            need('Employer name', p.employmentEmployerName);
5779            need('Occupation', p.employmentOccupation);
5780            need('Occupational basis', p.employmentOccupationalBasis);
5781            need('Occupational type', p.employmentOccupationalType);
5782            const y = parseInt(String(p.employmentYears ?? ''), 10) || 0;
5783            const m = parseInt(String(p.employmentMonths ?? ''), 10) || 0;
5784            if (y * 12 + m <= 0) {
5785                errors.push('Time in current employment is required.');
5786            }
5787        }
5788
5789        const gross = parseSterlingInputToNumber(p.totalAnnualIncomeBeforeTax);
5790        const netMonthly = parseSterlingInputToNumber(p.typicalMonthlyTakeHomePay);
5791        const otherAnnual = parseSterlingInputToNumber(p.otherAnnualHouseholdIncome);
5792        if (Number.isNaN(gross) || gross <= 0) {
5793            errors.push('Gross annual income before tax must be greater than zero.');
5794        }
5795        if (Number.isNaN(netMonthly) || netMonthly <= 0) {
5796            errors.push('Typical monthly take-home pay must be greater than zero.');
5797        }
5798        if (Number.isNaN(otherAnnual) || otherAnnual < 0) {
5799            errors.push('Other annual household income cannot be negative.');
5800        }
5801
5802        const mortgage = parseSterlingInputToNumber(p.monthlyMortgageRentShare);
5803        const bills = parseSterlingInputToNumber(p.otherTotalMonthlyBills);
5804        if (Number.isNaN(mortgage) || mortgage < 0) {
5805            errors.push('Monthly mortgage or rent share cannot be negative.');
5806        }
5807        if (Number.isNaN(bills) || bills < 0) {
5808            errors.push('Other monthly bills cannot be negative.');
5809        }
5810
5811        const repay = String(p.repayExistingVehicleFinanceAgreement || '').trim();
5812        if (repay !== 'Yes' && repay !== 'No') {
5813            errors.push('Please answer whether you will repay an existing vehicle finance agreement.');
5814        }
5815        if (repay === 'Yes') {
5816            const existingLoan = parseSterlingInputToNumber(p.existingVehicleLoanMonthlyPayment);
5817            if (Number.isNaN(existingLoan) || existingLoan < 0) {
5818                errors.push('Current monthly payment on your existing loan must be zero or more.');
5819            }
5820        }
5821
5822        const downturn = String(p.foreseeFinancialDownturn || '').trim();
5823        if (downturn !== 'Yes' && downturn !== 'No') {
5824            errors.push('Please answer whether you foresee a downturn in your finances.');
5825        }
5826        if (downturn === 'Yes') {
5827            need('Expected downturn reason', p.expectedDownturnReason);
5828            const reduced = parseSterlingInputToNumber(p.reducedAnnualIncomeBeforeTax);
5829            if (Number.isNaN(reduced) || reduced < 0) {
5830                errors.push('Reduced annual income (if downturn) cannot be negative.');
5831            }
5832        }
5833
5834        return errors;
5835    };
5836
5837    const hideAgreementView = () => {
5838        if (agreementView) {
5839            agreementView.hidden = true;
5840        }
5841    };
5842
5843    const formatHpCurrency = (v) => {
5844        const n = Number(v);
5845        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5846            return null;
5847        }
5848        const isWhole = Math.abs(n - Math.round(n)) < 1e-9;
5849        return `£${n.toLocaleString('en-GB', {
5850            minimumFractionDigits: isWhole ? 0 : 2,
5851            maximumFractionDigits: isWhole ? 0 : 2,
5852        })}`;
5853    };
5854
5855    const getActiveFinanceQuoteTypeKey = () => {
5856        const financePageQuote = getFinancePageQuoteSnapshot();
5857        if (financePageQuote?.quoteType) {
5858            return financePageQuote.quoteType === 'PCP' ? 'PCP' : 'HP';
5859        }
5860        const tab = document.querySelector('.tab-content-container');
5861        const current = String(tab?.getAttribute('currentTab') || 'HP').trim().toUpperCase();
5862        return current === 'PCP' ? 'PCP' : 'HP';
5863    };
5864
5865    const getFinancePageQuoteSnapshot = () => {
5866        const raw = panel.getAttribute('data-fe-finance-page-quote');
5867        if (!raw) {
5868            return null;
5869        }
5870        try {
5871            return JSON.parse(raw);
5872        } catch {
5873            return null;
5874        }
5875    };
5876
5877    const financeQuoteValue = (elementId) => {
5878        const el = document.getElementById(elementId);
5879        if (el) {
5880            const raw = el.getAttribute(`${getActiveFinanceQuoteTypeKey()}Value`);
5881            if (raw != null && raw !== '' && raw !== 'undefined') {
5882                return raw;
5883            }
5884        }
5885        return null;
5886    };
5887
5888    const calculatorQuoteValue = (elementId, snapshotKey) => {
5889        // Listing/special-offer CTA binds quote onto data-fe-finance-page-quote for a different
5890        // stock than the VDP calculator — prefer that snapshot so we do not show the page car's figures.
5891        if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true') {
5892            const snapshot = getFinancePageQuoteSnapshot();
5893            if (snapshot && snapshot[snapshotKey] != null && snapshot[snapshotKey] !== '') {
5894                return String(snapshot[snapshotKey]);
5895            }
5896            return null;
5897        }
5898        const fromVdp = financeQuoteValue(elementId);
5899        if (fromVdp != null) {
5900            return fromVdp;
5901        }
5902        const snapshot = getFinancePageQuoteSnapshot();
5903        if (!snapshot || snapshot[snapshotKey] == null || snapshot[snapshotKey] === '') {
5904            return null;
5905        }
5906        return String(snapshot[snapshotKey]);
5907    };
5908
5909    const financeTypeLabel = () =>
5910        getActiveFinanceQuoteTypeKey() === 'PCP' ? 'Personal Contract Purchase' : 'Hire Purchase';
5911
5912    const setAgreementText = (id, text) => {
5913        const el = document.getElementById(id);
5914        if (el && text != null && String(text).trim() !== '') {
5915            el.textContent = text;
5916        }
5917    };
5918
5919    const setAgreementRowHidden = (rowId, hidden) => {
5920        const row = document.getElementById(rowId);
5921        if (row) {
5922            row.hidden = !!hidden;
5923        }
5924    };
5925
5926    const formatAgreementApr = (v) => {
5927        const n = Number(v);
5928        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5929            return null;
5930        }
5931        return `APR ${n.toFixed(1)}%`;
5932    };
5933
5934    const formatAgreementPercent = (v) => {
5935        const n = Number(v);
5936        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5937            return null;
5938        }
5939        return `${n.toFixed(2)}%`;
5940    };
5941
5942    const formatAgreementMileage = (v) => {
5943        const n = Math.round(Number(v));
5944        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5945            return null;
5946        }
5947        return n.toLocaleString('en-GB');
5948    };
5949
5950    const formatAgreementExcess = (v) => {
5951        const n = Number(v);
5952        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5953            return null;
5954        }
5955        return `${n}p`;
5956    };
5957
5958    const formatAgreementCurrency = (v) => {
5959        const n = Number(v);
5960        if (v === undefined || v === null || v === '' || Number.isNaN(n)) {
5961            return null;
5962        }
5963        return `£${n.toLocaleString('en-GB', {
5964            minimumFractionDigits: 2,
5965            maximumFractionDigits: 2,
5966        })}`;
5967    };
5968
5969    const syncEligibilityAgreementRepresentative = () => {
5970        const mount = document.getElementById('finance-eligibility-rep-details');
5971        if (!mount) {
5972            return;
5973        }
5974
5975        const typeLabel = financeTypeLabel();
5976        const quoteType = getActiveFinanceQuoteTypeKey();
5977        const headline =
5978            document.querySelector('.finance-eligibility-agreement__vehicle-title')?.textContent?.trim() ||
5979            panel?.getAttribute('data-fe-stock-headline') ||
5980            'selected vehicle';
5981        const subtitleBase =
5982            panel?.getAttribute('data-fe-stock-headline') ||
5983            (panel?.getAttribute('data-fe-hide-listing') === 'true'
5984                ? headline
5985                : document.getElementById('finance-eligibility-agreement-subtitle')?.textContent?.split(' on ')[0]?.trim()) ||
5986            headline;
5987        setAgreementText('finance-eligibility-agreement-subtitle', `${subtitleBase} on ${typeLabel}`);
5988        setAgreementText('finance-eligibility-agreement-rep-vehicle', `Based on a ${headline} on ${typeLabel}`);
5989
5990        const cash = calculatorQuoteValue('finance-balance-to-finance', 'cash');
5991        const deposit = calculatorQuoteValue('finance-customer-deposit', 'deposit');
5992        const fda = calculatorQuoteValue('finance-deposit-allowance', 'fda');
5993        const credit = calculatorQuoteValue('finance-total-amount-credit', 'credit');
5994        const first = calculatorQuoteValue('finance-first-payment', 'first');
5995        const regular = calculatorQuoteValue('finance-regular-payment', 'regular');
5996        const finalPay = calculatorQuoteValue('finance-optional-final-payment', 'finalPay');
5997        const optFee = calculatorQuoteValue('finance-purchase-fee', 'optFee');
5998        const term = calculatorQuoteValue('finance-duration-agreement', 'term');
5999        const interest = calculatorQuoteValue('finance-total-charge-for-credit', 'interest');
6000        const totalPay = calculatorQuoteValue('finance-total-amount-payable', 'totalPay');
6001        const flat = calculatorQuoteValue('finance-interest-rate', 'flat');
6002        const excess = calculatorQuoteValue('finance-over-mileage', 'excess');
6003        const annualMileage = calculatorQuoteValue('finance-annual-mileage', 'annualMileage');
6004        const financePageQuote = getFinancePageQuoteSnapshot();
6005        let aprAttr = null;
6006        if (panel && panel.getAttribute('data-fe-bound-listing-cta') === 'true' && financePageQuote?.apr != null) {
6007            aprAttr = String(financePageQuote.apr);
6008        } else {
6009            aprAttr =
6010                document.getElementById('finance-header-apr')?.getAttribute(`${quoteType}Value`) ??
6011                document.getElementById('finance-apr')?.getAttribute(`${quoteType}Value`) ??
6012                (financePageQuote?.apr != null ? String(financePageQuote.apr) : null);
6013        }
6014
6015        // Top card summary (unchanged design / data).
6016        if (cash != null) {
6017            const cashFormatted = formatHpCurrency(cash);
6018            if (cashFormatted) {
6019                setAgreementText('finance-eligibility-agreement-cash', cashFormatted);
6020            }
6021        }
6022        if (deposit != null) {
6023            const depositFormatted = formatHpCurrency(deposit);
6024            if (depositFormatted) {
6025                setAgreementText('finance-eligibility-agreement-deposit', depositFormatted);
6026            }
6027        }
6028        if (term != null) {
6029            const termN = Math.round(Number(term));
6030            if (!Number.isNaN(termN)) {
6031                setAgreementText('finance-eligibility-agreement-term', `${termN} months`);
6032            }
6033        }
6034        if (annualMileage != null) {
6035            const milesN = Math.round(Number(annualMileage));
6036            if (!Number.isNaN(milesN)) {
6037                setAgreementText(
6038                    'finance-eligibility-agreement-mileage',
6039                    `${milesN.toLocaleString('en-GB')} miles`,
6040                );
6041            }
6042        }
6043
6044        // Representative Example — calculator-style label/value rows.
6045        const cashFormatted = formatAgreementCurrency(cash);
6046        if (cashFormatted) {
6047            setAgreementText('finance-eligibility-rep-cash', cashFormatted);
6048        }
6049
6050        const milesFormatted = formatAgreementMileage(annualMileage);
6051        if (milesFormatted) {
6052            setAgreementText('finance-eligibility-rep-mileage', milesFormatted);
6053        }
6054        setAgreementRowHidden('finance-eligibility-rep-row-mileage', false);
6055
6056        const depositFormatted = formatAgreementCurrency(deposit);
6057        if (depositFormatted) {
6058            setAgreementText('finance-eligibility-rep-deposit', depositFormatted);
6059        }
6060
6061        const fdaFormatted = formatAgreementCurrency(fda);
6062        const showFda = !!fdaFormatted && Number(fda) > 0;
6063        if (fdaFormatted) {
6064            setAgreementText('finance-eligibility-rep-fda', fdaFormatted);
6065        }
6066        setAgreementRowHidden('finance-eligibility-rep-row-fda', !showFda);
6067
6068        const creditFormatted = formatAgreementCurrency(credit);
6069        if (creditFormatted) {
6070            setAgreementText('finance-eligibility-rep-credit', creditFormatted);
6071        }
6072
6073        if (term != null) {
6074            const termN = Math.round(Number(term));
6075            if (!Number.isNaN(termN)) {
6076                setAgreementText('finance-eligibility-rep-term', `${termN} months`);
6077            }
6078        }
6079
6080        const firstFormatted = formatAgreementCurrency(first);
6081        if (firstFormatted) {
6082            setAgreementText('finance-eligibility-rep-first', firstFormatted);
6083        }
6084        const regularFormatted = formatAgreementCurrency(regular);
6085        if (regularFormatted) {
6086            setAgreementText('finance-eligibility-rep-regular', regularFormatted);
6087        }
6088
6089        const finalFormatted = formatAgreementCurrency(finalPay);
6090        const showFinal = quoteType === 'PCP' && !!finalFormatted;
6091        if (finalFormatted) {
6092            setAgreementText('finance-eligibility-rep-final', finalFormatted);
6093        }
6094        setAgreementRowHidden('finance-eligibility-rep-row-final', !showFinal);
6095
6096        const feeFormatted = formatAgreementCurrency(optFee);
6097        if (feeFormatted) {
6098            setAgreementText('finance-eligibility-rep-fee', feeFormatted);
6099        }
6100
6101        const totalFormatted = formatAgreementCurrency(totalPay);
6102        if (totalFormatted) {
6103            setAgreementText('finance-eligibility-rep-total', totalFormatted);
6104        }
6105
6106        const aprFormatted = formatAgreementApr(aprAttr);
6107        if (aprFormatted) {
6108            setAgreementText('finance-eligibility-rep-apr', aprFormatted);
6109        }
6110
6111        const interestFormatted = formatAgreementCurrency(interest);
6112        if (interestFormatted) {
6113            setAgreementText('finance-eligibility-rep-interest', interestFormatted);
6114        }
6115
6116        const flatFormatted = formatAgreementPercent(flat);
6117        if (flatFormatted) {
6118            setAgreementText('finance-eligibility-rep-flat', flatFormatted);
6119        }
6120
6121        const excessFormatted = formatAgreementExcess(excess);
6122        const showExcess = quoteType === 'PCP' && !!excessFormatted;
6123        if (excessFormatted) {
6124            setAgreementText('finance-eligibility-rep-excess', excessFormatted);
6125        }
6126        setAgreementRowHidden('finance-eligibility-rep-row-excess', !showExcess);
6127    };
6128
6129    const showAgreementView = () => {
6130        if (!agreementView || !intro) {
6131            return;
6132        }
6133        if (detailsStep) {
6134            detailsStep.hidden = true;
6135        }
6136        hideContactStep();
6137        hideMoreStep();
6138        hidePersonalGroupedStep();
6139        hideAddressStep();
6140        hideEmploymentStep();
6141        hideIncomeOutgoingsStep();
6142        hideOutgoingsFinalStep();
6143        hideIncomeOutgoingsGroupedStep();
6144        hideReviewStep();
6145        intro.hidden = true;
6146        agreementView.hidden = false;
6147        state.screen = 'agreement';
6148        scrollToTop(agreementView);
6149        syncEligibilityAgreementRepresentative();
6150        syncFinanceEligibilityJourneyUrl();
6151    };
6152
6153    const showPersonalGroupedStep = () => {
6154        applyFormDensityVariant();
6155        if (!intro || !personalGroupedStep || !feFormDensityComposed) {
6156            return;
6157        }
6158        hideAgreementView();
6159        if (detailsStep) {
6160            detailsStep.hidden = true;
6161        }
6162        hideContactStep();
6163        hideMoreStep();
6164        hideAddressStep();
6165        hideEmploymentStep();
6166        hideIncomeOutgoingsStep();
6167        hideOutgoingsFinalStep();
6168        hideIncomeOutgoingsGroupedStep();
6169        hideReviewStep();
6170        intro.hidden = true;
6171        personalGroupedStep.hidden = false;
6172        state.screen = 'personalGrouped';
6173        state.dirty = true;
6174        scrollToTop(personalGroupedStep);
6175        applyFinanceEligibilityUkDial();
6176        validatePersonalGrouped();
6177        syncFinanceEligibilityJourneyUrl();
6178    };
6179
6180    const showDetailsStep = () => {
6181        if (canRouteToGroupedSteps(personalGroupedStep)) {
6182            showPersonalGroupedStep();
6183            return;
6184        }
6185        if (!intro || !detailsStep) {
6186            return;
6187        }
6188        hideAgreementView();
6189        hideContactStep();
6190        hideMoreStep();
6191        hidePersonalGroupedStep();
6192        hideAddressStep();
6193        hideEmploymentStep();
6194        hideIncomeOutgoingsStep();
6195        hideOutgoingsFinalStep();
6196        hideIncomeOutgoingsGroupedStep();
6197        hideReviewStep();
6198        intro.hidden = true;
6199        detailsStep.hidden = false;
6200        state.screen = 'details';
6201        state.dirty = true;
6202        scrollToTop(detailsStep);
6203        validateDetails();
6204        syncFinanceEligibilityJourneyUrl();
6205    };
6206
6207    const showContactStep = () => {
6208        if (!detailsStep || !contactStep) {
6209            return;
6210        }
6211        detailsStep.hidden = true;
6212        hideMoreStep();
6213        hidePersonalGroupedStep();
6214        hideAddressStep();
6215        hideEmploymentStep();
6216        hideIncomeOutgoingsStep();
6217        hideOutgoingsFinalStep();
6218        hideIncomeOutgoingsGroupedStep();
6219        hideReviewStep();
6220        contactStep.hidden = false;
6221        state.screen = 'contact';
6222        state.dirty = true;
6223        scrollToTop(contactStep);
6224        applyFinanceEligibilityUkDial();
6225        validateContact();
6226        syncFinanceEligibilityJourneyUrl();
6227    };
6228
6229    const showMoreInformationStep = () => {
6230        if (!contactStep || !moreStep) {
6231            return;
6232        }
6233        contactStep.hidden = true;
6234        hidePersonalGroupedStep();
6235        hideAddressStep();
6236        hideEmploymentStep();
6237        hideIncomeOutgoingsStep();
6238        hideOutgoingsFinalStep();
6239        hideIncomeOutgoingsGroupedStep();
6240        hideReviewStep();
6241        moreStep.hidden = false;
6242        state.screen = 'more';
6243        state.dirty = true;
6244        scrollToTop(moreStep);
6245        validateMore();
6246        syncFinanceEligibilityJourneyUrl();
6247    };
6248
6249    const showAddressStep = () => {
6250        if (!addressStep) {
6251            return;
6252        }
6253        if (!feFormDensityComposed && !moreStep) {
6254            return;
6255        }
6256        hideMoreStep();
6257        hideContactStep();
6258        hidePersonalGroupedStep();
6259        hideEmploymentStep();
6260        hideIncomeOutgoingsStep();
6261        hideOutgoingsFinalStep();
6262        hideIncomeOutgoingsGroupedStep();
6263        hideReviewStep();
6264        if (detailsStep) {
6265            detailsStep.hidden = true;
6266        }
6267        // Discard any leftover mid-edit state so Next works from saved addresses.
6268        cancelUnsavedAddressEdit();
6269        addressStep.hidden = false;
6270        delete addressStep.dataset.carsFeAddressEntryActive;
6271        setManualAddressMode(false);
6272        state.screen = 'address';
6273        state.dirty = true;
6274        scrollToTop(addressStep);
6275        syncAddressMonthsDisabledForFifteenPlus();
6276        validateAddress();
6277        runAddressLookup();
6278        syncFinanceEligibilityJourneyUrl();
6279    };
6280
6281    const showMoreFromAddress = () => {
6282        if (canRouteToGroupedSteps(personalGroupedStep)) {
6283            cancelUnsavedAddressEdit();
6284            addressStep.hidden = true;
6285            hideEmploymentStep();
6286            hideIncomeOutgoingsStep();
6287            hideOutgoingsFinalStep();
6288            hideIncomeOutgoingsGroupedStep();
6289            hideReviewStep();
6290            showPersonalGroupedStep();
6291            return;
6292        }
6293        if (!moreStep || !addressStep) {
6294            return;
6295        }
6296        cancelUnsavedAddressEdit();
6297        addressStep.hidden = true;
6298        hideEmploymentStep();
6299        hideIncomeOutgoingsStep();
6300        hideOutgoingsFinalStep();
6301        hideIncomeOutgoingsGroupedStep();
6302        hideReviewStep();
6303        moreStep.hidden = false;
6304        state.screen = 'more';
6305        scrollToTop(moreStep);
6306        validateMore();
6307        syncFinanceEligibilityJourneyUrl();
6308    };
6309
6310    const showDetailsFromContact = () => {
6311        if (!detailsStep || !contactStep) {
6312            return;
6313        }
6314        contactStep.hidden = true;
6315        hideMoreStep();
6316        hidePersonalGroupedStep();
6317        hideAddressStep();
6318        hideEmploymentStep();
6319        hideIncomeOutgoingsStep();
6320        hideOutgoingsFinalStep();
6321        hideIncomeOutgoingsGroupedStep();
6322        hideReviewStep();
6323        detailsStep.hidden = false;
6324        state.screen = 'details';
6325        scrollToTop(detailsStep);
6326        validateDetails();
6327        syncFinanceEligibilityJourneyUrl();
6328    };
6329
6330    const showContactFromMore = () => {
6331        if (!contactStep || !moreStep) {
6332            return;
6333        }
6334        moreStep.hidden = true;
6335        hidePersonalGroupedStep();
6336        hideAddressStep();
6337        hideEmploymentStep();
6338        hideIncomeOutgoingsStep();
6339        hideOutgoingsFinalStep();
6340        hideIncomeOutgoingsGroupedStep();
6341        hideReviewStep();
6342        contactStep.hidden = false;
6343        state.screen = 'contact';
6344        scrollToTop(contactStep);
6345        validateContact();
6346        syncFinanceEligibilityJourneyUrl();
6347    };
6348
6349    const showIncomeOutgoingsGroupedStep = () => {
6350        applyFormDensityVariant();
6351        if (!addressStep || !incomeOutgoingsGroupedStep || !feFormDensityComposed) {
6352            return;
6353        }
6354        hidePersonalGroupedStep();
6355        hideMoreStep();
6356        hideContactStep();
6357        if (detailsStep) {
6358            detailsStep.hidden = true;
6359        }
6360        hideEmploymentStep();
6361        hideIncomeOutgoingsStep();
6362        hideOutgoingsFinalStep();
6363        hideReviewStep();
6364        addressStep.hidden = true;
6365        incomeOutgoingsGroupedStep.hidden = false;
6366        state.screen = 'incomeOutgoingsGrouped';
6367        state.dirty = true;
6368        scrollToTop(incomeOutgoingsGroupedStep);
6369        syncEmploymentSummaryUiState();
6370        validateIncomeOutgoingsGrouped();
6371        syncFinanceEligibilityJourneyUrl();
6372    };
6373
6374    const showEmploymentStep = () => {
6375        if (canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) {
6376            showIncomeOutgoingsGroupedStep();
6377            return;
6378        }
6379        if (!addressStep || !employmentStep) {
6380            return;
6381        }
6382        hideIncomeOutgoingsStep();
6383        hideOutgoingsFinalStep();
6384        hideIncomeOutgoingsGroupedStep();
6385        hideReviewStep();
6386        hideMoreStep();
6387        hideContactStep();
6388        hidePersonalGroupedStep();
6389        if (detailsStep) {
6390            detailsStep.hidden = true;
6391        }
6392        addressStep.hidden = true;
6393        employmentStep.hidden = false;
6394        state.screen = 'employment';
6395        state.dirty = true;
6396        scrollToTop(employmentStep);
6397        syncEmploymentSummaryUiState();
6398        validateEmployment();
6399        syncFinanceEligibilityJourneyUrl();
6400    };
6401
6402    const showIncomeOutgoingsStep = () => {
6403        if (canRouteToGroupedSteps(incomeOutgoingsGroupedStep)) {
6404            showIncomeOutgoingsGroupedStep();
6405            return;
6406        }
6407        if (!employmentStep || !incomeOutgoingsStep) {
6408            return;
6409        }
6410        hideOutgoingsFinalStep();
6411        hideIncomeOutgoingsGroupedStep();
6412        hideReviewStep();
6413        employmentStep.hidden = true;
6414        incomeOutgoingsStep.hidden = false;
6415        state.screen = 'incomeOutgoings';
6416        state.dirty = true;
6417        scrollToTop(incomeOutgoingsStep);
6418        validateIncomeOutgoings();
6419        syncFinanceEligibilityJourneyUrl();
6420    };
6421
6422    const showOutgoingsFinalStep = () => {
6423        if (!incomeOutgoingsStep || !outgoingsFinalStep) {
6424            return;
6425        }
6426        hideReviewStep();
6427        incomeOutgoingsStep.hidden = true;
6428        outgoingsFinalStep.hidden = false;
6429        state.screen = 'outgoingsFinal';
6430        state.dirty = true;
6431        scrollToTop(outgoingsFinalStep);
6432        validateOutgoingsFinal();
6433        syncFinanceEligibilityJourneyUrl();
6434    };
6435
6436    const backToIntro = () => {
6437        clearReviewEditReturnPending();
6438        hideAgreementView();
6439        hideContactStep();
6440        hideMoreStep();
6441        hidePersonalGroupedStep();
6442        hideAddressStep();
6443        hideEmploymentStep();
6444        hideIncomeOutgoingsStep();
6445        hideOutgoingsFinalStep();
6446        hideIncomeOutgoingsGroupedStep();
6447        hideReviewStep();
6448        if (detailsStep) {
6449            detailsStep.hidden = true;
6450        }
6451        if (intro) {
6452            intro.hidden = false;
6453        }
6454        state.screen = 'intro';
6455        state.dirty = false;
6456        scrollToTop(intro);
6457        syncFinanceEligibilityJourneyUrl();
6458    };
6459
6460    const resetJourneyForms = () => {
6461        feForgetTouchedFields();
6462        closeFinanceEligibilityResultsModal();
6463        eligibilityQuotesCache = [];
6464        ['cars-fe-title', 'cars-fe-first-name', 'cars-fe-last-name', 'cars-fe-dob', 'cars-fe-marital'].forEach((id) => {
6465            const el = document.getElementById(id);
6466            if (el) {
6467                el.value = '';
6468            }
6469        });
6470        syncDobPlaceholderState();
6471        detailsStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
6472        setBtnNextDisabled(nextBtn, true);
6473
6474        const email = document.getElementById('cars-fe-email');
6475        if (email) {
6476            email.value = '';
6477        }
6478        const national = document.getElementById('cars-fe-phone-national');
6479        if (national) {
6480            national.value = '';
6481        }
6482        applyFinanceEligibilityUkDial();
6483        ['cars-fe-license-type', 'cars-fe-dependants'].forEach((id) => {
6484            const el = document.getElementById(id);
6485            if (el) {
6486                el.value = '';
6487            }
6488        });
6489        moreStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
6490        personalGroupedStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
6491        getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
6492        const employmentStatusHidden = document.getElementById('cars-fe-employment-status');
6493        if (employmentStatusHidden) {
6494            employmentStatusHidden.value = '';
6495        }
6496        [
6497            'cars-fe-employment-employer-name',
6498            'cars-fe-employment-occupation',
6499            'cars-fe-employment-occupation-q',
6500            'cars-fe-employment-occupational-basis',
6501            'cars-fe-employment-occupational-type',
6502            'cars-fe-employment-years',
6503            'cars-fe-employment-months',
6504        ].forEach((id) => {
6505            const el = document.getElementById(id);
6506            if (el) {
6507                el.value = financeEligibilityFieldDefaultValue(id);
6508            }
6509        });
6510        financeEligibilityCloseOccupationPanels(getEmploymentUiHost());
6511        setBtnNextDisabled(contactNextBtn, true);
6512        setBtnNextDisabled(moreNextBtn, true);
6513        setBtnNextDisabled(personalGroupedNextBtn, true);
6514        setBtnNextDisabled(employmentNextBtn, true);
6515        setBtnNextDisabled(incomeNextBtn, true);
6516        setBtnNextDisabled(outgoingsSummaryBtn, true);
6517        setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true);
6518
6519        ['cars-fe-income-annual-before-tax', 'cars-fe-income-monthly-take-home', 'cars-fe-income-annual-household-other'].forEach(
6520            (id) => {
6521                const el = document.getElementById(id);
6522                if (el) {
6523                    el.value = '';
6524                }
6525            },
6526        );
6527
6528        [
6529            'cars-fe-outgoings-monthly-mortgage-rent-share',
6530            'cars-fe-outgoings-other-monthly-bills',
6531            'cars-fe-outgoings-existing-loan-monthly-payment',
6532            'cars-fe-outgoings-reduced-annual-income-before-tax',
6533        ].forEach((id) => {
6534            const el = document.getElementById(id);
6535            if (el) {
6536                el.value = '';
6537            }
6538        });
6539        const downturnReasonReset = document.getElementById('cars-fe-outgoings-downturn-reason');
6540        if (downturnReasonReset) {
6541            downturnReasonReset.value = '';
6542        }
6543        outgoingsFinalStep?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
6544        incomeOutgoingsGroupedStep?.querySelectorAll('.cars-fe-form-stack--outgoings-final .cars-fe-pill.is-selected').forEach((p) =>
6545            p.classList.remove('is-selected'),
6546        );
6547        const repayHidden = document.getElementById('cars-fe-outgoings-repay-existing-finance');
6548        if (repayHidden) {
6549            repayHidden.value = '';
6550        }
6551        const downturnHidden = document.getElementById('cars-fe-outgoings-downturn-expected');
6552        if (downturnHidden) {
6553            downturnHidden.value = '';
6554        }
6555        const repayFollowReset = document.getElementById('cars-fe-outgoings-repay-followup');
6556        if (repayFollowReset) {
6557            repayFollowReset.hidden = true;
6558        }
6559        const downturnFollowReset = document.getElementById('cars-fe-outgoings-downturn-followup');
6560        if (downturnFollowReset) {
6561            downturnFollowReset.hidden = true;
6562        }
6563
6564        clearTimeout(addressLookupTimer);
6565        addressLookupTimer = null;
6566        const lookupPc = document.getElementById('cars-fe-address-postcode');
6567        if (lookupPc) {
6568            lookupPc.value = '';
6569        }
6570        [
6571            'cars-fe-address-line1',
6572            'cars-fe-address-line2',
6573            'cars-fe-address-town',
6574            'cars-fe-address-postcode-manual',
6575            'cars-fe-address-county',
6576            'cars-fe-address-residency',
6577            'cars-fe-address-years-at',
6578            'cars-fe-address-months-at',
6579        ].forEach((id) => {
6580            const el = document.getElementById(id);
6581            if (el) {
6582                el.value = financeEligibilityFieldDefaultValue(id);
6583            }
6584        });
6585        addressStep?.classList.remove('cars-fe-address--manual-saved');
6586        addressStep?.classList.remove('cars-fe-address--lookup-saved');
6587        addressStep?.classList.remove('cars-fe-address--has-saved-summary');
6588        addressStep?.classList.remove('cars-fe-address--entry-flow-visible');
6589        if (addressStep) {
6590            delete addressStep.dataset.carsFeAddressEntryActive;
6591        }
6592        savedAddressEntries = [];
6593        editingEntryId = null;
6594        editingInsertIndex = null;
6595        lastSavedAddressCardsRenderKey = null;
6596        savedEmploymentEntries = [];
6597        employmentFormMode = 'editing';
6598        editingEmploymentId = null;
6599        editingEmploymentIndex = null;
6600        clearReviewEditReturnPending();
6601        employmentStep?.classList.remove('cars-fe-employment--has-saved-summary');
6602        incomeOutgoingsGroupedStep?.classList.remove(
6603            'cars-fe-employment--has-saved-summary',
6604            'cars-fe-employment--entry-flow-visible',
6605            'cars-fe-occupation-dropdown-open',
6606            'cars-fe-duration-dropdown-open',
6607        );
6608        const empSavedPanel = document.getElementById('cars-fe-employment-saved-panel');
6609        if (empSavedPanel) {
6610            empSavedPanel.hidden = true;
6611        }
6612        const empAddAnother = document.getElementById('cars-fe-employment-add-another-card');
6613        if (empAddAnother) {
6614            empAddAnother.hidden = true;
6615        }
6616        const empEntryBlock = document.getElementById('cars-fe-employment-entry-block');
6617        if (empEntryBlock) {
6618            empEntryBlock.hidden = false;
6619        }
6620        const empMount = document.getElementById('cars-fe-employment-saved-mount');
6621        if (empMount) {
6622            empMount.textContent = '';
6623        }
6624        const savedSummaryReset = document.getElementById('cars-fe-address-saved-summary-panel');
6625        if (savedSummaryReset) {
6626            savedSummaryReset.hidden = true;
6627        }
6628        const historyAlertReset = document.getElementById('cars-fe-address-history-alert');
6629        if (historyAlertReset) {
6630            historyAlertReset.hidden = true;
6631        }
6632        const employmentHistoryAlertReset = document.getElementById('cars-fe-employment-history-alert');
6633        if (employmentHistoryAlertReset) {
6634            employmentHistoryAlertReset.hidden = true;
6635        }
6636        const addrEntryBlockReset = document.getElementById('cars-fe-address-entry-block');
6637        if (addrEntryBlockReset) {
6638            addrEntryBlockReset.hidden = false;
6639        }
6640        renderSavedAddressCards();
6641        populateAddressSelect([]);
6642        setManualAddressMode(false);
6643        syncAllFinanceDurationSelectShells();
6644        syncEmploymentFormFieldLabelsForContext();
6645        syncEmploymentDurationLabelForStatus(val('cars-fe-employment-status'));
6646        if (reviewConsentCb) {
6647            reviewConsentCb.checked = false;
6648        }
6649        syncReviewConsentError(false);
6650        hideReviewStep();
6651    };
6652
6653    document.querySelectorAll('.finance-free-check-button').forEach((btn) => {
6654        btn.addEventListener('click', (e) => {
6655            e.preventDefault();
6656            e.stopPropagation();
6657            const sidepanelTestActive =
6658                typeof window.isFinanceEligibilitySidepanelVariantActive === 'function' &&
6659                window.isFinanceEligibilitySidepanelVariantActive();
6660            if (sidepanelTestActive) {
6661                if (!window.bootstrap || !bootstrap.Offcanvas) {
6662                    return;
6663                }
6664                bootstrap.Offcanvas.getOrCreateInstance(panel).show();
6665            } else if (typeof window.freeFinanceCheckFromVdp === 'function') {
6666                window.freeFinanceCheckFromVdp();
6667            }
6668        });
6669    });
6670
6671    document.querySelectorAll('[data-fe-open-agreement]').forEach((btn) => {
6672        btn.addEventListener('click', (e) => {
6673            e.preventDefault();
6674            showAgreementView();
6675        });
6676    });
6677
6678    document.getElementById('finance-eligibility-agreement-close-button')?.addEventListener('click', (e) => {
6679        e.preventDefault();
6680        backToIntro();
6681    });
6682
6683    document.getElementById('finance-eligibility-start-button')?.addEventListener('click', (e) => {
6684        e.preventDefault();
6685        applyFormDensityVariant();
6686        showDetailsStep();
6687    });
6688
6689    document.getElementById('cars-fe-footer-back-button')?.addEventListener('click', (e) => {
6690        e.preventDefault();
6691        clearReviewEditReturnPending();
6692        if (state.screen === 'details') {
6693            backToIntro();
6694        }
6695    });
6696
6697    document.getElementById('cars-fe-personal-grouped-footer-back-button')?.addEventListener('click', (e) => {
6698        e.preventDefault();
6699        clearReviewEditReturnPending();
6700        if (state.screen === 'personalGrouped') {
6701            backToIntro();
6702        }
6703    });
6704
6705    document.getElementById('cars-fe-contact-footer-back-button')?.addEventListener('click', (e) => {
6706        e.preventDefault();
6707        clearReviewEditReturnPending();
6708        if (state.screen === 'contact') {
6709            showDetailsFromContact();
6710        }
6711    });
6712
6713    nextBtn?.addEventListener('click', (e) => {
6714        e.preventDefault();
6715        attemptFeJourneyNext('details', detailsStep, validateDetails, () => {
6716            if (reviewEditReturnPending) {
6717                showReviewStep();
6718                return;
6719            }
6720            showContactStep();
6721        });
6722    });
6723
6724    personalGroupedNextBtn?.addEventListener('click', (e) => {
6725        e.preventDefault();
6726        attemptPersonalGroupedNext(() => {
6727            if (reviewEditReturnPending) {
6728                showReviewStep();
6729                return;
6730            }
6731            showAddressStep();
6732        });
6733    });
6734
6735    contactNextBtn?.addEventListener('click', (e) => {
6736        e.preventDefault();
6737        attemptFeJourneyNext('contact', contactStep, validateContact, () => {
6738            if (reviewEditReturnPending) {
6739                showReviewStep();
6740                return;
6741            }
6742            showMoreInformationStep();
6743        });
6744    });
6745
6746    document.getElementById('cars-fe-more-footer-back-button')?.addEventListener('click', (e) => {
6747        e.preventDefault();
6748        clearReviewEditReturnPending();
6749        if (state.screen === 'more') {
6750            showContactFromMore();
6751        }
6752    });
6753
6754    moreNextBtn?.addEventListener('click', (e) => {
6755        e.preventDefault();
6756        attemptFeJourneyNext('more', moreStep, validateMore, () => {
6757            if (reviewEditReturnPending) {
6758                showReviewStep();
6759                return;
6760            }
6761            showAddressStep();
6762        });
6763    });
6764
6765    document.getElementById('cars-fe-address-footer-back-button')?.addEventListener('click', (e) => {
6766        e.preventDefault();
6767        clearReviewEditReturnPending();
6768        if (state.screen === 'address') {
6769            showMoreFromAddress();
6770        }
6771    });
6772
6773    document.getElementById('cars-fe-address-manual-toggle')?.addEventListener('click', (e) => {
6774        e.preventDefault();
6775        if (addressStep?.classList.contains('cars-fe-address--manual')) {
6776            return;
6777        }
6778        if (addressStep) {
6779            addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUA
6779L_TOGGLE;
6780        }
6781        setManualAddressMode(true);
6782    });
6783
6784    document.getElementById('cars-fe-address-manual-save-button')?.addEventListener('click', (e) => {
6785        e.preventDefault();
6786        if (!validateManualFormForSave()) {
6787            requestFeStepFieldHighlights('address');
6788            applyAddressFieldHighlights();
6789            financeEligibilityScrollToFirstInvalid(addressStep);
6790            return;
6791        }
6792        const entry =
6793            typeof editingEntryId === 'string' && editingEntryId.length > 0
6794                ? buildManualEntryFromForm(editingEntryId)
6795                : buildManualEntryFromForm();
6796        if (editingInsertIndex != null && typeof editingEntryId === 'string' && editingEntryId.length > 0) {
6797            savedAddressEntries[editingInsertIndex] = entry;
6798            editingInsertIndex = null;
6799            editingEntryId = null;
6800        } else {
6801            savedAddressEntries.push(entry);
6802        }
6803        clearManualAddressFormFields();
6804        placeAddressManualWrapHome();
6805        addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved');
6806        if (addressStep) {
6807            delete addressStep.dataset.carsFeManualWrapIntent;
6808            delete addressStep.dataset.carsFeAddressEntryActive;
6809        }
6810        const addressOkAfterSave = validateAddress();
6811        if (reviewEditReturnPending && addressOkAfterSave) {
6812            showReviewStep();
6813        }
6814    });
6815
6816    document.getElementById('cars-fe-address-manual-delete-button')?.addEventListener('click', (e) => {
6817        e.preventDefault();
6818        if (editingInsertIndex == null || editingEntryId == null) {
6819            return;
6820        }
6821        const ix = savedAddressEntries.findIndex((x) => x.id === editingEntryId);
6822        if (ix >= 0) {
6823            savedAddressEntries.splice(ix, 1);
6824        } else if (
6825            typeof editingInsertIndex === 'number' &&
6826            editingInsertIndex >= 0 &&
6827            editingInsertIndex < savedAddressEntries.length
6828        ) {
6829            savedAddressEntries.splice(editingInsertIndex, 1);
6830        }
6831        editingEntryId = null;
6832        editingInsertIndex = null;
6833        clearManualAddressFormFields();
6834        placeAddressManualWrapHome();
6835        addressStep?.classList.remove('cars-fe-address--manual', 'cars-fe-address--editing-saved');
6836        if (addressStep) {
6837            delete addressStep.dataset.carsFeManualWrapIntent;
6838            delete addressStep.dataset.carsFeAddressEntryActive;
6839        }
6840        const deleteBtn = document.getElementById('cars-fe-address-manual-delete-button');
6841        if (deleteBtn) {
6842            deleteBtn.hidden = true;
6843        }
6844        validateAddress();
6845        if (reviewEditReturnPending && savedAddressEntries.length > 0 && validateAddress()) {
6846            showReviewStep();
6847        }
6848    });
6849
6850    document.getElementById('cars-fe-address-saved-cards')?.addEventListener('click', (e) => {
6851        const btn = e.target.closest('[data-cars-fe-entry-edit]');
6852        if (!btn || !addressStep) {
6853            return;
6854        }
6855        e.preventDefault();
6856        const id = btn.getAttribute('data-cars-fe-entry-edit');
6857        if (!id) {
6858            return;
6859        }
6860        const ix = savedAddressEntries.findIndex((x) => x.id === id);
6861        if (ix < 0) {
6862            return;
6863        }
6864        const entry = savedAddressEntries[ix];
6865        if (entry.source === 'lookup') {
6866            savedAddressEntries.splice(ix, 1);
6867            const sel = document.getElementById('cars-fe-address-select');
6868            if (sel) {
6869                sel.value = '';
6870            }
6871            editingEntryId = null;
6872            editingInsertIndex = null;
6873            validateAddress();
6874            return;
6875        }
6876        const savedCountBeforeEdit = savedAddressEntries.length;
6877        editingInsertIndex = ix;
6878        editingEntryId = entry.id;
6879        applyManualEntryToForm(entry);
6880        addressStep.dataset.carsFeAddressEntryActive = 'true';
6881        addressStep.dataset.carsFeManualWrapIntent =
6882            savedCountBeforeEdit >
6882 1 ? MANUAL_WRAP_INTENT_ADD_ANOTHER : MANUAL_WRAP_INTENT_MANUAL_TOGGLE;
6883        addressStep.classList.add('cars-fe-address--manual');
6884        validateAddress();
6885        const editSlot = document.getElementById('cars-fe-address-edit-slot');
6886        const manualWrap = document.getElementById('cars-fe-address-manual-wrap');
6887        const scrollTarget = editSlot || manualWrap;
6888        if (scrollTarget) {
6889            financeEligibilityScheduleFieldScrollIntoView(scrollTarget, { behavior: 'smooth', topPaddingPx: 16 });
6890        } else {
6891            scrollToTop(addressStep);
6892        }
6893    });
6894
6895    document.getElementById('cars-fe-address-add-another-button')?.addEventListener('click', (e) => {
6896        e.preventDefault();
6897        if (savedAddressEntries.length < 1 || !addressStep) {
6898            return;
6899        }
6900        addressStep.dataset.carsFeAddressEntryActive = 'true';
6901        addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_ADD_ANOTHER;
6902        editingEntryId = null;
6903        editingInsertIndex = null;
6904        clearManualAddressFormFields();
6905        const lookupPc = document.getElementById('cars-fe-address-postcode');
6906        if (lookupPc) {
6907            lookupPc.value = '';
6908        }
6909        populateAddressSelect([]);
6910        setManualAddressMode(false);
6911        validateAddress();
6912        scrollToTop(addressStep);
6913    });
6914
6915    /** Postcoder typeahead — replaces the legacy postcode-then-select dropdown. On select, the address
6916     *  drops straight into the manual-edit fields so the user only has to add residency + time. */
6917    (() => {
6918        if (typeof PostcoderAutocomplete !== 'function') {
6919            return;
6920        }
6921        const input = document.getElementById('cars-fe-address-postcode');
6922        const list = document.getElementById('cars-fe-address-suggestions');
6923        if (!input || !list) {
6924            return;
6925        }
6926        const apiKey =
6927            (panel.dataset && panel.dataset.fePostcoderApikey) ||
6928            (typeof window !== 'undefined' ? window.POSTCODER_API_KEY : '') ||
6929            '';
6930        if (!apiKey) {
6931            return;
6932        }
6933
6934        const syncAddressSuggestionsOpenState = () => {
6935            if (!addressStep) {
6936                return;
6937            }
6938            const open = list.childElementCount > 0;
6939            const wasOpen = addressStep.classList.contains('cars-fe-address-suggestions-open');
6940            addressStep.classList.toggle('cars-fe-address-suggestions-open', open);
6941            if (open && !wasOpen) {
6942                window.requestAnimationFrame(() => {
6943                    financeEligibilityScrollFieldIntoView(input, {
6944                        listPanelEl: list,
6945                        topPaddingPx: 8,
6946                        gapPx: 16,
6947                        behavior: 'auto',
6948                        reserveBelowPx: Math.min(280, Math.round(window.innerHeight * 0.45) || 280),
6949                    });
6950                });
6951            }
6952            if (!open && wasOpen) {
6953                const stack =
6954                    addressStep.querySelector('.cars-fe-form-stack--address') ||
6955                    addressStep.querySelector('.cars-fe-form-stack');
6956                if (stack) {
6957                    delete stack.dataset.feScrollPadExtra;
6958                    stack.style.removeProperty('padding-bottom');
6959                }
6960            }
6961        };
6962
6963        new PostcoderAutocomplete({
6964            apikey: apiKey,
6965            countrycode: 'UK',
6966            searchterm: '#cars-fe-address-postcode',
6967            suggestions: '#cars-fe-address-suggestions',
6968            singlesummary: true,
6969            addressline1: '#cars-fe-address-line1',
6970            addressline2: '#cars-fe-address-line2',
6971            posttown: '#cars-fe-address-town',
6972            county: '#cars-fe-address-county',
6973            postcode: '#cars-fe-address-postcode-manual',
6974            onAddressSelected: () => {
6975                if (!addressStep) {
6976                    return;
6977                }
6978                if (!addressStep.dataset.carsFeManualWrapIntent) {
6979                    addressStep.dataset.carsFeManualWrapIntent = MANUAL_WRAP_INTENT_MANUA
6979L_TOGGLE;
6980                }
6981                setManualAddressMode(true);
6982                syncAddressSuggestionsOpenState();
6983                const residency = document.getElementById('cars-fe-address-residency');
6984                if (residency && typeof residency.focus === 'function') {
6985                    try {
6986                        residency.focus({ preventScroll: true });
6987                    } catch (err) {
6988                        residency.focus();
6989                    }
6990                }
6991                validateAddress();
6992            },
6993        });
6994
6995        if (typeof MutationObserver === 'function') {
6996            const suggestionsObserver = new MutationObserver(() => {
6997                syncAddressSuggestionsOpenState();
6998            });
6999            suggestionsObserver.observe(list, { childList: true });
7000        }
7001    })();
7002
7003    addressNextBtn?.addEventListener('click', (e) => {
7004        e.preventDefault();
7005        attemptFeJourneyNext('address', addressStep, validateAddress, () => {
7006            if (reviewEditReturnPending) {
7007                showReviewStep();
7008                return;
7009            }
7010            showEmploymentStep();
7011        });
7012    });
7013
7014    document.getElementById('cars-fe-employment-footer-back-button')?.addEventListener('click', (e) => {
7015        e.preventDefault();
7016        clearReviewEditReturnPending();
7017        if (state.screen === 'employment') {
7018            showAddressStep();
7019        }
7020    });
7021
7022    document.getElementById('cars-fe-income-outgoings-grouped-footer-back-button')?.addEventListener('click', (e) => {
7023        e.preventDefault();
7024        clearReviewEditReturnPending();
7025        if (state.screen === 'incomeOutgoingsGrouped') {
7026            showAddressStep();
7027        }
7028    });
7029
7030    employmentNextBtn?.addEventListener('click', (e) => {
7031        e.preventDefault();
7032        attemptFeJourneyNext('employment', employmentStep, validateEmployment, () => {
7033            if (reviewEditReturnPending) {
7034                showReviewStep();
7035                return;
7036            }
7037            showIncomeOutgoingsStep();
7038        });
7039    });
7040
7041    document.getElementById('cars-fe-income-footer-back-button')?.addEventListener('click', (e) => {
7042        e.preventDefault();
7043        clearReviewEditReturnPending();
7044        if (state.screen === 'incomeOutgoings' && employmentStep && incomeOutgoingsStep) {
7045            hideOutgoingsFinalStep();
7046            hideReviewStep();
7047            incomeOutgoingsStep.hidden = true;
7048            employmentStep.hidden = false;
7049            state.screen = 'employment';
7050            scrollToTop(employmentStep);
7051            syncEmploymentSummaryUiState();
7052            validateEmployment();
7053            syncFinanceEligibilityJourneyUrl();
7054        }
7055    });
7056
7057    document.getElementById('cars-fe-outgoings-final-footer-back-button')?.addEventListener('click', (e) => {
7058        e.preventDefault();
7059        clearReviewEditReturnPending();
7060        if (state.screen === 'outgoingsFinal' && incomeOutgoingsStep && outgoingsFinalStep) {
7061            outgoingsFinalStep.hidden = true;
7062            incomeOutgoingsStep.hidden = false;
7063            state.screen = 'incomeOutgoings';
7064            scrollToTop(incomeOutgoingsStep);
7065            validateIncomeOutgoings();
7066            syncFinanceEligibilityJourneyUrl();
7067        }
7068    });
7069
7070    incomeNextBtn?.addEventListener('click', (e) => {
7071        e.preventDefault();
7072        attemptFeJourneyNext('income', incomeOutgoingsStep, validateIncomeOutgoings, () => {
7073            showOutgoingsFinalStep();
7074        });
7075    });
7076
7077    outgoingsSummaryBtn?.addEventListener('click', (e) => {
7078        e.preventDefault();
7079        attemptFeJourneyNext('outgoings', outgoingsFinalStep, validateOutgoingsFinal, () => {
7080            showReviewStep();
7081        });
7082    });
7083
7084    incomeOutgoingsGroupedSummaryBtn?.addEventListener('click', (e) => {
7085        e.preventDefault();
7086        // Both routes end on the review screen, whether this was a review edit or the first pass.
7087        attemptIncomeOutgoingsGroupedNext(() => {
7088            showReviewStep();
7089        });
7090    });
7091
7092    // Density now rides on the journey flag (test-c), so the journey-variant listener re-applies it.
7093    applyFormDensityVariant();
7094
7095    const closeFinanceEligibilityResultsGlossary = () => {
7096        const glossaryLay = document.getElementById('cars-fe-results-glossary');
7097        if (!glossaryLay || glossaryLay.hidden) {
7098            return;
7099        }
7100        glossaryLay.hidden = true;
7101        glossaryLay.setAttribute('aria-hidden', 'true');
7102        document.getElementById('cars-fe-results-glossary-open')?.setAttribute('aria-expanded', 'false');
7103        syncFinanceEligibilityJourneyUrl();
7104    };
7105
7106    const openFinanceEligibilityResultsGlossary = () => {
7107        const glossaryLay = document.getElementById('cars-fe-results-glossary');
7108        if (!glossaryLay || !resultsModal || resultsModal.hidden) {
7109            return;
7110        }
7111        glossaryLay.hidden = false;
7112        glossaryLay.setAttribute('aria-hidden', 'false');
7113        document.getElementById('cars-fe-results-glossary-open')?.setAttribute('aria-expanded', 'true');
7114        document.getElementById('cars-fe-results-glossary-close')?.focus?.();
7115        syncFinanceEligibilityJourneyUrl();
7116    };
7117
7118    const closeFinanceEligibilityResultsModal = () => {
7119        if (!resultsModal) {
7120            return;
7121        }
7122        setFinanceResultsModalLoading(false);
7123        closeFinanceEligibilityResultsGlossary();
7124        const lenderLay = document.getElementById('cars-fe-results-lender-detail');
7125        if (lenderLay && lenderLay.hidden === false) {
7126            lenderLay.hidden = true;
7127            lenderLay.setAttribute('aria-hidden', 'true');
7128        }
7129        resultsModal.hidden = true;
7130        resultsModal.setAttribute('aria-hidden', 'true');
7131        syncFinanceEligibilityJourneyUrl();
7132    };
7133
7134    /** Dismisses the whole finance offcanvas, leaving the customer on the VDP behind it. */
7135    const closeFinanceEligibilityPanel = () => {
7136        if (!panel || !window.bootstrap || !bootstrap.Offcanvas) {
7137            closeFinanceEligibilityResultsModal();
7138            return;
7139        }
7140        bootstrap.Offcanvas.getOrCreateInstance(panel).hide();
7141    };
7142
7143    const setFinanceResultsModalLoading = (on) => {
7144        const layer = document.getElementById('cars-fe-results-modal-loading');
7145        const scroll = resultsModal?.querySelector('.cars-fe-results-modal__scroll');
7146        const foot = resultsModal?.querySelector('.cars-fe-results-modal__footer');
7147        const vehicleStrip = resultsModal?.querySelector('.cars-fe-results-modal__vehicle');
7148        if (!layer || !scroll || !foot) {
7149            return;
7150        }
7151        layer.hidden = !on;
7152        layer.setAttribute('aria-hidden', on ? 'false' : 'true');
7153        scroll.hidden = on;
7154        foot.hidden = on;
7155        if (vehicleStrip) {
7156            vehicleStrip.hidden = on;
7157        }
7158        if (on) {
7159            resultsModal.setAttribute('aria-busy', 'true');
7160            setFinanceScanLoadingRetryVisible(false);
7161        } else {
7162            resultsModal.removeAttribute('aria-busy');
7163            feScanPollGeneration += 1;
7164            feScanPollRetryContext = null;
7165            setFinanceScanLoadingRetryVisible(false);
7166        }
7167        syncFinanceEligibilityJourneyUrl();
7168    };
7169
7170    const syncResultsVehicleStripFromPanel = () => {
7171        if (!panel || !resultsModal) {
7172            return;
7173        }
7174        const headline = panel.getAttribute('data-fe-stock-headline');
7175        const reg = panel.getAttribute('data-fe-stock-registration');
7176        const loc = panel.getAttribute('data-fe-stock-location');
7177        if (headline) {
7178            resultsModal
7179                .querySelectorAll(
7180                    '.cars-fe-results-modal__vehicle .cars-fe-listing__title, .cars-fe-results-modal__loading-vehicle .cars-fe-listing__title',
7181                )
7182                .forEach((titleEl) => {
7183                    titleEl.textContent = headline;
7184                });
7185        }
7186        ['cars-fe-results-modal-reg', 'cars-fe-results-modal-loading-reg'].forEach((id) => {
7187            const regEl = document.getElementById(id);
7188            if (regEl && reg) {
7189                regEl.textContent = reg;
7190            }
7191        });
7192        const locEl = document.getElementById('cars-fe-results-modal-location');
7193        if (locEl && loc) {
7194            locEl.textContent = loc;
7195        }
7196    };
7197
7198    /** Keep journey listing chrome + agreement title in sync when binding a VLP car onto a shared panel. */
7199    const syncFinanceEligibilityListingFromPanel = () => {
7200        if (!panel) {
7201            return;
7202        }
7203        const headline = panel.getAttribute('data-fe-stock-headline');
7204        const reg = panel.getAttribute('data-fe-stock-registration');
7205        if (headline) {
7206            panel.querySelectorAll('.cars-fe-listing__title').forEach((titleEl) => {
7207                titleEl.textContent = headline;
7208            });
7209            const agreementTitle = panel.querySelector('.finance-eligibility-agreement__vehicle-title');
7210            if (agreementTitle) {
7211                agreementTitle.textContent = headline;
7212            }
7213        }
7214        if (reg) {
7215            panel.querySelectorAll('.cars-fe-listing__reg').forEach((regEl) => {
7216                regEl.textContent = reg;
7217            });
7218        }
7219        syncFinanceEligibilityIntroVehicleCardFromPanel();
7220        syncResultsVehicleStripFromPanel();
7221    };
7222
7223    /**
7224     * The vehicle-led intro card (test-b and test-c) is rendered empty in the shared drawer (VLP cards
7225     * have no page car), so fill its photo and stats from the attributes the opening CTA put on the
7226     * panel. VDP renders the card server-side from its own car, so only the listing-CTA path fills it.
7227     */
7228    const syncFinanceEligibilityIntroVehicleCardFromPanel = () => {
7229        if (!panel || panel.getAttribute('data-fe-bound-listing-cta') !== 'true') {
7230            return;
7231        }
7232        const card = panel.querySelector('.finance-eligibility-intro__vehicle-led .finance-eligibility-intro__vehicle');
7233        if (!card) {
7234            return;
7235        }
7236
7237        const image = panel.getAttribute('data-fe-stock-image');
7238        const imageEl = card.querySelector('.finance-eligibility-intro__vehicle-image img');
7239        if (image && imageEl) {
7240            const alt = panel.getAttribute('data-fe-stock-headline') || '';
7241            // The card image is lozad-lazy, so data-src has to move too or the observer puts the
7242            // server-rendered placeholder back. data-loaded stops it re-running for the next car.
7243            imageEl.setAttribute('data-src', image);
7244            imageEl.setAttribute('data-alt', alt);
7245            imageEl.setAttribute('data-loaded', 'true');
7246            imageEl.src = image;
7247            imageEl.alt = alt;
7248        }
7249
7250        const setStat = (statKey, value) => {
7251            if (value == null || value === '') {
7252                return;
7253            }
7254            const statEl = card.querySelector(`[data-fe-vehicle-stat="${statKey}"]`);
7255            if (statEl) {
7256                statEl.textContent = value;
7257            }
7258        };
7259
7260        const price = Number(panel.getAttribute('data-fe-stock-price'));
7261        if (Number.isFinite(price) && price > 0) {
7262            setStat('price', `£${new Intl.NumberFormat('en-GB').format(Math.round(price))}`);
7263        }
7264
7265        const mileage = Number(panel.getAttribute('data-fe-stock-mileage'));
7266        if (Number.isFinite(mileage) && mileage >= 0) {
7267            setStat('mileage', new Intl.NumberFormat('en-GB').format(Math.round(mileage)));
7268        }
7269
7270        setStat('term', panel.getAttribute('data-fe-finance-term'));
7271    };
7272
7273    /**
7274     * Apply stock/get-finance-quote payload onto the eligibility offcanvas for the journey.
7275     */
7276    const applyFinanceQuoteResponseToEligibilityPanel = (data, options = {}) => {
7277        if (!panel || !data) {
7278            return null;
7279        }
7280
7281        const annualMileage = options.annualMileage != null ? Number(options.annualMileage) : 10000;
7282        const preferType =
7283            options.financeQuoteType === 0 || options.financeQuoteType === '0' || options.financeQuoteType === 'PCP'
7284                ? 'pcp'
7285                : options.financeQuoteType === 1 || options.financeQuoteType === '1' || options.financeQuoteType === 'HP'
7286                  ? 'hp'
7287                  : null;
7288
7289        const quotes = Array.isArray(data) ? data : data && Array.isArray(data.quotes) ? data.quotes : [];
7290        let eligibilityQuoteIds = [];
7291        if (data && Array.isArray(data.eligibilityQuoteIds)) {
7292            eligibilityQuoteIds = data.eligibilityQuoteIds
7293                .filter((id) => id && String(id).trim())
7294                .map((id) => String(id).trim());
7295        } else {
7296            eligibilityQuoteIds = quotes
7297                .map((q) => q && q.QuoteUID)
7298                .filter((id) => id && String(id).trim())
7299                .map((id) => String(id).trim());
7300        }
7301        if (eligibilityQuoteIds.length > 0) {
7302            panel.setAttribute('data-fe-finance-eligibility-quote-ids', JSON.stringify(eligibilityQuoteIds));
7303        } else {
7304            panel.removeAttribute('data-fe-finance-eligibility-quote-ids');
7305        }
7306
7307        if (data && data.connectRetailerId) {
7308            panel.setAttribute('data-fe-ivendi-retailer-id', String(data.connectRetailerId).trim());
7309        }
7310        if (data && data.retailerId) {
7311            panel.setAttribute('data-fe-quoteware-dealership-id', String(data.retailerId).trim());
7312        }
7313        if (data && data.siteId != null) {
7314            panel.setAttribute('data-fe-site-id', String(data.siteId));
7315        }
7316        if (data && data.stockId != null) {
7317            panel.setAttribute('data-fe-stock-id', String(data.stockId));
7318        }
7319
7320        const quote =
7321            (preferType && quotes.find((q) => q && String(q.Type || '').toLowerCase() === preferType)) ||
7322            quotes.find((q) => q);
7323        if (!quote) {
7324            return null;
7325        }
7326
7327        const quoteType = String(quote.Type || 'PCP').trim().toUpperCase() === 'HP' ? 'HP' : 'PCP';
7328        const termN = Math.round(Number(quote.Term));
7329        let numRegular =
7330            quote.NumberOfRegularPayments != null ? Math.round(Number(quote.NumberOfRegularPayments)) : NaN;
7331        if (Number.isNaN(numRegular) && !Number.isNaN(termN) && termN >= 3) {
7332            numRegular = termN - 2;
7333        }
7334        const cash = Number(quote.TotalCashPrice);
7335        const dep = Number(quote.TotalDeposit);
7336        const credit =
7337            quote.Advance != null && quote.Advance !== ''
7338                ? Number(quote.Advance)
7339                : !Number.isNaN(cash) && !Number.isNaN(dep)
7340                  ? cash - dep
7341                  : null;
7342
7343        panel.setAttribute(
7344            'data-fe-finance-page-quote',
7345            JSON.stringify({
7346                funderCode: quote.FunderCode || '',
7347                productName: quote.ProductName || '',
7348                quoteType,
7349                cash: quote.TotalCashPrice,
7350                deposit: quote.TotalDeposit,
7351                fda: quote.DepositAllowance,
7352                credit,
7353                first: quote.FirstPayment,
7354                regular: quote.RegularPayment,
7355                finalPay: quote.FinalPayment,
7356                optFee: quote.OptionToPurchaseFee,
7357                term: quote.Term,
7358                annualMileage,
7359                interest: quote.TotalCharges,
7360                totalPay: quote.TotalPayable,
7361                flat: quote.FlatRate,
7362                apr: quote.APR,
7363                excess: quote.ChargePerOverDistance,
7364                numRegular: Number.isNaN(numRegular) ? null : numRegular,
7365            }),
7366        );
7367        if (!Number.isNaN(termN) && termN > 0) {
7368            panel.setAttribute('data-fe-finance-term', String(termN));
7369        }
7370        return quote;
7371    };
7372
7373    /**
7374     * VLP card CTA: bind stock + stock/get-finance-quote result, then open the eligibility offcanvas.
7375     * Prefer options.quoteResponse from the CTA click (already fetched); otherwise fetch here.
7376     * Closing the panel leaves the user on the listing page (hash cleared on hidden).
7377     */
7378    window.openVlpFinanceEligibilityForCar = async (car, options = {}) => {
7379        if (!panel || !car || !window.bootstrap || !bootstrap.Offcanvas) {
7380            throw new Error('Finance eligibility panel is not ready');
7381        }
7382
7383        const termMonths = parseInt(options.termMonths, 10) || car.LowestPaymentTerm || 48;
7384        let deposit = options.deposit != null ? Number(options.deposit) : NaN;
7385        if (!Number.isFinite(deposit) || deposit <= 0) {
7386            deposit = Math.round((Number(car.AskingPrice) * 0.1) / 1000) * 1000;
7387        }
7388        const annualMileage =
7389            options.annualMileage != null && Number.isFinite(Number(options.annualMileage))
7390                ? Number(options.annualMileage)
7391                : 10000;
7392
7393        // Prefer get-finance-quote snapshot over VDP calculator DOM (page car may differ).
7394        panel.setAttribute('data-fe-bound-listing-cta', 'true');
7395        panel.removeAttribute('data-fe-hide-listing');
7396        panel.setAttribute('data-fe-stock-id', String(car.StockNumber));
7397        if (car.Registration) {
7398            panel.setAttribute('data-fe-stock-registration', String(car.Registration).trim());
7399        }
7400        if (car.HeadlineDescription) {
7401            panel.setAttribute('data-fe-stock-headline', String(car.HeadlineDescription).trim());
7402        }
7403        if (car.DisplayLocation) {
7404            panel.setAttribute('data-fe-stock-location', String(car.DisplayLocation).trim());
7405        }
7406        if (car.SiteID != null) {
7407            panel.setAttribute('data-fe-site-id', String(car.SiteID));
7408        }
7409        // Feeds the test-B intro vehicle card, which the shared drawer renders empty.
7410        const cardImage = Array.isArray(car.SliderImages) && car.SliderImages.length > 0
7411            ? car.SliderImages[0].ClearImage
7412            : null;
7413        if (cardImage) {
7414            panel.setAttribute('data-fe-stock-image', String(cardImage));
7415        }
7416        if (Number.isFinite(Number(car.AskingPrice))) {
7417            panel.setAttribute('data-fe-stock-price', String(Number(car.AskingPrice)));
7418        }
7419        if (Number.isFinite(Number(car.Mileage))) {
7420            panel.setAttribute('data-fe-stock-mileage', String(Number(car.Mileage)));
7421        }
7422        panel.setAttribute('data-fe-finance-term', String(termMonths));
7423        syncFinanceEligibilityListingFromPanel();
7424
7425        let quoteResponse = options.quoteResponse || null;
7426        if (!quoteResponse) {
7427            if (typeof API === 'undefined' || !API.Stock || typeof API.Stock.GetFinanceQuote !== 'function') {
7428                throw new Error('stock/get-finance-quote API unavailable');
7429            }
7430            quoteResponse = await API.Stock.GetFinanceQuote(
7431                car.StockNumber,
7432                termMonths,
7433                annualMileage,
7434                deposit,
7435            );
7436        }
7437
7438        const appliedQuote = applyFinanceQuoteResponseToEligibilityPanel(quoteResponse, {
7439            annualMileage,
7440            financeQuoteType: options.financeQuoteType,
7441            effectiveFinanceFilter: options.effectiveFinanceFilter,
7442        });
7443        if (!appliedQuote) {
7444            throw new Error('No finance quote returned for this vehicle');
7445        }
7446
7447        syncEligibilityAgreementRepresentative();
7448        syncReviewConsentLenderName();
7449        bootstrap.Offcanvas.getOrCreateInstance(panel).show();
7450        return quoteResponse;
7451    };
7452
7453    const formatFeMoney = (n) => {
7454        const x = Number(n);
7455        if (!Number.isFinite(x)) {
7456            return '—';
7457        }
7458        return new Intl.NumberFormat('en-GB', { style: 'currency', currency: 'GBP' }).format(x);
7459    };
7460
7461    const formatFeApr = (n) => {
7462        const x = Number(n);
7463        if (!Number.isFinite(x)) {
7464            return '—';
7465        }
7466        const s = x.toFixed(1);
7467        return `${s}%`;
7468    };
7469
7470    const firstNonBlank = (...vals) => {
7471        for (let i = 0; i < vals.length; i += 1) {
7472            const v = vals[i];
7473            if (v == null) {
7474                continue;
7475            }
7476            if (typeof v === 'string') {
7477                if (v.trim() !== '') {
7478                    return v;
7479                }
7480                continue;
7481            }
7482            return v;
7483        }
7484        return null;
7485    };
7486
7487    const toFiniteNumberOrNull = (v) => {
7488        const n = Number(v);
7489        return Number.isFinite(n) ? n : null;
7490    };
7491
7492    const pickPaymentSchedules = (figures) => {
7493        if (Array.isArray(figures.PaymentSchedules)) {
7494            return figures.PaymentSchedules;
7495        }
7496        if (Array.isArray(figures.paymentSchedules)) {
7497            return figures.paymentSchedules;
7498        }
7499        return [];
7500    };
7501
7502    const pickRegularSchedule = (schedules) => {
7503        const byPeriod = schedules.find((s) => Number(s?.Period) === 1);
7504        if (byPeriod) {
7505            return byPeriod;
7506        }
7507        return schedules.find((s) => Number(s?.period) === 1) || null;
7508    };
7509
7510    const resolveRegularPaymentFromFigures = (figures, regularSchedule) => {
7511        const fromFigures =
7512            toFiniteNumberOrNull(figures.RegularPayment) ??
7513            toFiniteNumberOrNull(figures.regularPayment);
7514        if (fromFigures != null) {
7515            return fromFigures;
7516        }
7517        const fromSchedule =
7518            toFiniteNumberOrNull(regularSchedule?.Amount) ??
7519            toFiniteNumberOrNull(regularSchedule?.amount);
7520        if (fromSchedule != null) {
7521            return fromSchedule;
7522        }
7523        return (
7524            toFiniteNumberOrNull(figures.FirstPayment) ??
7525            toFiniteNumberOrNull(figures.firstPayment)
7526        );
7527    };
7528
7529    const resolveFinanceTypeFromResult = (quoteRoot, productQuote, balloon) => {
7530        const declaredType = firstNonBlank(
7531            quoteRoot.Type,
7532            quoteRoot.type,
7533            productQuote.Type,
7534            productQuote.type,
7535        );
7536        if (declaredType != null && String(declaredType).trim() !== '') {
7537            return String(declaredType).trim();
7538        }
7539        if (balloon != null && balloon > 0) {
7540            return 'PCP';
7541        }
7542        return 'HP';
7543    };
7544
7545    const resolveEligibilityOfferedFromResultRow = (raw) =>
7546        firstNonBlank(
7547            raw.EligibilityOffered,
7548            raw.eligibilityOffered,
7549            raw?.LenderData?.Eligibility,
7550            raw?.lenderData?.eligibility,
7551        );
7552
7553    /**
7554     * Normalizes one Data.Results[] row for BMW UX cards/detail.
7555     * Finance figures: OfferedQuote.ProductQuote.Figures (+ Funder).
7556     * Eligibility badge: EligibilityOffered (LenderData.Eligibility fallback only).
7557     */
7558    const normalizeFinanceEligibilityQuote = (raw) => {
7559        if (!raw || typeof raw !== 'object') {
7560            return null;
7561        }
7562
7563        const offeredQuote = raw.OfferedQuote || raw.offeredQuote || null;
7564        const hasOfferedQuote = offeredQuote && typeof offeredQuote === 'object';
7565        const eligibilityOffered = resolveEligibilityOfferedFromResultRow(raw);
7566        if (!hasOfferedQuote && !eligibilityOffered) {
7567            return null;
7568        }
7569
7570        const quoteRoot = hasOfferedQuote ? offeredQuote : {};
7571        const productQuote = quoteRoot.ProductQuote || quoteRoot.productQuote || {};
7572        const figures = productQuote.Figures || productQuote.figures || {};
7573        const asset = figures.Asset || figures.asset || quoteRoot.Asset || quoteRoot.asset || {};
7574        const schedules = pickPaymentSchedules(figures);
7575        const regularSchedule = pickRegularSchedule(schedules);
7576        const regularPayment = resolveRegularPaymentFromFigures(figures, regularSchedule);
7577        const balloon = toFiniteNumberOrNull(figures.Balloon) ?? toFiniteNumberOrNull(figures.balloon);
7578        const financeType = resolveFinanceTypeFromResult(quoteRoot, productQuote, balloon);
7579        const funder = quoteRoot.Funder || quoteRoot.funder || {};
7580        const funderCode = firstNonBlank(funder.Code, funder.code, productQuote.FunderCode, productQuote.funderCode);
7581        const productName = firstNonBlank(productQuote.ProductName, productQuote.productName);
7582        const lenderName = firstNonBlank(funder.Name, funder.name, productName, funderCode, 'Finance offer');
7583
7584        return {
7585            OfferedQuote: offeredQuote,
7586            OriginalQuoteId: raw.OriginalQuoteId || raw.originalQuoteId || null,
7587            LenderData: raw.LenderData || raw.lenderData || null,
7588            PublicMessage: firstNonBlank(raw.PublicMessage, raw.publicMessage),
7589            Message: firstNonBlank(raw.Message, raw.message),
7590            Type: financeType,
7591            FunderCode: funderCode || '',
7592            ProductName: productName || lenderName,
7593            lenderName: String(lenderName || 'Finance offer'),
7594            QuoteUID: firstNonBlank(
7595                productQuote.QuoteId,
7596                productQuote.quoteId,
7597                productQuote.QuoteUID,
7598                productQuote.quoteUID,
7599                raw.OriginalQuoteId,
7600                raw.originalQuoteId,
7601            ),
7602            APR: toFiniteNumberOrNull(figures.APR) ?? toFiniteNumberOrNull(figures.apr),
7603            RegularPayment: regularPayment,
7604            Term: toFiniteNumberOrNull(figures.Term) ?? toFiniteNumberOrNull(figures.term),
7605            TotalPayable:
7606                toFiniteNumberOrNull(figures.TotalPayable) ?? toFiniteNumberOrNull(figures.totalPayable),
7607            CashPrice:
7608                toFiniteNumberOrNull(figures.TotalCashPrice) ??
7609                toFiniteNumberOrNull(figures.totalCashPrice),
7610            Deposit:
7611                toFiniteNumberOrNull(figures.TotalDeposit) ??
7612                toFiniteNumberOrNull(figures.totalDeposit) ??
7613                toFiniteNumberOrNull(asset.CashDeposit),
7614            CreditAmount:
7615                toFiniteNumberOrNull(figures.Advance) ?? toFiniteNumberOrNull(figures.advance),
7616            OptionToPurchaseFee:
7617                toFiniteNumberOrNull(figures.OptionToPurchaseFee) ??
7618                toFiniteNumberOrNull(figures.optionToPurchaseFee),
7619            FirstPayment:
7620                toFiniteNumberOrNull(figures.FirstPayment) ??
7621                toFiniteNumberOrNull(figures.firstPayment) ??
7622                regularPayment,
7623            FollowedByPayment: regularPayment,
7624            FinalPayment:
7625                toFiniteNumberOrNull(figures.FinalPayment) ??
7626                toFiniteNumberOrNull(figures.finalPayment) ??
7627                regularPayment,
7628            InterestCharges:
7629                toFiniteNumberOrNull(figures.InterestCharges) ??
7630                toFiniteNumberOrNull(figures.interestCharges),
7631            AnnualFixedRate:
7632                toFiniteNumberOrNull(figures.InterestRate) ??
7633                toFiniteNumberOrNull(figures.interestRate) ??
7634                toFiniteNumberOrNull(figures.FlatRate) ??
7635                toFiniteNumberOrNull(figures.flatRate),
7636            EligibilityOffered: eligibilityOffered,
7637            EligibilityCategory: eligibilityOffered,
7638        };
7639    };
7640
7641    const normalizeFinanceEligibilityQuotes = (arr) => {
7642        if (!Array.isArray(arr)) {
7643            return [];
7644        }
7645        return arr.map(normalizeFinanceEligibilityQuote).filter(Boolean);
7646    };
7647
7648    /** API `results` only (Data.Results). Never iVendi Quotes[]. */
7649    const extractFinanceEligibilityRawQuotes = (scanResponse) => {
7650        const src = scanResponse || {};
7651        if (Array.isArray(src.results) && src.results.length > 0) {
7652            return src.results;
7653        }
7654        if (Array.isArray(src.Results) && src.Results.length > 0) {
7655            return src.Results;
7656        }
7657        return [];
7658    };
7659
7660    const sortEligibilityQuotes = (mode, quotes) => {
7661        const arr = [...quotes];
7662        if (mode === 'payment') {
7663            arr.sort((a, b) => (Number(a.RegularPayment) || 0) - (Number(b.RegularPayment) || 0));
7664        } else if (mode === 'apr') {
7665            arr.sort((a, b) => (Number(a.APR) || 0) - (Number(b.APR) || 0));
7666        } else {
7667            const order = ['pre-approved', 'high', 'alternative-offer', 'medium', 'low', 'unable', 'unknown'];
7668            const rank = (q) => {
7669                const k = resolveFeEligibilityCategoryKey(q);
7670                const ix = order.indexOf(k);
7671                return ix >= 0 ? ix : 99;
7672            };
7673            arr.sort((a, b) => {
7674                const ra = rank(a);
7675                const rb = rank(b);
7676                if (ra !== rb) {
7677                    return ra - rb;
7678                }
7679                return (Number(a.RegularPayment) || 0) - (Number(b.RegularPayment) || 0);
7680            });
7681        }
7682        return arr;
7683    };
7684
7685    const parseQuoteFromResultsCard = (card) => {
7686        if (!card?.dataset?.feQuoteJson) {
7687            return null;
7688        }
7689        try {
7690            return JSON.parse(card.dataset.feQuoteJson);
7691        } catch {
7692            return null;
7693        }
7694    };
7695
7696    const escapeHtml = (s) =>
7697        String(s)
7698            .replace(/&/g, '&amp;')
7699            .replace(/</g, '&lt;')
7700            .replace(/>/g, '&gt;')
7701            .replace(/"/g, '&quot;');
7702
7703    const lenderDetailRowHtml = (label, value) => {
7704        const v = value == null || value === '' ? '—' : String(value);
7705        return `<div class="cars-fe-lender-detail__breakdown-row"><span class="cars-fe-lender-detail__breakdown-label">${escapeHtml(label)}</span><span class="cars-fe-lender-detail__breakdown-value">${escapeHtml(v)}</span></div>`;
7706    };
7707
7708    /** Figma BMW UX node 40000462:28429 — summary metric rows (distinct from quote breakdown list). */
7709    const lenderDetailSummaryMetricRowHtml = (label, value) => {
7710        const v = value == null || value === '' ? '—' : String(value);
7711        return `<div class="cars-fe-lender-detail__summary-metric-row"><span class="cars-fe-lender-detail__summary-label">${escapeHtml(label)}</span><span class="cars-fe-lender-detail__summary-value">${escapeHtml(v)}</span></div>`;
7712    };
7713
7714    const lenderDetailSummaryDividerHtml = () =>
7715        '<div class="cars-fe-lender-detail__summary-divider" aria-hidden="true"></div>';
7716
7717    const closeLenderDetailFromResults = () => {
7718        const layer = document.getElementById('cars-fe-results-lender-detail');
7719        if (!layer) {
7720            return;
7721        }
7722        layer.hidden = true;
7723        layer.setAttribute('aria-hidden', 'true');
7724        syncFinanceEligibilityJourneyUrl();
7725    };
7726
7727    /** CMS /finance/ story: no listing on panel (see data-fe-hide-listing). */
7728    const isFeFinanceStoryContext = () => panel?.getAttribute('data-fe-hide-listing') === 'true';
7729    /** Lender detail footer: "Apply now" (Ivendi) vs alternate CTA (test-drive or VLP on finance page). */
7730    // Grouped variant (Figma 40003936:37009) uses a single "More info" CTA on every result card.
7731    const feResultsCardCtaLabelForVariant = (variant) => {
7732        if (isGroupedFormActive()) {
7733            return 'More info';
7734        }
7735        return variant === 'book' ? 'More info' : 'More info and Apply';
7736    };
7737    const syncFeResultsCardCtaLabel = (card, variant) => {
7738        const btn = card?.querySelector?.('.cars-fe-results-card__cta-btn');
7739        if (btn) {
7740            btn.textContent = feResultsCardCtaLabelForVariant(variant);
7741        }
7742    };
7743
7744    const setLenderDetailFooterCtaVisible = (el, show) => {
7745        if (!el) {
7746            return;
7747        }
7748        if (show) {
7749            el.hidden = false;
7750            el.removeAttribute('hidden');
7751            el.setAttribute('aria-hidden', 'false');
7752        } else {
7753            el.hidden = true;
7754            el.setAttribute('hidden', '');
7755            el.setAttribute('aria-hidden', 'true');
7756        }
7757    };
7758
7759    const syncLenderDetailFooterCta = (variant, sourceCard) => {
7760        const useBook = variant === 'book';
7761        const applyEl = document.getElementById('cars-fe-lender-detail-apply');
7762        const bookEl = document.getElementById('cars-fe-lender-detail-book-appointment');
7763        setLenderDetailFooterCtaVisible(applyEl, !useBook);
7764        setLenderDetailFooterCtaVisible(bookEl, useBook);
7765        if (sourceCard) {
7766            syncFeResultsCardCtaLabel(sourceCard, variant);
7767        }
7768    };
7769    const resolveLenderDetailFooterCtaVariant = (quote) => {
7770        if (
7771            !isFeFinanceStoryContext() &&
7772            typeof window.getFinanceEligibilityLenderDetailFooterCta === 'function'
7773        ) {
7774            const fromPosthog = window.getFinanceEligibilityLenderDetailFooterCta();
7775            if (fromPosthog === 'apply' || fromPosthog === 'book') {
7776                return fromPosthog;
7777            }
7778        }
7779        const explicit = String(quote?.lenderDetailFooterCta || quote?.feLenderDetailFooterCta || '')
7780            .trim()
7781            .toLowerCase();
7782        if (explicit === 'book' || explicit === 'appointment') {
7783            return 'book';
7784        }
7785        if (explicit === 'apply') {
7786            return 'apply';
7787        }
7788        if (isFeFinanceStoryContext()) {
7789            return 'book';
7790        }
7791        return 'apply';
7792    };
7793
7794    const populateLenderDetailLayer = (quote, sourceCard) => {
7795        const titleEl = document.getElementById('cars-fe-lender-detail-title');
7796        const badgeText = document.getElementById('cars-fe-lender-detail-badge-text');
7797        const brandEl = document.getElementById('cars-fe-lender-detail-brand');
7798        const summaryType = document.getElementById('cars-fe-lender-detail-summary-type');
7799        const summaryRows = document.getElementById('cars-fe-lender-detail-summary-rows');
7800        const breakdownEl = document.getElementById('cars-fe-lender-detail-breakdown');
7801        const legalEl = document.getElementById('cars-fe-lender-detail-legal');
7802        if (!quote || !titleEl || !badgeText || !brandEl || !summaryType || !summaryRows || !breakdownEl) {
7803            return;
7804        }
7805        const lenderName = (quote.lenderName || quote.ProductName || quote.FunderCode || 'Finance offer').trim();
7806        titleEl.textContent = `Finance Provided by: ${lenderName}`;
7807        if (legalEl) {
7808            legalEl.textContent =
7809                'Credit is subject to status and approval and is only available to UK residents aged 18 and over. ' +
7810                `Credit is provided by ${lenderName}. ` +
7811                `${lenderName} is authorised and regulated by the Financial Conduct Authority.`;
7812        }
7813        const catKey = quote.eligibilityCategoryKey || resolveFeEligibilityCategoryKey(quote);
7814        const label = getFeEligibilityBadgeLabel(quote);
7815        const badgeEl = document.getElementById('cars-fe-lender-detail-badge');
7816        badgeText.textContent = label;
7817        syncFeEligibilityCategoryPillClass(badgeEl, catKey);
7818        if (badgeEl) {
7819            badgeEl.hidden = false;
7820            badgeEl.setAttribute('aria-hidden', 'false');
7821        }
7822
7823        const typeStr = String(quote.Type || '').toUpperCase();
7824        const isPcp = typeStr === 'PCP';
7825        summaryType.textContent = isPcp ? 'Personal Contract Purchase' : 'Hire Purchase';
7826
7827        brandEl.textContent = '';
7828        brandEl.hidden = true;
7829        if (quote.LenderLogoUrl) {
7830            const im = document.createElement('img');
7831            im.src = quote.LenderLogoUrl;
7832            im.alt = quote.LenderLogoAlt || lenderName;
7833            brandEl.appendChild(im);
7834            brandEl.hidden = false;
7835        }
7836
7837        const termM = Number(quote.Term);
7838        const termLabel = Number.isFinite(termM) ? `${Math.round(termM)} months` : '—';
7839        const termSummaryMonthsOnly = Number.isFinite(termM) ? String(Math.round(termM)) : '—';
7840        const rp = formatFeMoney(quote.RegularPayment);
7841        const apr = formatFeApr(quote.APR);
7842        const tp = formatFeMoney(quote.TotalPayable);
7843        const ft = typeStr || '—';
7844
7845        const d = lenderDetailSummaryDividerHtml;
7846        const summaryHtml = [
7847            lenderDetailSummaryMetricRowHtml('Finance type', ft),
7848            d(),
7849            lenderDetailSummaryMetricRowHtml('Monthly Payments', rp),
7850            d(),
7851            `<div class="cars-fe-lender-detail__summary-apr-wrap"><div class="cars-fe-lender-detail__rate-pill">Your Personalised Rate</div>${lenderDetailSummaryMetricRowHtml('APR', apr)}</div>`,
7852            d(),
7853            lenderDetailSummaryMetricRowHtml('Agreement Term', termSummaryMonthsOnly),
7854            d(),
7855            lenderDetailSummaryMetricRowHtml('Total Payable', tp),
7856        ].join('');
7857        summaryRows.innerHTML = summaryHtml;
7858
7859        const fmt = (n) => (n != null && Number.isFinite(Number(n)) ? formatFeMoney(n) : '—');
7860        const n = (v) => (v != null && Number.isFinite(Number(v)) ? Number(v) : null);
7861        const termInt = n(quote.Term);
7862        const midCount =
7863            termInt != null && termInt > 2 ? Math.max(0, Math.round(termInt) - 2) : null;
7864        const rows = [
7865            ['Cash Price', fmt(quote.CashPrice)],
7866            ['Total Deposit (Details)', fmt(quote.Deposit)],
7867            ['Total Amount of Credit', fmt(quote.CreditAmount)],
7868            ['Agreement Term', termLabel],
7869            ['Option to Purchase Fee', fmt(quote.OptionToPurchaseFee)],
7870            ['First Payment', fmt(quote.FirstPayment ?? quote.RegularPayment)],
7871            [
7872                midCount != null && midCount > 0 ? `Followed by ${midCount} Payments` : 'Followed by payments',
7873                fmt(quote.FollowedByPayment ?? quote.RegularPayment),
7874            ],
7875            ['Final Payment', fmt(quote.FinalPayment ?? quote.RegularPayment)],
7876            ['Interest Charges', fmt(quote.InterestCharges)],
7877            ['Annual Fixed Interest Rate', quote.AnnualFixedRate != null ? `${Number(quote.AnnualFixedRate).toFixed(2)}%` : '—'],
7878            ['APR', apr],
7879            ['Total Amount Payable', tp],
7880        ];
7881        breakdownEl.innerHTML = rows.map(([lab, val]) => lenderDetailRowHtml(lab, val)).join('');
7882        syncLenderDetailFooterCta(resolveLenderDetailFooterCtaVariant(quote), sourceCard);
7883    };
7884
7885    const openLenderDetailFromResults = (card) => {
7886        const layer = document.getElementById('cars-fe-results-lender-detail');
7887        if (!layer) {
7888            return;
7889        }
7890        closeFinanceEligibilityResultsGlossary();
7891        const quote = parseQuoteFromResultsCard(card);
7892        if (!quote) {
7893            return;
7894        }
7895        populateLenderDetailLayer(quote, card);
7896        layer.hidden = false;
7897        layer.setAttribute('aria-hidden', 'false');
7898        document.getElementById('cars-fe-lender-detail-close')?.focus?.();
7899        syncFinanceEligibilityJourneyUrl();
7900    };
7901
7902    const renderEligibilityResultCards = (quotes) => {
7903        if (!resultsCardsMount) {
7904            return;
7905        }
7906        resultsCardsMount.textContent = '';
7907        quotes.forEach((quote) => {
7908            if (!quote) {
7909                return;
7910            }
7911            const catKey = resolveFeEligibilityCategoryKey(quote);
7912            const badgeLabel = getFeEligibilityBadgeLabel(quote);
7913            const lenderName = quote.lenderName || quote.ProductName || quote.FunderCode || 'Finance offer';
7914
7915            const card = document.createElement('article');
7916            card.className = 'cars-fe-results-card';
7917            card.dataset.feLenderName = lenderName;
7918
7919            const head = document.createElement('div');
7920            head.className = 'cars-fe-results-card__head';
7921            const badge = document.createElement('div');
7922            badge.className = 'cars-fe-results-card__badge';
7923            syncFeEligibilityCategoryPillClass(badge, catKey);
7924            const badgeText = document.createElement('span');
7925            badgeText.textContent = badgeLabel;
7926            badge.appendChild(badgeText);
7927            const badgeIcon = document.createElement('span');
7928            badgeIcon.className = 'cars-fe-results-card__badge-icon';
7929            badgeIcon.setAttribute('role', 'button');
7930            badgeIcon.setAttribute('tabindex', '0');
7931            badgeIcon.setAttribute('aria-label', 'Open product details');
7932            badge.appendChild(badgeIcon);
7933            head.appendChild(badge);
7934
7935            const brand = document.createElement('div');
7936            brand.className = 'cars-fe-results-card__brand';
7937            if (quote.LenderLogoUrl) {
7938                const img = document.createElement('img');
7939                img.className = 'cars-fe-results-card__brand-img';
7940                img.src = quote.LenderLogoUrl;
7941                img.alt = quote.LenderLogoAlt || lenderName;
7942                img.loading = 'lazy';
7943                brand.appendChild(img);
7944            } else {
7945                brand.classList.add('cars-fe-results-card__brand--text');
7946                const lender = document.createElement('div');
7947                lender.className = 'cars-fe-results-card__lender';
7948                lender.textContent = lenderName;
7949                brand.appendChild(lender);
7950            }
7951            head.appendChild(brand);
7952            card.appendChild(head);
7953
7954            const rows = document.createElement('div');
7955            rows.className = 'cars-fe-results-card__rows';
7956
7957            const typeStr = String(quote.Type || '').toUpperCase();
7958            const termMonths = Number(quote.Term);
7959            const rowSpecs = [
7960                ['Finance type', typeStr || '—'],
7961                ['Monthly Payments', formatFeMoney(quote.RegularPayment)],
7962                // Grouped variant (Figma 40003936:37009) spells out that the APR is indicative.
7963                [isGroupedFormActive() ? 'APR (indicative)' : 'APR', formatFeApr(quote.APR)],
7964                ['Agreement Term', Number.isFinite(termMonths) ? String(Math.round(termMonths)) : '—'],
7965                ['Total Payable', formatFeMoney(quote.TotalPayable)],
7966            ];
7967            rowSpecs.forEach(([label, value], idx) => {
7968                const row = document.createElement('div');
7969                row.className = 'cars-fe-results-card__row';
7970                const lab = document.createElement('div');
7971                lab.className = 'cars-fe-results-card__label';
7972                lab.textContent = label;
7973                const valCell = document.createElement('div');
7974                valCell.className = 'cars-fe-results-card__value';
7975                valCell.textContent = value;
7976                row.appendChild(lab);
7977                row.appendChild(valCell);
7978                rows.appendChild(row);
7979                if (idx < rowSpecs.length - 1) {
7980                    const rule = document.createElement('div');
7981                    rule.className = 'cars-fe-results-card__rule';
7982                    rows.appendChild(rule);
7983                }
7984            });
7985
7986            card.appendChild(rows);
7987
7988            const ctaWrap = document.createElement('div');
7989            ctaWrap.className = 'cars-fe-results-card__cta';
7990            const cta = document.createElement('button');
7991            cta.type = 'button';
7992            cta.className = 'btn primary rounded cars-fe-results-card__cta-btn';
7993            const footerVariant = resolveLenderDetailFooterCtaVariant(quote);
7994            cta.textContent = feResultsCardCtaLabelForVariant(footerVariant);
7995            ctaWrap.appendChild(cta);
7996            card.appendChild(ctaWrap);
7997
7998            try {
7999                card.dataset.feQuoteJson = JSON.stringify({
8000                    ...quote,
8001                    lenderName,
8002                    badgeLabel,
8003                    eligibilityCategoryKey: catKey,
8004                });
8005            } catch {
8006                /* ignore non-serializable */
8007            }
8008
8009            resultsCardsMount.appendChild(card);
8010        });
8011    };
8012
8013    const showFinanceResultsModalChrome = () => {
8014        if (!resultsModal) {
8015            return;
8016        }
8017        closeFinanceEligibilityResultsGlossary();
8018        syncResultsVehicleStripFromPanel();
8019        resultsModal.hidden = false;
8020        resultsModal.setAttribute('aria-hidden', 'false');
8021        if (resultsModalStatus) {
8022            resultsModalStatus.hidden = true;
8023            resultsModalStatus.textContent = '';
8024        }
8025        syncResultsModalEmptyState(true);
8026        syncFinanceEligibilityJourneyUrl();
8027    };
8028
8029    const openFinanceEligibilityResultsModal = () => {
8030        if (!resultsModal) {
8031            return;
8032        }
8033        const resultsDemo =
8034            panel && String(panel.getAttribute('data-fe-eligibility-results-demo') || '').toLowerCase() === 'true';
8035        if (!resultsDemo) {
8036            return;
8037        }
8038        showFinanceResultsModalChrome();
8039        syncFinanceEligibilityJourneyUrl('submit-clicked');
8040
8041        const financeResultsMinLoadMs = 2000;
8042        const financeResultsMinLoadDelay = () => new Promise((resolve) => setTimeout(resolve, financeResultsMinLoadMs));
8043
8044        if (resultsCardsMount) {
8045            resultsCardsMount.textContent = '';
8046        }
8047        setFinanceResultsModalLoading(true);
8048        financeResultsMinLoadDelay().then(() => {
8049            setFinanceResultsModalLoading(false);
8050            if (!eligibilityQuotesCache.length) {
8051                syncResultsModalEmptyState(false);
8052                return;
8053            }
8054            syncResultsModalEmptyState(true);
8055            const mode = resultsSortSelect && resultsSortSelect.value ? resultsSortSelect.value : 'eligibility';
8056            renderEligibilityResultCards(sortEligibilityQuotes(mode, eligibilityQuotesCache));
8057        });
8058    };
8059
8060    const collectFinanceEligibilityQuoteIds = () => {
8061        const fromVdpTab = getVdpFinanceQuoteIdsFromTabContainer(document.querySelector('.tab-content-container'));
8062        if (fromVdpTab.length > 0) {
8063            return fromVdpTab;
8064        }
8065        try {
8066            const raw = panel?.getAttribute('data-fe-finance-eligibility-quote-ids');
8067            if (raw) {
8068                const parsed = JSON.parse(raw);
8069                if (Array.isArray(parsed)) {
8070                    return parsed.filter((x) => typeof x === 'string' && x.trim().length > 0);
8071                }
8072            }
8073        } catch {
8074            /* ignore */
8075        }
8076        return [];
8077    };
8078
8079    const resolveFinanceEligibilitySiteId = () => {
8080        const n = parseInt(panel?.getAttribute('data-fe-site-id') || '0', 10);
8081        return Number.isNaN(n) || n <= 0 ? 0 : n;
8082    };
8083
8084    const resolveFinanceEligibilityDealershipId = () => {
8085        // API field is dealershipId; Ivendi Connect Finance Scan expects IvendiRetailerId (not Quoteware IvendiDealerID).
8086        return String(panel?.getAttribute('data-fe-ivendi-retailer-id') || '').trim();
8087    };
8088
8089    const eligibilityPollMaxMs = 90000;
8090    const eligibilityPollIntervalMs = 2000;
8091
8092    const isFinanceScanStatusPending = (scanStatus) => {
8093        const s = String(scanStatus || '').trim().toLowerCase();
8094        if (!s) {
8095            return true;
8096        }
8097        return (
8098            s === 'pending' ||
8099            s === 'processing' ||
8100            s.includes('process') ||
8101            s.includes('progress') ||
8102            s === 'queued'
8103        );
8104    };
8105
8106    const isFinanceScanStatusTerminal = (scanStatus) => {
8107        const s = String(scanStatus || '').trim().toLowerCase();
8108        if (!s) {
8109            return false;
8110        }
8111        if (isFinanceScanStatusPending(s)) {
8112            return false;
8113        }
8114        const failHints = ['fail', 'error', 'cancel', 'reject', 'timeout', 'timed out', 'invalid'];
8115        if (failHints.some((h) => s.includes(h))) {
8116            return true;
8117        }
8118        const doneHints = ['complete', 'finished', 'done', 'succeeded', 'success'];
8119        return doneHints.some((h) => s === h || s.startsWith(h));
8120    };
8121
8122    /**
8123     * Keep polling while Status is Pending/Processing even if some Results already arrived.
8124     * When we know how many quoteIds were submitted, also wait until results.length >= that count
8125     * (iVendi returns one Results row per quoteId).
8126     */
8127    const shouldStopFinanceScanClientPoll = (
8128        resultsLength,
8129        scanStatus,
8130        elapsedMs,
8131        maxMs,
8132        expectedResultsCount = 0,
8133    ) => {
8134        if (elapsedMs >= maxMs) {
8135            return true;
8136        }
8137        const expected = Number(expectedResultsCount) || 0;
8138        if (expected > 0 && resultsLength >= expected) {
8139            return true;
8140        }
8141        return isFinanceScanStatusTerminal(scanStatus);
8142    };
8143
8144    const runFeScanClientPoll = async (
8145        retailerId,
8146        scanId,
8147        maxMs,
8148        intervalMs,
8149        generation,
8150        expectedResultsCount = 0,
8151    ) => {
8152        let list = [];
8153        const start = Date.now();
8154        while (true) {
8155            if (generation !== feScanPollGeneration) {
8156                return { list, cancelled: true };
8157            }
8158            const elapsed = Date.now() - start;
8159            let poll = null;
8160            try {
8161                poll = await API.Stock.PollFinanceEligibility(retailerId, scanId);
8162            } catch {
8163                poll = null;
8164            }
8165            if (poll != null) {
8166                setFinanceScanLoadingRetryVisible(false);
8167            } else {
8168                setFinanceScanLoadingRetryVisible(true);
8169            }
8170            const rawResults = extractFinanceEligibilityRawQuotes(poll);
8171            list = normalizeFinanceEligibilityQuotes(rawResults);
8172            const st = poll?.scanStatus;
8173            if (
8174                shouldStopFinanceScanClientPoll(
8175                    rawResults.length,
8176                    st,
8177                    elapsed,
8178                    maxMs,
8179                    expectedResultsCount,
8180                )
8181            ) {
8182                break;
8183            }
8184            await new Promise((r) => setTimeout(r, intervalMs));
8185        }
8186        return { list, cancelled: false };
8187    };
8188
8189    /** Success copy is variant-specific (Figma 40003936:37009 for grouped); empty/error copy is shared. */
8190    const resultsIntroSubCopy = (introSub, hasQuotes, isError) => {
8191        if (hasQuotes) {
8192            const grouped = panel?.dataset.feFormDensity === 'grouped';
8193            const attr = grouped ? 'data-fe-results-sub-grouped' : 'data-fe-results-sub-control';
8194            return introSub.getAttribute(attr) || 'Find your perfect finance option.';
8195        }
8196        if (isError) {
8197            return 'We couldn’t complete your finance check with the details provided.';
8198        }
8199        return 'No finance products matched with your current details.';
8200    };
8201
8202    const syncResultsModalEmptyState = (hasQuotes, options = {}) => {
8203        const toolbar = resultsModal?.querySelector('.cars-fe-results-modal__toolbar');
8204        const introSub = resultsModal?.querySelector('.cars-fe-results-modal__intro-sub');
8205        const bookBtn = document.getElementById('cars-fe-results-book-appointment');
8206        const updateBtn = document.getElementById('cars-fe-results-update-details');
8207        const isError = Boolean(options.isError);
8208        if (resultsModalStatus) {
8209            resultsModalStatus.hidden = true;
8210            resultsModalStatus.textContent = '';
8211        }
8212        if (toolbar) {
8213            toolbar.hidden = !hasQuotes;
8214        }
8215        if (introSub) {
8216            introSub.textContent = resultsIntroSubCopy(introSub, hasQuotes, isError);
8217        }
8218        if (bookBtn) {
8219            bookBtn.hidden = !hasQuotes;
8220        }
8221        if (updateBtn) {
8222            updateBtn.hidden = hasQuotes;
8223        }
8224        // The grouped variant's secondary link belongs with the primary CTA: with no results the
8225        // footer offers "Update your details" alone, so it must not linger beside it.
8226        const continueSearchingLink = document.getElementById('cars-fe-results-continue-searching');
8227        if (continueSearchingLink) {
8228            continueSearchingLink.hidden = !hasQuotes;
8229        }
8230        if (!hasQuotes && resultsCardsMount) {
8231            resultsCardsMount.textContent = '';
8232            const empty = document.createElement('div');
8233            empty.id = 'cars-fe-results-modal-empty';
8234            empty.className = 'cars-fe-results-modal__empty';
8235            empty.setAttribute('role', 'status');
8236            const title = document.createElement('p');
8237            title.className = 'cars-fe-results-modal__empty-title';
8238            title.textContent = 'Unable to retrieve any finance results';
8239            const body = document.createElement('p');
8240            body.className = 'cars-fe-results-modal__empty-body';
8241            body.textContent = 'Please check your details and try again';
8242            empty.appendChild(title);
8243            empty.appendChild(body);
8244            resultsCardsMount.appendChild(empty);
8245        }
8246    };
8247
8248    /** Scan/API failures: hide raw errors and offer Update your details instead. */
8249    const showResultsUpdateDetailsFallback = () => {
8250        eligibilityQuotesCache = [];
8251        setFinanceResultsModalLoading(false);
8252        syncResultsModalEmptyState(false, { isError: true });
8253    };
8254
8255    const applyFinanceEligibilityScanPollResult = (list) => {
8256        eligibilityQuotesCache = list;
8257        if (list.length === 0) {
8258            syncResultsModalEmptyState(false);
8259        } else {
8260            syncResultsModalEmptyState(true);
8261            const mode = resultsSortSelect && resultsSortSelect.value ? resultsSortSelect.value : 'eligibility';
8262            renderEligibilityResultCards(sortEligibilityQuotes(mode, list));
8263        }
8264        setFinanceResultsModalLoading(false);
8265    };
8266
8267    document.getElementById('cars-fe-results-loading-retry')?.addEventListener('click', async (e) => {
8268        e.preventDefault();
8269        const ctx = feScanPollRetryContext;
8270        if (
8271            !ctx?.retailerId ||
8272            !ctx?.scanId ||
8273            typeof API === 'undefined' ||
8274            !API.Stock ||
8275            typeof API.Stock.PollFinanceEligibility !== 'function'
8276        ) {
8277            return;
8278        }
8279        feScanPollGeneration += 1;
8280        const pollGen = feScanPollGeneration;
8281        const btn = e.currentTarget;
8282        btn.disabled = true;
8283        setFinanceScanLoadingRetryVisible(false);
8284        try {
8285            const { list, cancelled } = await runFeScanClientPoll(
8286                ctx.retailerId,
8287                ctx.scanId,
8288                eligibilityPollMaxMs,
8289                eligibilityPollIntervalMs,
8290                pollGen,
8291                ctx.expectedResultsCount || 0,
8292            );
8293            if (cancelled) {
8294                return;
8295            }
8296            applyFinanceEligibilityScanPollResult(list);
8297        } catch (err) {
8298            showResultsUpdateDetailsFallback();
8299        } finally {
8300            btn.disabled = false;
8301        }
8302    });
8303
8304    reviewViewResultsBtn?.addEventListener('click', (e) => {
8305        e.preventDefault();
8306        if (
8307            reviewViewResultsBtn.getAttribute('aria-disabled') === 'true' ||
8308            reviewViewResultsBtn.classList.contains('cars-fe-next--disabled')
8309        ) {
8310            if (!reviewConsentCb || !reviewConsentCb.checked) {
8311                syncReviewConsentError(true);
8312                const consentRow = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row');
8313                if (consentRow) {
8314                    financeEligibilityScheduleFieldScrollIntoView(consentRow, {
8315                        behavior: 'smooth',
8316                        topPaddingPx: 16,
8317                    });
8318                }
8319                reviewConsentCb?.focus?.();
8320            }
8321            return;
8322        }
8323        if (!reviewConsentCb || !reviewConsentCb.checked) {
8324            syncReviewConsentError(true);
8325            const consentRow = document.querySelector('#cars-fe-step-review .cars-fe-review-consent__row');
8326            if (consentRow) {
8327                financeEligibilityScheduleFieldScrollIntoView(consentRow, {
8328                    behavior: 'smooth',
8329                    topPaddingPx: 16,
8330                });
8331            }
8332            reviewConsentCb?.focus?.();
8333            return;
8334        }
8335        syncReviewConsentError(false);
8336        const journey = collectJourneyDetailPayload();
8337        const resultsDemo =
8338            panel && String(panel.getAttribute('data-fe-eligibility-results-demo') || '').toLowerCase() === 'true';
8339        if (!resultsDemo) {
8340            const ivendiFieldErrors = getFinanceEligibilityIvendiFieldErrors(journey);
8341            if (ivendiFieldErrors.length > 0) {
8342                window.alert(ivendiFieldErrors.join('\n'));
8343                return;
8344            }
8345            const panelStockId = panel.getAttribute('data-fe-stock-id') || '';
8346            if (!resolveFinanceEligibilityDealershipId()) {
8347                window.alert('Retailer ID is required before submitting a finance eligibility scan.');
8348                return;
8349            }
8350            if (!String(panelStockId).trim()) {
8351                window.alert('Stock is required before submitting a finance eligibility scan.');
8352                return;
8353            }
8354            if (collectFinanceEligibilityQuoteIds().length === 0) {
8355                window.alert(
8356                    'A finance quote ID is required. Generate or select a finance quote for this vehicle, then try again.',
8357                );
8358                return;
8359            }
8360        }
8361        window.dispatchEvent(
8362            new CustomEvent('cars-finance-eligibility-panel-complete', {
8363                bubbles: true,
8364                detail: journey,
8365            }),
8366        );
8367
8368        if (resultsDemo) {
8369            openFinanceEligibilityResultsModal();
8370            return;
8371        }
8372
8373        showFinanceResultsModalChrome();
8374        syncFinanceEligibilityJourneyUrl('submit-clicked');
8375
8376        const stockId = panel.getAttribute('data-fe-stock-id');
8377        const term = parseInt(panel.getAttribute('data-fe-finance-term') || '48', 10) || 48;
8378        if (
8379            !stockId ||
8380            typeof API === 'undefined' ||
8381            !API.Stock ||
8382            typeof API.Stock.SubmitFinanceEligibility !== 'function' ||
8383            typeof API.Stock.PollFinanceEligibility !== 'function'
8384        ) {
8385            showResultsUpdateDetailsFallback();
8386            return;
8387        }
8388
8389        setFinanceResultsModalLoading(true);
8390        if (resultsCardsMount) {
8391            resultsCardsMount.textContent = '';
8392        }
8393
8394        const quoteIds = collectFinanceEligibilityQuoteIds();
8395        const scanRequestPayload = {
8396            stockId: parseInt(stockId, 10),
8397            term,
8398            journey,
8399            dealershipId: resolveFinanceEligibilityDealershipId(),
8400            webhookCallbackUrl: '',
8401            quoteIds,
8402            clientPoll: true,
8403        };
8404
8405        (async () => {
8406            try {
8407                const res = await API.Stock.SubmitFinanceEligibility(scanRequestPayload);
8408                let list = normalizeFinanceEligibilityQuotes(extractFinanceEligibilityRawQuotes(res));
8409                const retailerForPoll = res && res.retailerId;
8410                const financeScanId = res && res.financeScanId;
8411                const expectedResultsCount = quoteIds.length;
8412
8413                if (list.length === 0 && financeScanId && retailerForPoll && res?.clientPoll) {
8414                    feScanPollRetryContext = {
8415                        retailerId: retailerForPoll,
8416                        scanId: financeScanId,
8417                        expectedResultsCount,
8418                    };
8419                    feScanPollGeneration += 1;
8420                    const pollGen = feScanPollGeneration;
8421                    const { list: polled, cancelled } = await runFeScanClientPoll(
8422                        retailerForPoll,
8423                        financeScanId,
8424                        eligibilityPollMaxMs,
8425                        eligibilityPollIntervalMs,
8426                        pollGen,
8427                        expectedResultsCount,
8428                    );
8429                    if (cancelled) {
8430                        return;
8431                    }
8432                    list = polled;
8433                } else {
8434                    feScanPollRetryContext = null;
8435                }
8436
8437                applyFinanceEligibilityScanPollResult(list);
8438            } catch (err) {
8439                showResultsUpdateDetailsFallback();
8440            }
8441        })();
8442    });
8443
8444    reviewConsentCb?.addEventListener('change', () => {
8445        validateReviewConsent();
8446        if (reviewStep && reviewStep.hidden === false) {
8447            syncFinanceEligibilityJourneyUrl();
8448        }
8449    });
8450
8451    document.getElementById('cars-fe-results-modal-close')?.addEventListener('click', (e) => {
8452        e.preventDefault();
8453        closeFinanceEligibilityPanel();
8454    });
8455
8456    document.getElementById('cars-fe-results-continue-searching')?.addEventListener('click', (e) => {
8457        e.preventDefault();
8458        closeFinanceEligibilityPanel();
8459    });
8460
8461    const closeFinanceEligibilityInfoOverlay = (overlay) => {
8462        if (!overlay || overlay.hidden) {
8463            return;
8464        }
8465        overlay.hidden = true;
8466        overlay.setAttribute('aria-hidden', 'true');
8467        panel?.querySelector(`[data-fe-open-info-overlay="${overlay.id}"]`)?.focus?.();
8468    };
8469
8470    const openFinanceEligibilityInfoOverlay = (overlayId) => {
8471        const overlay = document.getElementById(overlayId);
8472        if (!overlay) {
8473            return;
8474        }
8475        overlay.hidden = false;
8476        overlay.setAttribute('aria-hidden', 'false');
8477        overlay.querySelector('[data-fe-close-info-overlay]')?.focus?.();
8478    };
8479
8480    panel?.addEventListener('click', (e) => {
8481        const opener = e.target.closest?.('[data-fe-open-info-overlay]');
8482        if (opener) {
8483            e.preventDefault();
8484            openFinanceEligibilityInfoOverlay(opener.getAttribute('data-fe-open-info-overlay'));
8485            return;
8486        }
8487        const overlay = e.target.closest?.('.cars-fe-info-overlay');
8488        if (!overlay) {
8489            return;
8490        }
8491        // The close button and the backdrop dismiss it; a click inside the sheet does not.
8492        const dismissed =
8493            e.target.closest?.('[data-fe-close-info-overlay]') || !e.target.closest?.('.cars-fe-info-overlay__sheet');
8494        if (dismissed) {
8495            e.preventDefault();
8496            closeFinanceEligibilityInfoOverlay(overlay);
8497        }
8498    });
8499
8500    document.getElementById('cars-fe-results-glossary-open')?.addEventListener('click', (e) => {
8501        e.preventDefault();
8502        openFinanceEligibilityResultsGlossary();
8503    });
8504
8505    document.getElementById('cars-fe-results-glossary-close')?.addEventListener('click', (e) => {
8506        e.preventDefault();
8507        closeFinanceEligibilityResultsGlossary();
8508    });
8509
8510    document.getElementById('cars-fe-lender-detail-close')?.addEventListener('click', (e) => {
8511        e.preventDefault();
8512        closeLenderDetailFromResults();
8513    });
8514
8515    document.getElementById('cars-fe-lender-detail-apply')?.addEventListener('click', (e) => {
8516        e.preventDefault();
8517        if (typeof window.applyForFinanceFromVdp === 'function') {
8518            window.applyForFinanceFromVdp();
8519        }
8520    });
8521
8522    document.addEventListener('cars-fe-check-eligibility-journey-variant', () => {
8523        // The flag also decides form density (test-c), so re-apply it whatever screen is showing.
8524        applyFormDensityVariant();
8525        const layer = document.getElementById('cars-fe-results-lender-detail');
8526        if (!layer || layer.hidden) {
8527            return;
8528        }
8529        syncLenderDetailFooterCta(resolveLenderDetailFooterCtaVariant(null), null);
8530    });
8531
8532    resultsModal?.addEventListener('click', (e) => {
8533        const badgeIcon = e.target.closest?.('.cars-fe-results-card__badge-icon');
8534        const ctaBtn = e.target.closest?.('.cars-fe-results-card__cta-btn');
8535        if (!badgeIcon && !ctaBtn) {
8536            return;
8537        }
8538        const card = (badgeIcon || ctaBtn).closest?.('.cars-fe-results-card');
8539        if (!card || !resultsCardsMount?.contains(card)) {
8540            return;
8541        }
8542        e.preventDefault();
8543        e.stopPropagation();
8544        openLenderDetailFromResults(card);
8545    });
8546
8547    resultsCardsMount?.addEventListener('keydown', (e) => {
8548        const icon = e.target.closest?.('.cars-fe-results-card__badge-icon');
8549        if (!icon || !resultsCardsMount.contains(icon)) {
8550            return;
8551        }
8552        if (e.key !== 'Enter' && e.key !== ' ') {
8553            return;
8554        }
8555        e.preventDefault();
8556        const card = icon.closest('.cars-fe-results-card');
8557        openLenderDetailFromResults(card);
8558    });
8559
8560    document.addEventListener('keydown', (e) => {
8561        if (e.key !== 'Escape') {
8562            return;
8563        }
8564        const infoOverlay = panel?.querySelector('.cars-fe-info-overlay:not([hidden])');
8565        if (infoOverlay) {
8566            e.preventDefault();
8567            closeFinanceEligibilityInfoOverlay(infoOverlay);
8568            return;
8569        }
8570        const glossaryLay = document.getElementById('cars-fe-results-glossary');
8571        if (glossaryLay && glossaryLay.hidden === false) {
8572            e.preventDefault();
8573            closeFinanceEligibilityResultsGlossary();
8574            return;
8575        }
8576        const layer = document.getElementById('cars-fe-results-lender-detail');
8577        if (!layer || layer.hidden) {
8578            return;
8579        }
8580        e.preventDefault();
8581        closeLenderDetailFromResults();
8582    });
8583
8584    document.getElementById('cars-fe-results-modal-view-details')?.addEventListener('click', () => {
8585        closeFinanceEligibilityResultsModal();
8586        jumpToReviewEdit('details');
8587    });
8588
8589    document.getElementById('cars-fe-results-update-details')?.addEventListener('click', (e) => {
8590        e.preventDefault();
8591        closeFinanceEligibilityResultsModal();
8592        showReviewStep();
8593    });
8594
8595    resultsSortSelect?.addEventListener('change', () => {
8596        if (!resultsSortSelect || eligibilityQuotesCache.length === 0) {
8597            return;
8598        }
8599        renderEligibilityResultCards(sortEligibilityQuotes(resultsSortSelect.value, eligibilityQuotesCache));
8600    });
8601
8602    panel.addEventListener('click', (e) => {
8603        const jumpBtn = e.target.closest('[data-cars-fe-review-jump]');
8604        if (!jumpBtn || !reviewStep || reviewStep.hidden || !reviewStep.contains(jumpBtn)) {
8605            return;
8606        }
8607        e.preventDefault();
8608        jumpToReviewEdit(jumpBtn.getAttribute('data-cars-fe-review-jump'));
8609    });
8610
8611    document.getElementById('cars-fe-employment-details-save-button')?.addEventListener('click', (e) => {
8612        e.preventDefault();
8613        if (!employmentFormValidForSave()) {
8614            requestFeStepFieldHighlights('employment');
8615            applyEmploymentFormFieldHighlights();
8616            financeEligibilityScrollToFirstInvalid(getEmploymentUiHost());
8617            return;
8618        }
8619        const entry = buildEmploymentEntryFromForm(editingEmploymentId != null ? editingEmploymentId : undefined);
8620        if (editingEmploymentId != null && editingEmploymentIndex != null) {
8621            savedEmploymentEntries.splice(editingEmploymentIndex, 1, entry);
8622        } else {
8623            savedEmploymentEntries.push(entry);
8624        }
8625        editingEmploymentId = null;
8626        editingEmploymentIndex = null;
8627        employmentFormMode = 'hidden';
8628        clearEmploymentDetailFormFields();
8629        getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
8630        const employmentStatusHidden = document.getElementById('cars-fe-employment-status');
8631        if (employmentStatusHidden) {
8632            employmentStatusHidden.value = '';
8633        }
8634        syncEmploymentSummaryUiState();
8635        const okAfterSave =
8636            feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped'
8637                ? validateIncomeOutgoingsGrouped()
8638                : validateEmployment();
8639        // Only go back to review once the screen is valid, otherwise a short employment history
8640        // would leave the step before its alert could be read (address saves gate the same way).
8641        if (reviewEditReturnPending && okAfterSave) {
8642            showReviewStep();
8643        }
8644    });
8645
8646    document.getElementById('cars-fe-employment-add-another-button')?.addEventListener('click', (e) => {
8647        e.preventDefault();
8648        if (savedEmploymentEntries.length < 1) {
8649            return;
8650        }
8651        employmentFormMode = 'editing';
8652        editingEmploymentId = null;
8653        editingEmploymentIndex = null;
8654        clearEmploymentDetailFormFields();
8655        getEmploymentUiHost()?.querySelectorAll('.cars-fe-pill.is-selected').forEach((p) => p.classList.remove('is-selected'));
8656        const employmentStatusHidden = document.getElementById('cars-fe-employment-status');
8657        if (employmentStatusHidden) {
8658            employmentStatusHidden.value = '';
8659        }
8660        syncEmploymentSummaryUiState();
8661        if (feFormDensityComposed && state.screen === 'incomeOutgoingsGrouped') {
8662            validateIncomeOutgoingsGrouped();
8663        } else {
8664            validateEmployment();
8665        }
8666        scrollToTop(getEmploymentUiHost());
8667    });
8668
8669    personalGroupedStep?.addEventListener('input', (e) => {
8670        if (personalGroupedStep.hidden || !e.target.matches('input')) {
8671            return;
8672        }
8673        if (e.target.id === 'cars-fe-first-name' || e.target.id === 'cars-fe-last-name') {
8674            stripDigitsFromPersonNameInput(e.target);
8675        }
8676        if (e.target.id === 'cars-fe-phone-national') {
8677            syncLivePhoneFormatFeedback();
8678        }
8679        if (e.target.id === 'cars-fe-email' || e.target.id === 'cars-fe-dob') {
8680            feFieldBeingEdited = e.target.id;
8681        }
8682        validatePersonalGrouped();
8683    });
8684
8685    personalGroupedStep?.addEventListener('change', (e) => {
8686        if (personalGroupedStep.hidden || !e.target.matches('input')) {
8687            return;
8688        }
8689        validatePersonalGrouped();
8690    });
8691
8692    personalGroupedStep?.addEventListener('click', (e) => {
8693        const pill = e.target.closest('.cars-fe-pill');
8694        if (!pill || !personalGroupedStep.contains(pill)) {
8695            return;
8696        }
8697        const group = pill.closest('.cars-fe-pills');
8698        if (!group) {
8699            return;
8700        }
8701        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
8702        pill.classList.add('is-selected');
8703        const hidden = group.querySelector('input[type="hidden"]');
8704        if (hidden) {
8705            hidden.value = pill.dataset.value || '';
8706        }
8707        validatePersonalGrouped();
8708    });
8709
8710    detailsStep?.addEventListener('input', (e) => {
8711        if (!detailsStep.hidden && e.target.matches('input')) {
8712            if (e.target.id === 'cars-fe-first-name' || e.target.id === 'cars-fe-last-name') {
8713                stripDigitsFromPersonNameInput(e.target);
8714            }
8715            if (e.target.id === 'cars-fe-dob') {
8716                feFieldBeingEdited = e.target.id;
8717            }
8718            validateDetails();
8719        }
8720    });
8721
8722    detailsStep?.addEventListener('change', (e) => {
8723        if (!detailsStep.hidden && e.target.matches('input')) {
8724            validateDetails();
8725        }
8726    });
8727
8728    contactStep?.addEventListener('input', (e) => {
8729        if (!contactStep.hidden && e.target.matches('input')) {
8730            if (e.target.id === 'cars-fe-phone-national') {
8731                syncLivePhoneFormatFeedback();
8732            }
8733            if (e.target.id === 'cars-fe-email') {
8734                feFieldBeingEdited = e.target.id;
8735            }
8736            validateContact();
8737        }
8738    });
8739
8740    detailsStep?.addEventListener('click', (e) => {
8741        const pill = e.target.closest('.cars-fe-pill');
8742        if (!pill || !detailsStep.contains(pill)) {
8743            return;
8744        }
8745        const group = pill.closest('.cars-fe-pills');
8746        if (!group) {
8747            return;
8748        }
8749        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
8750        pill.classList.add('is-selected');
8751        const hidden = group.querySelector('input[type="hidden"]');
8752        if (hidden) {
8753            hidden.value = pill.dataset.value || '';
8754        }
8755        validateDetails();
8756    });
8757
8758    /** Re-runs the validator owning `el`, so leaving a field can surface just that field's error. */
8759    const revalidateFeStepContaining = (el) => {
8760        const steps = [
8761            [personalGroupedStep, validatePersonalGrouped],
8762            [detailsStep, validateDetails],
8763            [contactStep, validateContact],
8764            [moreStep, validateMore],
8765            [addressStep, validateAddress],
8766            [incomeOutgoingsGroupedStep, validateIncomeOutgoingsGrouped],
8767            [getEmploymentUiHost(), validateEmployment],
8768            [incomeOutgoingsStep, validateIncomeOutgoings],
8769            [outgoingsFinalStep, validateOutgoingsFinal],
8770        ];
8771        for (const [stepEl, validate] of steps) {
8772            if (stepEl && !stepEl.hidden && stepEl.contains(el)) {
8773                validate();
8774                return;
8775            }
8776        }
8777    };
8778
8779    // Leaving a field marks it touched so its error shows before Next is pressed.
8780    panel.addEventListener('focusout', (e) => {
8781        const field = e.target;
8782        if (!field || !field.id || !field.matches?.('input, select, textarea')) {
8783            return;
8784        }
8785        // A tracked field's error is held back while it is being edited, so leaving it has to
8786        // revalidate even when the field was already marked touched earlier in the journey.
8787        const leftFieldBeingEdited = feFieldBeingEdited === field.id;
8788        if (leftFieldBeingEdited) {
8789            feFieldBeingEdited = null;
8790        }
8791        if (!feMarkFieldTouched(field.id) && !leftFieldBeingEdited) {
8792            return;
8793        }
8794        revalidateFeStepContaining(field);
8795    });
8796
8797    addressStep?.addEventListener('input', (e) => {
8798        if (addressStep.hidden || !e.target.matches('input, select')) {
8799            return;
8800        }
8801        // Postcoder owns autocomplete for #cars-fe-address-postcode — skip legacy select lookup.
8802        if (
8803            e.target.id === 'cars-fe-address-postcode' &&
8804            typeof PostcoderAutocomplete !== 'function'
8805        ) {
8806            clearTimeout(addressLookupTimer);
8807            addressLookupTimer = setTimeout(runAddressLookup, 400);
8808        }
8809        if (e.target.id === 'cars-fe-address-postcode-manual') {
8810            syncLiveManualUkPostcodeFeedback();
8811        }
8812        validateAddress();
8813    });
8814
8815    addressStep?.addEventListener('change', (e) => {
8816        if (addressStep.hidden || !addressStep.contains(e.target)) {
8817            return;
8818        }
8819        if (e.target.id === 'cars-fe-address-select') {
8820            openManualCardForLookupSelection(e.target.value);
8821        }
8822        validateAddress();
8823    });
8824
8825    moreStep?.addEventListener('click', (e) => {
8826        const pill = e.target.closest('.cars-fe-pill');
8827        if (!pill || !moreStep.contains(pill)) {
8828            return;
8829        }
8830        const group = pill.closest('.cars-fe-pills');
8831        if (!group) {
8832            return;
8833        }
8834        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
8835        pill.classList.add('is-selected');
8836        const hidden = group.querySelector('input[type="hidden"]');
8837        if (hidden) {
8838            hidden.value = pill.dataset.value || '';
8839        }
8840        validateMore();
8841    });
8842
8843    employmentStep?.addEventListener('click', (e) => {
8844        if (!e.target.closest('.cars-fe-occupation-combobox')) {
8845            financeEligibilityCloseOccupationPanels(employmentStep);
8846        }
8847        if (!e.target.closest('.cars-fe-duration-listbox')) {
8848            financeEligibilityCloseDurationListboxes(employmentStep);
8849        }
8850        const editBtn = e.target.closest('[data-cars-fe-employment-edit]');
8851        if (editBtn && employmentStep.contains(editBtn)) {
8852            const entryId = editBtn.getAttribute('data-cars-fe-employment-edit');
8853            const idx = savedEmploymentEntries.findIndex((x) => x.id === entryId);
8854            if (idx < 0) {
8855                return;
8856            }
8857            const entry = savedEmploymentEntries[idx];
8858            employmentFormMode = 'editing';
8859            editingEmploymentId = entry.id;
8860            editingEmploymentIndex = idx;
8861            applyEmploymentEntryToForm(entry);
8862            syncEmploymentSummaryUiState();
8863            validateEmployment();
8864            scrollToTop(employmentStep);
8865            return;
8866        }
8867        const pill = e.target.closest('.cars-fe-pill');
8868        if (!pill || !employmentStep.contains(pill)) {
8869            return;
8870        }
8871        const group = pill.closest('.cars-fe-pills');
8872        if (!group) {
8873            return;
8874        }
8875        if (!group.classList.contains('cars-fe-pills--employment')) {
8876            return;
8877        }
8878        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
8879        pill.classList.add('is-selected');
8880        const hidden = group.querySelector('input[type="hidden"]');
8881        const pillVal = pill.getAttribute('data-value') || pill.dataset.value || '';
8882        if (hidden) {
8883            hidden.value = pillVal;
8884        }
8885        applyEmploymentStatusAutofill(pillVal);
8886        syncEmploymentDurationLabelForStatus(pillVal);
8887        validateEmployment();
8888    });
8889
8890    const handleIncomeOutgoingsGroupedClick = (e) => {
8891        const host = incomeOutgoingsGroupedStep;
8892        if (!host || host.hidden) {
8893            return;
8894        }
8895        if (!e.target.closest('.cars-fe-occupation-combobox')) {
8896            financeEligibilityCloseOccupationPanels(host);
8897        }
8898        if (!e.target.closest('.cars-fe-duration-listbox')) {
8899            financeEligibilityCloseDurationListboxes(host);
8900        }
8901        const editBtn = e.target.closest('[data-cars-fe-employment-edit]');
8902        if (editBtn && host.contains(editBtn)) {
8903            const entryId = editBtn.getAttribute('data-cars-fe-employment-edit');
8904            const idx = savedEmploymentEntries.findIndex((x) => x.id === entryId);
8905            if (idx < 0) {
8906                return;
8907            }
8908            const entry = savedEmploymentEntries[idx];
8909            employmentFormMode = 'editing';
8910            editingEmploymentId = entry.id;
8911            editingEmploymentIndex = idx;
8912            applyEmploymentEntryToForm(entry);
8913            syncEmploymentSummaryUiState();
8914            validateIncomeOutgoingsGrouped();
8915            scrollToTop(host);
8916            return;
8917        }
8918        const pill = e.target.closest('.cars-fe-pill');
8919        if (!pill || !host.contains(pill)) {
8920            return;
8921        }
8922        const group = pill.closest('.cars-fe-pills');
8923        if (!group) {
8924            return;
8925        }
8926        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
8927        pill.classList.add('is-selected');
8928        const hidden = group.querySelector('input[type="hidden"]');
8929        const pillVal = pill.getAttribute('data-value') || pill.dataset.value || '';
8930        if (hidden) {
8931            hidden.value = pillVal;
8932        }
8933        if (group.classList.contains('cars-fe-pills--employment')) {
8934            applyEmploymentStatusAutofill(pillVal);
8935            syncEmploymentDurationLabelForStatus(pillVal);
8936        }
8937        syncOutgoingsConditionalVisibility();
8938        validateIncomeOutgoingsGrouped();
8939    };
8940
8941    incomeOutgoingsGroupedStep?.addEventListener('click', handleIncomeOutgoingsGroupedClick);
8942
8943    incomeOutgoingsGroupedStep?.addEventListener('input', (e) => {
8944        if (incomeOutgoingsGroupedStep.hidden || !incomeOutgoingsGroupedStep.contains(e.target)) {
8945            return;
8946        }
8947        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
8948            applySterlingThousandsFormatting(e.target);
8949        }
8950        if (e.target.matches('input, select')) {
8951            syncOutgoingsConditionalVisibility();
8952            validateIncomeOutgoingsGrouped();
8953        }
8954    });
8955
8956    incomeOutgoingsGroupedStep?.addEventListener('change', (e) => {
8957        if (incomeOutgoingsGroupedStep.hidden || !incomeOutgoingsGroupedStep.contains(e.target)) {
8958            return;
8959        }
8960        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
8961            applySterlingThousandsFormatting(e.target);
8962        }
8963        syncOutgoingsConditionalVisibility();
8964        validateIncomeOutgoingsGrouped();
8965    });
8966
8967    employmentStep?.addEventListener('input', (e) => {
8968        if (employmentStep.hidden || !employmentStep.contains(e.target)) {
8969            return;
8970        }
8971        if (e.target.matches('input, select')) {
8972            validateEmployment();
8973        }
8974    });
8975
8976    employmentStep?.addEventListener('change', (e) => {
8977        if (employmentStep.hidden || !employmentStep.contains(e.target)) {
8978            return;
8979        }
8980        validateEmployment();
8981    });
8982
8983    incomeOutgoingsStep?.addEventListener('input', (e) => {
8984        if (incomeOutgoingsStep.hidden || !incomeOutgoingsStep.contains(e.target)) {
8985            return;
8986        }
8987        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
8988            applySterlingThousandsFormatting(e.target);
8989        }
8990        if (e.target.matches('input')) {
8991            validateIncomeOutgoings();
8992        }
8993    });
8994
8995    incomeOutgoingsStep?.addEventListener('change', (e) => {
8996        if (incomeOutgoingsStep.hidden || !incomeOutgoingsStep.contains(e.target)) {
8997            return;
8998        }
8999        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
9000            applySterlingThousandsFormatting(e.target);
9001        }
9002        validateIncomeOutgoings();
9003    });
9004
9005    outgoingsFinalStep?.addEventListener('input', (e) => {
9006        if (outgoingsFinalStep.hidden || !outgoingsFinalStep.contains(e.target)) {
9007            return;
9008        }
9009        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
9010            applySterlingThousandsFormatting(e.target);
9011        }
9012        if (e.target.matches('input')) {
9013            validateOutgoingsFinal();
9014        }
9015    });
9016
9017    outgoingsFinalStep?.addEventListener('change', (e) => {
9018        if (outgoingsFinalStep.hidden || !outgoingsFinalStep.contains(e.target)) {
9019            return;
9020        }
9021        if (e.target.matches('input.cars-fe-money-gbp__input, .cars-fe-field-group--money-gbp input')) {
9022            applySterlingThousandsFormatting(e.target);
9023        }
9024        validateOutgoingsFinal();
9025    });
9026
9027    outgoingsFinalStep?.addEventListener('click', (e) => {
9028        const pill = e.target.closest('.cars-fe-pill');
9029        if (!pill || !outgoingsFinalStep.contains(pill)) {
9030            return;
9031        }
9032        const group = pill.closest('.cars-fe-pills');
9033        if (!group) {
9034            return;
9035        }
9036        group.querySelectorAll('.cars-fe-pill').forEach((p) => p.classList.remove('is-selected'));
9037        pill.classList.add('is-selected');
9038        const hidden = group.querySelector('input[type="hidden"]');
9039        if (hidden) {
9040            hidden.value = pill.dataset.value || '';
9041        }
9042        syncOutgoingsConditionalVisibility();
9043        if (val('cars-fe-outgoings-repay-existing-finance') !== 'Yes') {
9044            clearOutgoingsRepayFollowupFields();
9045        }
9046        if (val('cars-fe-outgoings-downturn-expected') !== 'Yes') {
9047            clearOutgoingsDownturnFollowupFields();
9048        }
9049        validateOutgoingsFinal();
9050    });
9051
9052    panel.addEventListener('click', (e) => {
9053        if (!e.target.closest('.cars-fe-duration-listbox')) {
9054            financeEligibilityCloseDurationListboxes();
9055        }
9056    });
9057
9058    panel.addEventListener('focusin', (e) => {
9059        const t = e.target;
9060        if (!t || !panel.contains(t)) {
9061            return;
9062        }
9063        // Duration open already scrolls once; skip focusin to avoid flicker.
9064        if (t.matches('.cars-fe-duration-listbox__trigger')) {
9065            return;
9066        }
9067        if (t.matches('input[type="hidden"], button, .cars-fe-pill, .cars-fe-footer__back')) {
9068            return;
9069        }
9070        // Postcode suggestions handle their own scroll when the list opens.
9071        if (t.id === 'cars-fe-address-postcode') {
9072            return;
9073        }
9074        if (
9075            t.matches(
9076                'select, .cars-fe-occupation-combobox__input, .cars-fe-field-group__input--manual-select, .cars-fe-field-group__input--address-select, #cars-fe-address-residency',
9077            )
9078        ) {
9079            financeEligibilityScheduleFieldScrollIntoView(t);
9080        }
9081    });
9082
9083    panel.addEventListener('change', (e) => {
9084        const t = e.target;
9085        if (t && t.closest && t.closest('.cars-fe-field-group__shell--duration')) {
9086            syncFinanceDurationSelectShell(t);
9087        }
9088    });
9089
9090    panel.addEventListener('show.bs.offcanvas', () => {
9091        syncFinanceEligibilityPanelIvendiFromVdp();
9092        syncFinanceEligibilityListingFromPanel();
9093        syncEligibilityAgreementRepresentative();
9094
9095        financeEligibilityLockedScrollY = window.scrollY || window.pageYOffset || 0;
9096        document.documentElement.classList.add('finance-eligibility-offcanvas-open');
9097        const body = document.body;
9098        body.style.position = 'fixed';
9099        body.style.top = `-${financeEligibilityLockedScrollY}px`;
9100        body.style.left = '0';
9101        body.style.right = '0';
9102        body.style.width = '100%';
9103
9104        if (financeEligibilityViewportLockCleanup) {
9105            financeEligibilityViewportLockCleanup();
9106            financeEligibilityViewportLockCleanup = null;
9107        }
9108
9109        const feScrollSelector = [
9110            '.cars-fe-scroll',
9111            '.finance-eligibility-intro__scroll',
9112            '.finance-eligibility-agreement__scroll',
9113            '.cars-fe-results-modal__scroll',
9114            '.cars-fe-lender-detail__scroll',
9115            '.cars-fe-results-glossary__scroll',
9116            '.cars-fe-occupation-combobox__list',
9117            '.cars-fe-address-suggestions',
9118            '.cars-fe-duration-listbox',
9119        ].join(', ');
9120
9121        const findFeScrollParent = (target) => {
9122            if (!target || typeof target.closest !== 'function') {
9123                return null;
9124            }
9125            return target.closest(feScrollSelector);
9126        };
9127
9128        let feTouchLastY = 0;
9129        const onTouchStartWhileOpen = (e) => {
9130            feTouchLastY = e.touches && e.touches[0] ? e.touches[0].clientY : 0;
9131        };
9132
9133        const onTouchMoveWhileOpen = (e) => {
9134            if (!panel.classList.contains('show')) {
9135                return;
9136            }
9137            const scrollEl = findFeScrollParent(e.target);
9138            if (!scrollEl) {
9139                if (e.cancelable) {
9140                    e.preventDefault();
9141                }
9142                return;
9143            }
9144            const y = e.touches && e.touches[0] ? e.touches[0].clientY : feTouchLastY;
9145            const deltaY = y - feTouchLastY;
9146            feTouchLastY = y;
9147            const maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
9148            if (maxScroll <= 1) {
9149                if (e.cancelable) {
9150                    e.preventDefault();
9151                }
9152                return;
9153            }
9154            const atTop = scrollEl.scrollTop <= 0;
9155            const atBottom = scrollEl.scrollTop >= maxScroll - 1;
9156            if ((atTop && deltaY > 0) || (atBottom && deltaY < 0)) {
9157                if (e.cancelable) {
9158                    e.preventDefault();
9159                }
9160            }
9161        };
9162
9163        document.addEventListener('touchstart', onTouchStartWhileOpen, { passive: true, capture: true });
9164        document.addEventListener('touchmove', onTouchMoveWhileOpen, { passive: false, capture: true });
9165
9166        const vv = window.visualViewport;
9167        const syncKeyboardOverlayClass = () => {
9168            if (!panel.classList.contains('show')) {
9169                return;
9170            }
9171            panel.style.removeProperty('height');
9172            panel.style.removeProperty('max-height');
9173            panel.style.removeProperty('min-height');
9174            panel.style.top = '0px';
9175            panel.style.bottom = '0px';
9176            if (vv) {
9177                const h = Math.max(1, Math.round(vv.height));
9178                const layoutH = window.innerHeight || h;
9179                panel.classList.toggle('cars-fe-keyboard-open', layoutH - h > 150);
9180            }
9181        };
9182        syncKeyboardOverlayClass();
9183        if (vv) {
9184            vv.addEventListener('resize', syncKeyboardOverlayClass);
9185        }
9186
9187        financeEligibilityViewportLockCleanup = () => {
9188            document.removeEventListener('touchstart', onTouchStartWhileOpen, true);
9189            document.removeEventListener('touchmove', onTouchMoveWhileOpen, true);
9190            if (vv) {
9191                vv.removeEventListener('resize', syncKeyboardOverlayClass);
9192            }
9193            panel.style.removeProperty('height');
9194            panel.style.removeProperty('max-height');
9195            panel.style.removeProperty('min-height');
9196            panel.style.removeProperty('top');
9197            panel.style.removeProperty('bottom');
9198            panel.classList.remove('cars-fe-keyboard-open');
9199            body.style.removeProperty('position');
9200            body.style.removeProperty('top');
9201            body.style.removeProperty('left');
9202            body.style.removeProperty('right');
9203            body.style.removeProperty('width');
9204            window.scrollTo(0, financeEligibilityLockedScrollY || 0);
9205            financeEligibilityLockedScrollY = 0;
9206        };
9207    });
9208
9209    panel.addEventListener('shown.bs.offcanvas', () => {
9210        applyFormDensityVariant();
9211        syncAllFinanceDurationSelectShells();
9212        if (agreementView && agreementView.hidden === false) {
9213            syncEligibilityAgreementRepresentative();
9214        }
9215        if (personalGroupedStep && personalGroupedStep.hidden === false) {
9216            applyFinanceEligibilityUkDial();
9217            validatePersonalGrouped();
9218        }
9219        if (detailsStep && detailsStep.hidden === false) {
9220            validateDetails();
9221        }
9222        if (contactStep && contactStep.hidden === false) {
9223            applyFinanceEligibilityUkDial();
9224            validateContact();
9225        }
9226        if (moreStep && moreStep.hidden === false) {
9227            validateMore();
9228        }
9229        if (addressStep && addressStep.hidden === false) {
9230            validateAddress();
9231        }
9232        if (incomeOutgoingsGroupedStep && incomeOutgoingsGroupedStep.hidden === false) {
9233            financeEligibilityCloseOccupationPanels(incomeOutgoingsGroupedStep);
9234            syncEmploymentSummaryUiState();
9235            validateIncomeOutgoingsGrouped();
9236        }
9237        if (employmentStep && employmentStep.hidden === false) {
9238            financeEligibilityCloseOccupationPanels(employmentStep);
9239            syncEmploymentSummaryUiState();
9240            validateEmployment();
9241        }
9242        if (incomeOutgoingsStep && incomeOutgoingsStep.hidden === false) {
9243            validateIncomeOutgoings();
9244        }
9245        if (outgoingsFinalStep && outgoingsFinalStep.hidden === false) {
9246            validateOutgoingsFinal();
9247        }
9248        if (reviewStep && reviewStep.hidden === false) {
9249            validateReviewConsent();
9250        }
9251        syncFinanceEligibilityJourneyUrl();
9252    });
9253
9254    document.addEventListener('big-finance-quotes-updated', () => {
9255        syncFinanceEligibilityPanelIvendiFromVdp();
9256        syncEligibilityAgreementRepresentative();
9257        syncReviewConsentLenderName();
9258    });
9259
9260    panel.addEventListener('hidden.bs.offcanvas', () => {
9261        clearFinanceEligibilityJourneyUrl();
9262        panel.removeAttribute('data-fe-bound-listing-cta');
9263        // Listing-CTA vehicle details are per-card, so never let them carry over to the next open.
9264        panel.removeAttribute('data-fe-stock-image');
9265        panel.removeAttribute('data-fe-stock-price');
9266        panel.removeAttribute('data-fe-stock-mileage');
9267        if (financeEligibilityViewportLockCleanup) {
9268            financeEligibilityViewportLockCleanup();
9269            financeEligibilityViewportLockCleanup = null;
9270        }
9271        document.documentElement.classList.remove('finance-eligibility-offcanvas-open');
9272        if (intro) {
9273            intro.hidden = false;
9274        }
9275        if (agreementView) {
9276            agreementView.hidden = true;
9277        }
9278        if (detailsStep) {
9279            detailsStep.hidden = true;
9280        }
9281        hideContactStep();
9282        hideMoreStep();
9283        hidePersonalGroupedStep();
9284        hideAddressStep();
9285        hideEmploymentStep();
9286        hideIncomeOutgoingsStep();
9287        hideOutgoingsFinalStep();
9288        hideIncomeOutgoingsGroupedStep();
9289        hideReviewStep();
9290        restoreControlFormDomIfComposed();
9291        if (panel) {
9292            panel.dataset.feFormDensity = isGroupedFormActive() ? 'grouped' : 'control';
9293        }
9294        resetJourneyForms();
9295        state.screen = 'intro';
9296        state.dirty = false;
9297        // Re-apply after reset so intro chrome matches the current flag for the next open.
9298        applyFormDensityVariant();
9299    });
9300
9301    panel.addEventListener('hide.bs.offcanvas', (e) => {
9302        if (
9303            state.dirty &&
9304            (state.screen === 'details' ||
9305                state.screen === 'contact' ||
9306                state.screen === 'more' ||
9307                state.screen === 'personalGrouped' ||
9308                state.screen === 'address' ||
9309                state.screen === 'employment' ||
9310                state.screen === 'incomeOutgoings' ||
9311                state.screen === 'outgoingsFinal' ||
9312                state.screen === 'incomeOutgoingsGrouped' ||
9313                state.screen === 'review')
9314        ) {
9315            if (!window.confirm('You have unsaved information. Close and lose progress?')) {
9316                e.preventDefault();
9317            }
9318        }
9319    });
9320
9321    setBtnNextDisabled(nextBtn, true);
9322    setBtnNextDisabled(contactNextBtn, true);
9323    setBtnNextDisabled(moreNextBtn, true);
9324    setBtnNextDisabled(personalGroupedNextBtn, true);
9325    setBtnNextDisabled(addressNextBtn, true);
9326    setBtnNextDisabled(employmentNextBtn, true);
9327    setBtnNextDisabled(incomeNextBtn, true);
9328    setBtnNextDisabled(outgoingsSummaryBtn, true);
9329    setBtnNextDisabled(incomeOutgoingsGroupedSummaryBtn, true);
9330    setBtnNextDisabled(reviewViewResultsBtn, true);
9331}
9332
9333(function initFinanceEligibilityPanelWhenReady() {
9334    if (document.readyState === 'loading') {
9335        document.addEventListener('DOMContentLoaded', setupCarsFinanceEligibilityPanel);
9336    } else {
9337        setupCarsFinanceEligibilityPanel();
9338    }
9339})();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.