2026-09-24 13:00:57 -04:00
<!DOCTYPE html>
2026-09-24 15:50:35 -04:00
<!-- This directory is the source of truth for both the website and the apps. The website
serves it directly; mobile/scripts/sync-web.js copies it into the iOS and Android
bundles at build time, so the apps carry a FROZEN copy taken when they were last built.
A change here therefore reaches the two in different ways: pushing to main redeploys
the website within a minute, and separately rebuilds both apps on the ingest Mac and
emails that they are behind — but nothing reaches a phone until someone runs
./release.sh --ios --android and the stores approve it. -->
2026-09-24 13:00:57 -04:00
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, viewport-fit=cover" >
<!-- Stop iOS Safari's data detectors from auto-linking IPs/ports/credentials in
task descriptions as "phone numbers" — that drew a stray underline and hijacked
taps into the call/share sheet. Nothing in this app is ever a phone number. -->
< meta name = "format-detection" content = "telephone=no, date=no, address=no, email=no" >
< title > Recurring Checklist</ title >
<!-- Inter — modern productivity typeface (falls back to the system stack offline) -->
< link rel = "preconnect" href = "https://fonts.googleapis.com" >
< link rel = "preconnect" href = "https://fonts.gstatic.com" crossorigin >
< link rel = "stylesheet" href = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" >
< link rel = "stylesheet" href = "styles.css" >
<!-- PWA / installable web app -->
< link rel = "manifest" href = "/manifest.webmanifest" >
< meta name = "theme-color" content = "#1d2026" >
< meta name = "color-scheme" content = "dark" >
<!-- iOS Safari support -->
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "apple-mobile-web-app-status-bar-style" content = "black-translucent" >
< meta name = "apple-mobile-web-app-title" content = "Checklist" >
< link rel = "apple-touch-icon" href = "/icons/apple-touch-icon.png?v=2" >
<!-- Favicons (v2 = new checkmark logo; query busts the browser's favicon cache) -->
< link rel = "icon" type = "image/svg+xml" href = "/icons/icon.svg?v=2" >
< link rel = "icon" type = "image/png" sizes = "32x32" href = "/icons/favicon-32.png?v=2" >
</ head >
< body >
< div id = "loadingOverlay" class = "loading-overlay" >
< div class = "loading-spinner" ></ div >
< div class = "loading-text" > Loading checklist...</ div >
</ div >
<!-- Shown when offline or while queued check-offs are syncing. -->
< div id = "offlineBadge" class = "offline-badge" style = "display:none;" ></ div >
<!-- First-launch setup, native apps only. The store build ships to anyone, so the
server is a per-install choice rather than something baked into the binary. -->
< div id = "setupOverlay" class = "setup-overlay" style = "display:none;" >
< div class = "setup-card" >
< div class = "setup-logo" aria-hidden = "true" >
< svg viewBox = "0 0 512 512" xmlns = "http://www.w3.org/2000/svg" >
< defs >< linearGradient id = "sg" x1 = "0" y1 = "0" x2 = "1" y2 = "1" >
< stop offset = "0" stop-color = "#39a8ff" />< stop offset = "1" stop-color = "#2ecc71" />
</ linearGradient ></ defs >
< rect width = "512" height = "512" rx = "112" fill = "url(#sg)" />
< path d = "M150 262 L224 338 L366 178" fill = "none" stroke = "#fff" stroke-width = "46"
stroke-linecap = "round" stroke-linejoin = "round" />
</ svg >
</ div >
< h2 > Set up Checklist</ h2 >
< p class = "setup-sub" > How do you want to use it? You can change this later.</ p >
< div id = "setupChoices" >
< button type = "button" class = "setup-choice" data-setup = "local" >
< span class = "setup-choice-title" > Just on this device</ span >
< span class = "setup-choice-desc" > Everything stays on your phone. No server,
no account, works offline — but only you see it.</ span >
</ button >
< button type = "button" class = "setup-choice" data-setup = "join" >
< span class = "setup-choice-title" > Join a server</ span >
< span class = "setup-choice-desc" > Your church already runs one. Everyone on your
team shares the same lists.</ span >
</ button >
< button type = "button" class = "setup-choice" data-setup = "host" >
< span class = "setup-choice-title" > Host it yourself</ span >
< span class = "setup-choice-desc" > Run it on your own computer or NAS. Your data
stays on your hardware.</ span >
</ button >
</ div >
<!-- Address entry, shared by both choices above. -->
< div id = "setupJoin" style = "display:none;" >
< label for = "setupAddress" > Server address</ label >
< input id = "setupAddress" type = "url" inputmode = "url" autocapitalize = "off"
autocorrect = "off" spellcheck = "false" placeholder = "checklist.yourchurch.org" />
< div id = "setupError" class = "setup-error" style = "display:none;" ></ div >
< div class = "setup-actions" >
< button type = "button" id = "setupBack" class = "btn btn-secondary btn-small" > Back</ button >
< button type = "button" id = "setupConnect" class = "btn btn-primary btn-small" > Connect</ button >
</ div >
</ div >
< div id = "setupHostInfo" style = "display:none;" >
< p class = "setup-sub" style = "text-align:left;" >
Checklist runs as a small Docker service on any always-on machine — a NAS, a
spare computer, or a cheap VPS. Once it is running, come back and enter its
address.
</ p >
< div class = "setup-actions" >
< button type = "button" id = "setupHostBack" class = "btn btn-secondary btn-small" > Back</ button >
< button type = "button" id = "setupHostNext" class = "btn btn-primary btn-small" > I have it running</ button >
</ div >
</ div >
</ div >
</ div >
< div id = "loginModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal" >
< h3 > Sign in</ h3 >
< div class = "form-group" >
< label for = "loginLocation" > Location</ label >
< select id = "loginLocation" ></ select >
</ div >
< div class = "form-group" >
< label for = "loginTeam" > Team</ label >
< select id = "loginTeam" ></ select >
</ div >
< div class = "form-group" id = "loginExecNameGroup" style = "display:none;" >
< label for = "loginExecName" > Who are you?</ label >
< input id = "loginExecName" type = "text" autocomplete = "off" autocapitalize = "words" placeholder = "Your name" />
</ div >
< div class = "form-group" >
< label for = "loginPassword" > Password</ label >
< div class = "password-field" >
< input id = "loginPassword" type = "password" autocomplete = "current-password" />
< button id = "loginPasswordToggle" class = "password-toggle" type = "button" aria-label = "Show password" title = "Show password" > 🐵</ button >
</ div >
</ div >
< div id = "loginError" class = "modal-message danger" style = "display:none;" ></ div >
< p style = "font-size:11px;color:#888;margin:0;" > Your location, team, and password. Editors: choose < code > Executive</ code > . < a href = "/privacy" target = "_blank" rel = "noopener noreferrer" style = "color:#7c9cc4;" > Privacy</ a >< span id = "changeServerWrap" style = "display:none;" > · < a href = "#" id = "changeServerLink" style = "color:#7c9cc4;" > Change server</ a ></ span ></ p >
< div class = "modal-actions" >
< button id = "loginSubmit" class = "btn btn-primary btn-small" type = "button" > Sign in</ button >
</ div >
</ div >
</ div >
< div id = "userMgmtModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > Manage Access</ h3 >
< button id = "userMgmtClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< div id = "userMgmtError" class = "modal-message danger" style = "display:none;" ></ div >
< div class = "mgmt-grid" >
< section class = "mgmt-section" >
< h4 > Team logins</ h4 >
< input id = "userMgmtSearch" class = "mgmt-search" type = "search" placeholder = "🔍 Filter by location, team, or email…" />
< div id = "userMgmtList" class = "user-mgmt-list" ></ div >
< div class = "mgmt-add" >
< h5 > Add a team login</ h5 >
< div class = "user-mgmt-grid" >
< select id = "newUserLocation" aria-label = "Location" ></ select >
< select id = "newUserTeam" aria-label = "Team" ></ select >
< input id = "newUserPassword" type = "password" placeholder = "Password (4+ chars)" />
< input id = "newUserEmails" type = "text" placeholder = "Report email(s), comma-separated — optional" />
< button id = "newUserCreate" class = "btn btn-primary btn-small" type = "button" > Create login</ button >
</ div >
< p class = "mgmt-tip" > One shared login per location + team. Report emails get the weekly digest.</ p >
</ div >
</ section >
< section class = "mgmt-section" >
< h4 > Executives</ h4 >
< input id = "execSearch" class = "mgmt-search" type = "search" placeholder = "🔍 Filter by name…" />
< div id = "execList" class = "user-mgmt-list" ></ div >
< div class = "mgmt-add" >
< h5 > Add an executive</ h5 >
< div class = "user-mgmt-grid" >
< input id = "newExecName" type = "text" placeholder = "Name (e.g. Jordan)" autocapitalize = "words" />
< input id = "newExecPassword" type = "password" placeholder = "Password (4+ chars)" />
< button id = "newExecCreate" class = "btn btn-primary btn-small" type = "button" > Create executive</ button >
</ div >
< p class = "mgmt-tip" > Sign in under “ Executive” with their name. “ Edit access” sets the locations/teams they can edit.</ p >
</ div >
</ section >
< section class = "mgmt-section mgmt-wide" >
< h4 > Locations & teams</ h4 >
< div id = "directoryEditor" class = "directory-editor" ></ div >
</ section >
</ div >
</ div >
</ div >
< div id = "listModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal" >
< h3 id = "listModalTitle" ></ h3 >
< div id = "listModalMessage" class = "modal-message" style = "display:none;" ></ div >
< input id = "listModalInput" type = "text" placeholder = "List name" />
< div id = "listModalTemplateGroup" class = "form-group" style = "margin-top:8px; display:none;" >
< label for = "listModalTemplate" style = "font-size:12px;color:#b0b0b0;" > Start from</ label >
< select id = "listModalTemplate" ></ select >
</ div >
< div id = "listModalLocationGroup" class = "form-group" style = "margin-top:8px;" >
< label for = "listModalLocation" style = "font-size:12px;color:#b0b0b0;" > Assign to location</ label >
< select id = "listModalLocation" ></ select >
</ div >
< div id = "listModalTeamGroup" class = "form-group" style = "margin-top:8px;" >
< label for = "listModalTeam" style = "font-size:12px;color:#b0b0b0;" > Assign to team</ label >
< select id = "listModalTeam" ></ select >
</ div >
< div id = "listModalColorGroup" class = "form-group" style = "margin-top:8px;" >
< label style = "font-size:12px;color:#b0b0b0;" > Color</ label >
< div id = "listModalColors" class = "color-swatches" >
< button type = "button" class = "swatch swatch-none" data-color = "" title = "No color" ></ button >
< button type = "button" class = "swatch" data-color = "blue" title = "Blue" ></ button >
< button type = "button" class = "swatch" data-color = "green" title = "Green" ></ button >
< button type = "button" class = "swatch" data-color = "amber" title = "Amber" ></ button >
< button type = "button" class = "swatch" data-color = "red" title = "Red" ></ button >
< button type = "button" class = "swatch" data-color = "purple" title = "Purple" ></ button >
< button type = "button" class = "swatch" data-color = "teal" title = "Teal" ></ button >
< button type = "button" class = "swatch" data-color = "pink" title = "Pink" ></ button >
< button type = "button" class = "swatch" data-color = "slate" title = "Slate" ></ button >
< button type = "button" class = "swatch" data-color = "orange" title = "Orange" ></ button >
< button type = "button" class = "swatch" data-color = "yellow" title = "Yellow" ></ button >
< button type = "button" class = "swatch" data-color = "lime" title = "Lime" ></ button >
< button type = "button" class = "swatch" data-color = "cyan" title = "Cyan" ></ button >
< button type = "button" class = "swatch" data-color = "indigo" title = "Indigo" ></ button >
< button type = "button" class = "swatch" data-color = "brown" title = "Brown" ></ button >
< label class = "swatch swatch-custom" title = "Custom color — pick your own" >
< input type = "color" id = "listModalCustomColor" value = "#3498db" aria-label = "Custom color" />
</ label >
</ div >
</ div >
< div id = "listModalHiddenGroup" class = "form-group" style = "margin-top:10px;" >
< label class = "checkbox-label" style = "font-size:13px;" >< input type = "checkbox" id = "listModalHidden" /> Hidden — only editors see it</ label >
< label class = "checkbox-label" style = "font-size:13px;margin-top:6px;" >< input type = "checkbox" id = "listModalShowNotes" /> Show a Notes column — reference info, no checking off</ label >
< label class = "checkbox-label" style = "font-size:13px;margin-top:6px;" >< input type = "checkbox" id = "listModalNotesOnly" /> Notes only — hide the To Do and Done columns</ label >
< label class = "checkbox-label" style = "font-size:13px;margin-top:6px;" >< input type = "checkbox" id = "listModalShowFeedback" /> Show a Feedback column — anyone on the list can add input</ label >
< div class = "form-group" id = "listModalSectionsGroup" style = "margin-top:10px;" >
< label style = "font-size:13px;" > Task sections — extra To Do columns (e.g. Monday, Tuesday). The first is the default and can be renamed.</ label >
< div id = "listModalSections" ></ div >
< button type = "button" class = "btn btn-secondary btn-small" id = "listModalSectionAdd" > + Add section</ button >
</ div >
</ div >
< div id = "listModalRepeatGroup" class = "form-group" style = "margin-top:8px;" >
< label for = "listModalResetType" style = "font-size:12px;color:#b0b0b0;" > 🔁 Repeat this list < span style = "color:#7c828c;" > — every item resets to To Do on this schedule</ span ></ label >
< select id = "listModalResetType" >
< option value = "" > Don't repeat — reset manually</ option >
< option value = "daily" > Daily</ option >
< option value = "weekly" > Weekly</ option >
< option value = "monthly" > Monthly</ option >
< option value = "yearly" > Yearly</ option >
</ select >
< div id = "listModalResetEvery" class = "reset-row" style = "display:none;" >
< span > Every</ span >
< input type = "number" id = "listModalResetInterval" min = "1" max = "99" value = "1" />
< span id = "listModalResetUnit" > weeks</ span >
< span id = "listModalResetHint" class = "reset-hint" ></ span >
</ div >
< div id = "listModalResetStartRow" class = "reset-row" style = "display:none;" >
< span > starting on</ span >
< select id = "listModalResetStartDow" >
< option value = "0" > Sunday</ option >< option value = "1" > Monday</ option >< option value = "2" > Tuesday</ option >
< option value = "3" > Wednesday</ option >< option value = "4" > Thursday</ option >< option value = "5" > Friday</ option >< option value = "6" > Saturday</ option >
</ select >
</ div >
< div id = "listModalResetDowRow" class = "reset-row" style = "display:none;" >
< span > on</ span >
< select id = "listModalResetDow" >
< option value = "0" > Sunday</ option >< option value = "1" > Monday</ option >< option value = "2" > Tuesday</ option >
< option value = "3" > Wednesday</ option >< option value = "4" > Thursday</ option >< option value = "5" > Friday</ option >< option value = "6" > Saturday</ option >
</ select >
</ div >
< div id = "listModalResetMonthRow" class = "reset-row" style = "display:none;" >
< span > in</ span >
< select id = "listModalResetMonth" >
< option value = "0" > January</ option >< option value = "1" > February</ option >< option value = "2" > March</ option >
< option value = "3" > April</ option >< option value = "4" > May</ option >< option value = "5" > June</ option >
< option value = "6" > July</ option >< option value = "7" > August</ option >< option value = "8" > September</ option >
< option value = "9" > October</ option >< option value = "10" > November</ option >< option value = "11" > December</ option >
</ select >
</ div >
< div id = "listModalResetDomRow" class = "reset-row" style = "display:none;" >
< span > on day</ span >
< input type = "number" id = "listModalResetDom" min = "1" max = "31" value = "1" />
< span > of the month</ span >
</ div >
< div id = "listModalResetTimesRow" class = "reset-row reset-times-row" style = "display:none;" >
< span > at</ span >
< div id = "listModalResetTimesList" class = "reset-times-list" ></ div >
< button type = "button" id = "listModalResetTimesAdd" class = "btn btn-secondary btn-small reset-time-add" > + Add time</ button >
< span class = "reset-hint reset-times-hint" > leave empty to reset once a day</ span >
</ div >
</ div >
< div class = "modal-actions" >
< button id = "listModalCancel" class = "btn btn-secondary btn-small" type = "button" > Cancel</ button >
< button id = "listModalSave" class = "btn btn-primary btn-small" type = "button" > Save</ button >
</ div >
</ div >
</ div >
< div id = "editTaskModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal wide" >
< h3 > Edit Task</ h3 >
< div class = "modal-body" >
< div class = "form-group" >
< label for = "editTitle" > Title</ label >
< input id = "editTitle" type = "text" placeholder = "Task title..." />
</ div >
< div class = "form-group" id = "editGroupRow" style = "display:none;" >
< label for = "editGroupSelect" > Section</ label >
< select id = "editGroupSelect" ></ select >
</ div >
< div class = "form-group" >
< label for = "editDescription" > Description</ label >
< div class = "md-toolbar" data-md-target = "editDescription" >
< button type = "button" class = "md-btn" data-md = "bold" title = "Bold" >< b > B</ b ></ button >
< button type = "button" class = "md-btn" data-md = "italic" title = "Italic" >< i > I</ i ></ button >
< button type = "button" class = "md-btn" data-md = "h" title = "Heading" > H</ button >
< button type = "button" class = "md-btn" data-md = "ul" title = "Bullet list" > • List</ button >
< button type = "button" class = "md-btn" data-md = "ol" title = "Numbered list" > 1. List</ button >
< button type = "button" class = "md-btn" data-md = "task" title = "Checkbox — a step users tick off (the task auto-completes when all are checked)" > ☑ Step</ button >
< button type = "button" class = "md-btn" data-md = "link" title = "Add a link (web page, Google Doc…)" > 🔗 Link</ button >
< button type = "button" class = "md-btn" data-md = "attach" title = "Attach a document (PDF, Word, Excel, PowerPoint…) — uploads it and links it here" > 📎 File</ button >
< button type = "button" class = "md-btn md-preview-btn" data-md = "preview" aria-pressed = "false" title = "Toggle formatted markdown view" > Markdown</ button >
</ div >
< textarea id = "editDescription" placeholder = "Description — format with the buttons above." style = "display:none;" ></ textarea >
< div class = "md-rich" contenteditable = "true" data-md-rich-for = "editDescription" data-placeholder = "Description — format with the buttons above." role = "textbox" aria-multiline = "true" ></ div >
</ div >
< div class = "form-group" >
< label class = "checkbox-label" >< input type = "checkbox" id = "editIsRecurring" /> Recurring Task</ label >
</ div >
< div class = "form-group" id = "editRecurrenceOptions" style = "display: none;" >
< div class = "rec-editor" ></ div >
</ div >
</ div >
< div class = "modal-actions" >
< button id = "editModalCancel" class = "btn btn-secondary btn-small" type = "button" > Cancel</ button >
< button id = "editModalSave" class = "btn btn-primary btn-small" type = "button" > Save</ button >
</ div >
</ div >
</ div >
< div id = "moveItemModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal" >
< h3 > Move Task</ h3 >
< div class = "modal-body" >
< div class = "form-group" >
< label for = "moveItemSelect" > Select destination list</ label >
< select id = "moveItemSelect" ></ select >
</ div >
</ div >
< div class = "modal-actions" >
< button id = "moveItemCancel" class = "btn btn-secondary btn-small" type = "button" > Cancel</ button >
< button id = "moveItemSave" class = "btn btn-primary btn-small" type = "button" > Move</ button >
</ div >
</ div >
</ div >
<!-- Inline "Move to…" dropdown anchored to a task's Move button (replaces the modal flow). -->
< div id = "moveMenuBackdrop" class = "move-menu-backdrop" style = "display:none;" ></ div >
< div id = "moveMenu" class = "move-menu" role = "menu" aria-label = "Move task to list" style = "display:none;" ></ div >
<!-- Cross-department request (editors only): lands in the target team's Requests inbox. -->
< div id = "requestModal" class = "modal-overlay" style = "display:none;" >
< div class = "modal" >
< div class = "modal-header" >< h3 > ✋ Request something</ h3 ></ div >
< div class = "modal-body" >
< div id = "requestError" class = "modal-message danger" style = "display:none;" ></ div >
< div class = "form-group" >
< label for = "requestTarget" > From which department?</ label >
< select id = "requestTarget" ></ select >
</ div >
< div class = "form-group" >
< label for = "requestTitle" > What do you need?</ label >
< input id = "requestTitle" type = "text" maxlength = "120" placeholder = "e.g. Toilet paper restock in the office bathroom" />
</ div >
< div class = "form-group" >
< label for = "requestDetails" > Details (optional)</ label >
< textarea id = "requestDetails" rows = "3" maxlength = "2000" placeholder = "Where exactly, how urgent, anything helpful…" ></ textarea >
</ div >
< div id = "requestPhotos" class = "equip-photos" ></ div >
< div class = "equip-photo-row" >
< label class = "btn btn-secondary btn-small equip-upload-label" >
📷 Add photo
< input id = "requestPhotoFile" type = "file" accept = "image/*" hidden />
</ label >
< span id = "requestUploadStatus" class = "equip-upload-status" ></ span >
</ div >
< div style = "display:flex;justify-content:flex-end;gap:8px;margin-top:12px;" >
< button id = "requestCancel" class = "btn btn-secondary" type = "button" > Cancel</ button >
< button id = "requestSend" class = "btn btn-primary" type = "button" > Send request</ button >
</ div >
</ div >
</ div >
</ div >
<!-- Equipment photo popup (shown when anyone taps an equipment word).
The image sits in a pan/zoom stage: pinch or scroll to zoom, drag to pan,
double-tap to reset. The note panel below scrolls independently. -->
< div id = "equipModal" class = "modal-overlay equip-lightbox" style = "display: none;" >
< div class = "modal equip-modal" >
< div class = "equip-lb-head" >
< h3 id = "equipModalName" ></ h3 >
< button id = "equipReportBtn" class = "equip-report-btn" type = "button" title = "Report a problem with this gear" > ⚠ Report a problem</ button >
< button id = "equipModalClose" class = "equip-lb-close" type = "button" aria-label = "Close" > ✕</ button >
</ div >
< div id = "equipStage" class = "equip-stage" >
< img id = "equipModalImg" class = "equip-img" alt = "" draggable = "false" />
< button id = "equipPrev" class = "equip-nav equip-nav-prev" type = "button" aria-label = "Previous photo" > ‹ </ button >
< button id = "equipNext" class = "equip-nav equip-nav-next" type = "button" aria-label = "Next photo" > › </ button >
< div id = "equipZoomHint" class = "equip-zoom-hint" ></ div >
< div class = "equip-zoom-controls" >
< button id = "equipZoomOut" class = "equip-zoom-btn" type = "button" aria-label = "Zoom out" > − </ button >
< button id = "equipZoomReset" class = "equip-zoom-btn" type = "button" aria-label = "Fit to screen" > ⤢</ button >
< button id = "equipZoomIn" class = "equip-zoom-btn" type = "button" aria-label = "Zoom in" > +</ button >
</ div >
</ div >
< div id = "equipCaption" class = "equip-caption" ></ div >
< div id = "equipThumbs" class = "equip-thumbs" ></ div >
< div id = "equipModalNote" class = "equip-note" ></ div >
< div id = "equipIssues" class = "equip-issues" ></ div >
</ div >
</ div >
<!-- Equipment library manager (master only) -->
< div id = "equipMgmtModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > Equipment Library</ h3 >
< button id = "equipMgmtClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< div id = "equipMgmtError" class = "modal-message danger" style = "display:none;" ></ div >
< p class = "mgmt-tip" > Add a photo for any gear — its name becomes tappable in descriptions for the locations/teams you pick. Each location can keep its own photo.</ p >
< input id = "equipSearch" class = "mgmt-search" type = "search" placeholder = "🔍 Filter equipment…" />
< div id = "equipMgmtList" class = "user-mgmt-list" ></ div >
< div class = "mgmt-add" id = "equipAddSection" >
< button type = "button" class = "mgmt-add-toggle" id = "equipAddToggle" aria-expanded = "false" aria-controls = "equipAddBody" >
< span class = "mgmt-add-caret" aria-hidden = "true" > + </ span >
< span id = "equipFormTitle" > Add equipment</ span >
</ button >
< div class = "mgmt-add-body" id = "equipAddBody" >
< div class = "user-mgmt-grid" >
< input id = "equipName" type = "text" placeholder = "Name" />
< input id = "equipAliases" type = "text" placeholder = "Aliases, comma-separated (optional)" />
< textarea id = "equipNote" class = "equip-note-input" rows = "2" placeholder = "Note / troubleshooting steps (optional) — Markdown & line breaks supported" ></ textarea >
</ div >
< div id = "equipPhotos" class = "equip-photos" ></ div >
< div class = "equip-photo-row" >
< label class = "btn btn-secondary btn-small equip-upload-label" >
📁 Add photo / video
< input id = "equipFile" type = "file" accept = "image/*,video/mp4,video/webm,video/quicktime" hidden />
</ label >
< button id = "equipCamBtn" class = "btn btn-secondary btn-small" type = "button" > 📷 Take photo</ button >
< span id = "equipUploadStatus" class = "equip-upload-status" ></ span >
</ div >
< div class = "equip-url-row" >
< input id = "equipImage" type = "text" class = "equip-url-input" placeholder = "…or paste an image / video URL (JPG, PNG, MP4)" />
< button id = "equipAddUrl" class = "btn btn-secondary btn-small" type = "button" > Add</ button >
</ div >
< div id = "equipDocs" class = "equip-docs-editor" ></ div >
< div class = "equip-photo-row" >
< label class = "btn btn-secondary btn-small equip-upload-label" >
📎 Add document
< input id = "equipDocFile" type = "file" accept = ".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.csv,.rtf" hidden />
</ label >
</ div >
< div class = "equip-url-row" >
< input id = "equipDocUrl" type = "text" class = "equip-url-input" placeholder = "…or paste a link (Google Doc, manual, web page)" />
< button id = "equipDocAdd" class = "btn btn-secondary btn-small" type = "button" > Add link</ button >
</ div >
< div class = "equip-scope" >
< div id = "equipScopeHelp" class = "exec-access-help" > Where it appears — check location(s) and/or team(s).</ div >
< div class = "dir-head" > Locations</ div >
< div id = "equipLocRow" class = "exec-cb-row" ></ div >
< div class = "dir-head" style = "margin-top:6px;" > Teams</ div >
< div id = "equipTeamRow" class = "exec-cb-row" ></ div >
</ div >
< div class = "equip-form-actions" >
< button id = "equipSave" class = "btn btn-primary btn-small" type = "button" > Add equipment</ button >
< button id = "equipCancelEdit" class = "btn btn-secondary btn-small" type = "button" style = "display:none;" > Cancel edit</ button >
</ div >
</ div > <!-- /.mgmt-add-body -->
</ div >
</ div >
</ div >
<!-- In-app camera capture (works on desktop + mobile via getUserMedia) -->
< div id = "equipCamModal" class = "modal-overlay" style = "display:none; z-index:10002;" >
< div class = "modal equip-cam" >
< div class = "mgmt-head" >
< h3 > Take a photo</ h3 >
< button id = "equipCamCancel" class = "btn btn-secondary btn-small" type = "button" > Cancel</ button >
</ div >
< div class = "equip-cam-stage" >
< video id = "equipCamVideo" playsinline autoplay muted ></ video >
</ div >
< div id = "equipCamError" class = "modal-message danger" style = "display:none;" ></ div >
< div class = "equip-cam-actions" >
< button id = "equipCamCapture" class = "btn btn-primary" type = "button" > 📸 Capture</ button >
< button id = "equipCamSwitch" class = "btn btn-secondary btn-small" type = "button" > 🔄 Switch camera</ button >
</ div >
</ div >
</ div >
<!-- Overview: each team's progress today + recent activity (master + execs) -->
< div id = "overviewModal" class = "modal-overlay" style = "display:none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > Today at a glance</ h3 >
< button id = "overviewClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< div id = "overviewBody" class = "overview-body" ></ div >
</ div >
</ div >
< div id = "activityModal" class = "modal-overlay" style = "display:none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > 🛡 Activity Log</ h3 >
< button id = "activityClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< input id = "activitySearch" class = "activity-search" type = "search" placeholder = "Filter by person, action, or list…" autocomplete = "off" />
< div id = "activityBody" class = "activity-body" ></ div >
</ div >
</ div >
<!-- "Who's checking?" name picker (shared team logins only) -->
< div id = "checkerModal" class = "modal-overlay" style = "display:none;" >
< div class = "modal" >
< h3 > Who's checking?</ h3 >
< p style = "font-size:12px;color:#888;margin:0;" > Pick your name so your check-offs are recorded as you — no password needed.</ p >
< select id = "checkerSelect" style = "margin-top:8px;" ></ select >
< div id = "checkerAddRow" style = "display:none; gap:8px; margin-top:8px;" >
< input id = "checkerNewName" type = "text" placeholder = "Type your name" autocapitalize = "words" autocomplete = "off" style = "flex:1;" />
< button id = "checkerAddBtn" class = "btn btn-primary btn-small" type = "button" > Add</ button >
</ div >
< div class = "modal-actions" >
< button id = "checkerClose" class = "btn btn-secondary btn-small" type = "button" > Done</ button >
</ div >
</ div >
</ div >
<!-- Team name rosters — manage who's on each team (master + execs) -->
< div id = "rosterModal" class = "modal-overlay" style = "display:none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > Team names</ h3 >
< button id = "rosterClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< p class = "mgmt-tip" > Names checkers can tap when signing in. Add or remove names for any team you manage.</ p >
< input id = "rosterSearch" class = "mgmt-search" type = "search" placeholder = "🔍 Filter by location or team…" />
< div id = "rosterBody" class = "roster-body user-mgmt-list" ></ div >
</ div >
</ div >
< div class = "app-container" >
< header >
< div class = "header-left" >
< svg class = "app-logo" viewBox = "0 0 44 44" role = "img" aria-label = "Checklist" xmlns = "http://www.w3.org/2000/svg" >
< defs >
< linearGradient id = "appLogoGrad" x1 = "0" y1 = "0" x2 = "1" y2 = "1" >
< stop offset = "0%" stop-color = "#39a8ff" />
< stop offset = "100%" stop-color = "#2ecc71" />
</ linearGradient >
</ defs >
< rect x = "3" y = "3" width = "38" height = "38" rx = "12" fill = "url(#appLogoGrad)" />
< path class = "app-logo-check" d = "M13.5 22.5 L19.5 28.5 L30.5 15.5" fill = "none" stroke = "#fff" stroke-width = "3.6" stroke-linecap = "round" stroke-linejoin = "round" />
</ svg >
</ div >
< div class = "header-controls admin-only" >
< div class = "list-tabs-controls" >
< button id = "listViewToggle" class = "lists-toggle" type = "button" title = "Switch between the tab strip and a Location → Team → List tree" > 🌳 Tree view</ button >
< button id = "listTabsToggle" class = "lists-toggle" type = "button" aria-expanded = "true" title = "Show/hide the lists" > Hide lists</ button >
< div class = "list-filter master-only" >
< select id = "adminFilterLocation" aria-label = "Filter by location" ></ select >
< select id = "adminFilterTeam" aria-label = "Filter by team" ></ select >
</ div >
</ div >
< div class = "undo-redo admin-only" role = "group" aria-label = "Undo and redo" >
< button id = "undoBtn" class = "icon-btn" type = "button" title = "Undo (Ctrl/⌘+Z)" aria-label = "Undo" disabled > ↶</ button >
< button id = "redoBtn" class = "icon-btn" type = "button" title = "Redo (Ctrl/⌘+Shift+Z)" aria-label = "Redo" disabled > ↷</ button >
</ div >
< div id = "listMenu" class = "list-menu" >
< button id = "listMenuHandle" class = "list-menu-btn" type = "button" aria-haspopup = "true" aria-expanded = "false" aria-controls = "listMenuPanel" aria-label = "List tools menu" >< span class = "lmh-icon" aria-hidden = "true" > ☰</ span >< span class = "lmh-label" > Menu</ span ></ button >
< div id = "listMenuBackdrop" class = "list-menu-backdrop" ></ div >
< div id = "listMenuPanel" class = "list-menu-panel" role = "menu" aria-label = "List tools" >
< div class = "menu-group-label" > This list</ div >
< button id = "addTaskBtn" class = "btn btn-primary btn-small" > + Add Task</ button >
< button id = "addNoteBtn" class = "btn btn-secondary btn-small" title = "Add a reference note (no checkbox)" > + Add Note</ button >
< button id = "renameListBtn" class = "btn btn-secondary btn-small" > ✏️ Edit List</ button >
< button id = "duplicateListBtn" class = "btn btn-secondary btn-small" title = "Make a copy of this list (items copied, checkmarks fresh)" > ⧉ Duplicate List</ button >
< button id = "resetListBtn" class = "btn btn-secondary btn-small" title = "Move every item back to To Do" > ↺ Reset List</ button >
< button id = "deleteListBtn" class = "btn btn-danger btn-small" > 🗑 Delete List</ button >
< div class = "menu-group-label" > Lists</ div >
< button id = "addListBtn" class = "btn btn-primary btn-small" > + Add List</ button >
< div class = "menu-group-label" > Tools</ div >
< button id = "helpTipsBtn" class = "btn btn-secondary btn-small" type = "button" title = "Quick tips on how this works" > ❔ How this works</ button >
< button id = "overviewBtn" class = "btn btn-secondary btn-small admin-only" title = "See each team's progress today" > 📊 Overview</ button >
< button id = "rosterBtn" class = "btn btn-secondary btn-small admin-only" title = "Manage the names checkers can pick" > 🧑 Team Names</ button >
< button id = "manageUsersBtn" class = "btn btn-secondary btn-small master-only" > 👥 Manage Users</ button >
< button id = "activityBtn" class = "btn btn-secondary btn-small master-only" title = "See everything done in the app — who checked, created, edited, moved, or deleted" > 🛡 Activity Log</ button >
< button id = "incidentsBtn" class = "btn btn-secondary btn-small admin-only" title = "See and resolve reported gear problems in your area" > ⚠ Incidents< span id = "incidentsBtnCount" class = "menu-count" style = "display:none;" ></ span ></ button >
< button id = "reliabilityBtn" class = "btn btn-secondary btn-small admin-only" title = "Which gear breaks most — ranked by reported issues" > 📊 Reliability</ button >
< button id = "equipMgmtBtn" class = "btn btn-secondary btn-small admin-only" > 🔧 Equipment</ button >
< button id = "sendDigestBtn" class = "btn btn-secondary btn-small admin-only" title = "Email the checklist report now to the configured recipients for your area" > 📧 Send Report</ button >
< button id = "exportJsonBtn" class = "btn btn-secondary btn-small admin-only" title = "Download a backup of your lists (master = full backup)" > ⬇ Export JSON</ button >
< button id = "importJsonBtn" class = "btn btn-secondary btn-small admin-only" > ⬆ Import JSON</ button >
< input id = "importJsonInput" type = "file" accept = "application/json,.json" style = "display:none;" />
</ div >
</ div >
</ div >
< div class = "header-right" >
< button id = "searchBtn" class = "icon-btn search-bar-btn" type = "button" title = "Search gear, notes & tasks (press /)" aria-label = "Search" >< span class = "sb-icon" aria-hidden = "true" > 🔍</ span >< span class = "sb-label" > Search…</ span ></ button >
< button id = "reportIssueBtn" class = "icon-btn report-issue-btn" type = "button" title = "Report a gear problem" aria-label = "Report a gear problem" > ⚠</ button >
< button id = "requestsBadge" class = "requests-badge" type = "button" style = "display:none;" title = "Open requests waiting — tap to view" aria-label = "Open requests" > ✋ < span id = "requestsBadgeCount" > 0</ span ></ button >
< button id = "requestSendBtn" class = "icon-btn request-send-btn admin-only" type = "button" title = "Request something from another department" aria-label = "Request something from another department" > ✋</ button >
< button id = "checkerChip" class = "checker-chip checker-only" type = "button" title = "Who's checking? Tap to pick your name" >
< span class = "checker-chip-icon" > 👤</ span >
< span id = "checkerChipName" > Who's checking?</ span >
</ button >
< button id = "userBadge" class = "user-badge" type = "button" title = "Click to sign out" >
< span class = "user-badge-icon" > 👤</ span >
< span id = "userBadgeName" > Sign in</ span >
</ button >
</ div >
</ header >
< div class = "main-content" >
< div id = "tasksView" class = "tab-view active" >
< div class = "list-tabs-row" >
< div id = "activeListName" class = "active-list-name admin-only" ></ div >
< div id = "listTabs" class = "list-tabs" ></ div >
< div id = "listTree" class = "list-tree admin-only" ></ div >
</ div >
< div class = "checklists-section" >
< div class = "checklist-column" data-col = "todo" >
< h2 >< span class = "col-title" > To Do</ span ></ h2 >
< div id = "todoList" class = "checklist-list" ></ div >
</ div >
< div class = "checklist-column" data-col = "done" >
< h2 >< span class = "col-title" > Done</ span ></ h2 >
< div id = "doneList" class = "checklist-list" ></ div >
</ div >
< div class = "checklist-column notes-column" data-col = "notes" >
< h2 >< span class = "col-title" > Notes</ span ></ h2 >
< div id = "notesList" class = "checklist-list" ></ div >
</ div >
< div class = "checklist-column feedback-column" data-col = "feedback" >
< h2 >< span class = "col-title" > Feedback</ span ></ h2 >
< div id = "feedbackList" class = "checklist-list" ></ div >
</ div >
</ div >
</ div >
</ div >
< div id = "searchOverlay" class = "search-overlay" style = "display:none;" >
< div class = "search-panel" role = "dialog" aria-label = "Search" >
< div class = "search-inputrow" >
< svg viewBox = "0 0 24 24" aria-hidden = "true" >< circle cx = "11" cy = "11" r = "7" />< path d = "M21 21l-4.3-4.3" /></ svg >
< input id = "globalSearchInput" type = "search" placeholder = "Search gear, notes, tasks & lists…" autocomplete = "off" />
< button id = "searchClose" type = "button" class = "icon-btn" aria-label = "Close search" > ✕</ button >
</ div >
< div id = "searchResults" class = "search-results" ></ div >
</ div >
</ div >
<!-- Report a gear problem (any signed-in user) -->
< div id = "incidentModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal" >
< div class = "modal-header" >
< h3 > ⚠ Report a problem</ h3 >
< button id = "incidentClose" class = "btn btn-secondary btn-small" type = "button" > Cancel</ button >
</ div >
< div class = "modal-body" >
< div id = "incidentGearRow" class = "incident-gear-row" style = "display:none;" >
< span class = "incident-gear-label" > Gear:</ span >
< span id = "incidentGearName" class = "incident-gear-name" ></ span >
</ div >
< div class = "form-group" >
< label > How bad is it?</ label >
< div class = "severity-toggle" id = "incidentSeverity" >
< button type = "button" class = "sev-btn sev-down" data-sev = "down" >
< span class = "sev-emoji" > 🔴</ span >
< span class = "sev-title" > It's down now</ span >
< span class = "sev-sub" > Can't be used — alerts the team right away</ span >
</ button >
< button type = "button" class = "sev-btn sev-minor is-selected" data-sev = "minor" >
< span class = "sev-emoji" > 🟡</ span >
< span class = "sev-title" > Minor issue</ span >
< span class = "sev-sub" > Still works — goes in the report</ span >
</ button >
</ div >
</ div >
< div class = "form-group" >
< label for = "incidentWhat" > What's wrong?</ label >
< input type = "text" id = "incidentWhat" maxlength = "120" placeholder = "e.g. HDMI port loose, won't hold signal" />
</ div >
< div class = "form-group" >
< label for = "incidentDetails" > Details < span class = "label-hint" > (optional)</ span ></ label >
< textarea id = "incidentDetails" rows = "3" maxlength = "2000" placeholder = "Anything that helps whoever fixes it…" ></ textarea >
</ div >
< div id = "incidentReportingAs" class = "incident-reporting-as" style = "display:none;" ></ div >
< div class = "modal-footer" >
< button id = "incidentSubmit" class = "btn btn-primary" type = "button" > Send report</ button >
</ div >
</ div >
</ div >
</ div >
<!-- Reported gear problems (editors) -->
< div id = "incidentsModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > ⚠ Gear Issues</ h3 >
< button id = "incidentsClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< div class = "incidents-controls" >
< label class = "incidents-openonly" >< input type = "checkbox" id = "incidentsOpenOnly" checked /> Open only</ label >
< input type = "search" id = "incidentsSearch" class = "incidents-search" placeholder = "Filter by gear, place, or text…" autocomplete = "off" />
</ div >
< div id = "incidentsList" class = "incidents-list" ></ div >
</ div >
</ div >
<!-- Reliability dashboard (editors) -->
< div id = "reliabilityModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal mgmt" >
< div class = "mgmt-head" >
< h3 > 📊 Gear Reliability</ h3 >
< button id = "reliabilityClose" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
</ div >
< div class = "reliability-controls" >
< label class = "incidents-openonly" >< input type = "checkbox" id = "reliabilityOpenFirst" checked /> Sort by open issues</ label >
< span id = "reliabilitySummary" class = "reliability-summary" ></ span >
</ div >
< div id = "reliabilityList" class = "reliability-list" ></ div >
</ div >
</ div >
< div id = "addTaskModal" class = "modal-overlay" style = "display: none;" >
< div class = "modal wide" >
< div class = "add-header" >
< h3 id = "addModalTitle" > Add Task</ h3 >
< div class = "add-list-target" >
< label for = "addListSelect" > Add to list:</ label >
< select id = "addListSelect" ></ select >
</ div >
</ div >
< div class = "modal-body" >
< div class = "form-group" >
< input type = "text" id = "itemTitle" placeholder = "Task title..." />
</ div >
< div class = "form-group" id = "itemGroupRow" style = "display:none;" >
< label for = "itemGroupSelect" > Section</ label >
< select id = "itemGroupSelect" ></ select >
</ div >
< div class = "form-group" >
< div class = "md-toolbar" data-md-target = "itemDescription" >
< button type = "button" class = "md-btn" data-md = "bold" title = "Bold" >< b > B</ b ></ button >
< button type = "button" class = "md-btn" data-md = "italic" title = "Italic" >< i > I</ i ></ button >
< button type = "button" class = "md-btn" data-md = "h" title = "Heading" > H</ button >
< button type = "button" class = "md-btn" data-md = "ul" title = "Bullet list" > • List</ button >
< button type = "button" class = "md-btn" data-md = "ol" title = "Numbered list" > 1. List</ button >
< button type = "button" class = "md-btn" data-md = "task" title = "Checkbox — a step users tick off (the task auto-completes when all are checked)" > ☑ Step</ button >
< button type = "button" class = "md-btn" data-md = "link" title = "Add a link (web page, Google Doc…)" > 🔗 Link</ button >
< button type = "button" class = "md-btn" data-md = "attach" title = "Attach a document (PDF, Word, Excel, PowerPoint…) — uploads it and links it here" > 📎 File</ button >
< button type = "button" class = "md-btn md-preview-btn" data-md = "preview" aria-pressed = "false" title = "Toggle formatted markdown view" > Markdown</ button >
</ div >
< textarea id = "itemDescription" placeholder = "Description (optional)" style = "display:none;" ></ textarea >
< div class = "md-rich" contenteditable = "true" data-md-rich-for = "itemDescription" data-placeholder = "Description (optional)" role = "textbox" aria-multiline = "true" ></ div >
</ div >
< div class = "form-group" id = "recurringFieldGroup" >
< label class = "checkbox-label" >
< input type = "checkbox" id = "isRecurring" />
Recurring Task
</ label >
</ div >
< div class = "form-group" id = "recurrenceOptions" style = "display: none;" >
< div class = "rec-editor" ></ div >
</ div >
</ div >
< div class = "modal-actions" >
< button id = "addTaskCancel" class = "btn btn-secondary btn-small" type = "button" > Close</ button >
< button id = "addItemBtn" class = "btn btn-primary btn-small" > Add Task</ button >
</ div >
</ div >
</ div >
</ div >
< div id = "toast" class = "toast" ></ div >
<!-- Motion One (vendored, self-contained ~15KB): exposes window.Motion synchronously
before app.js so item FLIP, spring modals and the completion burst can use it.
If it ever fails to load, app.js falls back to the pure-CSS animations. -->
< script src = "vendor/motion.global.js" ></ script >
<!-- Native shim: a no-op on the web; inside the iOS/Android app bundles it points
server-absolute URLs (/api/…, /equip-images/…) at the real server. The mobile
build injects native-config.js + native-shell.js ahead of it. -->
< script src = "native-bridge.js" ></ script >
< script src = "md.js" ></ script >
< script src = "app.js" ></ script >
</ body >
</ html >