<!DOCTYPE html>
<!--
KEILTY Owner Portal - CLICKABLE PROTOTYPE - 2026-10-07
For internal review by A.J. and Brandon only. Not wired to anything:
no backend, no SharePoint, no email. All state lives in this browser's
localStorage. Any 6-digit code "works". Files are never uploaded,
only their names are shown to demonstrate the flow.
Seeded with the real Red Book taxonomy from 87 Division Street
(230 questions, 82 documents) so the adaptive depth is realistic.
-->
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="robots" content="noindex">
<title>KEILTY Owner Portal | Prototype</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--green:#00842F; --green-dark:#006A26; --deep:#0A3D1F; --tint:#EAF7EA;
--ink:#101613; --body:#55635B; --mute:#7A867E; --line:#E3E8E4; --input:#C9D3CC;
--paper:#F8F8F8; --error:#B3261E; --amber:#B97700; --amberbg:#FFF4E0;
}
*{box-sizing:border-box;margin:0;padding:0;}
body{font-family:Inter,-apple-system,sans-serif;background:var(--paper);color:var(--ink);line-height:1.55;-webkit-font-smoothing:antialiased;}
a{color:var(--green);font-weight:600;text-decoration:none;}
.proto{background:var(--deep);color:#fff;font-size:12.5px;text-align:center;padding:7px 14px;}
.proto strong{color:#9fdcae;}
header.top{background:#fff;border-bottom:1px solid var(--line);padding:0 22px;display:flex;align-items:center;justify-content:space-between;height:62px;position:sticky;top:0;z-index:40;}
.wordmark{font-weight:800;letter-spacing:2.5px;color:var(--green);font-size:18px;}
.wordmark span{color:var(--ink);font-weight:600;letter-spacing:0;font-size:13.5px;margin-left:10px;}
.hright{display:flex;align-items:center;gap:14px;font-size:13.5px;}
.toggle{display:flex;border:1px solid var(--input);border-radius:999px;overflow:hidden;font-size:12.5px;font-weight:700;cursor:pointer;}
.toggle div{padding:6px 13px;color:var(--body);}
.toggle div.on{background:var(--deep);color:#fff;}
.logout{color:var(--body);font-weight:600;cursor:pointer;}
main{max-width:980px;margin:0 auto;padding:34px 20px 90px;}
.teambar{background:var(--amberbg);border:1px solid #E8D5AA;color:var(--amber);border-radius:10px;padding:10px 16px;font-size:13.5px;font-weight:600;margin-bottom:20px;display:none;}
body.team .teambar{display:block;}
/* cards + buttons */
.card{background:#fff;border:1px solid var(--line);border-radius:12px;padding:30px;}
.btn{display:inline-block;background:var(--green);color:#fff;font-weight:700;font-size:15px;padding:13px 26px;border-radius:8px;border:none;cursor:pointer;font-family:inherit;text-align:center;}
.btn:hover{background:var(--green-dark);}
.btn:disabled{opacity:.5;cursor:default;}
.btn.ghost{background:#fff;color:var(--ink);border:1px solid var(--input);}
.btn.sm{padding:8px 14px;font-size:13px;border-radius:7px;}
.linkbtn{background:none;border:none;color:var(--green);font-weight:700;font-size:13px;cursor:pointer;font-family:inherit;padding:0;}
input[type=text],input[type=email],input[type=number]{font-family:inherit;font-size:15px;color:var(--ink);background:#fff;border:1px solid var(--input);border-radius:8px;padding:11px 13px;width:100%;}
input:focus{outline:none;border-color:var(--green);box-shadow:0 0 0 3px rgba(0,132,47,.13);}
/* auth */
.auth{max-width:460px;margin:7vh auto 0;}
.auth .card{padding:40px;}
.auth h1{font-size:28px;font-weight:800;margin-bottom:8px;letter-spacing:-.3px;}
.auth p.lead{color:var(--body);font-size:15px;margin-bottom:24px;}
.auth label{font-size:13px;font-weight:700;display:block;margin-bottom:6px;}
.code6{letter-spacing:14px;font-size:26px;font-weight:700;text-align:center;}
.hintline{font-size:12.5px;color:var(--mute);margin-top:14px;}
/* dashboard */
.dgrid{display:grid;grid-template-columns:1fr 1fr;gap:20px;margin-top:22px;}
.pcard h3{font-size:19px;font-weight:800;margin-bottom:2px;}
.pcard .meta{font-size:13px;color:var(--mute);margin-bottom:14px;}
.chip{display:inline-block;font-size:11.5px;font-weight:700;padding:3px 10px;border-radius:999px;margin-right:6px;}
.chip.g{background:var(--tint);color:#005A24;}
.chip.b{background:#EDF3FA;color:#1F4E8C;}
.chip.y{background:#FFF4D6;color:#8a6d00;}
.chip.o{background:#FFE9D6;color:#9a4d00;}
.chip.gr{background:#EEF1EF;color:var(--body);}
.pbarwrap{background:#EEF1EF;border-radius:999px;height:9px;margin:10px 0 6px;overflow:hidden;}
.pbar{background:var(--green);height:100%;border-radius:999px;transition:width .4s;}
.pnums{font-size:12.5px;color:var(--mute);margin-bottom:16px;}
.contact{background:#fff;border:1px solid var(--line);border-radius:12px;padding:20px 22px;margin-top:26px;display:flex;gap:18px;align-items:center;flex-wrap:wrap;}
.contact .avatar{width:44px;height:44px;border-radius:50%;background:var(--deep);color:#fff;font-weight:800;display:flex;align-items:center;justify-content:center;font-size:16px;}
.contact p{font-size:13.5px;color:var(--body);}
.contact b{color:var(--ink);font-size:14.5px;}
/* property page */
.crumb{font-size:13px;margin-bottom:14px;display:inline-block;cursor:pointer;}
.phead h1{font-size:26px;font-weight:800;letter-spacing:-.3px;}
.phead .sub{color:var(--mute);font-size:13.5px;margin:2px 0 14px;}
.tabs{display:flex;gap:8px;margin:20px 0;}
.tab{padding:9px 20px;border-radius:999px;border:1px solid var(--input);background:#fff;font-weight:700;font-size:14px;cursor:pointer;color:var(--body);}
.tab.on{background:var(--green);border-color:var(--green);color:#fff;}
.tab .n{font-weight:600;opacity:.8;font-size:12px;}
/* gateway */
.gate h2{font-size:21px;font-weight:800;margin-bottom:6px;}
.gate p.lead{color:var(--body);font-size:14.5px;margin-bottom:20px;max-width:640px;}
.tiles{display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:12px;margin:18px 0 8px;}
.tile{border:1.5px solid var(--input);border-radius:12px;background:#fff;padding:16px 14px;cursor:pointer;user-select:none;}
.tile:hover{border-color:var(--green);}
.tile.on{border-color:var(--green);background:var(--tint);}
.tile b{display:block;font-size:14.5px;margin-bottom:3px;}
.tile small{color:var(--mute);font-size:12px;line-height:1.4;display:block;}
.tile .tick{float:right;width:20px;height:20px;border-radius:50%;border:1.5px solid var(--input);display:flex;align-items:center;justify-content:center;font-size:12px;color:#fff;}
.tile.on .tick{background:var(--green);border-color:var(--green);}
.gnum{display:grid;grid-template-columns:1fr 1fr;gap:14px;max-width:420px;margin:16px 0 22px;}
.gnum label{font-size:13px;font-weight:700;display:block;margin-bottom:5px;}
/* sections accordion */
.summary{background:var(--tint);border:1px solid var(--green);border-radius:10px;padding:12px 16px;font-size:13.5px;color:#005A24;margin-bottom:18px;}
.sec{background:#fff;border:1px solid var(--line);border-radius:12px;margin-bottom:12px;overflow:hidden;}
.sechead{display:flex;align-items:center;justify-content:space-between;padding:16px 20px;cursor:pointer;gap:12px;}
.sechead h3{font-size:15.5px;font-weight:800;}
.sechead .right{display:flex;align-items:center;gap:12px;font-size:12.5px;color:var(--mute);white-space:nowrap;}
.minibar{width:90px;background:#EEF1EF;height:7px;border-radius:999px;overflow:hidden;}
.minibar i{display:block;height:100%;background:var(--green);border-radius:999px;}
.secbody{border-top:1px solid var(--line);display:none;}
.sec.open .secbody{display:block;}
.item{padding:15px 20px;border-bottom:1px solid #F0F3F1;}
.item:last-child{border-bottom:none;}
.item .lbl{font-weight:700;font-size:14px;}
.item .note{font-size:12.5px;color:var(--mute);margin:2px 0 8px;max-width:620px;}
.item .ans-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center;}
.item .ans-row input{flex:1;min-width:220px;}
.escapes{margin-top:7px;display:flex;gap:14px;flex-wrap:wrap;}
.escapes button{background:none;border:none;color:var(--mute);font-size:12.5px;font-weight:600;cursor:pointer;font-family:inherit;text-decoration:underline;text-underline-offset:3px;}
.escapes button:hover{color:var(--green);}
.saved{display:flex;align-items:center;gap:10px;flex-wrap:wrap;}
.saved .val{font-size:14px;color:var(--ink);background:var(--paper);border:1px solid var(--line);border-radius:7px;padding:7px 12px;}
.saved .val.muted{color:var(--mute);font-style:italic;}
/* documents */
.bulk{border:2px dashed var(--input);border-radius:12px;background:#fff;padding:26px;text-align:center;color:var(--body);font-size:14px;margin-bottom:20px;cursor:pointer;}
.bulk:hover,.bulk.drag{border-color:var(--green);background:var(--tint);}
.bulk b{color:var(--ink);}
.dslot{padding:15px 20px;border-bottom:1px solid #F0F3F1;}
.dslot:last-child{border-bottom:none;}
.dslot .top{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;flex-wrap:wrap;}
.dz{border:1.5px dashed var(--input);border-radius:9px;padding:13px;text-align:center;font-size:13px;color:var(--body);margin-top:10px;cursor:pointer;}
.dz:hover,.dz.drag{border-color:var(--green);background:var(--tint);}
.flist{margin-top:8px;font-size:13px;}
.flist .f{display:flex;justify-content:space-between;gap:10px;color:var(--body);padding:4px 0;border-bottom:1px dotted var(--line);}
.statusicon{width:26px;height:26px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;font-size:13px;margin-right:10px;flex-shrink:0;color:#fff;}
.st-pending{background:#E7C544;} .st-received{background:#E88A2E;} .st-accepted{background:var(--green);}
footer{max-width:980px;margin:0 auto;padding:0 20px 40px;font-size:12px;color:var(--mute);display:flex;justify-content:space-between;gap:10px;flex-wrap:wrap;}
.hide{display:none!important;}
@media(max-width:760px){.dgrid{grid-template-columns:1fr;}.auth .card{padding:26px;}main{padding-top:22px;}}
</style>
</head>
<body>
<div class="proto"><strong>Prototype.</strong> Internal review only. Nothing entered here is saved to KEILTY systems or sent anywhere; it stays in this browser.</div>
<header class="top">
<div class="wordmark">KEILTY<span>Owner Portal</span></div>
<div class="hright">
<div class="toggle" id="modeToggle" title="Demo: switch between what the owner sees and what the team sees">
<div id="mOwner" class="on">Owner view</div><div id="mTeam">KEILTY team view</div>
</div>
<span class="logout hide" id="logout">Sign out</span>
</div>
</header>
<main id="main"></main>
<footer>
<span>KEILTY Realty Management · prototype seeded with the 87 Division Street Red Book (230 questions, 82 documents)</span>
<button class="linkbtn" id="resetDemo">Reset demo</button>
</footer>
<script>
const DATA = {"qs":[{"ref":"f:prop_addr","section":"The property","item":"Address","note":""},
{"ref":"f:prop_city","section":"The property","item":"City","note":""},
{"ref":"f:prop_type","section":"The property","item":"Building type","note":""},
{"ref":"f:year_built","section":"The property","item":"Year built","note":""},
{"ref":"f:storeys","section":"The property","item":"Storeys","note":""},
{"ref":"f:units","section":"The property","item":"Units","note":""},
{"ref":"f:unit_mix","section":"The property","item":"Unit mix and bed count","note":""},
{"ref":"f:sqft_total","section":"The property","item":"Total square footage","note":""},
{"ref":"f:is_condo","section":"The property","item":"Is it a condominium","note":""},
{"ref":"f:roll_number","section":"The property","item":"Assessment roll number","note":""},
{"ref":"f:licence","section":"The property","item":"Municipal licence","note":""},
{"ref":"f:pms","section":"The property","item":"Property management system","note":""},
{"ref":"f:ho_entrata","section":"The property","item":"Set up in the management system","note":""},
{"ref":"f:bldg_line","section":"The property","item":"Building main line","note":""},
{"ref":"f:condo_corp","section":"The property","item":"Condominium corporation","note":""},
{"ref":"f:condo_mgr","section":"The property","item":"Condo property manager","note":""},
{"ref":"f:condo_contact","section":"The property","item":"Condo manager contact","note":""},
{"ref":"f:condo_site","section":"The property","item":"Condo site administrator","note":""},
{"ref":"f:condo_phone","section":"The property","item":"Condo corporation phone","note":""},
{"ref":"f:own_legal","section":"Who owns it, and the agreement","item":"Owner, legal name","note":""},
{"ref":"f:own_contact","section":"Who owns it, and the agreement","item":"Owner contact","note":""},
{"ref":"f:own_email","section":"Who owns it, and the agreement","item":"Owner email","note":""},
{"ref":"f:own_phone","section":"Who owns it, and the agreement","item":"Owner phone","note":""},
{"ref":"f:own_mailing","section":"Who owns it, and the agreement","item":"Owner mailing address","note":""},
{"ref":"f:own_sec_name","section":"Who owns it, and the agreement","item":"Second contact","note":""},
{"ref":"f:own_sec_title","section":"Who owns it, and the agreement","item":"Second contact, role","note":""},
{"ref":"f:own_sec_email","section":"Who owns it, and the agreement","item":"Second contact email","note":""},
{"ref":"f:own_sec_phone","section":"Who owns it, and the agreement","item":"Second contact phone","note":""},
{"ref":"f:own_cadence","section":"Who owns it, and the agreement","item":"Reporting cadence","note":""},
{"ref":"f:own_dash_kind","section":"Who owns it, and the agreement","item":"Client segment","note":""},
{"ref":"f:own_nonres","section":"Who owns it, and the agreement","item":"Non-resident owner for tax","note":""},
{"ref":"f:own_nonres_detail","section":"Who owns it, and the agreement","item":"Non-resident withholding detail","note":""},
{"ref":"f:agr_start","section":"Who owns it, and the agreement","item":"Agreement start","note":""},
{"ref":"f:agr_term","section":"Who owns it, and the agreement","item":"Term and renewal","note":""},
{"ref":"f:agr_notice","section":"Who owns it, and the agreement","item":"Notice to terminate","note":""},
{"ref":"f:agr_fee","section":"Who owns it, and the agreement","item":"Fees","note":""},
{"ref":"f:agr_scope","section":"Who owns it, and the agreement","item":"Scope, and what the owner pays for","note":""},
{"ref":"f:agr_reporting","section":"Who owns it, and the agreement","item":"What we must report, and when","note":""},
{"ref":"f:spend_threshold","section":"Who owns it, and the agreement","item":"Spend limit without approval","note":""},
{"ref":"f:spend_approver","section":"Who owns it, and the agreement","item":"Who approves spend","note":""},
{"ref":"f:spend_emergency","section":"Who owns it, and the agreement","item":"Emergency spend authority","note":""},
{"ref":"f:pay_method","section":"Who owns it, and the agreement","item":"How the owner is paid","note":""},
{"ref":"f:pay_name","section":"Who owns it, and the agreement","item":"Payee name","note":""},
{"ref":"f:pay_on_file","section":"Who owns it, and the agreement","item":"Banking details on file","note":""},
{"ref":"f:pay_where","section":"Who owns it, and the agreement","item":"Where banking details are held","note":""},
{"ref":"f:dep_paid","section":"Who owns it, and the agreement","item":"Deposits held and paid across","note":""},
{"ref":"f:dist_stage","section":"Who owns it, and the agreement","item":"Owner distribution stage","note":""},
{"ref":"f:insurer","section":"Who owns it, and the agreement","item":"Insurer and policy number","note":""},
{"ref":"f:entry_main","section":"Getting in","item":"Getting in, main entrance","note":""},
{"ref":"f:entry_rear","section":"Getting in","item":"Getting in, rear or side","note":""},
{"ref":"f:entry_mech","section":"Getting in","item":"Mechanical room access","note":""},
{"ref":"f:entry_elecrm","section":"Getting in","item":"Electrical room access","note":""},
{"ref":"f:entry_roof","section":"Getting in","item":"Roof access","note":""},
{"ref":"f:entry_waste","section":"Getting in","item":"Waste area access","note":""},
{"ref":"f:entry_park","section":"Getting in","item":"Parking access","note":""},
{"ref":"f:entry_amen","section":"Getting in","item":"Amenity access","note":""},
{"ref":"f:lockbox","section":"Getting in","item":"Lockboxes, where they are","note":""},
{"ref":"f:lockbox_contents","section":"Getting in","item":"What is in each lockbox","note":""},
{"ref":"f:lockbox_access","section":"Getting in","item":"Door and lockbox codes","note":""},
{"ref":"f:lockbox_code_held","section":"Getting in","item":"Where the codes are held","note":""},
{"ref":"f:lockbox_who","section":"Getting in","item":"Who has the codes","note":""},
{"ref":"f:lockbox_changed","section":"Getting in","item":"Codes last changed","note":""},
{"ref":"f:keyway","section":"Getting in","item":"Keyway and key system","note":""},
{"ref":"f:rekey","section":"Getting in","item":"Rekeying record","note":""},
{"ref":"f:spare_keys","section":"Getting in","item":"Where the spare keys are","note":""},
{"ref":"f:contractor_access","section":"Getting in","item":"How contractors get in","note":""},
{"ref":"f:fob_system","section":"Getting in","item":"Fob and access control system","note":""},
{"ref":"f:cam_exists","section":"Getting in","item":"Cameras on site","note":""},
{"ref":"f:cam_system","section":"Getting in","item":"Camera system","note":""},
{"ref":"f:cam_cover","section":"Getting in","item":"What the cameras cover","note":""},
{"ref":"f:cam_who","section":"Getting in","item":"Who can view the cameras","note":""},
{"ref":"f:monitor_co","section":"Getting in","item":"Alarm monitoring company","note":""},
{"ref":"f:monitor_line","section":"Getting in","item":"Monitoring line","note":""},
{"ref":"f:monitor_acct","section":"Getting in","item":"Monitoring account number","note":""},
{"ref":"f:id_type","section":"Getting in","item":"Owner identification held","note":""},
{"ref":"f:id_verified","section":"Getting in","item":"Identification verified","note":""},
{"ref":"f:id_date","section":"Getting in","item":"Identification verified on","note":""},
{"ref":"f:id_by","section":"Getting in","item":"Identification verified by","note":""},
{"ref":"f:water_main","section":"What is in the building","item":"Water main shut off","note":""},
{"ref":"f:water_stacks","section":"What is in the building","item":"Stack isolation valves","note":""},
{"ref":"f:gas_shut","section":"What is in the building","item":"Gas shut off","note":""},
{"ref":"f:elec_main","section":"What is in the building","item":"Main electrical disconnect","note":""},
{"ref":"f:elec_service","section":"What is in the building","item":"Electrical service size","note":""},
{"ref":"f:has_panel","section":"What is in the building","item":"Fire alarm panel on site","note":""},
{"ref":"f:panel_make","section":"What is in the building","item":"Fire alarm panel, make and model","note":""},
{"ref":"f:panel_loc","section":"What is in the building","item":"Fire alarm panel location","note":""},
{"ref":"f:boiler_stop","section":"What is in the building","item":"Boiler emergency stop","note":""},
{"ref":"f:heat_plant","section":"What is in the building","item":"Heating plant","note":""},
{"ref":"f:cooling","section":"What is in the building","item":"Cooling","note":""},
{"ref":"f:thermostat_loc","section":"What is in the building","item":"Thermostats","note":""},
{"ref":"f:dhw","section":"What is in the building","item":"Domestic hot water","note":""},
{"ref":"f:meter_elec","section":"What is in the building","item":"Electricity metering","note":""},
{"ref":"f:meter_gas","section":"What is in the building","item":"Gas metering","note":""},
{"ref":"f:meter_water","section":"What is in the building","item":"Water metering","note":""},
{"ref":"f:meter_heat","section":"What is in the building","item":"Heat metering","note":""},
{"ref":"f:meter_net","section":"What is in the building","item":"Internet and telecom","note":""},
{"ref":"f:has_generator","section":"What is in the building","item":"Generator","note":""},
{"ref":"f:has_sprinkler","section":"What is in the building","item":"Sprinklered","note":""},
{"ref":"f:sprink_valve","section":"What is in the building","item":"Sprinkler control valve","note":""},
{"ref":"f:has_elevator","section":"What is in the building","item":"Elevator","note":""},
{"ref":"f:elev_make","section":"What is in the building","item":"Elevator, make and model","note":""},
{"ref":"f:elev_lic","section":"What is in the building","item":"Elevator licence number","note":""},
{"ref":"f:elev_mr","section":"What is in the building","item":"Machine room location","note":""},
{"ref":"f:elev_line","section":"What is in the building","item":"Elevator emergency line","note":""},
{"ref":"f:elev_proc","section":"What is in the building","item":"Elevator entrapment procedure","note":""},
{"ref":"f:elev_recall","section":"What is in the building","item":"Elevator fire recall","note":""},
{"ref":"f:laundry","section":"What is in the building","item":"Laundry","note":""},
{"ref":"f:parking","section":"What is in the building","item":"Parking","note":""},
{"ref":"f:storage","section":"What is in the building","item":"Storage","note":""},
{"ref":"f:amenities","section":"What is in the building","item":"Amenities","note":""},
{"ref":"f:roof","section":"What is in the building","item":"Roof, type and age","note":""},
{"ref":"f:roof_access","section":"What is in the building","item":"Roof access route","note":""},
{"ref":"f:waste_day","section":"What is in the building","item":"Waste and recycling collection","note":""},
{"ref":"f:has_staff","section":"What is in the building","item":"On site staff","note":""},
{"ref":"f:has_com","section":"What is in the building","item":"Commercial units","note":""},
{"ref":"f:construction","section":"What is in the building","item":"Construction type","note":""},
{"ref":"f:alarm_smoke","section":"Life safety, and the certificates an inspector asks for","item":"Smoke alarms, type and make","note":""},
{"ref":"f:alarm_co","section":"Life safety, and the certificates an inspector asks for","item":"Carbon monoxide alarms","note":""},
{"ref":"f:alarm_tested","section":"Life safety, and the certificates an inspector asks for","item":"Alarms last tested","note":""},
{"ref":"f:alarm_expiry","section":"Life safety, and the certificates an inspector asks for","item":"Alarm expiry or replacement due","note":""},
{"ref":"f:alarms_certified","section":"Life safety, and the certificates an inspector asks for","item":"Alarms certified to code","note":""},
{"ref":"f:extinguishers","section":"Life safety, and the certificates an inspector asks for","item":"Fire extinguishers, where and how many","note":""},
{"ref":"f:emerg_light","section":"Life safety, and the certificates an inspector asks for","item":"Emergency lighting","note":""},
{"ref":"f:fsp_exists","section":"Life safety, and the certificates an inspector asks for","item":"Fire safety plan","note":""},
{"ref":"f:assist_exists","section":"Life safety, and the certificates an inspector asks for","item":"Persons requiring assistance list","note":""},
{"ref":"f:vital_services","section":"Life safety, and the certificates an inspector asks for","item":"Vital services plan","note":""},
{"ref":"f:false_alarm","section":"Life safety, and the certificates an inspector asks for","item":"False alarm procedure and charges","note":""},
{"ref":"f:silence_reset","section":"Life safety, and the certificates an inspector asks for","item":"How to silence and reset the panel","note":""},
{"ref":"f:impairment","section":"Life safety, and the certificates an inspector asks for","item":"Fire system impairment procedure","note":""},
{"ref":"f:ver_alarm","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, fire alarm verification","note":""},
{"ref":"f:ver_pump","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, fire pump","note":""},
{"ref":"f:ver_standpipe","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, standpipe","note":""},
{"ref":"f:ver_fdc","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, fire department connection","note":""},
{"ref":"f:ver_firedoor","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, fire doors","note":""},
{"ref":"f:ver_sprink","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, sprinkler system","note":""},
{"ref":"f:ver_ext","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, extinguishers","note":""},
{"ref":"f:ver_light","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, emergency lighting","note":""},
{"ref":"f:ver_elev","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, elevator","note":""},
{"ref":"f:ver_boiler","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, boiler and pressure vessel","note":""},
{"ref":"f:ver_backflow","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, backflow prevention","note":""},
{"ref":"f:ver_esa","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, electrical safety authority","note":""},
{"ref":"f:ver_dryer","section":"Life safety, and the certificates an inspector asks for","item":"Certificate, dryer vent cleaning","note":""},
{"ref":"f:open_ltb","section":"Life safety, and the certificates an inspector asks for","item":"Open Landlord and Tenant Board matters","note":""},
{"ref":"f:open_orders","section":"Life safety, and the certificates an inspector asks for","item":"Open work orders or municipal orders","note":""},
{"ref":"f:esc_notes","section":"Life safety, and the certificates an inspector asks for","item":"Anything to escalate","note":""},
{"ref":"f:resp_split","section":"How this building is run","item":"Who repairs and who pays","note":""},
{"ref":"f:proc_snow","section":"How this building is run","item":"Snow and ice clearing","note":""},
{"ref":"f:proc_waste","section":"How this building is run","item":"Waste procedure","note":""},
{"ref":"f:proc_notice","section":"How this building is run","item":"Where notices are posted","note":""},
{"ref":"f:proc_movein","section":"How this building is run","item":"Move in procedure","note":""},
{"ref":"f:proc_after","section":"How this building is run","item":"After hours procedure","note":""},
{"ref":"f:proc_media","section":"How this building is run","item":"Who speaks to the media","note":""},
{"ref":"f:proc_displace","section":"How this building is run","item":"If residents have to be displaced","note":""},
{"ref":"f:proc_deviations","section":"How this building is run","item":"Agreed departures from KEILTY procedure","note":""},
{"ref":"f:owner_performed","section":"How this building is run","item":"Work the owner does themselves","note":""},
{"ref":"f:risks","section":"How this building is run","item":"Known risks","note":""},
{"ref":"f:quirks","section":"How this building is run","item":"Quirks worth knowing","note":""},
{"ref":"f:claims_proc","section":"How this building is run","item":"Insurance claims procedure","note":""},
{"ref":"f:sub_permitted","section":"How this building is run","item":"Subletting permitted","note":""},
{"ref":"f:sub_assign","section":"How this building is run","item":"Sublet and assignment procedure","note":""},
{"ref":"f:sub_liable","section":"How this building is run","item":"Who stays liable on a sublet","note":""},
{"ref":"f:sub_docs","section":"How this building is run","item":"Sublet paperwork","note":""},
{"ref":"f:sub_issues","section":"How this building is run","item":"Past sublet problems","note":""},
{"ref":"f:sub_season","section":"How this building is run","item":"Seasonal subletting","note":""},
{"ref":"f:lease_term","section":"Leasing and marketing","item":"Minimum lease term","note":""},
{"ref":"f:lease_start","section":"Leasing and marketing","item":"Standard lease start dates","note":""},
{"ref":"f:rent_control","section":"Leasing and marketing","item":"Rent controlled","note":""},
{"ref":"f:deposit_pms","section":"Leasing and marketing","item":"Deposit taken","note":""},
{"ref":"f:incl_utils_pms","section":"Leasing and marketing","item":"Utilities included in rent","note":""},
{"ref":"f:rate_parking","section":"Leasing and marketing","item":"Parking rate","note":""},
{"ref":"f:rate_storage","section":"Leasing and marketing","item":"Storage rate","note":""},
{"ref":"f:pets","section":"Leasing and marketing","item":"Pets","note":""},
{"ref":"f:smoking","section":"Leasing and marketing","item":"Smoking","note":""},
{"ref":"f:guarantor","section":"Leasing and marketing","item":"Guarantor policy","note":""},
{"ref":"f:applicant_approval","section":"Leasing and marketing","item":"Who approves an applicant","note":""},
{"ref":"f:showing","section":"Leasing and marketing","item":"How a showing is booked","note":""},
{"ref":"f:market","section":"Leasing and marketing","item":"The market, in a line","note":""},
{"ref":"f:peak","section":"Leasing and marketing","item":"Peak leasing season","note":""},
{"ref":"f:mkt_headline","section":"Leasing and marketing","item":"Listing headline","note":""},
{"ref":"f:mkt_description","section":"Leasing and marketing","item":"Listing description","note":""},
{"ref":"f:mkt_location","section":"Leasing and marketing","item":"Location copy","note":""},
{"ref":"f:mkt_unit_amen","section":"Leasing and marketing","item":"Unit amenities for the listing","note":""},
{"ref":"f:fin_walls","section":"Standard finishes","item":"Walls, paint and colour code","note":""},
{"ref":"f:fin_ceiling","section":"Standard finishes","item":"Ceilings","note":""},
{"ref":"f:fin_trim","section":"Standard finishes","item":"Trim, baseboard and casing","note":""},
{"ref":"f:fin_floor_main","section":"Standard finishes","item":"Flooring, main areas","note":""},
{"ref":"f:fin_floor_wet","section":"Standard finishes","item":"Flooring, kitchens and washrooms","note":""},
{"ref":"f:fin_doors","section":"Standard finishes","item":"Interior doors","note":""},
{"ref":"f:fin_hardware","section":"Standard finishes","item":"Door hardware and finish","note":""},
{"ref":"f:fin_lighting","section":"Standard finishes","item":"Lighting and bulb temperature","note":""},
{"ref":"f:fin_window","section":"Standard finishes","item":"Window coverings","note":""},
{"ref":"f:fin_cabinets","section":"Standard finishes","item":"Cabinets and millwork","note":""},
{"ref":"f:fin_counter","section":"Standard finishes","item":"Counters","note":""},
{"ref":"f:fin_sink","section":"Standard finishes","item":"Sinks and taps","note":""},
{"ref":"f:fin_appliances","section":"Standard finishes","item":"Appliances, make and model","note":""},
{"ref":"f:fin_bath","section":"Standard finishes","item":"Washroom fixtures","note":""},
{"ref":"f:fin_hvac_unit","section":"Standard finishes","item":"In suite heating and cooling","note":""},
{"ref":"f:fin_common","section":"Standard finishes","item":"Common area finishes","note":""},
{"ref":"f:fin_elevator_int","section":"Standard finishes","item":"Elevator interior","note":""},
{"ref":"f:fin_signage","section":"Standard finishes","item":"Signage and wayfinding","note":""},
{"ref":"f:fin_supplier","section":"Standard finishes","item":"Where finishes are bought","note":""},
{"ref":"f:fin_approve","section":"Standard finishes","item":"Who approves a change of finish","note":""},
{"ref":"f:dwg_folder","section":"Drawings received at onboarding","item":"Drawings folder","note":""},
{"ref":"f:dwg_held_site","section":"Drawings received at onboarding","item":"Site plan received","note":""},
{"ref":"f:dwg_held_floor","section":"Drawings received at onboarding","item":"Floor plans received","note":""},
{"ref":"f:dwg_held_riser","section":"Drawings received at onboarding","item":"Mechanical and riser drawings received","note":""},
{"ref":"f:dwg_held_elec","section":"Drawings received at onboarding","item":"Electrical drawings received","note":""},
{"ref":"f:dwg_held_asbuilt","section":"Drawings received at onboarding","item":"As built drawings received","note":""},
{"ref":"f:dwg_held_survey","section":"Drawings received at onboarding","item":"Survey received","note":""},
{"ref":"f:dwg_held_reno","section":"Drawings received at onboarding","item":"Renovation drawings received","note":""},
{"ref":"f:dwg_notes","section":"Drawings received at onboarding","item":"Notes on the drawings","note":""},
{"ref":"f:map_site","section":"Maps and imagery","item":"Site or lot map","note":""},
{"ref":"f:map_parking","section":"Maps and imagery","item":"Parking map and stall numbers","note":""},
{"ref":"f:map_waste","section":"Maps and imagery","item":"Waste and bin locations","note":""},
{"ref":"f:map_fire","section":"Maps and imagery","item":"Fire route, exits and muster point","note":""},
{"ref":"f:map_access","section":"Maps and imagery","item":"Access and lockbox map","note":""},
{"ref":"f:map_other","section":"Maps and imagery","item":"Other imagery","note":""},
{"ref":"f:log_util_e","section":"Accounts and logins","item":"Electricity account","note":""},
{"ref":"f:log_util_w","section":"Accounts and logins","item":"Water and sewer account","note":""},
{"ref":"f:log_util_g","section":"Accounts and logins","item":"Gas account","note":""},
{"ref":"f:log_net","section":"Accounts and logins","item":"Internet and telecom account","note":""},
{"ref":"f:log_access","section":"Accounts and logins","item":"Access control or intercom account","note":""},
{"ref":"f:log_alarm","section":"Accounts and logins","item":"Alarm or monitoring account","note":""},
{"ref":"f:log_elev","section":"Accounts and logins","item":"Elevator service account","note":""},
{"ref":"f:log_portal","section":"Accounts and logins","item":"Owner or tenant portal","note":""},
{"ref":"f:log_other","section":"Accounts and logins","item":"Any other account","note":""},
{"ref":"f:log_user","section":"Accounts and logins","item":"Username used for these accounts","note":""},
{"ref":"f:log_where","section":"Accounts and logins","item":"Where the passwords are held","note":""},
{"ref":"f:ob_anything","section":"Anything else","item":"Anything else we should know","note":""},
{"ref":"f:ob_ref","section":"Anything else","item":"Reference or file number","note":""}],
"docs":[{"ref":"d:coi","item":"Certificate of insurance","note":"","group":"Money, accounts & agreements"},
{"ref":"d:banking","item":"Owner banking details","note":"","group":"Money, accounts & agreements"},
{"ref":"d:autorenew","item":"Auto renewal record","note":"","group":"Money, accounts & agreements"},
{"ref":"d:commitments","item":"Commitments to the owner","note":"","group":"Money, accounts & agreements"},
{"ref":"d:contracts_all","item":"Signed vendor contracts","note":"","group":"Money, accounts & agreements"},
{"ref":"d:equip_list","item":"Equipment list","note":"","group":"Building records"},
{"ref":"d:manuals","item":"Equipment manuals","note":"","group":"Building records"},
{"ref":"d:warranties","item":"Warranties","note":"","group":"Building records"},
{"ref":"d:keys_inv","item":"Key and fob inventory","note":"","group":"Building records"},
{"ref":"d:keyway","item":"Keyway record","note":"","group":"Building records"},
{"ref":"d:access_admin","item":"Access system administration","note":"","group":"Building records"},
{"ref":"d:photo_id","item":"Owner photo identification","note":"","group":"Money, accounts & agreements"},
{"ref":"d:licence_doc","item":"Municipal licence","note":"","group":"Certificates & life safety"},
{"ref":"d:tax","item":"Property tax bill","note":"","group":"Money, accounts & agreements"},
{"ref":"d:fsp_doc","item":"Fire safety plan","note":"","group":"Certificates & life safety"},
{"ref":"d:assist_doc","item":"Persons requiring assistance list","note":"","group":"Certificates & life safety"},
{"ref":"d:alarm_cert","item":"Fire alarm verification certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:fire_test","item":"Annual fire test report","note":"","group":"Certificates & life safety"},
{"ref":"d:sprink_cert","item":"Sprinkler certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:ext_cert","item":"Extinguisher certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:light_cert","item":"Emergency lighting certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:elev_cert","item":"Elevator certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:boiler_cert","item":"Boiler certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:backflow_cert","item":"Backflow certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:esa_cert","item":"Electrical safety certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:gen_cert","item":"Generator certificate","note":"","group":"Certificates & life safety"},
{"ref":"d:dryer","item":"Dryer vent cleaning record","note":"","group":"Certificates & life safety"},
{"ref":"d:dss_doc","item":"Designated substances survey","note":"","group":"Certificates & life safety"},
{"ref":"d:asbestos","item":"Asbestos report","note":"","group":"Certificates & life safety"},
{"ref":"d:enviro","item":"Environmental report","note":"","group":"Certificates & life safety"},
{"ref":"d:bca","item":"Building condition assessment","note":"","group":"Building records"},
{"ref":"d:reserve","item":"Reserve fund study","note":"","group":"Building records"},
{"ref":"d:capex","item":"Capital plan","note":"","group":"Building records"},
{"ref":"d:budget","item":"Approved budget","note":"","group":"Money, accounts & agreements"},
{"ref":"d:t12","item":"Trailing twelve month statement","note":"","group":"Money, accounts & agreements"},
{"ref":"d:rentroll","item":"Rent roll","note":"","group":"Leases & residents"},
{"ref":"d:arrears","item":"Arrears report","note":"","group":"Money, accounts & agreements"},
{"ref":"d:ledgers","item":"Resident ledgers","note":"","group":"Leases & residents"},
{"ref":"d:ap","item":"Accounts payable","note":"","group":"Money, accounts & agreements"},
{"ref":"d:pap","item":"Pre-authorized payment forms","note":"","group":"Money, accounts & agreements"},
{"ref":"d:util_accts","item":"Utility accounts","note":"","group":"Money, accounts & agreements"},
{"ref":"d:util_auth","item":"Utility authorization letter","note":"","group":"Money, accounts & agreements"},
{"ref":"d:util_bills","item":"Utility bills","note":"","group":"Money, accounts & agreements"},
{"ref":"d:telecom","item":"Telecom agreement","note":"","group":"Money, accounts & agreements"},
{"ref":"d:laundry","item":"Laundry agreement","note":"","group":"Money, accounts & agreements"},
{"ref":"d:parking_agr","item":"Parking agreements","note":"","group":"Money, accounts & agreements"},
{"ref":"d:ev_agr","item":"EV charging agreement","note":"","group":"Money, accounts & agreements"},
{"ref":"d:rooftop","item":"Rooftop or antenna agreement","note":"","group":"Money, accounts & agreements"},
{"ref":"d:commercial","item":"Commercial leases","note":"","group":"Leases & residents"},
{"ref":"d:leases","item":"Resident leases","note":"","group":"Leases & residents"},
{"ref":"d:n1","item":"Rent increase notices","note":"","group":"Leases & residents"},
{"ref":"d:rentcontrol","item":"Rent control history","note":"","group":"Leases & residents"},
{"ref":"d:res_contacts","item":"Resident contact details","note":"","group":"Leases & residents"},
{"ref":"d:lmr","item":"Last month's rent deposits and interest history","note":"","group":"Leases & residents"},
{"ref":"d:side_agr","item":"Side agreements and accommodations","note":"","group":"Leases & residents"},
{"ref":"d:movein_insp","item":"Move in inspection and unit condition reports","note":"","group":"Leases & residents"},
{"ref":"d:assign_sublet","item":"Assignments and sublets in force","note":"","group":"Leases & residents"},
{"ref":"d:addenda","item":"Pet, parking and storage addenda","note":"","group":"Leases & residents"},
{"ref":"d:tenant_ins","item":"Tenant insurance certificates","note":"","group":"Leases & residents"},
{"ref":"d:agi","item":"Above guideline increase applications","note":"","group":"Money, accounts & agreements"},
{"ref":"d:concessions","item":"Concessions record","note":"","group":"Money, accounts & agreements"},
{"ref":"d:prelease","item":"Pre lease report","note":"","group":"Leasing & marketing"},
{"ref":"d:avail","item":"Availability report","note":"","group":"Leasing & marketing"},
{"ref":"d:listings","item":"Listings","note":"","group":"Leasing & marketing"},
{"ref":"d:assets_mkt","item":"Marketing assets","note":"","group":"Leasing & marketing"},
{"ref":"d:pipeline","item":"Leasing pipeline","note":"","group":"Leasing & marketing"},
{"ref":"d:portal","item":"Resident portal setup","note":"","group":"Leases & residents"},
{"ref":"d:pms_export","item":"Management system export","note":"","group":"Building records"},
{"ref":"d:staff","item":"Staff records","note":"","group":"Building records"},
{"ref":"d:afterhours","item":"After hours instructions","note":"","group":"Building records"},
{"ref":"d:complaints","item":"Complaints log","note":"","group":"Building records"},
{"ref":"d:disputes","item":"Disputes log","note":"","group":"Building records"},
{"ref":"d:ltb","item":"Landlord and Tenant Board files","note":"","group":"Building records"},
{"ref":"d:orders","item":"Municipal orders","note":"","group":"Building records"},
{"ref":"d:claims","item":"Insurance claims","note":"","group":"Building records"},
{"ref":"d:permits","item":"Permits","note":"","group":"Building records"},
{"ref":"d:dwg_site_doc","item":"Site plan drawing","note":"","group":"Drawings & plans"},
{"ref":"d:dwg_mech","item":"Mechanical drawings","note":"","group":"Drawings & plans"},
{"ref":"d:dwg_elec_doc","item":"Electrical drawings","note":"","group":"Drawings & plans"},
{"ref":"d:dwg_ab","item":"As built drawings","note":"","group":"Drawings & plans"},
{"ref":"d:dwg_surv","item":"Survey","note":"","group":"Drawings & plans"},
{"ref":"d:reno_dwg","item":"Renovation drawings","note":"","group":"Drawings & plans"}]};
/* ---------- gateways: tiles that expand or collapse whole branches ---------- */
const GATEWAYS = [
{id:'condo', label:'Condominium', desc:'Unit(s) inside a condo corporation', yes:'f:is_condo', refs:['f:condo_corp','f:condo_mgr','f:condo_contact','f:condo_site','f:condo_phone']},
{id:'elev', label:'Elevator', desc:'One or more passenger or freight elevators', yes:'f:has_elevator', refs:['f:elev_make','f:elev_lic','f:elev_mr','f:elev_line','f:elev_proc','f:elev_recall','f:ver_elev','f:fin_elevator_int','d:elev_cert']},
{id:'sprink', label:'Sprinkler system', desc:'Building is fully or partly sprinklered', yes:'f:has_sprinkler', refs:['f:sprink_valve','f:ver_sprink','f:ver_standpipe','f:ver_fdc','f:ver_pump','d:sprink_cert']},
{id:'panel', label:'Fire alarm panel', desc:'A monitored or unmonitored alarm panel', yes:'f:has_panel', refs:['f:panel_make','f:panel_loc','f:silence_reset','f:impairment','f:false_alarm','f:ver_alarm','d:alarm_cert','d:fire_test']},
{id:'boiler', label:'Boiler / central heat', desc:'Central boiler or heating plant', yes:null, refs:['f:boiler_stop','f:ver_boiler','d:boiler_cert']},
{id:'gen', label:'Generator', desc:'Backup power on site', yes:'f:has_generator', refs:['d:gen_cert']},
{id:'com', label:'Commercial units', desc:'Shops, offices or other commercial space', yes:'f:has_com', refs:['d:commercial']},
{id:'staff', label:'On-site staff', desc:'Superintendent, cleaner or site staff', yes:'f:has_staff', refs:['d:staff']},
{id:'cam', label:'Cameras', desc:'CCTV anywhere on the property', yes:'f:cam_exists', refs:['f:cam_system','f:cam_cover','f:cam_who']},
{id:'monitor', label:'Monitored alarm', desc:'A company is paid to monitor alarms', yes:null, refs:['f:monitor_co','f:monitor_line','f:monitor_acct']},
{id:'laundry', label:'Shared laundry', desc:'Common laundry room or machines', yes:null, refs:['d:laundry']},
{id:'park', label:'Parking', desc:'Stalls, a lot or a garage', yes:null, refs:['f:rate_parking','f:map_parking','d:parking_agr']},
{id:'storage', label:'Storage / lockers', desc:'Rentable storage for residents', yes:null, refs:['f:rate_storage']},
{id:'ev', label:'EV charging', desc:'Chargers installed or under agreement', yes:null, refs:['d:ev_agr']},
];
const condRefs = new Set(GATEWAYS.flatMap(g=>g.refs.concat(g.yes?[g.yes]:[])));
/* ---------- demo properties ---------- */
const PROPS = [
{id:'p87', addr:'87 Division Street, Kingston', kind:'Red Book catch-up', chip:'y', meta:'Managed since 2019 · record rebuild requested 2026-09-15'},
{id:'pnew', addr:'124 Colborne Street, Kingston', kind:'New onboarding', chip:'b', meta:'Agreement AGR-2026-1042 signed 2026-10-03 · welcome!'},
];
/* ---------- state ---------- */
const KEY='k2portal-demo-v1';
let S = JSON.parse(localStorage.getItem(KEY)||'null') || {email:'', authed:false, team:false, view:'login', propId:null, tab:'q', props:{}};
PROPS.forEach(p=>{ if(!S.props[p.id]) S.props[p.id]={gateDone:false, gates:{}, units:'', storeys:'', answers:{}, docs:{}}; });
function save(){ localStorage.setItem(KEY, JSON.stringify(S)); }
/* ---------- helpers ---------- */
const $=s=>document.querySelector(s);
const esc=s=>String(s).replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
function ps(){ return S.props[S.propId]; }
function applicable(p){
const on=r=>!condRefs.has(r) || GATEWAYS.some(g=>p.gates[g.id] && (g.refs.includes(r)||g.yes===r));
return { qs: DATA.qs.filter(q=>on(q.ref)), docs: DATA.docs.filter(d=>on(d.ref)) };
}
function settledCount(p){
const a=applicable(p); let n=0;
a.qs.forEach(q=>{ const st=p.answers[q.ref]; if(st && st.status!=='later') n++; });
a.docs.forEach(d=>{ const st=p.docs[d.ref]; if(st && (st.status==='received'||st.status==='accepted'||st.status==='none')) n++; });
return {done:n, total:a.qs.length+a.docs.length};
}
/* ---------- views ---------- */
function render(){
document.body.classList.toggle('team', S.team);
$('#mOwner').classList.toggle('on', !S.team);
$('#mTeam').classList.toggle('on', S.team);
$('#logout').classList.toggle('hide', !S.authed);
const m=$('#main');
if(S.view==='login') m.innerHTML=vLogin();
else if(S.view==='code') m.innerHTML=vCode();
else if(S.view==='dash') m.innerHTML=vDash();
else if(S.view==='prop') m.innerHTML=vProp();
wire(); save();
}
function vLogin(){ return `
<div class="auth"><div class="card">
<h1>Sign in</h1>
<p class="lead">Enter the email address your KEILTY agreement was sent to. No password to remember, we email you a 6-digit code each time.</p>
<label>Email address</label>
<input type="email" id="email" placeholder="you@email.com" value="${esc(S.email)}">
<div style="margin-top:18px"><button class="btn" id="goCode" style="width:100%">Continue</button></div>
<p class="hintline">Your onboarding file is linked to this address automatically.</p>
</div></div>`; }
function vCode(){ return `
<div class="auth"><div class="card">
<h1>Check your email</h1>
<p class="lead">We sent a 6-digit code to <b>${esc(S.email||'you')}</b>. Enter it below. Codes expire after 10 minutes.</p>
<input type="text" id="code" class="code6" maxlength="6" inputmode="numeric" placeholder="······" autocomplete="one-time-code">
<div style="margin-top:18px"><button class="btn" id="verify" style="width:100%">Verify and sign in</button></div>
<p class="hintline">Prototype note: any 6 digits work here. <button class="linkbtn" id="backLogin">Use a different email</button></p>
</div></div>`; }
function vDash(){
const cards = PROPS.map(p=>{
const st=S.props[p.id]; const c=settledCount(st);
const pct = st.gateDone ? Math.round(100*c.done/Math.max(1,c.total)) : 0;
const btn = st.gateDone ? (pct>0?'Continue onboarding':'Start the questions') : 'Start onboarding';
return `<div class="card pcard">
<span class="chip ${p.chip}">${esc(p.kind)}</span>
<h3 style="margin-top:10px">${esc(p.addr)}</h3>
<div class="meta">${esc(p.meta)}</div>
<div class="pbarwrap"><div class="pbar" style="width:${pct}%"></div></div>
<div class="pnums">${st.gateDone ? c.done+' of '+c.total+' items settled ('+pct+'%)' : 'Not started. A few quick questions will build your list.'}</div>
<button class="btn" data-open="${p.id}">${btn} →</button>
</div>`; }).join('');
return `
<h1 style="font-size:26px;font-weight:800;">Welcome back${S.team?' (team view)':''}</h1>
<p style="color:var(--body);font-size:14.5px;margin-top:4px">Pick up where you left off. Everything saves as you go, there is nothing to submit at the end.</p>
<div class="teambar">KEILTY team view (demo): open a property to review received answers and documents, and mark them Accepted.</div>
<div class="dgrid">${cards}</div>
<div class="contact"><div class="avatar">BA</div>
<p><b>Brandon Ackerman</b> · VP, Rental Management<br>Your onboarding contact: <a href="tel:6135453322">613-545-3322</a> · <a href="mailto:ClientCare@KEILTY.com">ClientCare@KEILTY.com</a><br>Stuck on anything? A real person answers, same day on business days.</p>
</div>`;
}
function vProp(){
const prop=PROPS.find(p=>p.id===S.propId); const st=ps();
const c=settledCount(st); const pct=Math.round(100*c.done/Math.max(1,c.total));
const head=`
<span class="crumb" id="backDash">← All properties</span>
<div class="phead"><h1>${esc(prop.addr)}</h1>
<div class="sub">${esc(prop.kind)} · ${st.gateDone? c.done+' of '+c.total+' items settled':'a few basics to start'}</div>
<div class="pbarwrap" style="max-width:420px"><div class="pbar" style="width:${st.gateDone?pct:0}%"></div></div></div>`;
if(!st.gateDone) return head+vGateway(st);
const a=applicable(st);
const tabs=`<div class="tabs">
<div class="tab ${S.tab==='q'?'on':''}" id="tabQ">Questions <span class="n">(${a.qs.length})</span></div>
<div class="tab ${S.tab==='d'?'on':''}" id="tabD">Documents <span class="n">(${a.docs.length})</span></div>
</div>
<div class="teambar">KEILTY team view (demo): items the owner answered show an Accept action. Accepting locks them in green.</div>`;
return head+tabs+(S.tab==='q'?vQuestions(st,a):vDocs(st,a));
}
function vGateway(st){
const tiles=GATEWAYS.map(g=>`
<div class="tile ${st.gates[g.id]?'on':''}" data-gate="${g.id}">
<span class="tick">${st.gates[g.id]?'✓':''}</span>
<b>${esc(g.label)}</b><small>${esc(g.desc)}</small>
</div>`).join('');
return `<div class="gate card" style="margin-top:8px">
<h2>First, the basics</h2>
<p class="lead">Tick everything this property has. Each one you tick adds the right questions and document requests; everything you leave off stays out of your way. You can change these later.</p>
<div class="gnum">
<div><label>How many units?</label><input type="number" id="gUnits" min="1" value="${esc(st.units)}" placeholder="e.g. 6"></div>
<div><label>How many storeys?</label><input type="number" id="gStoreys" min="1" value="${esc(st.storeys)}" placeholder="e.g. 3"></div>
</div>
<div class="tiles">${tiles}</div>
<p style="font-size:12.5px;color:var(--mute);margin:6px 0 18px">Not sure about one? Leave it unticked, we will catch it together later.</p>
<button class="btn" id="buildList">Build my list →</button>
</div>`;
}
function statusIcon(kind){
if(kind==='accepted') return '<span class="statusicon st-accepted">✓</span>';
if(kind==='received') return '<span class="statusicon st-received">↑</span>';
return '<span class="statusicon st-pending">!</span>';
}
function chipFor(stt){
if(!stt) return '<span class="chip y">To answer</span>';
if(stt.status==='answered') return '<span class="chip o">Answered, awaiting KEILTY review</span>';
if(stt.status==='accepted') return '<span class="chip g">Accepted by KEILTY</span>';
if(stt.status==='none') return '<span class="chip gr">There is none</span>';
if(stt.status==='unknown') return '<span class="chip gr">Marked: not known, KEILTY to chase</span>';
if(stt.status==='later') return '<span class="chip y">Saved for later</span>';
return '';
}
function vQuestions(st,a){
const secs=[]; a.qs.forEach(q=>{ if(!secs.includes(q.section)) secs.push(q.section); });
const html=secs.map((sec,si)=>{
const items=a.qs.filter(q=>q.section===sec);
const done=items.filter(q=>st.answers[q.ref] && st.answers[q.ref].status!=='later').length;
const open = S.openSec===sec ? 'open':'';
const rows=items.map(q=>{
const stt=st.answers[q.ref];
let body;
if(stt && stt.status!=='later'){
const valTxt = stt.status==='answered'||stt.status==='accepted' ? esc(stt.value) :
(stt.status==='none' ? 'There is none' : 'I do not know, please help us find it');
const accept = S.team && stt.status==='answered' ? `<button class="btn sm" data-acceptq="${q.ref}">Accept</button>` : '';
body=`<div class="saved">${chipFor(stt)}<span class="val ${stt.status!=='answered'&&stt.status!=='accepted'?'muted':''}">${valTxt}</span>
${accept}${S.team?'':'<button class="linkbtn" data-editq="'+q.ref+'">Edit</button>'}</div>`;
} else {
body=`<div class="ans-row"><input type="text" data-q="${q.ref}" placeholder="Type your answer">
<button class="btn sm" data-saveq="${q.ref}">Save</button></div>
<div class="escapes">
<button data-noneq="${q.ref}">There is none</button>
<button data-unkq="${q.ref}">I don't know</button>
<button data-laterq="${q.ref}">Ask me later</button>
</div>`;
}
return `<div class="item"><div class="lbl">${esc(q.item)}</div>
${q.note?'<div class="note">'+esc(q.note)+'</div>':''}${body}</div>`;
}).join('');
return `<div class="sec ${open}" data-sec="${si}">
<div class="sechead" data-sechead="${esc(sec)}"><h3>${esc(sec)}</h3>
<div class="right"><span>${done}/${items.length}</span><div class="minibar"><i style="width:${Math.round(100*done/Math.max(1,items.length))}%"></i></div><span>${open?'▲':'▼'}</span></div>
</div><div class="secbody">${rows}</div></div>`;
}).join('');
return `<div class="summary">Based on your basics, <b>${a.qs.length} questions</b> and <b>${a.docs.length} documents</b> apply to this property. "There is none" and "I don't know" are real answers, they stop us asking twice. <button class="linkbtn" id="editGates">Edit the basics</button></div>${html}`;
}
function vDocs(st,a){
const groups=[]; a.docs.forEach(d=>{ if(!groups.includes(d.group)) groups.push(d.group); });
const bulk=`<div class="bulk" id="bulk">Got a pile from a previous manager or your own files? <b>Drop everything here</b> and KEILTY will sort it into the right slots for you.<input type="file" id="bulkFile" multiple class="hide"></div>`;
const html=groups.map((g,gi)=>{
const items=a.docs.filter(d=>d.group===g);
const done=items.filter(d=>st.docs[d.ref]&&st.docs[d.ref].status!=='pending').length;
const open=S.openSec===g?'open':'';
const rows=items.map(d=>{
const stt=st.docs[d.ref]||{status:'pending',files:[]};
const files=(stt.files||[]).map(f=>`<div class="f"><span>${esc(f.name)}</span><span>${esc(f.date)}</span></div>`).join('');
const chip= stt.status==='accepted'?'<span class="chip g">Accepted by KEILTY</span>'
: stt.status==='received'?'<span class="chip o">Received, awaiting KEILTY review</span>'
: stt.status==='none'?'<span class="chip gr">There is none</span>'
: '<span class="chip y">Pending upload</span>';
const accept=S.team && stt.status==='received'?`<button class="btn sm" data-acceptd="${d.ref}">Accept</button>`:'';
const ownerActs = S.team?'':`<div class="dz" data-dz="${d.ref}">Drag & drop here, or click to choose files<input type="file" multiple class="hide" data-file="${d.ref}"></div>
<div class="escapes"><button data-noned="${d.ref}">There is none</button></div>`;
return `<div class="dslot"><div class="top"><div style="display:flex;align-items:flex-start">${statusIcon(stt.status==='none'?'accepted':stt.status)}
<div><div class="lbl" style="font-weight:700;font-size:14px">${esc(d.item)}</div>
${d.note?'<div class="note">'+esc(d.note)+'</div>':''}</div></div><div>${chip} ${accept}</div></div>
${files?'<div class="flist">'+files+'</div>':''}
${stt.status==='accepted'?'':ownerActs}</div>`;
}).join('');
return `<div class="sec ${open}">
<div class="sechead" data-sechead="${esc(g)}"><h3>${esc(g)}</h3>
<div class="right"><span>${done}/${items.length}</span><div class="minibar"><i style="width:${Math.round(100*done/Math.max(1,items.length))}%"></i></div><span>${open?'▲':'▼'}</span></div>
</div><div class="secbody">${rows}</div></div>`;
}).join('');
return (S.team?'':bulk)+html;
}
/* ---------- wiring ---------- */
function wire(){
$('#modeToggle').onclick=()=>{ S.team=!S.team; render(); };
$('#logout').onclick=()=>{ S.authed=false; S.view='login'; render(); };
$('#resetDemo').onclick=()=>{ if(confirm('Reset the whole demo?')){ localStorage.removeItem(KEY); location.reload(); } };
const on=(sel,fn)=>document.querySelectorAll(sel).forEach(el=>el.onclick=e=>fn(el,e));
if($('#goCode')) $('#goCode').onclick=()=>{ const v=$('#email').value.trim(); if(!/^\S+@\S+\.\S+$/.test(v)){alert('Enter a valid email for the demo');return;} S.email=v; S.view='code'; render(); };
if($('#verify')) $('#verify').onclick=()=>{ const v=$('#code').value.trim(); if(!/^\d{6}$/.test(v)){alert('Enter any 6 digits');return;} S.authed=true; S.view='dash'; render(); };
if($('#backLogin')) $('#backLogin').onclick=()=>{ S.view='login'; render(); };
on('[data-open]',el=>{ S.propId=el.dataset.open; S.view='prop'; S.tab='q'; S.openSec=null; render(); });
if($('#backDash')) $('#backDash').onclick=()=>{ S.view='dash'; render(); };
if($('#tabQ')) $('#tabQ').onclick=()=>{ S.tab='q'; S.openSec=null; render(); };
if($('#tabD')) $('#tabD').onclick=()=>{ S.tab='d'; S.openSec=null; render(); };
if($('#editGates')) $('#editGates').onclick=()=>{ ps().gateDone=false; render(); };
on('[data-gate]',el=>{ const p=ps(); p.gates[el.dataset.gate]=!p.gates[el.dataset.gate]; render(); });
if($('#buildList')) $('#buildList').onclick=()=>{ const p=ps();
p.units=$('#gUnits').value; p.storeys=$('#gStoreys').value;
GATEWAYS.forEach(g=>{ if(g.yes){ if(p.gates[g.id]) p.answers[g.yes]={status:'answered',value:'Yes'}; else p.answers[g.yes]={status:'answered',value:'No'}; }});
if(p.units) p.answers['f:units']={status:'answered',value:p.units};
if(p.storeys) p.answers['f:storeys']={status:'answered',value:p.storeys};
p.gateDone=true; render(); };
const setA=(ref,obj)=>{ ps().answers[ref]=obj; render(); };
on('[data-saveq]',el=>{ const ref=el.dataset.saveq; const inp=document.querySelector('input[data-q="'+ref.replace(/"/g,'\\"')+'"]'); const v=inp.value.trim(); if(!v){alert('Type an answer, or use one of the options below the box.');return;} setA(ref,{status:'answered',value:v}); });
on('[data-noneq]',el=>setA(el.dataset.noneq,{status:'none'}));
on('[data-unkq]',el=>setA(el.dataset.unkq,{status:'unknown'}));
on('[data-laterq]',el=>setA(el.dataset.laterq,{status:'later'}));
on('[data-editq]',el=>{ delete ps().answers[el.dataset.editq]; render(); });
on('[data-acceptq]',el=>{ const a=ps().answers[el.dataset.acceptq]; a.status='accepted'; render(); });
on('[data-sechead]',el=>{ const name=el.dataset.sechead; S.openSec=(S.openSec===name?null:name); render(); });
// documents
const fakeFiles=fl=>Array.from(fl).map(f=>({name:f.name,date:new Date().toISOString().slice(0,10)}));
on('[data-dz]',(el,e)=>{ if(e.target.tagName==='INPUT')return; el.querySelector('input[type=file]').click(); });
document.querySelectorAll('input[data-file]').forEach(inp=>{ inp.onchange=()=>{ const ref=inp.dataset.file; const d=ps().docs[ref]||{status:'pending',files:[]};
d.files=(d.files||[]).concat(fakeFiles(inp.files)); d.status='received'; ps().docs[ref]=d; render(); }; });
on('[data-noned]',el=>{ ps().docs[el.dataset.noned]={status:'none',files:[]}; render(); });
on('[data-acceptd]',el=>{ ps().docs[el.dataset.acceptd].status='accepted'; render(); });
document.querySelectorAll('.dz,.bulk').forEach(z=>{
z.ondragover=e=>{e.preventDefault();z.classList.add('drag');};
z.ondragleave=()=>z.classList.remove('drag');
z.ondrop=e=>{ e.preventDefault(); z.classList.remove('drag');
if(z.id==='bulk'){ alert('In the real portal this pile goes straight to KEILTY to sort into slots. (Prototype: nothing is uploaded.)'); return; }
const ref=z.dataset.dz; const d=ps().docs[ref]||{status:'pending',files:[]};
d.files=(d.files||[]).concat(fakeFiles(e.dataTransfer.files)); d.status='received'; ps().docs[ref]=d; render(); };
});
if($('#bulk')) $('#bulk').onclick=e=>{ if(e.target.tagName!=='INPUT') $('#bulkFile').click(); };
if($('#bulkFile')) $('#bulkFile').onchange=()=>alert('In the real portal this pile goes straight to KEILTY to sort into slots. (Prototype: nothing is uploaded.)');
}
render();
</script>
</body>
</html>
// no external data feed; everything is inline in Page HTML