workedH on every preview/export. Fixed: getDays() now reads calcData.days[i].x1, .x3, and .total directly — TA corrections flow through to HTML preview, PDF, and XLSX.</div> on #jobHoldSection caused all other day-type toggles (Pre-light, Weather, Prep, Wrap) to be DOM children of the Hold section — they only appeared when Hold was on. Fixed with one closing tag.th was white text on dark — invisible on the white preview card. XLSX headers had no cell styles at all (aoa_to_sheet produces unstyled cells). Both now use dark #1C1C1E text on light grey #D1D1D6 readable on any background. PDF header fixed in the same pass.doc.setFillColor(209,209,214) was called once outside the column loop. jsPDF bleeds the last fill colour into subsequent rect() calls — a preceding dark element was overwriting the grey. Fixed by moving all colour resets inside the per-cell loop.expSubmit, invInit, and others 4× each — bloating the file from 626KB to 1.3MB and causing an Invalid or unexpected token runtime crash. Rebuilt from clean uploaded source applying only genuinely missing patches atomically via position-map replacement.job_expenses Supabase table — no separate storage bucket required for typical usage.calcDay()) was computing OT against elapsed clock time — call to wrap — rather than hours worked. This meant an unpaid 1-hour lunch was being counted toward the OT threshold. A standard 10-hour worked day (e.g. 7:30–18:30 with a 13:30–14:30 lunch) was incorrectly reporting 1 hour of OT and showing 11.5 billable hours instead of 10. The fix removes the elapsedH variable entirely and calculates OT from workedH — which already has the unpaid lunch deducted — matching ACCP guidelines: "overtime pay for time worked beyond the daily minimum hours." Meal penalty OT tier boundaries were also updated to shift by the lunch gap so tier thresholds remain aligned with the corrected worked-hours clock.onclick handlers built inside JS template strings were using '' (two single quotes) as an escape sequence, which actually terminated the surrounding string literal and broke the parser. Affected calls: expOpenPMView(), expViewStoredReceipt() (×2), and expUpdateStatus() (×3). All six were corrected to use proper \' escaping, resolving the Uncaught SyntaxError: Unexpected string console error.emergency_contact JSON on their Supabase profiles row. Fills in once and syncs across devices automatically.crew_job_status Supabase table (keyed on job_code + crew_user_id) and persists across sessions. Crew can also change their own status from the Jobs tab on their end.alter table profiles add column emergency_contact jsonb stores the crew member's emergency contact on their profile. A new crew_job_status table (job_code, crew_user_id, pm_user_id, status) with row-level security stores Hold/Booked per crew per job — PM can read/write all rows for their jobs, crew can read their own row.str_replace during the per-day copy button addition used function showInvoice(){ as a boundary marker in the old string but omitted it from the replacement. This silently deleted the function declaration, leaving its body as orphaned loose statements. JavaScript failed to parse the entire script block — all functions were undefined and every button was unresponsive. Fixed by restoring the missing declaration.billWorked (actual hours, e.g. 13.5h) while the header showed invoiceHrs (weighted equivalent, e.g. 16h). Fixed — both now show invoiceHrs consistently.lunchOut + 30min rather than the actual early return time. A standard 30-min lunch is unaffected.transform="translate(-190,-40)" offset to re-origin design sheet coordinates. Clip paths apply before transforms so they were misaligned — the clapper arm was cut off at the top and the squircle clip wasn't applied. Rewrote the SVG with native 0 0 300 300 coordinates and a wrapping <g clip-path="url(#sq)">.</div> was closing #splash prematurely after the logo and title only, leaving Get Started, What's New, and Add to Home Screen outside the screen container and invisible.#12082A background and a "Major Update" badge. The renderer was updated to support dark-background featured entries. Dark Mode entry corrected to V1.3.7.1.#12082A) containing a white document with folded corner, a striped purple clapper arm and body, a separation line with hinge dots, document content lines, and a purple clock in the bottom-right. All elements are crisp at every size from 20pt to 1024pt.apple-touch-icon.png (180×180)icon-192.png and icon-512.png with the new design. The 512px version is used by Android for high-resolution displays and splash screens. Both are tagged maskable in the manifest so Android can apply its adaptive icon shape.favicon-32x32.png<link rel="icon"> inline SVG in <head> has been redrawn to match the new icon: dark navy squircle, document with folded corner, striped clapper, and clock. This renders in browsers that support SVG favicons (Chrome, Firefox, Edge) at any resolution without pixelation.#12082A across all surfaces#2d2d45 to the icon's deep navy #12082A. Updated in: <meta name="theme-color"> (browser chrome on Android), manifest.json theme_color (Android splash), and manifest.json background_color (Android splash background). The result is a seamless dark launch experience — no flash of grey.apple-mobile-web-app-title changed from "Set Calc" to "Set Calculator" — this is the label that appears under the icon on an iOS home screen. The manifest short_name is also updated to match..splash-svg CSS was updated for the new square icon proportions: width: min(160px, 44vw), border-radius: 36px, and a deeper drop shadow matching the dark squircle (rgba(18,8,42,0.45)). The floating animation is unchanged.submitted_timesheets tableid (UUID PK), job_code (FK → posted_jobs), crew_user_id (FK → auth.users), crew_name, crew_role, sheet_data (full timesheet JSON), total_pay, submitted_at. A unique index on (job_code, crew_user_id) means re-submitting overwrites the previous version. RLS: crew read/write own rows; PMs read all rows for jobs they own.saveToSession() in index.html now also writes activeCalcJob to sessionStorage when the calculator was launched from a calendar job. This carries jobCode, jobId, and prod into the timesheet page so the Submit button knows which job to link the submission to.mjInit() runs — a single query fetches all submission counts for all the PM's jobs at once.job.accountingEmail and pushed to Supabase with all other job data.accountingEmail, falling back to the company's stored email. Saved on "Save Changes" and synced to Supabase.[Camera] ..., [G&E / Lighting] ...). Accounting gets a complete picture of all role-specific instructions in one email.deptNotes array on the job object and pushed to Supabase alongside all other job data.renderDeptNotesList() rendererrenderDeptNotesList(containerId, notesArr, prefix, ...) function drives both the Claim a Job form and the Edit modal. It accepts the target container and array by reference so both forms share identical UI logic with no duplication.body.dataset.built = '1' that never cleared — so after every new deploy, the old content was silently shown. Fixed by switching to body.dataset.builtVersion = 'V1.3.7.2'. The sheet now rebuilds whenever the stored version doesn't match the current app version. Each deploy automatically shows fresh content.release-notes-march-18-2026.html in a new tab for the full detailed release history.#f2f2f7 and #1c1c1e. Changed to var(--surface-card) and var(--text). Tag pill colours in buildWhatsNewHTML() now have dark-mode variants. applyDarkMode() also clears the built-version cache so the sheet rebuilds with correct colours on next open.[data-theme="dark"] CSS variable systemdata-theme="dark" attribute on <html>. All CSS variables flip: backgrounds go to near-black #0f0f10, glass surfaces use dark semi-transparent fills, text inverts to #f2f2f7, accents brighten for contrast. Every surface is covered — calculator, calendar, role picker, job preview, modals, sheets, nav bar, inputs, and turnaround cards.profiles.dark_modedark_mode boolean column on the profiles table persists the preference across devices. On login it loads from Supabase; on toggle it saves immediately. Before auth resolves, the setting loads from localStorage so there's no flash of light mode.#f2f2f7 and #e4e4ea in CSS classes (.cal-confirm-sheet, .save-sheet, .install-steps, calendar header) are now replaced with new --surface-card, --surface-bg, and --nav-bg variables so every surface flips correctly.profiles table — cross-device role syncprofiles table (id UUID → auth.users, roles text[]) stores each user's selected roles. Row-level security ensures users can only access their own row. On every login, roles are loaded from Supabase and cached in localStorage. Any change to roles is immediately upserted to Supabase — so switching devices picks up where you left off.more-roles-list container existed but was never populated — populateMore() wasn't calling renderRoleCheckboxes() with the right target. Fixed. Additionally, toggleUserRole() and clearUserRoles() now refresh all three role containers simultaneously (Account, More, Onboarding) so they're always in sync without requiring a navigation round-trip.targetRoles array is saved with the job and pushed to Supabase alongside all other job data.targetRoles are pre-loaded into the pill UI. Saving updates the job locally and re-upserts to Supabase so crew see the change within 30 seconds.targetRoles. If there's a match, a blue banner shows "Showing info for: Assistant Camera" with a "Show all" button. Tapping "Show all" reveals the full job details with a green "Showing all job information" banner and a "Show my info" button to return to the filtered view. PMs always see the full view plus a role targeting summary below the job code.ALL_ROLES — single source of truthALL_ROLES constant with name, rate, minHrs, type, group, and prepWrapRate. This drives the role picker, account checkboxes, onboarding, PM role targeting, and crew filtering. Previously role data was duplicated in three separate places.profiles tableprofiles table stores each user's selected roles as a text array. Row-level security ensures users can only read and write their own profile. Roles sync across devices — selecting Assistant Camera on your phone shows it pre-selected on your iPad.ALL_ROLES master constantALL_ROLES array with name, rate, minHrs, type, group, and prepWrapRate. This is the single source of truth used by the role picker, the calculator dropdown, and getDays(). Previously role data was duplicated across three separate places in the code.showScreen('account') now automatically calls populateAccount(), which renders the user's email, name, and role checkboxes. Previously the account screen required a separate manual call to populate.claimJobAuth(), claimJobCloseModal(), and claimJobSubmit() functions are all removed. Access control will be handled by account-level role permissions in a future update.buildTSHtml but not at the data layer in getDays(). Since PDF export and Excel use the raw day data from getDays(), they were unaffected. Fixed by applying the shortfall in getDays() itself — subtracting shortfall hours from x1 and adding them to x3 before the data is passed anywhere. For 1h shortfall on Day 2: x1 = 9.00h, x3 = 1.00h, Total Pay Hrs = 12.00, Total $ = $853.92.crewJobsAddToCal(code) looked up the job in the in-memory _crewJobsData array, which is only populated after the Jobs tab has loaded and synced from Supabase. If the crew member hadn't opened the tab yet, the array was empty and the function silently did nothing. Fixed by making the function async — if the job isn't in memory, it fetches it directly from posted_jobs in Supabase before proceeding.crewJobsViewFull(code) — if the job isn't in the in-memory cache, it fetches from Supabase before opening the Job Preview screen.id field, so calendar entry IDs were generating as cj_undefined_timestamp. Changed to job.id || job.jobCode || Date.now() for a reliable unique ID.buildTSHtml (display layer). The proper fix moves it to getDays() so the adjusted x1/x3 values flow through to PDF, HTML preview, and Excel consistently.posted_jobs tableACMEAUT-2026-042) to subscribe to a job. The code is looked up in Supabase in real-time — if found, it's added to the crew member's personal job list. The code is stored locally so the crew member never has to enter it again.posted_jobs in Supabase. No manual publish step needed — the job goes live for crew the moment it's saved.openWhatsNew() was calling nav.classList.remove('visible') to hide the nav — but the close function sometimes failed to restore it, leaving the nav permanently hidden. Removed the hide/show entirely since the sheet is z-index: 1100 which naturally covers the nav at z-index: 1000. Also moved #whatsNewSheet and its overlay to direct children of <body> via DOMContentLoaded to prevent transform containment issues.var(--glass) = rgba(255,255,255,0.52) — semi-transparent over a dark blurred backdrop, making labels nearly invisible. Changed to solid #f2f2f7 with targeted CSS overrides for label colour (#636366) and input background (rgba(255,255,255,0.85)).position:fixed; inset:0) with a sticky header containing a circular × close button. Previously a bottom sheet capped at 90vh with no close button visible on desktop.tag-new, tag-fix, and tag-improve tag in the document. Added a 4th "Releases" counter showing 17 distinct versions shipped._sb but all sync functions were checking for window._sb — always undefined — so every sync call bailed out immediately without an error. Fixed by correcting all variable references. Jobs now correctly upsert and delete from the cloud.job.pay is now correctly treated as a per-day rate and added once per calendar day._subDayState.hold. When the job was saved and reopened, the hold date entries were still present in the data, so openEditJob restored the toggle as checked and the picker locked again. The Hold toggle now immediately clears all hold date entries and resets the Booked checkbox when switched off — so save, close, and reopen produces a clean non-hold job.checked !== false defaulted to "meal provided" when the toggle was absent or unchecked, suppressing the dinner penalty incorrectly. Now reads the toggle directly — if no dinner is entered and the toggle is off, the penalty correctly runs from 6h after lunch-in to wrap at 2× for the first hour then 3× thereafter, per ACCP guidelines.billAfternoon was showing the full actual afternoon (e.g. 7.5h) instead of just the base-fill portion (4h). OT hours now display separately below as always. Fixed in both single-day and multi-day calculators.camWrap instead of yourWrap. Fixed in both invoice preview and PDF export.billMorning = max(6h, actual morning) and billAfternoon = 10h − billMorning. A 6.5h morning (late lunch) correctly shows 6.5h morning + 3.5h afternoon. An early or on-time lunch floors at 6h morning + 4h afternoon. The two lines always add to exactly 10h = base pay.decToTime() function was applying parseFloat() to strings like "22:30", producing 22.3 → "22:18" instead of "22:30". Times are now passed as HH:MM strings directly, with decimal fallback for legacy data only.renderDayPanels() was rebuilding all panel HTML from scratch, resetting every toggle (meal penalties, second meal, prep day, separate call) back to defaults. All input values and toggle states are now saved before the rebuild and restored immediately after.invoiceHrs is now total / rate rounded to 2 decimal places, so that Invoice qty × rate always equals the exact day total including all meal penalties. Previously used DAY_MIN + ot1×1.5 + ot2×2 + ot3×3 which excluded penalties from the calculation, making the math inconsistent with what you'd enter on an invoice.10 × $71.16 produces 711.5999999999999, making total / rate = 18.999999.... fmtH() was flooring to 18h and rounding 0.9999×60 = 60min, displaying "18h 60m". Fixed by rounding total to 2dp before dividing, rounding invoiceHrs to 2dp, and adding a guard in fmtH() to carry 60min over to the next hour.Xh × $rate = $total, making it unambiguous what number to enter as quantity on an invoice.rgb(220,220,228) with setTextColor(20,20,40) applied explicitly per cell before drawing text. The previous approach set fill colour once for all cells, causing jsPDF color state to bleed — text was effectively black-on-grey but the fill wasn't being applied correctly to all cells.setFillColor(255,255,255) before rendering, preventing any bleed from the grey header fill into data rows.data-m attribute conflict with the click listener's .closest() call prevented them from rendering selected state. Fixed by using a unified data-val attribute across all columns (hour, minute, AM/PM)..tpk-lists wrapper had overflow: hidden which clipped all three inner scroll columns, preventing touch and mouse scrolling entirely. Changed to explicit height: 200px and overflow-y: scroll on each column independently.d.total from the calculator payload, which already correctly accounts for all penalties. The column totals (1x, 1.5x, 2x, 3x) reflect the adjusted values after folding. Applied to PDF export, HTML preview, and Excel export.d.total payload value. For a 10h day with 30min late lunch: 9.5×$71.16 + 0.5×$142.32 = $676.02 + $71.16 = $747.18.saveToSession was never including camWrap in the payload. The restore block was using setT('camWrap', day.out) — literally setting camera wrap to your wrap time. Fixed: payload now saves camWrap: minsToHHMM(c.camWrap) as a dedicated field, and the restore uses day.camWrap explicitly.mpToggle (the global meal penalties switch) was not included in the payload and always reset to default; (2) secondMealToggle (Dinner at camera wrap) was similarly lost. Both are now saved in the payload (mpOn and secondMealProvided per day) and restored correctly — mpToggle before renderDayPanels(), and secondMealToggle after checkCamWrapVisibility() makes its row visible.ACMEAUT-2026-042). Crew can enter this code in their app to find and add the job to their calendar, or the PM can share a direct deep link that opens the app and pre-fills the job automatically.claimjob screen div had style="display:none" hardcoded inline. Inline styles have higher CSS specificity than class rules, so even after showScreen() added the .active class (which sets display:flex), the inline display:none won every time. Removed the inline style.display:none written twice in its style attribute — a copy-paste duplicate. Cleaned up to a single initial state. Also added claimjob and later myjobs, jobpreview to NAV_SCREENS and updateNav so the nav bar stays visible on those screens.rgba(0,0,0,0.5) and the card used the transparent glass style — both blended into the page background. Changed to a solid rgba(0,0,0,0.72) overlay and a solid #f2f2f7 card so the modal stands out clearly on any background.mjInit() call) instead of Claim a Job..toFixed(1)), causing values like 11.3 instead of 11.25 and 0.8 instead of 0.75. All changed to .toFixed(2) across PDF, HTML preview, and Excel.<nav id="bottomNav"> was nested inside #calSheet, which uses transform: translateY() to animate open/close. In CSS, position: fixed elements lose viewport-relative positioning when inside any ancestor with a transform — they become fixed relative to that transformed ancestor instead. The nav was rendering at top: 1563px (652px below the 911px viewport). Fixed by moving the nav to a direct child of <body> via JavaScript at DOMContentLoaded, ensuring it is never inside a transformed container.#calSheet was missing its closing </div> tag. Every screen after it in the HTML — #more, #claimjob, #myjobs, #jobpreview, #jobbrief, and the nav bar — was nested inside #calSheet. Since #calSheet uses transform: translateY(652px) to sit off-screen, everything inside it rendered 652px below the viewport. Fixed by inserting the missing </div> after #whatsNewSheet closes.#whatsNewSheet was also inside the transformed #calSheet container, rendering 652px below its intended position. Now correctly slides up from the bottom of the screen with its Done button accessible.#more screen content was rendering 652px below the visible viewport due to the same transform containment issue. Fully resolved by the structural fix.